detached ce24e6e

fix(startup): scope trace to accelerated CEF

commit
ce24e6e95f377cb706fa2d5138de8eaac6190552
author
brice · · 2026-07-15 08:33 UTC
parent
80795e3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +164 -125
M cmd/dumber/main.go +6 -9
@@ -143,8 +143,8 @@ func configureBrowserLaunchRelay(cfg *config.Config) {
143143 func main() {
144144 // This is intentionally before subprocess detection: each process records
145145 // its own first observable main event before CEF can take over execution.
146- logging.InitStartupTrace("")
147- logging.Trace().Mark("process_entry")
146+ logging.InitCEFStartupTrace()
147+ logging.CEFTrace().Mark("process_entry")
148148
149149 // CEF subprocess handling: when CEF re-launches this binary with
150150 // --type=renderer/gpu/etc, we must call ExecuteProcess before anything
@@ -223,7 +223,7 @@ func runGUI(cfg *config.Config) int {
223223 cfg = initConfig()
224224 configureBrowserLaunchRelay(cfg)
225225 }
226- logging.Trace().Mark("config_complete")
226+ logging.CEFTrace().Mark("config_complete")
227227 timer.Mark("config")
228228
229229 ctx := initStartupContextWithTrace(cfg)
@@ -259,7 +259,7 @@ func runGUI(cfg *config.Config) int {
259259 timer.Mark("session")
260260
261261 log := logging.FromContext(ctx)
262- logging.Trace().UpdateLogger(log)
262+ logging.CEFTrace().SetLogger(log)
263263 logCoreDumpLimits(ctx)
264264
265265 engine.SetHandlerContext(ctx)
@@ -419,19 +419,17 @@ func resolveCurrentExecutable(executable func() (string, error)) (string, error)
419419 }
420420
421421 func initStartupContextWithTrace(cfg *config.Config) context.Context {
422- logging.InitStartupTrace(cfg.Logging.Level)
422+ logging.InitCEFStartupTrace()
423423
424424 ctx := initStartupContext(cfg)
425425 bootstrapLog := logging.FromContext(ctx)
426426
427- logging.Trace().SetLogger(bootstrapLog)
428- logging.Trace().Mark("logger_init")
427+ logging.CEFTrace().SetLogger(bootstrapLog)
429428
430429 return ctx
431430 }
432431
433432 func runParallelInitPhase(ctx context.Context, cfg *config.Config) (*bootstrap.ParallelInitResult, error) {
434- logging.Trace().Mark("parallel_start")
435433 initResult, err := bootstrap.RunParallelInit(bootstrap.ParallelInitInput{
436434 Ctx: ctx,
437435 Config: cfg,
@@ -440,7 +438,6 @@ func runParallelInitPhase(ctx context.Context, cfg *config.Config) (*bootstrap.P
440438 handleParallelInitError(ctx, err)
441439 return nil, err
442440 }
443- logging.Trace().Mark("parallel_done")
444441 return initResult, nil
445442 }
446443
@@ -143,8 +143,8 @@ func configureBrowserLaunchRelay(cfg *config.Config) {
143 func main() {143 func main() {
144 // This is intentionally before subprocess detection: each process records144 // This is intentionally before subprocess detection: each process records
145 // its own first observable main event before CEF can take over execution.145 // its own first observable main event before CEF can take over execution.
146- logging.InitStartupTrace("")146+ logging.InitCEFStartupTrace()
147- logging.Trace().Mark("process_entry")147+ logging.CEFTrace().Mark("process_entry")
148 148
149 // CEF subprocess handling: when CEF re-launches this binary with149 // CEF subprocess handling: when CEF re-launches this binary with
150 // --type=renderer/gpu/etc, we must call ExecuteProcess before anything150 // --type=renderer/gpu/etc, we must call ExecuteProcess before anything
@@ -223,7 +223,7 @@ func runGUI(cfg *config.Config) int {
223 cfg = initConfig()223 cfg = initConfig()
224 configureBrowserLaunchRelay(cfg)224 configureBrowserLaunchRelay(cfg)
225 }225 }
226- logging.Trace().Mark("config_complete")226+ logging.CEFTrace().Mark("config_complete")
227 timer.Mark("config")227 timer.Mark("config")
228 228
229 ctx := initStartupContextWithTrace(cfg)229 ctx := initStartupContextWithTrace(cfg)
@@ -259,7 +259,7 @@ func runGUI(cfg *config.Config) int {
259 timer.Mark("session")259 timer.Mark("session")
260 260
261 log := logging.FromContext(ctx)261 log := logging.FromContext(ctx)
262- logging.Trace().UpdateLogger(log)262+ logging.CEFTrace().SetLogger(log)
263 logCoreDumpLimits(ctx)263 logCoreDumpLimits(ctx)
264 264
265 engine.SetHandlerContext(ctx)265 engine.SetHandlerContext(ctx)
@@ -419,19 +419,17 @@ func resolveCurrentExecutable(executable func() (string, error)) (string, error)
419 }419 }
420 420
421 func initStartupContextWithTrace(cfg *config.Config) context.Context {421 func initStartupContextWithTrace(cfg *config.Config) context.Context {
422- logging.InitStartupTrace(cfg.Logging.Level)422+ logging.InitCEFStartupTrace()
423 423
424 ctx := initStartupContext(cfg)424 ctx := initStartupContext(cfg)
425 bootstrapLog := logging.FromContext(ctx)425 bootstrapLog := logging.FromContext(ctx)
426 426
427- logging.Trace().SetLogger(bootstrapLog)427+ logging.CEFTrace().SetLogger(bootstrapLog)
428- logging.Trace().Mark("logger_init")
429 428
430 return ctx429 return ctx
431 }430 }
432 431
433 func runParallelInitPhase(ctx context.Context, cfg *config.Config) (*bootstrap.ParallelInitResult, error) {432 func runParallelInitPhase(ctx context.Context, cfg *config.Config) (*bootstrap.ParallelInitResult, error) {
434- logging.Trace().Mark("parallel_start")
435 initResult, err := bootstrap.RunParallelInit(bootstrap.ParallelInitInput{433 initResult, err := bootstrap.RunParallelInit(bootstrap.ParallelInitInput{
436 Ctx: ctx,434 Ctx: ctx,
437 Config: cfg,435 Config: cfg,
@@ -440,7 +438,6 @@ func runParallelInitPhase(ctx context.Context, cfg *config.Config) (*bootstrap.P
440 handleParallelInitError(ctx, err)438 handleParallelInitError(ctx, err)
441 return nil, err439 return nil, err
442 }440 }
443- logging.Trace().Mark("parallel_done")
444 return initResult, nil441 return initResult, nil
445 }442 }
446 443
M docs/first-presentation.md +20 -8
@@ -1,8 +1,11 @@
1-# First presentation startup trace
1+# CEF accelerated first-presentation startup trace
22
3-Dumber records one cold-start timeline for each GUI process. The trace begins
4-at `process_entry`, before CEF subprocess handling, and accepts these one-shot
5-milestones only in order:
3+Dumber records this cold-start timeline **only for the accelerated CEF → DMABUF
4+→ GTK path**. It is not a backend-neutral startup metric: the WebKit backend
5+does not add milestones to this trace and never emits this CEF summary.
6+
7+For a CEF GUI process, the trace begins at `process_entry`, before CEF
8+subprocess handling, and accepts these one-shot milestones only in order:
69
710 1. `process_entry`
811 2. `config_complete`
@@ -13,11 +16,19 @@ milestones only in order:
1316 7. `first_dmabuf_texture_swap` (after `GtkPicture.SetPaintable` succeeds)
1417 8. `first_gtk_presentation` (the subsequent GTK frame-clock after-paint)
1518
16-CEF library-load completion is intentionally not recorded: `InitWithApp` is an
19+The CEF-to-GTK bridge owns the native accelerated-paint, DMABUF texture-swap,
20+and GTK presentation boundaries. Dumber records the bridge's ordered callbacks;
21+it does not synthesize them for another engine or rendering path. CEF
22+library-load completion is intentionally not recorded: `InitWithApp` is an
1723 opaque operation. Duplicate, unknown, and out-of-order transitions are
1824 rejected. At the last milestone Dumber emits exactly one normal-level JSON
19-summary (`startup_trace: first presentation`) with the selected `backend`, an
20-`incomplete_reason`, total milliseconds, and monotonic milestones.
25+summary (`startup_trace: first presentation`) with the selected CEF render
26+backend, an `incomplete_reason`, total milliseconds, and monotonic milestones.
27+
28+A non-DMABUF CEF backend or a missing accelerated callback does not produce a
29+complete summary and is not comparable with this measurement. In particular,
30+WebKit startup must be measured with a separately defined WebKit-specific
31+contract rather than this CEF trace.
2132
2233 ## Reproducible collection
2334
@@ -29,7 +40,8 @@ DUMBER_MACHINE_GPU_PROFILE=integrated-gpu \
2940 scripts/collect_first_presentation.sh
3041 ```
3142
32-By default each collection is a fresh directory below
43+The collector is for the accelerated CEF/DMABUF/GTK contract above. By default
44+each collection is a fresh directory below
3345 `$XDG_STATE_HOME/dumber/roadmap-evidence` (or
3446 `$HOME/.local/state/dumber/roadmap-evidence` when `XDG_STATE_HOME` is unset),
3547 not in the repository. `DUMBER_FIRST_PRESENTATION_OUTPUT` may override it only
@@ -1,8 +1,11 @@
1-# First presentation startup trace1+# CEF accelerated first-presentation startup trace
2 2
3-Dumber records one cold-start timeline for each GUI process. The trace begins3+Dumber records this cold-start timeline **only for the accelerated CEF → DMABUF
4-at `process_entry`, before CEF subprocess handling, and accepts these one-shot4+→ GTK path**. It is not a backend-neutral startup metric: the WebKit backend
5-milestones only in order:5+does not add milestones to this trace and never emits this CEF summary.
6+
7+For a CEF GUI process, the trace begins at `process_entry`, before CEF
8+subprocess handling, and accepts these one-shot milestones only in order:
6 9
7 1. `process_entry`10 1. `process_entry`
8 2. `config_complete`11 2. `config_complete`
@@ -13,11 +16,19 @@ milestones only in order:
13 7. `first_dmabuf_texture_swap` (after `GtkPicture.SetPaintable` succeeds)16 7. `first_dmabuf_texture_swap` (after `GtkPicture.SetPaintable` succeeds)
14 8. `first_gtk_presentation` (the subsequent GTK frame-clock after-paint)17 8. `first_gtk_presentation` (the subsequent GTK frame-clock after-paint)
15 18
16-CEF library-load completion is intentionally not recorded: `InitWithApp` is an19+The CEF-to-GTK bridge owns the native accelerated-paint, DMABUF texture-swap,
20+and GTK presentation boundaries. Dumber records the bridge's ordered callbacks;
21+it does not synthesize them for another engine or rendering path. CEF
22+library-load completion is intentionally not recorded: `InitWithApp` is an
17 opaque operation. Duplicate, unknown, and out-of-order transitions are23 opaque operation. Duplicate, unknown, and out-of-order transitions are
18 rejected. At the last milestone Dumber emits exactly one normal-level JSON24 rejected. At the last milestone Dumber emits exactly one normal-level JSON
19-summary (`startup_trace: first presentation`) with the selected `backend`, an25+summary (`startup_trace: first presentation`) with the selected CEF render
20-`incomplete_reason`, total milliseconds, and monotonic milestones.26+backend, an `incomplete_reason`, total milliseconds, and monotonic milestones.
27+
28+A non-DMABUF CEF backend or a missing accelerated callback does not produce a
29+complete summary and is not comparable with this measurement. In particular,
30+WebKit startup must be measured with a separately defined WebKit-specific
31+contract rather than this CEF trace.
21 32
22 ## Reproducible collection33 ## Reproducible collection
23 34
@@ -29,7 +40,8 @@ DUMBER_MACHINE_GPU_PROFILE=integrated-gpu \
29 scripts/collect_first_presentation.sh40 scripts/collect_first_presentation.sh
30 ```41 ```
31 42
32-By default each collection is a fresh directory below43+The collector is for the accelerated CEF/DMABUF/GTK contract above. By default
44+each collection is a fresh directory below
33 `$XDG_STATE_HOME/dumber/roadmap-evidence` (or45 `$XDG_STATE_HOME/dumber/roadmap-evidence` (or
34 `$HOME/.local/state/dumber/roadmap-evidence` when `XDG_STATE_HOME` is unset),46 `$HOME/.local/state/dumber/roadmap-evidence` when `XDG_STATE_HOME` is unset),
35 not in the repository. `DUMBER_FIRST_PRESENTATION_OUTPUT` may override it only47 not in the repository. `DUMBER_FIRST_PRESENTATION_OUTPUT` may override it only
M internal/infrastructure/cef/engine_init.go +3 -3
@@ -56,7 +56,7 @@ func NewEngine(
5656 return nil, err
5757 }
5858 cef2gtk.ConfigureRenderStackEnvironment(renderStackPlan)
59- logging.Trace().SetBackend(renderStackPlan.Backend.String())
59+ logging.CEFTrace().SetBackend(renderStackPlan.Backend.String())
6060
6161 settings, err := prepareCEFSettings(opts, paths, cfg, logger)
6262 if err != nil {
@@ -218,11 +218,11 @@ func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logge
218218 logger.Debug().Msg("cef: calling InitWithApp")
219219 // InitWithApp encapsulates loading libcef, so only the begin boundary is
220220 // observable here; do not invent a separate library-load completion event.
221- logging.Trace().Mark("cef_library_load_begin")
221+ logging.CEFTrace().Mark("cef_library_load_begin")
222222 if err := purecef.InitWithApp(settings, app); err != nil {
223223 return fmt.Errorf("cef.InitWithApp: %w", err)
224224 }
225- logging.Trace().Mark("cef_initialized")
225+ logging.CEFTrace().Mark("cef_initialized")
226226 if libcefPath := loadedLibCEFPath(); libcefPath != "" {
227227 logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded")
228228 } else {
@@ -56,7 +56,7 @@ func NewEngine(
56 return nil, err56 return nil, err
57 }57 }
58 cef2gtk.ConfigureRenderStackEnvironment(renderStackPlan)58 cef2gtk.ConfigureRenderStackEnvironment(renderStackPlan)
59- logging.Trace().SetBackend(renderStackPlan.Backend.String())59+ logging.CEFTrace().SetBackend(renderStackPlan.Backend.String())
60 60
61 settings, err := prepareCEFSettings(opts, paths, cfg, logger)61 settings, err := prepareCEFSettings(opts, paths, cfg, logger)
62 if err != nil {62 if err != nil {
@@ -218,11 +218,11 @@ func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logge
218 logger.Debug().Msg("cef: calling InitWithApp")218 logger.Debug().Msg("cef: calling InitWithApp")
219 // InitWithApp encapsulates loading libcef, so only the begin boundary is219 // InitWithApp encapsulates loading libcef, so only the begin boundary is
220 // observable here; do not invent a separate library-load completion event.220 // observable here; do not invent a separate library-load completion event.
221- logging.Trace().Mark("cef_library_load_begin")221+ logging.CEFTrace().Mark("cef_library_load_begin")
222 if err := purecef.InitWithApp(settings, app); err != nil {222 if err := purecef.InitWithApp(settings, app); err != nil {
223 return fmt.Errorf("cef.InitWithApp: %w", err)223 return fmt.Errorf("cef.InitWithApp: %w", err)
224 }224 }
225- logging.Trace().Mark("cef_initialized")225+ logging.CEFTrace().Mark("cef_initialized")
226 if libcefPath := loadedLibCEFPath(); libcefPath != "" {226 if libcefPath := loadedLibCEFPath(); libcefPath != "" {
227 logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded")227 logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded")
228 } else {228 } else {
M internal/infrastructure/cef/factory.go +1 -1
@@ -279,7 +279,7 @@ func (f *WebViewFactory) postPendingBrowserCreate(ctx context.Context, wv *WebVi
279279 // OnAfterCreated once the host is fully wired.
280280 initialURL := "about:blank"
281281 // This is the request boundary, not the later OnAfterCreated callback.
282- logging.Trace().Mark("browser_create_requested")
282+ logging.CEFTrace().Mark("browser_create_requested")
283283 result := cefBrowserHostCreateBrowser(
284284 pc.windowInfo,
285285 pc.client,
@@ -279,7 +279,7 @@ func (f *WebViewFactory) postPendingBrowserCreate(ctx context.Context, wv *WebVi
279 // OnAfterCreated once the host is fully wired.279 // OnAfterCreated once the host is fully wired.
280 initialURL := "about:blank"280 initialURL := "about:blank"
281 // This is the request boundary, not the later OnAfterCreated callback.281 // This is the request boundary, not the later OnAfterCreated callback.
282- logging.Trace().Mark("browser_create_requested")282+ logging.CEFTrace().Mark("browser_create_requested")
283 result := cefBrowserHostCreateBrowser(283 result := cefBrowserHostCreateBrowser(
284 pc.windowInfo,284 pc.windowInfo,
285 pc.client,285 pc.client,
M internal/infrastructure/cef/render_handler_adapter.go +6 -6
@@ -26,19 +26,19 @@ var _ purecef.RenderHandler = (*dumberRenderHandler)(nil)
2626 // startupPresentationHooks consumes the one-shot facts emitted by
2727 // purego-cef2gtk. The bridge owns the native DMABUF and frame-clock boundaries;
2828 // Dumber only records their ordered application-level timeline.
29-func startupPresentationHooks() cef2gtk.Hooks {
29+func startupPresentationHooks(trace *logging.CEFStartupTrace) cef2gtk.Hooks {
3030 return cef2gtk.Hooks{
3131 OnFirstAcceleratedPaint: func() {
32- logging.Trace().Mark("first_accelerated_paint_received")
32+ trace.Mark("first_accelerated_paint_received")
3333 },
3434 OnFirstDMABUFTextureSwap: func() {
35- logging.Trace().Mark("first_dmabuf_texture_swap")
35+ trace.Mark("first_dmabuf_texture_swap")
3636 },
3737 OnFirstPresentation: func() {
38- logging.Trace().MarkGTKAfterPaint()
38+ trace.MarkGTKAfterPaint()
3939 },
4040 OnDMABUFUnsupported: func() {
41- logging.Trace().SetIncompleteReason("dmabuf_texture_swap_unavailable")
41+ trace.SetIncompleteReason("dmabuf_texture_swap_unavailable")
4242 },
4343 }
4444 }
@@ -50,7 +50,7 @@ func newDumberRenderHandler(wv *WebView) purecef.RenderHandler {
5050 h := &dumberRenderHandler{wv: wv}
5151
5252 var unsupportedPaintOnce sync.Once
53- hooks := startupPresentationHooks()
53+ hooks := startupPresentationHooks(logging.CEFTrace())
5454 hooks.OnUnsupportedPaint = func() {
5555 unsupportedPaintOnce.Do(func() {
5656 if wv.ctx != nil {
@@ -26,19 +26,19 @@ var _ purecef.RenderHandler = (*dumberRenderHandler)(nil)
26 // startupPresentationHooks consumes the one-shot facts emitted by26 // startupPresentationHooks consumes the one-shot facts emitted by
27 // purego-cef2gtk. The bridge owns the native DMABUF and frame-clock boundaries;27 // purego-cef2gtk. The bridge owns the native DMABUF and frame-clock boundaries;
28 // Dumber only records their ordered application-level timeline.28 // Dumber only records their ordered application-level timeline.
29-func startupPresentationHooks() cef2gtk.Hooks {29+func startupPresentationHooks(trace *logging.CEFStartupTrace) cef2gtk.Hooks {
30 return cef2gtk.Hooks{30 return cef2gtk.Hooks{
31 OnFirstAcceleratedPaint: func() {31 OnFirstAcceleratedPaint: func() {
32- logging.Trace().Mark("first_accelerated_paint_received")32+ trace.Mark("first_accelerated_paint_received")
33 },33 },
34 OnFirstDMABUFTextureSwap: func() {34 OnFirstDMABUFTextureSwap: func() {
35- logging.Trace().Mark("first_dmabuf_texture_swap")35+ trace.Mark("first_dmabuf_texture_swap")
36 },36 },
37 OnFirstPresentation: func() {37 OnFirstPresentation: func() {
38- logging.Trace().MarkGTKAfterPaint()38+ trace.MarkGTKAfterPaint()
39 },39 },
40 OnDMABUFUnsupported: func() {40 OnDMABUFUnsupported: func() {
41- logging.Trace().SetIncompleteReason("dmabuf_texture_swap_unavailable")41+ trace.SetIncompleteReason("dmabuf_texture_swap_unavailable")
42 },42 },
43 }43 }
44 }44 }
@@ -50,7 +50,7 @@ func newDumberRenderHandler(wv *WebView) purecef.RenderHandler {
50 h := &dumberRenderHandler{wv: wv}50 h := &dumberRenderHandler{wv: wv}
51 51
52 var unsupportedPaintOnce sync.Once52 var unsupportedPaintOnce sync.Once
53- hooks := startupPresentationHooks()53+ hooks := startupPresentationHooks(logging.CEFTrace())
54 hooks.OnUnsupportedPaint = func() {54 hooks.OnUnsupportedPaint = func() {
55 unsupportedPaintOnce.Do(func() {55 unsupportedPaintOnce.Do(func() {
56 if wv.ctx != nil {56 if wv.ctx != nil {
M internal/infrastructure/cef/startup_presentation_hooks_test.go +64 -1
@@ -1,16 +1,79 @@
11 package cef
22
33 import (
4+ "bytes"
5+ "encoding/json"
6+ "strings"
47 "testing"
58
9+ "github.com/bnema/dumber/internal/logging"
10+ "github.com/rs/zerolog"
611 "github.com/stretchr/testify/require"
712 )
813
914 func TestStartupPresentationHooksWireEveryUpstreamFirstPresentationCallback(t *testing.T) {
10- hooks := startupPresentationHooks()
15+ var output bytes.Buffer
16+ trace := logging.NewCEFStartupTrace()
17+ trace.SetBackend("gdk-dmabuf")
18+ logger := zerolog.New(&output)
19+ trace.SetLogger(&logger)
20+ for _, milestone := range []string{
21+ "process_entry",
22+ "config_complete",
23+ "cef_library_load_begin",
24+ "cef_initialized",
25+ "browser_create_requested",
26+ } {
27+ require.Truef(t, trace.Mark(milestone), "seed milestone %q", milestone)
28+ }
1129
30+ hooks := startupPresentationHooks(trace)
1231 require.NotNil(t, hooks.OnFirstAcceleratedPaint)
1332 require.NotNil(t, hooks.OnFirstDMABUFTextureSwap)
1433 require.NotNil(t, hooks.OnFirstPresentation)
1534 require.NotNil(t, hooks.OnDMABUFUnsupported)
35+
36+ // The unsupported fact may arrive before the successful presentation path.
37+ // Each callback is invoked twice to make no-op, swapped, and duplicate bodies
38+ // observable through the exact trace output below.
39+ hooks.OnDMABUFUnsupported()
40+ hooks.OnDMABUFUnsupported()
41+ hooks.OnFirstAcceleratedPaint()
42+ hooks.OnFirstAcceleratedPaint()
43+ hooks.OnFirstDMABUFTextureSwap()
44+ hooks.OnFirstDMABUFTextureSwap()
45+ hooks.OnFirstPresentation()
46+ hooks.OnFirstPresentation()
47+
48+ var milestones []string
49+ summaryCount := 0
50+ var incompleteReason string
51+ for _, line := range strings.Split(strings.TrimSpace(output.String()), "\n") {
52+ var event struct {
53+ Message string `json:"message"`
54+ Milestone string `json:"milestone"`
55+ IncompleteReason string `json:"incomplete_reason"`
56+ }
57+ require.NoError(t, json.Unmarshal([]byte(line), &event))
58+ switch event.Message {
59+ case "startup_trace: milestone":
60+ milestones = append(milestones, event.Milestone)
61+ case "startup_trace: first presentation":
62+ summaryCount++
63+ incompleteReason = event.IncompleteReason
64+ }
65+ }
66+
67+ require.Equal(t, []string{
68+ "process_entry",
69+ "config_complete",
70+ "cef_library_load_begin",
71+ "cef_initialized",
72+ "browser_create_requested",
73+ "first_accelerated_paint_received",
74+ "first_dmabuf_texture_swap",
75+ "first_gtk_presentation",
76+ }, milestones)
77+ require.Equal(t, "dmabuf_texture_swap_unavailable", incompleteReason)
78+ require.Equal(t, 1, summaryCount)
1679 }
@@ -1,16 +1,79 @@
1 package cef1 package cef
2 2
3 import (3 import (
4+ "bytes"
5+ "encoding/json"
6+ "strings"
4 "testing"7 "testing"
5 8
9+ "github.com/bnema/dumber/internal/logging"
10+ "github.com/rs/zerolog"
6 "github.com/stretchr/testify/require"11 "github.com/stretchr/testify/require"
7 )12 )
8 13
9 func TestStartupPresentationHooksWireEveryUpstreamFirstPresentationCallback(t *testing.T) {14 func TestStartupPresentationHooksWireEveryUpstreamFirstPresentationCallback(t *testing.T) {
10- hooks := startupPresentationHooks()15+ var output bytes.Buffer
16+ trace := logging.NewCEFStartupTrace()
17+ trace.SetBackend("gdk-dmabuf")
18+ logger := zerolog.New(&output)
19+ trace.SetLogger(&logger)
20+ for _, milestone := range []string{
21+ "process_entry",
22+ "config_complete",
23+ "cef_library_load_begin",
24+ "cef_initialized",
25+ "browser_create_requested",
26+ } {
27+ require.Truef(t, trace.Mark(milestone), "seed milestone %q", milestone)
28+ }
11 29
30+ hooks := startupPresentationHooks(trace)
12 require.NotNil(t, hooks.OnFirstAcceleratedPaint)31 require.NotNil(t, hooks.OnFirstAcceleratedPaint)
13 require.NotNil(t, hooks.OnFirstDMABUFTextureSwap)32 require.NotNil(t, hooks.OnFirstDMABUFTextureSwap)
14 require.NotNil(t, hooks.OnFirstPresentation)33 require.NotNil(t, hooks.OnFirstPresentation)
15 require.NotNil(t, hooks.OnDMABUFUnsupported)34 require.NotNil(t, hooks.OnDMABUFUnsupported)
35+
36+ // The unsupported fact may arrive before the successful presentation path.
37+ // Each callback is invoked twice to make no-op, swapped, and duplicate bodies
38+ // observable through the exact trace output below.
39+ hooks.OnDMABUFUnsupported()
40+ hooks.OnDMABUFUnsupported()
41+ hooks.OnFirstAcceleratedPaint()
42+ hooks.OnFirstAcceleratedPaint()
43+ hooks.OnFirstDMABUFTextureSwap()
44+ hooks.OnFirstDMABUFTextureSwap()
45+ hooks.OnFirstPresentation()
46+ hooks.OnFirstPresentation()
47+
48+ var milestones []string
49+ summaryCount := 0
50+ var incompleteReason string
51+ for _, line := range strings.Split(strings.TrimSpace(output.String()), "\n") {
52+ var event struct {
53+ Message string `json:"message"`
54+ Milestone string `json:"milestone"`
55+ IncompleteReason string `json:"incomplete_reason"`
56+ }
57+ require.NoError(t, json.Unmarshal([]byte(line), &event))
58+ switch event.Message {
59+ case "startup_trace: milestone":
60+ milestones = append(milestones, event.Milestone)
61+ case "startup_trace: first presentation":
62+ summaryCount++
63+ incompleteReason = event.IncompleteReason
64+ }
65+ }
66+
67+ require.Equal(t, []string{
68+ "process_entry",
69+ "config_complete",
70+ "cef_library_load_begin",
71+ "cef_initialized",
72+ "browser_create_requested",
73+ "first_accelerated_paint_received",
74+ "first_dmabuf_texture_swap",
75+ "first_gtk_presentation",
76+ }, milestones)
77+ require.Equal(t, "dmabuf_texture_swap_unavailable", incompleteReason)
78+ require.Equal(t, 1, summaryCount)
16 }79 }
M internal/infrastructure/webkit/engine_init.go +0 -7
@@ -40,7 +40,6 @@ func NewEngine(
4040
4141 // --- Configure rendering environment (must be before GTK/WebKit init) ---
4242 engineConfigureRenderingEnvironment(ctx, cfg, wkCfg, &perfSettings, logger)
43- logging.Trace().Mark("render_env")
4443
4544 // --- Build webKitContextOptions from opts + wkCfg + perfSettings ---
4645 wkOpts := engineBuildContextOptions(opts, profile, wkCfg, &perfSettings)
@@ -53,7 +52,6 @@ func NewEngine(
5352 if err != nil {
5453 return nil, err
5554 }
56- logging.Trace().Mark("webkit_context")
5755
5856 // --- Filter manager ---
5957 filterManager := engineInitFilterManager(ctx, cfg, profile.Shared.DataDir, logger)
@@ -77,7 +75,6 @@ func NewEngine(
7775 }
7876
7977 messageRouter := NewMessageRouter(ctx)
80- logging.Trace().Mark("settings_manager")
8178
8279 // --- WebView pool ---
8380 poolCfg := DefaultPoolConfig()
@@ -100,7 +97,6 @@ func NewEngine(
10097 } else {
10198 logger.Warn().Msg("skipping first webview prewarm: no GDK display available yet")
10299 }
103- logging.Trace().Mark("pool_prewarm_first")
104100
105101 // --- WebView factory ---
106102 factory := NewWebViewFactory(wkCtx, settings, pool, injector, messageRouter)
@@ -147,7 +143,6 @@ func engineSurveyHardwareAndResolveProfile(
147143 ) config.ResolvedPerformanceSettings {
148144 hwSurveyor := env.NewHardwareSurveyor()
149145 hwInfo := hwSurveyor.Survey(ctx)
150- logging.Trace().Mark("hardware_survey")
151146 logger.Info().
152147 Int("cpu_cores", hwInfo.CPUCores).
153148 Int("cpu_threads", hwInfo.CPUThreads).
@@ -158,7 +153,6 @@ func engineSurveyHardwareAndResolveProfile(
158153
159154 perfCfg := config.PerformanceConfigFromEngine(&cfg.Engine)
160155 perfSettings := config.ResolvePerformanceProfile(&perfCfg, &hwInfo)
161- logging.Trace().Mark("performance_profile")
162156 logger.Info().
163157 Str("profile", string(cfg.Engine.Profile)).
164158 Int("skia_cpu_threads", perfSettings.SkiaCPUPaintingThreads).
@@ -284,7 +278,6 @@ func engineInitFilterManager(ctx context.Context, cfg *config.Config, dataDir st
284278 if err := filterManager.Initialize(ctx); err != nil {
285279 logger.Warn().Err(err).Msg("failed to initialize filters, will load async")
286280 }
287- logging.Trace().Mark("filter_manager")
288281 return filterManager
289282 }
290283
@@ -40,7 +40,6 @@ func NewEngine(
40 40
41 // --- Configure rendering environment (must be before GTK/WebKit init) ---41 // --- Configure rendering environment (must be before GTK/WebKit init) ---
42 engineConfigureRenderingEnvironment(ctx, cfg, wkCfg, &perfSettings, logger)42 engineConfigureRenderingEnvironment(ctx, cfg, wkCfg, &perfSettings, logger)
43- logging.Trace().Mark("render_env")
44 43
45 // --- Build webKitContextOptions from opts + wkCfg + perfSettings ---44 // --- Build webKitContextOptions from opts + wkCfg + perfSettings ---
46 wkOpts := engineBuildContextOptions(opts, profile, wkCfg, &perfSettings)45 wkOpts := engineBuildContextOptions(opts, profile, wkCfg, &perfSettings)
@@ -53,7 +52,6 @@ func NewEngine(
53 if err != nil {52 if err != nil {
54 return nil, err53 return nil, err
55 }54 }
56- logging.Trace().Mark("webkit_context")
57 55
58 // --- Filter manager ---56 // --- Filter manager ---
59 filterManager := engineInitFilterManager(ctx, cfg, profile.Shared.DataDir, logger)57 filterManager := engineInitFilterManager(ctx, cfg, profile.Shared.DataDir, logger)
@@ -77,7 +75,6 @@ func NewEngine(
77 }75 }
78 76
79 messageRouter := NewMessageRouter(ctx)77 messageRouter := NewMessageRouter(ctx)
80- logging.Trace().Mark("settings_manager")
81 78
82 // --- WebView pool ---79 // --- WebView pool ---
83 poolCfg := DefaultPoolConfig()80 poolCfg := DefaultPoolConfig()
@@ -100,7 +97,6 @@ func NewEngine(
100 } else {97 } else {
101 logger.Warn().Msg("skipping first webview prewarm: no GDK display available yet")98 logger.Warn().Msg("skipping first webview prewarm: no GDK display available yet")
102 }99 }
103- logging.Trace().Mark("pool_prewarm_first")
104 100
105 // --- WebView factory ---101 // --- WebView factory ---
106 factory := NewWebViewFactory(wkCtx, settings, pool, injector, messageRouter)102 factory := NewWebViewFactory(wkCtx, settings, pool, injector, messageRouter)
@@ -147,7 +143,6 @@ func engineSurveyHardwareAndResolveProfile(
147 ) config.ResolvedPerformanceSettings {143 ) config.ResolvedPerformanceSettings {
148 hwSurveyor := env.NewHardwareSurveyor()144 hwSurveyor := env.NewHardwareSurveyor()
149 hwInfo := hwSurveyor.Survey(ctx)145 hwInfo := hwSurveyor.Survey(ctx)
150- logging.Trace().Mark("hardware_survey")
151 logger.Info().146 logger.Info().
152 Int("cpu_cores", hwInfo.CPUCores).147 Int("cpu_cores", hwInfo.CPUCores).
153 Int("cpu_threads", hwInfo.CPUThreads).148 Int("cpu_threads", hwInfo.CPUThreads).
@@ -158,7 +153,6 @@ func engineSurveyHardwareAndResolveProfile(
158 153
159 perfCfg := config.PerformanceConfigFromEngine(&cfg.Engine)154 perfCfg := config.PerformanceConfigFromEngine(&cfg.Engine)
160 perfSettings := config.ResolvePerformanceProfile(&perfCfg, &hwInfo)155 perfSettings := config.ResolvePerformanceProfile(&perfCfg, &hwInfo)
161- logging.Trace().Mark("performance_profile")
162 logger.Info().156 logger.Info().
163 Str("profile", string(cfg.Engine.Profile)).157 Str("profile", string(cfg.Engine.Profile)).
164 Int("skia_cpu_threads", perfSettings.SkiaCPUPaintingThreads).158 Int("skia_cpu_threads", perfSettings.SkiaCPUPaintingThreads).
@@ -284,7 +278,6 @@ func engineInitFilterManager(ctx context.Context, cfg *config.Config, dataDir st
284 if err := filterManager.Initialize(ctx); err != nil {278 if err := filterManager.Initialize(ctx); err != nil {
285 logger.Warn().Err(err).Msg("failed to initialize filters, will load async")279 logger.Warn().Err(err).Msg("failed to initialize filters, will load async")
286 }280 }
287- logging.Trace().Mark("filter_manager")
288 return filterManager281 return filterManager
289 }282 }
290 283
M internal/logging/startup_trace.go +52 -57
@@ -7,30 +7,30 @@ import (
77 "github.com/rs/zerolog"
88 )
99
10-// StartupTrace records the single truthful path from process entry to the first
11-// GTK presentation. It deliberately rejects unknown, duplicate, and out of
12-// order milestones: accepting a convenient timestamp would make the result
13-// unsuitable for cold-start measurement.
14-type StartupTrace struct {
10+// CEFStartupTrace records the accelerated CEF/DMABUF/GTK path from process
11+// entry to the first GTK presentation. It deliberately rejects unknown,
12+// duplicate, and out-of-order milestones. Accepting a convenient timestamp
13+// would make the result unsuitable for cold-start measurement.
14+type CEFStartupTrace struct {
1515 mu sync.Mutex
1616 now func() time.Time
1717 t0 time.Time
18- milestones []Milestone
18+ milestones []CEFStartupMilestone
1919 logger *zerolog.Logger
20- buffered []Milestone
20+ buffered []CEFStartupMilestone
2121 backend string
2222 incompleteReason string
2323 summaryEmitted bool
2424 }
2525
26-// Milestone represents a timing checkpoint measured from process entry.
27-type Milestone struct {
26+// CEFStartupMilestone represents a timing checkpoint from process entry.
27+type CEFStartupMilestone struct {
2828 Name string
2929 Elapsed time.Duration
3030 Delta time.Duration
3131 }
3232
33-var startupMilestoneOrder = []string{
33+var cefStartupMilestoneOrder = []string{
3434 "process_entry",
3535 "config_complete",
3636 "cef_library_load_begin",
@@ -42,46 +42,49 @@ var startupMilestoneOrder = []string{
4242 }
4343
4444 var (
45- globalTrace *StartupTrace
46- globalTraceMu sync.Mutex
47- globalTraceOnce sync.Once
45+ globalCEFTrace *CEFStartupTrace
46+ globalCEFTraceMu sync.Mutex
47+ globalCEFTraceOnce sync.Once
4848 )
4949
50-func newStartupTrace(now func() time.Time) *StartupTrace {
50+func newCEFStartupTrace(now func() time.Time) *CEFStartupTrace {
5151 start := now()
52- return &StartupTrace{
52+ return &CEFStartupTrace{
5353 now: now,
5454 t0: start,
55- milestones: make([]Milestone, 0, len(startupMilestoneOrder)),
56- buffered: make([]Milestone, 0, len(startupMilestoneOrder)),
55+ milestones: make([]CEFStartupMilestone, 0, len(cefStartupMilestoneOrder)),
56+ buffered: make([]CEFStartupMilestone, 0, len(cefStartupMilestoneOrder)),
5757 }
5858 }
5959
60-// InitStartupTrace must be called at the first instruction in main. logLevel is
61-// retained for source compatibility; this measurement is always collected so
62-// its final structured summary is available at normal log level.
63-func InitStartupTrace(_ string) {
64- globalTraceOnce.Do(func() {
65- trace := newStartupTrace(time.Now)
66- globalTraceMu.Lock()
67- globalTrace = trace
68- globalTraceMu.Unlock()
60+// NewCEFStartupTrace creates an isolated accelerated CEF/DMABUF/GTK trace.
61+// Production startup uses InitCEFStartupTrace and CEFTrace.
62+func NewCEFStartupTrace() *CEFStartupTrace { return newCEFStartupTrace(time.Now) }
63+
64+// InitCEFStartupTrace begins the CEF/DMABUF/GTK measurement at process entry.
65+// It is intentionally safe to call again during GUI bootstrap.
66+func InitCEFStartupTrace() {
67+ globalCEFTraceOnce.Do(func() {
68+ trace := newCEFStartupTrace(time.Now)
69+ globalCEFTraceMu.Lock()
70+ globalCEFTrace = trace
71+ globalCEFTraceMu.Unlock()
6972 })
7073 }
7174
72-// Trace returns the process startup trace. Callers before initialization get a
73-// harmless trace which rejects all transitions.
74-func Trace() *StartupTrace {
75- globalTraceMu.Lock()
76- defer globalTraceMu.Unlock()
77- if globalTrace == nil {
78- return &StartupTrace{}
75+// CEFTrace returns the accelerated CEF/DMABUF/GTK trace. Callers before
76+// initialization get a harmless trace which rejects all transitions.
77+func CEFTrace() *CEFStartupTrace {
78+ globalCEFTraceMu.Lock()
79+ defer globalCEFTraceMu.Unlock()
80+ if globalCEFTrace == nil {
81+ return &CEFStartupTrace{}
7982 }
80- return globalTrace
83+ return globalCEFTrace
8184 }
8285
8386 // SetLogger makes buffered milestones available to the process logger.
84-func (st *StartupTrace) SetLogger(logger *zerolog.Logger) {
87+func (st *CEFStartupTrace) SetLogger(logger *zerolog.Logger) {
8588 if st == nil || logger == nil {
8689 return
8790 }
@@ -89,18 +92,14 @@ func (st *StartupTrace) SetLogger(logger *zerolog.Logger) {
8992 defer st.mu.Unlock()
9093 st.logger = logger
9194 for _, milestone := range st.buffered {
92- st.emitMilestone(milestone)
95+ st.emitCEFStartupMilestone(milestone)
9396 }
9497 st.buffered = nil
9598 st.emitSummaryLocked()
9699 }
97100
98-// UpdateLogger switches the destination without replaying events. Replaying
99-// would violate the one-shot property in session logs.
100-func (st *StartupTrace) UpdateLogger(logger *zerolog.Logger) { st.SetLogger(logger) }
101-
102101 // SetBackend records the selected presentation backend for the final summary.
103-func (st *StartupTrace) SetBackend(backend string) {
102+func (st *CEFStartupTrace) SetBackend(backend string) {
104103 if st == nil {
105104 return
106105 }
@@ -110,7 +109,7 @@ func (st *StartupTrace) SetBackend(backend string) {
110109 }
111110
112111 // SetIncompleteReason records why a trace cannot reach a valid DMABUF result.
113-func (st *StartupTrace) SetIncompleteReason(reason string) {
112+func (st *CEFStartupTrace) SetIncompleteReason(reason string) {
114113 if st == nil {
115114 return
116115 }
@@ -124,7 +123,7 @@ func (st *StartupTrace) SetIncompleteReason(reason string) {
124123 // Mark accepts exactly the next non-presentation startup milestone and returns
125124 // whether it was recorded. first_gtk_presentation is reserved for the GTK
126125 // frame-clock after-paint hook through MarkGTKAfterPaint.
127-func (st *StartupTrace) Mark(name string) bool {
126+func (st *CEFStartupTrace) Mark(name string) bool {
128127 if name == "first_gtk_presentation" {
129128 return false
130129 }
@@ -133,24 +132,24 @@ func (st *StartupTrace) Mark(name string) bool {
133132
134133 // MarkGTKAfterPaint records the final milestone. It is called only from the
135134 // upstream CEF-to-GTK bridge's first frame-clock after-paint callback.
136-func (st *StartupTrace) MarkGTKAfterPaint() bool {
135+func (st *CEFStartupTrace) MarkGTKAfterPaint() bool {
137136 return st.mark("first_gtk_presentation")
138137 }
139138
140139 // mark accepts the next milestone and is safe for callbacks from CEF and GTK
141140 // threads.
142-func (st *StartupTrace) mark(name string) bool {
141+func (st *CEFStartupTrace) mark(name string) bool {
143142 if st == nil || st.now == nil {
144143 return false
145144 }
146145 st.mu.Lock()
147146 defer st.mu.Unlock()
148- if len(st.milestones) >= len(startupMilestoneOrder) || name != startupMilestoneOrder[len(st.milestones)] {
147+ if len(st.milestones) >= len(cefStartupMilestoneOrder) || name != cefStartupMilestoneOrder[len(st.milestones)] {
149148 return false
150149 }
151150
152151 now := st.now()
153- milestone := Milestone{Name: name, Elapsed: now.Sub(st.t0)}
152+ milestone := CEFStartupMilestone{Name: name, Elapsed: now.Sub(st.t0)}
154153 // Published fields are integer milliseconds. Compute delta on that same
155154 // scale so delta_ms always exactly reconstructs t_ms in collected evidence.
156155 previousMilliseconds := int64(0)
@@ -162,13 +161,13 @@ func (st *StartupTrace) mark(name string) bool {
162161 if st.logger == nil {
163162 st.buffered = append(st.buffered, milestone)
164163 } else {
165- st.emitMilestone(milestone)
164+ st.emitCEFStartupMilestone(milestone)
166165 }
167166 st.emitSummaryLocked()
168167 return true
169168 }
170169
171-func (st *StartupTrace) emitMilestone(m Milestone) {
170+func (st *CEFStartupTrace) emitCEFStartupMilestone(m CEFStartupMilestone) {
172171 if st.logger == nil {
173172 return
174173 }
@@ -179,8 +178,8 @@ func (st *StartupTrace) emitMilestone(m Milestone) {
179178 Msg("startup_trace: milestone")
180179 }
181180
182-func (st *StartupTrace) emitSummaryLocked() {
183- if st.summaryEmitted || st.logger == nil || len(st.milestones) != len(startupMilestoneOrder) {
181+func (st *CEFStartupTrace) emitSummaryLocked() {
182+ if st.summaryEmitted || st.logger == nil || len(st.milestones) != len(cefStartupMilestoneOrder) {
184183 return
185184 }
186185 st.summaryEmitted = true
@@ -192,13 +191,9 @@ func (st *StartupTrace) emitSummaryLocked() {
192191 Msg("startup_trace: first presentation")
193192 }
194193
195-// Finish is retained as a harmless compatibility no-op. The only valid source
196-// for first_gtk_presentation is the upstream GTK frame-clock after-paint hook.
197-func (*StartupTrace) Finish() {}
198-
199-func (st *StartupTrace) Enabled() bool { return st != nil && st.now != nil }
194+func (st *CEFStartupTrace) Enabled() bool { return st != nil && st.now != nil }
200195
201-func (st *StartupTrace) TotalElapsed() time.Duration {
196+func (st *CEFStartupTrace) TotalElapsed() time.Duration {
202197 if st == nil || st.now == nil {
203198 return 0
204199 }
@@ -7,30 +7,30 @@ import (
7 "github.com/rs/zerolog"7 "github.com/rs/zerolog"
8 )8 )
9 9
10-// StartupTrace records the single truthful path from process entry to the first10+// CEFStartupTrace records the accelerated CEF/DMABUF/GTK path from process
11-// GTK presentation. It deliberately rejects unknown, duplicate, and out of11+// entry to the first GTK presentation. It deliberately rejects unknown,
12-// order milestones: accepting a convenient timestamp would make the result12+// duplicate, and out-of-order milestones. Accepting a convenient timestamp
13-// unsuitable for cold-start measurement.13+// would make the result unsuitable for cold-start measurement.
14-type StartupTrace struct {14+type CEFStartupTrace struct {
15 mu sync.Mutex15 mu sync.Mutex
16 now func() time.Time16 now func() time.Time
17 t0 time.Time17 t0 time.Time
18- milestones []Milestone18+ milestones []CEFStartupMilestone
19 logger *zerolog.Logger19 logger *zerolog.Logger
20- buffered []Milestone20+ buffered []CEFStartupMilestone
21 backend string21 backend string
22 incompleteReason string22 incompleteReason string
23 summaryEmitted bool23 summaryEmitted bool
24 }24 }
25 25
26-// Milestone represents a timing checkpoint measured from process entry.26+// CEFStartupMilestone represents a timing checkpoint from process entry.
27-type Milestone struct {27+type CEFStartupMilestone struct {
28 Name string28 Name string
29 Elapsed time.Duration29 Elapsed time.Duration
30 Delta time.Duration30 Delta time.Duration
31 }31 }
32 32
33-var startupMilestoneOrder = []string{33+var cefStartupMilestoneOrder = []string{
34 "process_entry",34 "process_entry",
35 "config_complete",35 "config_complete",
36 "cef_library_load_begin",36 "cef_library_load_begin",
@@ -42,46 +42,49 @@ var startupMilestoneOrder = []string{
42 }42 }
43 43
44 var (44 var (
45- globalTrace *StartupTrace45+ globalCEFTrace *CEFStartupTrace
46- globalTraceMu sync.Mutex46+ globalCEFTraceMu sync.Mutex
47- globalTraceOnce sync.Once47+ globalCEFTraceOnce sync.Once
48 )48 )
49 49
50-func newStartupTrace(now func() time.Time) *StartupTrace {50+func newCEFStartupTrace(now func() time.Time) *CEFStartupTrace {
51 start := now()51 start := now()
52- return &StartupTrace{52+ return &CEFStartupTrace{
53 now: now,53 now: now,
54 t0: start,54 t0: start,
55- milestones: make([]Milestone, 0, len(startupMilestoneOrder)),55+ milestones: make([]CEFStartupMilestone, 0, len(cefStartupMilestoneOrder)),
56- buffered: make([]Milestone, 0, len(startupMilestoneOrder)),56+ buffered: make([]CEFStartupMilestone, 0, len(cefStartupMilestoneOrder)),
57 }57 }
58 }58 }
59 59
60-// InitStartupTrace must be called at the first instruction in main. logLevel is60+// NewCEFStartupTrace creates an isolated accelerated CEF/DMABUF/GTK trace.
61-// retained for source compatibility; this measurement is always collected so61+// Production startup uses InitCEFStartupTrace and CEFTrace.
62-// its final structured summary is available at normal log level.62+func NewCEFStartupTrace() *CEFStartupTrace { return newCEFStartupTrace(time.Now) }
63-func InitStartupTrace(_ string) {63+
64- globalTraceOnce.Do(func() {64+// InitCEFStartupTrace begins the CEF/DMABUF/GTK measurement at process entry.
65- trace := newStartupTrace(time.Now)65+// It is intentionally safe to call again during GUI bootstrap.
66- globalTraceMu.Lock()66+func InitCEFStartupTrace() {
67- globalTrace = trace67+ globalCEFTraceOnce.Do(func() {
68- globalTraceMu.Unlock()68+ trace := newCEFStartupTrace(time.Now)
69+ globalCEFTraceMu.Lock()
70+ globalCEFTrace = trace
71+ globalCEFTraceMu.Unlock()
69 })72 })
70 }73 }
71 74
72-// Trace returns the process startup trace. Callers before initialization get a75+// CEFTrace returns the accelerated CEF/DMABUF/GTK trace. Callers before
73-// harmless trace which rejects all transitions.76+// initialization get a harmless trace which rejects all transitions.
74-func Trace() *StartupTrace {77+func CEFTrace() *CEFStartupTrace {
75- globalTraceMu.Lock()78+ globalCEFTraceMu.Lock()
76- defer globalTraceMu.Unlock()79+ defer globalCEFTraceMu.Unlock()
77- if globalTrace == nil {80+ if globalCEFTrace == nil {
78- return &StartupTrace{}81+ return &CEFStartupTrace{}
79 }82 }
80- return globalTrace83+ return globalCEFTrace
81 }84 }
82 85
83 // SetLogger makes buffered milestones available to the process logger.86 // SetLogger makes buffered milestones available to the process logger.
84-func (st *StartupTrace) SetLogger(logger *zerolog.Logger) {87+func (st *CEFStartupTrace) SetLogger(logger *zerolog.Logger) {
85 if st == nil || logger == nil {88 if st == nil || logger == nil {
86 return89 return
87 }90 }
@@ -89,18 +92,14 @@ func (st *StartupTrace) SetLogger(logger *zerolog.Logger) {
89 defer st.mu.Unlock()92 defer st.mu.Unlock()
90 st.logger = logger93 st.logger = logger
91 for _, milestone := range st.buffered {94 for _, milestone := range st.buffered {
92- st.emitMilestone(milestone)95+ st.emitCEFStartupMilestone(milestone)
93 }96 }
94 st.buffered = nil97 st.buffered = nil
95 st.emitSummaryLocked()98 st.emitSummaryLocked()
96 }99 }
97 100
98-// UpdateLogger switches the destination without replaying events. Replaying
99-// would violate the one-shot property in session logs.
100-func (st *StartupTrace) UpdateLogger(logger *zerolog.Logger) { st.SetLogger(logger) }
101-
102 // SetBackend records the selected presentation backend for the final summary.101 // SetBackend records the selected presentation backend for the final summary.
103-func (st *StartupTrace) SetBackend(backend string) {102+func (st *CEFStartupTrace) SetBackend(backend string) {
104 if st == nil {103 if st == nil {
105 return104 return
106 }105 }
@@ -110,7 +109,7 @@ func (st *StartupTrace) SetBackend(backend string) {
110 }109 }
111 110
112 // SetIncompleteReason records why a trace cannot reach a valid DMABUF result.111 // SetIncompleteReason records why a trace cannot reach a valid DMABUF result.
113-func (st *StartupTrace) SetIncompleteReason(reason string) {112+func (st *CEFStartupTrace) SetIncompleteReason(reason string) {
114 if st == nil {113 if st == nil {
115 return114 return
116 }115 }
@@ -124,7 +123,7 @@ func (st *StartupTrace) SetIncompleteReason(reason string) {
124 // Mark accepts exactly the next non-presentation startup milestone and returns123 // Mark accepts exactly the next non-presentation startup milestone and returns
125 // whether it was recorded. first_gtk_presentation is reserved for the GTK124 // whether it was recorded. first_gtk_presentation is reserved for the GTK
126 // frame-clock after-paint hook through MarkGTKAfterPaint.125 // frame-clock after-paint hook through MarkGTKAfterPaint.
127-func (st *StartupTrace) Mark(name string) bool {126+func (st *CEFStartupTrace) Mark(name string) bool {
128 if name == "first_gtk_presentation" {127 if name == "first_gtk_presentation" {
129 return false128 return false
130 }129 }
@@ -133,24 +132,24 @@ func (st *StartupTrace) Mark(name string) bool {
133 132
134 // MarkGTKAfterPaint records the final milestone. It is called only from the133 // MarkGTKAfterPaint records the final milestone. It is called only from the
135 // upstream CEF-to-GTK bridge's first frame-clock after-paint callback.134 // upstream CEF-to-GTK bridge's first frame-clock after-paint callback.
136-func (st *StartupTrace) MarkGTKAfterPaint() bool {135+func (st *CEFStartupTrace) MarkGTKAfterPaint() bool {
137 return st.mark("first_gtk_presentation")136 return st.mark("first_gtk_presentation")
138 }137 }
139 138
140 // mark accepts the next milestone and is safe for callbacks from CEF and GTK139 // mark accepts the next milestone and is safe for callbacks from CEF and GTK
141 // threads.140 // threads.
142-func (st *StartupTrace) mark(name string) bool {141+func (st *CEFStartupTrace) mark(name string) bool {
143 if st == nil || st.now == nil {142 if st == nil || st.now == nil {
144 return false143 return false
145 }144 }
146 st.mu.Lock()145 st.mu.Lock()
147 defer st.mu.Unlock()146 defer st.mu.Unlock()
148- if len(st.milestones) >= len(startupMilestoneOrder) || name != startupMilestoneOrder[len(st.milestones)] {147+ if len(st.milestones) >= len(cefStartupMilestoneOrder) || name != cefStartupMilestoneOrder[len(st.milestones)] {
149 return false148 return false
150 }149 }
151 150
152 now := st.now()151 now := st.now()
153- milestone := Milestone{Name: name, Elapsed: now.Sub(st.t0)}152+ milestone := CEFStartupMilestone{Name: name, Elapsed: now.Sub(st.t0)}
154 // Published fields are integer milliseconds. Compute delta on that same153 // Published fields are integer milliseconds. Compute delta on that same
155 // scale so delta_ms always exactly reconstructs t_ms in collected evidence.154 // scale so delta_ms always exactly reconstructs t_ms in collected evidence.
156 previousMilliseconds := int64(0)155 previousMilliseconds := int64(0)
@@ -162,13 +161,13 @@ func (st *StartupTrace) mark(name string) bool {
162 if st.logger == nil {161 if st.logger == nil {
163 st.buffered = append(st.buffered, milestone)162 st.buffered = append(st.buffered, milestone)
164 } else {163 } else {
165- st.emitMilestone(milestone)164+ st.emitCEFStartupMilestone(milestone)
166 }165 }
167 st.emitSummaryLocked()166 st.emitSummaryLocked()
168 return true167 return true
169 }168 }
170 169
171-func (st *StartupTrace) emitMilestone(m Milestone) {170+func (st *CEFStartupTrace) emitCEFStartupMilestone(m CEFStartupMilestone) {
172 if st.logger == nil {171 if st.logger == nil {
173 return172 return
174 }173 }
@@ -179,8 +178,8 @@ func (st *StartupTrace) emitMilestone(m Milestone) {
179 Msg("startup_trace: milestone")178 Msg("startup_trace: milestone")
180 }179 }
181 180
182-func (st *StartupTrace) emitSummaryLocked() {181+func (st *CEFStartupTrace) emitSummaryLocked() {
183- if st.summaryEmitted || st.logger == nil || len(st.milestones) != len(startupMilestoneOrder) {182+ if st.summaryEmitted || st.logger == nil || len(st.milestones) != len(cefStartupMilestoneOrder) {
184 return183 return
185 }184 }
186 st.summaryEmitted = true185 st.summaryEmitted = true
@@ -192,13 +191,9 @@ func (st *StartupTrace) emitSummaryLocked() {
192 Msg("startup_trace: first presentation")191 Msg("startup_trace: first presentation")
193 }192 }
194 193
195-// Finish is retained as a harmless compatibility no-op. The only valid source194+func (st *CEFStartupTrace) Enabled() bool { return st != nil && st.now != nil }
196-// for first_gtk_presentation is the upstream GTK frame-clock after-paint hook.
197-func (*StartupTrace) Finish() {}
198-
199-func (st *StartupTrace) Enabled() bool { return st != nil && st.now != nil }
200 195
201-func (st *StartupTrace) TotalElapsed() time.Duration {196+func (st *CEFStartupTrace) TotalElapsed() time.Duration {
202 if st == nil || st.now == nil {197 if st == nil || st.now == nil {
203 return 0198 return 0
204 }199 }
M internal/logging/startup_trace_test.go +12 -13
@@ -9,9 +9,9 @@ import (
99 "github.com/stretchr/testify/require"
1010 )
1111
12-func TestStartupTraceAcceptsOnlyOrderedOneShotMilestones(t *testing.T) {
12+func TestCEFStartupTraceAcceptsOnlyOrderedOneShotMilestones(t *testing.T) {
1313 now := time.Unix(100, 0)
14- trace := newStartupTrace(func() time.Time { return now })
14+ trace := newCEFStartupTrace(func() time.Time { return now })
1515
1616 require.True(t, trace.Mark("process_entry"))
1717 require.False(t, trace.Mark("cef_initialized"), "out-of-order transition must be rejected")
@@ -36,9 +36,9 @@ func TestStartupTraceAcceptsOnlyOrderedOneShotMilestones(t *testing.T) {
3636 require.False(t, trace.Mark("first_gtk_presentation"))
3737 }
3838
39-func TestStartupTraceQuantizesDeltaToPublishedMilliseconds(t *testing.T) {
39+func TestCEFStartupTraceQuantizesDeltaToPublishedMilliseconds(t *testing.T) {
4040 now := time.Unix(100, 0)
41- trace := newStartupTrace(func() time.Time { return now })
41+ trace := newCEFStartupTrace(func() time.Time { return now })
4242
4343 now = now.Add(1500 * time.Microsecond)
4444 require.True(t, trace.Mark("process_entry"))
@@ -51,31 +51,30 @@ func TestStartupTraceQuantizesDeltaToPublishedMilliseconds(t *testing.T) {
5151 require.Equal(t, int64(1), trace.milestones[1].Delta.Milliseconds())
5252 }
5353
54-func TestStartupTraceFinishCannotFabricateFirstGTKPresentation(t *testing.T) {
54+func TestCEFStartupTraceReservesFirstGTKPresentationForAfterPaint(t *testing.T) {
5555 now := time.Unix(100, 0)
56- trace := newStartupTrace(func() time.Time { return now })
56+ trace := newCEFStartupTrace(func() time.Time { return now })
5757
58- for _, name := range startupMilestoneOrder[:len(startupMilestoneOrder)-1] {
58+ for _, name := range cefStartupMilestoneOrder[:len(cefStartupMilestoneOrder)-1] {
5959 now = now.Add(time.Millisecond)
6060 require.True(t, trace.Mark(name))
6161 }
6262
63- trace.Finish() // Legacy callers do not observe the upstream GTK after-paint.
64- require.Len(t, trace.milestones, len(startupMilestoneOrder)-1)
65- require.False(t, trace.Mark("first_gtk_presentation"), "generic callers cannot record the reserved milestone")
63+ require.Len(t, trace.milestones, len(cefStartupMilestoneOrder)-1)
64+ require.False(t, trace.Mark("first_gtk_presentation"), "only the CEF-to-GTK after-paint hook can record the reserved milestone")
6665 require.False(t, trace.summaryEmitted)
6766 require.True(t, trace.MarkGTKAfterPaint(), "only the after-paint hook may record this milestone")
6867 }
6968
70-func TestStartupTraceEmitsOneNormalSummaryAtFirstPresentation(t *testing.T) {
69+func TestCEFStartupTraceEmitsOneNormalSummaryAtFirstPresentation(t *testing.T) {
7170 var output bytes.Buffer
7271 logger := zerolog.New(&output)
7372 now := time.Unix(100, 0)
74- trace := newStartupTrace(func() time.Time { return now })
73+ trace := newCEFStartupTrace(func() time.Time { return now })
7574 trace.SetBackend("gdk-dmabuf")
7675 trace.SetLogger(&logger)
7776
78- for _, name := range startupMilestoneOrder[:len(startupMilestoneOrder)-1] {
77+ for _, name := range cefStartupMilestoneOrder[:len(cefStartupMilestoneOrder)-1] {
7978 now = now.Add(time.Millisecond)
8079 require.True(t, trace.Mark(name))
8180 }
@@ -9,9 +9,9 @@ import (
9 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
10 )10 )
11 11
12-func TestStartupTraceAcceptsOnlyOrderedOneShotMilestones(t *testing.T) {12+func TestCEFStartupTraceAcceptsOnlyOrderedOneShotMilestones(t *testing.T) {
13 now := time.Unix(100, 0)13 now := time.Unix(100, 0)
14- trace := newStartupTrace(func() time.Time { return now })14+ trace := newCEFStartupTrace(func() time.Time { return now })
15 15
16 require.True(t, trace.Mark("process_entry"))16 require.True(t, trace.Mark("process_entry"))
17 require.False(t, trace.Mark("cef_initialized"), "out-of-order transition must be rejected")17 require.False(t, trace.Mark("cef_initialized"), "out-of-order transition must be rejected")
@@ -36,9 +36,9 @@ func TestStartupTraceAcceptsOnlyOrderedOneShotMilestones(t *testing.T) {
36 require.False(t, trace.Mark("first_gtk_presentation"))36 require.False(t, trace.Mark("first_gtk_presentation"))
37 }37 }
38 38
39-func TestStartupTraceQuantizesDeltaToPublishedMilliseconds(t *testing.T) {39+func TestCEFStartupTraceQuantizesDeltaToPublishedMilliseconds(t *testing.T) {
40 now := time.Unix(100, 0)40 now := time.Unix(100, 0)
41- trace := newStartupTrace(func() time.Time { return now })41+ trace := newCEFStartupTrace(func() time.Time { return now })
42 42
43 now = now.Add(1500 * time.Microsecond)43 now = now.Add(1500 * time.Microsecond)
44 require.True(t, trace.Mark("process_entry"))44 require.True(t, trace.Mark("process_entry"))
@@ -51,31 +51,30 @@ func TestStartupTraceQuantizesDeltaToPublishedMilliseconds(t *testing.T) {
51 require.Equal(t, int64(1), trace.milestones[1].Delta.Milliseconds())51 require.Equal(t, int64(1), trace.milestones[1].Delta.Milliseconds())
52 }52 }
53 53
54-func TestStartupTraceFinishCannotFabricateFirstGTKPresentation(t *testing.T) {54+func TestCEFStartupTraceReservesFirstGTKPresentationForAfterPaint(t *testing.T) {
55 now := time.Unix(100, 0)55 now := time.Unix(100, 0)
56- trace := newStartupTrace(func() time.Time { return now })56+ trace := newCEFStartupTrace(func() time.Time { return now })
57 57
58- for _, name := range startupMilestoneOrder[:len(startupMilestoneOrder)-1] {58+ for _, name := range cefStartupMilestoneOrder[:len(cefStartupMilestoneOrder)-1] {
59 now = now.Add(time.Millisecond)59 now = now.Add(time.Millisecond)
60 require.True(t, trace.Mark(name))60 require.True(t, trace.Mark(name))
61 }61 }
62 62
63- trace.Finish() // Legacy callers do not observe the upstream GTK after-paint.63+ require.Len(t, trace.milestones, len(cefStartupMilestoneOrder)-1)
64- require.Len(t, trace.milestones, len(startupMilestoneOrder)-1)64+ require.False(t, trace.Mark("first_gtk_presentation"), "only the CEF-to-GTK after-paint hook can record the reserved milestone")
65- require.False(t, trace.Mark("first_gtk_presentation"), "generic callers cannot record the reserved milestone")
66 require.False(t, trace.summaryEmitted)65 require.False(t, trace.summaryEmitted)
67 require.True(t, trace.MarkGTKAfterPaint(), "only the after-paint hook may record this milestone")66 require.True(t, trace.MarkGTKAfterPaint(), "only the after-paint hook may record this milestone")
68 }67 }
69 68
70-func TestStartupTraceEmitsOneNormalSummaryAtFirstPresentation(t *testing.T) {69+func TestCEFStartupTraceEmitsOneNormalSummaryAtFirstPresentation(t *testing.T) {
71 var output bytes.Buffer70 var output bytes.Buffer
72 logger := zerolog.New(&output)71 logger := zerolog.New(&output)
73 now := time.Unix(100, 0)72 now := time.Unix(100, 0)
74- trace := newStartupTrace(func() time.Time { return now })73+ trace := newCEFStartupTrace(func() time.Time { return now })
75 trace.SetBackend("gdk-dmabuf")74 trace.SetBackend("gdk-dmabuf")
76 trace.SetLogger(&logger)75 trace.SetLogger(&logger)
77 76
78- for _, name := range startupMilestoneOrder[:len(startupMilestoneOrder)-1] {77+ for _, name := range cefStartupMilestoneOrder[:len(cefStartupMilestoneOrder)-1] {
79 now = now.Add(time.Millisecond)78 now = now.Add(time.Millisecond)
80 require.True(t, trace.Mark(name))79 require.True(t, trace.Mark(name))
81 }80 }
M internal/ui/app.go +0 -4
@@ -295,7 +295,6 @@ func (a *App) Run(ctx context.Context, args []string) int {
295295 // Initialize libadwaita once (required before using StyleManager).
296296 // This also initializes GTK implicitly.
297297 EnsureAdwaitaInitialized()
298- logging.Trace().Mark("gtk_init")
299298
300299 // Mark adwaita detector as available now that adw.Init() is complete.
301300 // This enables the highest-priority color scheme detector.
@@ -317,7 +316,6 @@ func (a *App) Run(ctx context.Context, args []string) int {
317316 return 1
318317 }
319318 defer a.gtkApp.Unref()
320- logging.Trace().Mark("gtk_app_created")
321319 a.dispatchOnMainThread = a.runOnMainThread
322320
323321 // Connect activate signal
@@ -353,7 +351,6 @@ func (a *App) onActivate(ctx context.Context) {
353351 log.Error().Err(err).Msg("failed to create main window")
354352 return
355353 }
356- logging.Trace().Mark("window_created")
357354
358355 a.installCrashReportNotifier(ctx)
359356 a.initFocusManager()
@@ -362,7 +359,6 @@ func (a *App) onActivate(ctx context.Context) {
362359
363360 a.initCoordinators(ctx)
364361 a.wireWebRTCPermissionIndicator()
365- logging.Trace().Mark("coordinators_init")
366362 a.initKeyboardHandler(ctx)
367363 a.initOmniboxConfig(ctx)
368364 a.initFindBarConfig(ctx)
@@ -295,7 +295,6 @@ func (a *App) Run(ctx context.Context, args []string) int {
295 // Initialize libadwaita once (required before using StyleManager).295 // Initialize libadwaita once (required before using StyleManager).
296 // This also initializes GTK implicitly.296 // This also initializes GTK implicitly.
297 EnsureAdwaitaInitialized()297 EnsureAdwaitaInitialized()
298- logging.Trace().Mark("gtk_init")
299 298
300 // Mark adwaita detector as available now that adw.Init() is complete.299 // Mark adwaita detector as available now that adw.Init() is complete.
301 // This enables the highest-priority color scheme detector.300 // This enables the highest-priority color scheme detector.
@@ -317,7 +316,6 @@ func (a *App) Run(ctx context.Context, args []string) int {
317 return 1316 return 1
318 }317 }
319 defer a.gtkApp.Unref()318 defer a.gtkApp.Unref()
320- logging.Trace().Mark("gtk_app_created")
321 a.dispatchOnMainThread = a.runOnMainThread319 a.dispatchOnMainThread = a.runOnMainThread
322 320
323 // Connect activate signal321 // Connect activate signal
@@ -353,7 +351,6 @@ func (a *App) onActivate(ctx context.Context) {
353 log.Error().Err(err).Msg("failed to create main window")351 log.Error().Err(err).Msg("failed to create main window")
354 return352 return
355 }353 }
356- logging.Trace().Mark("window_created")
357 354
358 a.installCrashReportNotifier(ctx)355 a.installCrashReportNotifier(ctx)
359 a.initFocusManager()356 a.initFocusManager()
@@ -362,7 +359,6 @@ func (a *App) onActivate(ctx context.Context) {
362 359
363 a.initCoordinators(ctx)360 a.initCoordinators(ctx)
364 a.wireWebRTCPermissionIndicator()361 a.wireWebRTCPermissionIndicator()
365- logging.Trace().Mark("coordinators_init")
366 a.initKeyboardHandler(ctx)362 a.initKeyboardHandler(ctx)
367 a.initOmniboxConfig(ctx)363 a.initOmniboxConfig(ctx)
368 a.initFindBarConfig(ctx)364 a.initFindBarConfig(ctx)
M internal/ui/coordinator/content/coordinator.go +0 -6
@@ -314,12 +314,6 @@ func (c *Coordinator) activePaneOverrideID() (entity.PaneID, bool) {
314314 return c.activePaneOverride, true
315315 }
316316
317-func (c *Coordinator) webViewCount() int {
318- c.webViewsMu.RLock()
319- defer c.webViewsMu.RUnlock()
320- return len(c.webViews)
321-}
322-
323317 func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) port.WebView {
324318 c.webViewsMu.RLock()
325319 defer c.webViewsMu.RUnlock()
@@ -314,12 +314,6 @@ func (c *Coordinator) activePaneOverrideID() (entity.PaneID, bool) {
314 return c.activePaneOverride, true314 return c.activePaneOverride, true
315 }315 }
316 316
317-func (c *Coordinator) webViewCount() int {
318- c.webViewsMu.RLock()
319- defer c.webViewsMu.RUnlock()
320- return len(c.webViews)
321-}
322-
323 func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) port.WebView {317 func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) port.WebView {
324 c.webViewsMu.RLock()318 c.webViewsMu.RLock()
325 defer c.webViewsMu.RUnlock()319 defer c.webViewsMu.RUnlock()
M internal/ui/coordinator/content/lifecycle.go +0 -7
@@ -28,16 +28,10 @@ func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (
2828 return nil, fmt.Errorf("webview pool not configured")
2929 }
3030
31- // Mark tab_created on first webview (first tab)
32- if c.webViewCount() == 0 {
33- logging.Trace().Mark("tab_created")
34- }
35-
3631 wv, err := c.pool.Acquire(ctx)
3732 if err != nil {
3833 return nil, err
3934 }
40- logging.Trace().Mark("webview_acquired")
4135
4236 // setWebViewLocked atomically resets presentation state for the acquired
4337 // WebView, including a pooled instance previously revealed in another pane.
@@ -144,7 +138,6 @@ func (c *Coordinator) AttachToWorkspace(ctx context.Context, ws *entity.Workspac
144138 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to attach webview widget")
145139 continue
146140 }
147- logging.Trace().Mark("webview_attached")
148141 }
149142 }
150143
@@ -28,16 +28,10 @@ func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (
28 return nil, fmt.Errorf("webview pool not configured")28 return nil, fmt.Errorf("webview pool not configured")
29 }29 }
30 30
31- // Mark tab_created on first webview (first tab)
32- if c.webViewCount() == 0 {
33- logging.Trace().Mark("tab_created")
34- }
35-
36 wv, err := c.pool.Acquire(ctx)31 wv, err := c.pool.Acquire(ctx)
37 if err != nil {32 if err != nil {
38 return nil, err33 return nil, err
39 }34 }
40- logging.Trace().Mark("webview_acquired")
41 35
42 // setWebViewLocked atomically resets presentation state for the acquired36 // setWebViewLocked atomically resets presentation state for the acquired
43 // WebView, including a pooled instance previously revealed in another pane.37 // WebView, including a pooled instance previously revealed in another pane.
@@ -144,7 +138,6 @@ func (c *Coordinator) AttachToWorkspace(ctx context.Context, ws *entity.Workspac
144 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to attach webview widget")138 log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to attach webview widget")
145 continue139 continue
146 }140 }
147- logging.Trace().Mark("webview_attached")
148 }141 }
149 }142 }
150 143
M internal/ui/coordinator/content/navigation.go +0 -3
@@ -19,7 +19,6 @@ import (
1919 // Also shows the WebView widget (it's hidden during creation to avoid white flash).
2020 func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv port.WebView, identity webViewIdentity) {
2121 log := logging.FromContext(ctx)
22- logging.Trace().Mark("load_committed")
2322
2423 uri := wv.URI()
2524 if uri == "" {
@@ -177,8 +176,6 @@ func (c *Coordinator) updatePaneURI(paneID entity.PaneID, uri string) {
177176
178177 // onLoadStarted shows the progress bar when page loading begins.
179178 func (c *Coordinator) onLoadStarted(paneID entity.PaneID) {
180- logging.Trace().Mark("load_started")
181-
182179 // Trigger deferred initialization on first load_started.
183180 // This ensures non-critical init runs after initial navigation starts.
184181 c.loadStartedOnce.Do(func() {
@@ -19,7 +19,6 @@ import (
19 // Also shows the WebView widget (it's hidden during creation to avoid white flash).19 // Also shows the WebView widget (it's hidden during creation to avoid white flash).
20 func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv port.WebView, identity webViewIdentity) {20 func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv port.WebView, identity webViewIdentity) {
21 log := logging.FromContext(ctx)21 log := logging.FromContext(ctx)
22- logging.Trace().Mark("load_committed")
23 22
24 uri := wv.URI()23 uri := wv.URI()
25 if uri == "" {24 if uri == "" {
@@ -177,8 +176,6 @@ func (c *Coordinator) updatePaneURI(paneID entity.PaneID, uri string) {
177 176
178 // onLoadStarted shows the progress bar when page loading begins.177 // onLoadStarted shows the progress bar when page loading begins.
179 func (c *Coordinator) onLoadStarted(paneID entity.PaneID) {178 func (c *Coordinator) onLoadStarted(paneID entity.PaneID) {
180- logging.Trace().Mark("load_started")
181-
182 // Trigger deferred initialization on first load_started.179 // Trigger deferred initialization on first load_started.
183 // This ensures non-critical init runs after initial navigation starts.180 // This ensures non-critical init runs after initial navigation starts.
184 c.loadStartedOnce.Do(func() {181 c.loadStartedOnce.Do(func() {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close