fix(startup): scope trace to accelerated CEF
- commit
- ce24e6e95f377cb706fa2d5138de8eaac6190552
- author
- brice · · 2026-07-15 08:33 UTC
- parent
- 80795e3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 143 | 143 | func main() { | |
| 144 | 144 | // This is intentionally before subprocess detection: each process records | |
| 145 | 145 | // 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") | |
| 148 | 148 | ||
| 149 | 149 | // CEF subprocess handling: when CEF re-launches this binary with | |
| 150 | 150 | // --type=renderer/gpu/etc, we must call ExecuteProcess before anything | |
| @@ -223,7 +223,7 @@ func runGUI(cfg *config.Config) int { | |||
| 223 | 223 | cfg = initConfig() | |
| 224 | 224 | configureBrowserLaunchRelay(cfg) | |
| 225 | 225 | } | |
| 226 | - | logging.Trace().Mark("config_complete") | |
| 226 | + | logging.CEFTrace().Mark("config_complete") | |
| 227 | 227 | timer.Mark("config") | |
| 228 | 228 | ||
| 229 | 229 | ctx := initStartupContextWithTrace(cfg) | |
| @@ -259,7 +259,7 @@ func runGUI(cfg *config.Config) int { | |||
| 259 | 259 | timer.Mark("session") | |
| 260 | 260 | ||
| 261 | 261 | log := logging.FromContext(ctx) | |
| 262 | - | logging.Trace().UpdateLogger(log) | |
| 262 | + | logging.CEFTrace().SetLogger(log) | |
| 263 | 263 | logCoreDumpLimits(ctx) | |
| 264 | 264 | ||
| 265 | 265 | engine.SetHandlerContext(ctx) | |
| @@ -419,19 +419,17 @@ func resolveCurrentExecutable(executable func() (string, error)) (string, error) | |||
| 419 | 419 | } | |
| 420 | 420 | ||
| 421 | 421 | func initStartupContextWithTrace(cfg *config.Config) context.Context { | |
| 422 | - | logging.InitStartupTrace(cfg.Logging.Level) | |
| 422 | + | logging.InitCEFStartupTrace() | |
| 423 | 423 | ||
| 424 | 424 | ctx := initStartupContext(cfg) | |
| 425 | 425 | bootstrapLog := logging.FromContext(ctx) | |
| 426 | 426 | ||
| 427 | - | logging.Trace().SetLogger(bootstrapLog) | |
| 428 | - | logging.Trace().Mark("logger_init") | |
| 427 | + | logging.CEFTrace().SetLogger(bootstrapLog) | |
| 429 | 428 | ||
| 430 | 429 | return ctx | |
| 431 | 430 | } | |
| 432 | 431 | ||
| 433 | 432 | func runParallelInitPhase(ctx context.Context, cfg *config.Config) (*bootstrap.ParallelInitResult, error) { | |
| 434 | - | logging.Trace().Mark("parallel_start") | |
| 435 | 433 | initResult, err := bootstrap.RunParallelInit(bootstrap.ParallelInitInput{ | |
| 436 | 434 | Ctx: ctx, | |
| 437 | 435 | Config: cfg, | |
| @@ -440,7 +438,6 @@ func runParallelInitPhase(ctx context.Context, cfg *config.Config) (*bootstrap.P | |||
| 440 | 438 | handleParallelInitError(ctx, err) | |
| 441 | 439 | return nil, err | |
| 442 | 440 | } | |
| 443 | - | logging.Trace().Mark("parallel_done") | |
| 444 | 441 | return initResult, nil | |
| 445 | 442 | } | |
| 446 | 443 | ||
| @@ -143,8 +143,8 @@ func configureBrowserLaunchRelay(cfg *config.Config) { | |||||
| 143 | func main() { | 143 | func main() { | ||
| 144 | // This is intentionally before subprocess detection: each process records | 144 | // 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 with | 149 | // CEF subprocess handling: when CEF re-launches this binary with | ||
| 150 | // --type=renderer/gpu/etc, we must call ExecuteProcess before anything | 150 | // --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 ctx | 429 | 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, err | 439 | return nil, err | ||
| 442 | } | 440 | } | ||
| 443 | - | logging.Trace().Mark("parallel_done") | |||
| 444 | return initResult, nil | 441 | 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 | |
| 2 | 2 | ||
| 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: | |
| 6 | 9 | ||
| 7 | 10 | 1. `process_entry` | |
| 8 | 11 | 2. `config_complete` | |
| @@ -13,11 +16,19 @@ milestones only in order: | |||
| 13 | 16 | 7. `first_dmabuf_texture_swap` (after `GtkPicture.SetPaintable` succeeds) | |
| 14 | 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 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 | |
| 17 | 23 | opaque operation. Duplicate, unknown, and out-of-order transitions are | |
| 18 | 24 | 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. | |
| 21 | 32 | ||
| 22 | 33 | ## Reproducible collection | |
| 23 | 34 | ||
| @@ -29,7 +40,8 @@ DUMBER_MACHINE_GPU_PROFILE=integrated-gpu \ | |||
| 29 | 40 | scripts/collect_first_presentation.sh | |
| 30 | 41 | ``` | |
| 31 | 42 | ||
| 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 | |
| 33 | 45 | `$XDG_STATE_HOME/dumber/roadmap-evidence` (or | |
| 34 | 46 | `$HOME/.local/state/dumber/roadmap-evidence` when `XDG_STATE_HOME` is unset), | |
| 35 | 47 | not in the repository. `DUMBER_FIRST_PRESENTATION_OUTPUT` may override it only | |
| @@ -1,8 +1,11 @@ | |||||
| 1 | - | # First presentation startup trace | 1 | + | # CEF accelerated first-presentation startup trace |
| 2 | 2 | ||||
| 3 | - | Dumber records one cold-start timeline for each GUI process. The trace begins | 3 | + | Dumber records this cold-start timeline **only for the accelerated CEF → DMABUF |
| 4 | - | at `process_entry`, before CEF subprocess handling, and accepts these one-shot | 4 | + | → 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 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 | |||
| 17 | opaque operation. Duplicate, unknown, and out-of-order transitions are | 23 | opaque operation. Duplicate, unknown, and out-of-order transitions are | ||
| 18 | rejected. At the last milestone Dumber emits exactly one normal-level JSON | 24 | rejected. At the last milestone Dumber emits exactly one normal-level JSON | ||
| 19 | - | summary (`startup_trace: first presentation`) with the selected `backend`, an | 25 | + | 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 collection | 33 | ## Reproducible collection | ||
| 23 | 34 | ||||
| @@ -29,7 +40,8 @@ DUMBER_MACHINE_GPU_PROFILE=integrated-gpu \ | |||||
| 29 | scripts/collect_first_presentation.sh | 40 | scripts/collect_first_presentation.sh | ||
| 30 | ``` | 41 | ``` | ||
| 31 | 42 | ||||
| 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 | |||
| 33 | `$XDG_STATE_HOME/dumber/roadmap-evidence` (or | 45 | `$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 only | 47 | 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( | |||
| 56 | 56 | return nil, err | |
| 57 | 57 | } | |
| 58 | 58 | cef2gtk.ConfigureRenderStackEnvironment(renderStackPlan) | |
| 59 | - | logging.Trace().SetBackend(renderStackPlan.Backend.String()) | |
| 59 | + | logging.CEFTrace().SetBackend(renderStackPlan.Backend.String()) | |
| 60 | 60 | ||
| 61 | 61 | settings, err := prepareCEFSettings(opts, paths, cfg, logger) | |
| 62 | 62 | if err != nil { | |
| @@ -218,11 +218,11 @@ func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logge | |||
| 218 | 218 | logger.Debug().Msg("cef: calling InitWithApp") | |
| 219 | 219 | // InitWithApp encapsulates loading libcef, so only the begin boundary is | |
| 220 | 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 | 222 | if err := purecef.InitWithApp(settings, app); err != nil { | |
| 223 | 223 | return fmt.Errorf("cef.InitWithApp: %w", err) | |
| 224 | 224 | } | |
| 225 | - | logging.Trace().Mark("cef_initialized") | |
| 225 | + | logging.CEFTrace().Mark("cef_initialized") | |
| 226 | 226 | if libcefPath := loadedLibCEFPath(); libcefPath != "" { | |
| 227 | 227 | logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded") | |
| 228 | 228 | } else { | |
| @@ -56,7 +56,7 @@ func NewEngine( | |||||
| 56 | return nil, err | 56 | 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 is | 219 | // 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 | |||
| 279 | 279 | // OnAfterCreated once the host is fully wired. | |
| 280 | 280 | initialURL := "about:blank" | |
| 281 | 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 | 283 | result := cefBrowserHostCreateBrowser( | |
| 284 | 284 | pc.windowInfo, | |
| 285 | 285 | 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) | |||
| 26 | 26 | // startupPresentationHooks consumes the one-shot facts emitted by | |
| 27 | 27 | // purego-cef2gtk. The bridge owns the native DMABUF and frame-clock boundaries; | |
| 28 | 28 | // Dumber only records their ordered application-level timeline. | |
| 29 | - | func startupPresentationHooks() cef2gtk.Hooks { | |
| 29 | + | func startupPresentationHooks(trace *logging.CEFStartupTrace) cef2gtk.Hooks { | |
| 30 | 30 | return cef2gtk.Hooks{ | |
| 31 | 31 | OnFirstAcceleratedPaint: func() { | |
| 32 | - | logging.Trace().Mark("first_accelerated_paint_received") | |
| 32 | + | trace.Mark("first_accelerated_paint_received") | |
| 33 | 33 | }, | |
| 34 | 34 | OnFirstDMABUFTextureSwap: func() { | |
| 35 | - | logging.Trace().Mark("first_dmabuf_texture_swap") | |
| 35 | + | trace.Mark("first_dmabuf_texture_swap") | |
| 36 | 36 | }, | |
| 37 | 37 | OnFirstPresentation: func() { | |
| 38 | - | logging.Trace().MarkGTKAfterPaint() | |
| 38 | + | trace.MarkGTKAfterPaint() | |
| 39 | 39 | }, | |
| 40 | 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 | 50 | h := &dumberRenderHandler{wv: wv} | |
| 51 | 51 | ||
| 52 | 52 | var unsupportedPaintOnce sync.Once | |
| 53 | - | hooks := startupPresentationHooks() | |
| 53 | + | hooks := startupPresentationHooks(logging.CEFTrace()) | |
| 54 | 54 | hooks.OnUnsupportedPaint = func() { | |
| 55 | 55 | unsupportedPaintOnce.Do(func() { | |
| 56 | 56 | if wv.ctx != nil { | |
| @@ -26,19 +26,19 @@ var _ purecef.RenderHandler = (*dumberRenderHandler)(nil) | |||||
| 26 | // startupPresentationHooks consumes the one-shot facts emitted by | 26 | // 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.Once | 52 | 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 @@ | |||
| 1 | 1 | package cef | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "encoding/json" | |
| 6 | + | "strings" | |
| 4 | 7 | "testing" | |
| 5 | 8 | ||
| 9 | + | "github.com/bnema/dumber/internal/logging" | |
| 10 | + | "github.com/rs/zerolog" | |
| 6 | 11 | "github.com/stretchr/testify/require" | |
| 7 | 12 | ) | |
| 8 | 13 | ||
| 9 | 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 | 31 | require.NotNil(t, hooks.OnFirstAcceleratedPaint) | |
| 13 | 32 | require.NotNil(t, hooks.OnFirstDMABUFTextureSwap) | |
| 14 | 33 | require.NotNil(t, hooks.OnFirstPresentation) | |
| 15 | 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 | } | |
| @@ -1,16 +1,79 @@ | |||||
| 1 | package cef | 1 | 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( | |||
| 40 | 40 | ||
| 41 | 41 | // --- Configure rendering environment (must be before GTK/WebKit init) --- | |
| 42 | 42 | engineConfigureRenderingEnvironment(ctx, cfg, wkCfg, &perfSettings, logger) | |
| 43 | - | logging.Trace().Mark("render_env") | |
| 44 | 43 | ||
| 45 | 44 | // --- Build webKitContextOptions from opts + wkCfg + perfSettings --- | |
| 46 | 45 | wkOpts := engineBuildContextOptions(opts, profile, wkCfg, &perfSettings) | |
| @@ -53,7 +52,6 @@ func NewEngine( | |||
| 53 | 52 | if err != nil { | |
| 54 | 53 | return nil, err | |
| 55 | 54 | } | |
| 56 | - | logging.Trace().Mark("webkit_context") | |
| 57 | 55 | ||
| 58 | 56 | // --- Filter manager --- | |
| 59 | 57 | filterManager := engineInitFilterManager(ctx, cfg, profile.Shared.DataDir, logger) | |
| @@ -77,7 +75,6 @@ func NewEngine( | |||
| 77 | 75 | } | |
| 78 | 76 | ||
| 79 | 77 | messageRouter := NewMessageRouter(ctx) | |
| 80 | - | logging.Trace().Mark("settings_manager") | |
| 81 | 78 | ||
| 82 | 79 | // --- WebView pool --- | |
| 83 | 80 | poolCfg := DefaultPoolConfig() | |
| @@ -100,7 +97,6 @@ func NewEngine( | |||
| 100 | 97 | } else { | |
| 101 | 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 | 101 | // --- WebView factory --- | |
| 106 | 102 | factory := NewWebViewFactory(wkCtx, settings, pool, injector, messageRouter) | |
| @@ -147,7 +143,6 @@ func engineSurveyHardwareAndResolveProfile( | |||
| 147 | 143 | ) config.ResolvedPerformanceSettings { | |
| 148 | 144 | hwSurveyor := env.NewHardwareSurveyor() | |
| 149 | 145 | hwInfo := hwSurveyor.Survey(ctx) | |
| 150 | - | logging.Trace().Mark("hardware_survey") | |
| 151 | 146 | logger.Info(). | |
| 152 | 147 | Int("cpu_cores", hwInfo.CPUCores). | |
| 153 | 148 | Int("cpu_threads", hwInfo.CPUThreads). | |
| @@ -158,7 +153,6 @@ func engineSurveyHardwareAndResolveProfile( | |||
| 158 | 153 | ||
| 159 | 154 | perfCfg := config.PerformanceConfigFromEngine(&cfg.Engine) | |
| 160 | 155 | perfSettings := config.ResolvePerformanceProfile(&perfCfg, &hwInfo) | |
| 161 | - | logging.Trace().Mark("performance_profile") | |
| 162 | 156 | logger.Info(). | |
| 163 | 157 | Str("profile", string(cfg.Engine.Profile)). | |
| 164 | 158 | Int("skia_cpu_threads", perfSettings.SkiaCPUPaintingThreads). | |
| @@ -284,7 +278,6 @@ func engineInitFilterManager(ctx context.Context, cfg *config.Config, dataDir st | |||
| 284 | 278 | if err := filterManager.Initialize(ctx); err != nil { | |
| 285 | 279 | logger.Warn().Err(err).Msg("failed to initialize filters, will load async") | |
| 286 | 280 | } | |
| 287 | - | logging.Trace().Mark("filter_manager") | |
| 288 | 281 | return filterManager | |
| 289 | 282 | } | |
| 290 | 283 | ||
| @@ -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, err | 53 | 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 filterManager | 281 | return filterManager | ||
| 289 | } | 282 | } | ||
| 290 | 283 | ||||
M internal/logging/startup_trace.go +52 -57
| @@ -7,30 +7,30 @@ import ( | |||
| 7 | 7 | "github.com/rs/zerolog" | |
| 8 | 8 | ) | |
| 9 | 9 | ||
| 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 { | |
| 15 | 15 | mu sync.Mutex | |
| 16 | 16 | now func() time.Time | |
| 17 | 17 | t0 time.Time | |
| 18 | - | milestones []Milestone | |
| 18 | + | milestones []CEFStartupMilestone | |
| 19 | 19 | logger *zerolog.Logger | |
| 20 | - | buffered []Milestone | |
| 20 | + | buffered []CEFStartupMilestone | |
| 21 | 21 | backend string | |
| 22 | 22 | incompleteReason string | |
| 23 | 23 | summaryEmitted bool | |
| 24 | 24 | } | |
| 25 | 25 | ||
| 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 { | |
| 28 | 28 | Name string | |
| 29 | 29 | Elapsed time.Duration | |
| 30 | 30 | Delta time.Duration | |
| 31 | 31 | } | |
| 32 | 32 | ||
| 33 | - | var startupMilestoneOrder = []string{ | |
| 33 | + | var cefStartupMilestoneOrder = []string{ | |
| 34 | 34 | "process_entry", | |
| 35 | 35 | "config_complete", | |
| 36 | 36 | "cef_library_load_begin", | |
| @@ -42,46 +42,49 @@ var startupMilestoneOrder = []string{ | |||
| 42 | 42 | } | |
| 43 | 43 | ||
| 44 | 44 | var ( | |
| 45 | - | globalTrace *StartupTrace | |
| 46 | - | globalTraceMu sync.Mutex | |
| 47 | - | globalTraceOnce sync.Once | |
| 45 | + | globalCEFTrace *CEFStartupTrace | |
| 46 | + | globalCEFTraceMu sync.Mutex | |
| 47 | + | globalCEFTraceOnce sync.Once | |
| 48 | 48 | ) | |
| 49 | 49 | ||
| 50 | - | func newStartupTrace(now func() time.Time) *StartupTrace { | |
| 50 | + | func newCEFStartupTrace(now func() time.Time) *CEFStartupTrace { | |
| 51 | 51 | start := now() | |
| 52 | - | return &StartupTrace{ | |
| 52 | + | return &CEFStartupTrace{ | |
| 53 | 53 | now: now, | |
| 54 | 54 | 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)), | |
| 57 | 57 | } | |
| 58 | 58 | } | |
| 59 | 59 | ||
| 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() | |
| 69 | 72 | }) | |
| 70 | 73 | } | |
| 71 | 74 | ||
| 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{} | |
| 79 | 82 | } | |
| 80 | - | return globalTrace | |
| 83 | + | return globalCEFTrace | |
| 81 | 84 | } | |
| 82 | 85 | ||
| 83 | 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 | 88 | if st == nil || logger == nil { | |
| 86 | 89 | return | |
| 87 | 90 | } | |
| @@ -89,18 +92,14 @@ func (st *StartupTrace) SetLogger(logger *zerolog.Logger) { | |||
| 89 | 92 | defer st.mu.Unlock() | |
| 90 | 93 | st.logger = logger | |
| 91 | 94 | for _, milestone := range st.buffered { | |
| 92 | - | st.emitMilestone(milestone) | |
| 95 | + | st.emitCEFStartupMilestone(milestone) | |
| 93 | 96 | } | |
| 94 | 97 | st.buffered = nil | |
| 95 | 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 | 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 | 103 | if st == nil { | |
| 105 | 104 | return | |
| 106 | 105 | } | |
| @@ -110,7 +109,7 @@ func (st *StartupTrace) SetBackend(backend string) { | |||
| 110 | 109 | } | |
| 111 | 110 | ||
| 112 | 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 | 113 | if st == nil { | |
| 115 | 114 | return | |
| 116 | 115 | } | |
| @@ -124,7 +123,7 @@ func (st *StartupTrace) SetIncompleteReason(reason string) { | |||
| 124 | 123 | // Mark accepts exactly the next non-presentation startup milestone and returns | |
| 125 | 124 | // whether it was recorded. first_gtk_presentation is reserved for the GTK | |
| 126 | 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 | 127 | if name == "first_gtk_presentation" { | |
| 129 | 128 | return false | |
| 130 | 129 | } | |
| @@ -133,24 +132,24 @@ func (st *StartupTrace) Mark(name string) bool { | |||
| 133 | 132 | ||
| 134 | 133 | // MarkGTKAfterPaint records the final milestone. It is called only from the | |
| 135 | 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 | 136 | return st.mark("first_gtk_presentation") | |
| 138 | 137 | } | |
| 139 | 138 | ||
| 140 | 139 | // mark accepts the next milestone and is safe for callbacks from CEF and GTK | |
| 141 | 140 | // threads. | |
| 142 | - | func (st *StartupTrace) mark(name string) bool { | |
| 141 | + | func (st *CEFStartupTrace) mark(name string) bool { | |
| 143 | 142 | if st == nil || st.now == nil { | |
| 144 | 143 | return false | |
| 145 | 144 | } | |
| 146 | 145 | st.mu.Lock() | |
| 147 | 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 | 148 | return false | |
| 150 | 149 | } | |
| 151 | 150 | ||
| 152 | 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 | 153 | // Published fields are integer milliseconds. Compute delta on that same | |
| 155 | 154 | // scale so delta_ms always exactly reconstructs t_ms in collected evidence. | |
| 156 | 155 | previousMilliseconds := int64(0) | |
| @@ -162,13 +161,13 @@ func (st *StartupTrace) mark(name string) bool { | |||
| 162 | 161 | if st.logger == nil { | |
| 163 | 162 | st.buffered = append(st.buffered, milestone) | |
| 164 | 163 | } else { | |
| 165 | - | st.emitMilestone(milestone) | |
| 164 | + | st.emitCEFStartupMilestone(milestone) | |
| 166 | 165 | } | |
| 167 | 166 | st.emitSummaryLocked() | |
| 168 | 167 | return true | |
| 169 | 168 | } | |
| 170 | 169 | ||
| 171 | - | func (st *StartupTrace) emitMilestone(m Milestone) { | |
| 170 | + | func (st *CEFStartupTrace) emitCEFStartupMilestone(m CEFStartupMilestone) { | |
| 172 | 171 | if st.logger == nil { | |
| 173 | 172 | return | |
| 174 | 173 | } | |
| @@ -179,8 +178,8 @@ func (st *StartupTrace) emitMilestone(m Milestone) { | |||
| 179 | 178 | Msg("startup_trace: milestone") | |
| 180 | 179 | } | |
| 181 | 180 | ||
| 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) { | |
| 184 | 183 | return | |
| 185 | 184 | } | |
| 186 | 185 | st.summaryEmitted = true | |
| @@ -192,13 +191,9 @@ func (st *StartupTrace) emitSummaryLocked() { | |||
| 192 | 191 | Msg("startup_trace: first presentation") | |
| 193 | 192 | } | |
| 194 | 193 | ||
| 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 } | |
| 200 | 195 | ||
| 201 | - | func (st *StartupTrace) TotalElapsed() time.Duration { | |
| 196 | + | func (st *CEFStartupTrace) TotalElapsed() time.Duration { | |
| 202 | 197 | if st == nil || st.now == nil { | |
| 203 | 198 | return 0 | |
| 204 | 199 | } | |
| @@ -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 first | 10 | + | // CEFStartupTrace records the accelerated CEF/DMABUF/GTK path from process |
| 11 | - | // GTK presentation. It deliberately rejects unknown, duplicate, and out of | 11 | + | // entry to the first GTK presentation. It deliberately rejects unknown, |
| 12 | - | // order milestones: accepting a convenient timestamp would make the result | 12 | + | // 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.Mutex | 15 | mu sync.Mutex | ||
| 16 | now func() time.Time | 16 | now func() time.Time | ||
| 17 | t0 time.Time | 17 | t0 time.Time | ||
| 18 | - | milestones []Milestone | 18 | + | milestones []CEFStartupMilestone |
| 19 | logger *zerolog.Logger | 19 | logger *zerolog.Logger | ||
| 20 | - | buffered []Milestone | 20 | + | buffered []CEFStartupMilestone |
| 21 | backend string | 21 | backend string | ||
| 22 | incompleteReason string | 22 | incompleteReason string | ||
| 23 | summaryEmitted bool | 23 | 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 string | 28 | Name string | ||
| 29 | Elapsed time.Duration | 29 | Elapsed time.Duration | ||
| 30 | Delta time.Duration | 30 | 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 *StartupTrace | 45 | + | globalCEFTrace *CEFStartupTrace |
| 46 | - | globalTraceMu sync.Mutex | 46 | + | globalCEFTraceMu sync.Mutex |
| 47 | - | globalTraceOnce sync.Once | 47 | + | 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 is | 60 | + | // NewCEFStartupTrace creates an isolated accelerated CEF/DMABUF/GTK trace. |
| 61 | - | // retained for source compatibility; this measurement is always collected so | 61 | + | // 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 = trace | 67 | + | 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 a | 75 | + | // 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 globalTrace | 83 | + | 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 | return | 89 | 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 = logger | 93 | 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 = nil | 97 | 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 | return | 104 | 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 | return | 114 | return | ||
| 116 | } | 115 | } | ||
| @@ -124,7 +123,7 @@ func (st *StartupTrace) SetIncompleteReason(reason string) { | |||||
| 124 | // Mark accepts exactly the next non-presentation startup milestone and returns | 123 | // Mark accepts exactly the next non-presentation startup milestone and returns | ||
| 125 | // whether it was recorded. first_gtk_presentation is reserved for the GTK | 124 | // 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 false | 128 | 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 the | 133 | // 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 GTK | 139 | // 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 false | 143 | 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 false | 148 | 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 same | 153 | // 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 true | 167 | 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 | return | 172 | 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 | return | 183 | return | ||
| 185 | } | 184 | } | ||
| 186 | st.summaryEmitted = true | 185 | 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 source | 194 | + | 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 0 | 198 | return 0 | ||
| 204 | } | 199 | } | ||
M internal/logging/startup_trace_test.go +12 -13
| @@ -9,9 +9,9 @@ import ( | |||
| 9 | 9 | "github.com/stretchr/testify/require" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | - | func TestStartupTraceAcceptsOnlyOrderedOneShotMilestones(t *testing.T) { | |
| 12 | + | func TestCEFStartupTraceAcceptsOnlyOrderedOneShotMilestones(t *testing.T) { | |
| 13 | 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 | 16 | require.True(t, trace.Mark("process_entry")) | |
| 17 | 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 | 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 | 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 | 43 | now = now.Add(1500 * time.Microsecond) | |
| 44 | 44 | require.True(t, trace.Mark("process_entry")) | |
| @@ -51,31 +51,30 @@ func TestStartupTraceQuantizesDeltaToPublishedMilliseconds(t *testing.T) { | |||
| 51 | 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 | 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 | 59 | now = now.Add(time.Millisecond) | |
| 60 | 60 | require.True(t, trace.Mark(name)) | |
| 61 | 61 | } | |
| 62 | 62 | ||
| 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") | |
| 66 | 65 | require.False(t, trace.summaryEmitted) | |
| 67 | 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 | 70 | var output bytes.Buffer | |
| 72 | 71 | logger := zerolog.New(&output) | |
| 73 | 72 | now := time.Unix(100, 0) | |
| 74 | - | trace := newStartupTrace(func() time.Time { return now }) | |
| 73 | + | trace := newCEFStartupTrace(func() time.Time { return now }) | |
| 75 | 74 | trace.SetBackend("gdk-dmabuf") | |
| 76 | 75 | trace.SetLogger(&logger) | |
| 77 | 76 | ||
| 78 | - | for _, name := range startupMilestoneOrder[:len(startupMilestoneOrder)-1] { | |
| 77 | + | for _, name := range cefStartupMilestoneOrder[:len(cefStartupMilestoneOrder)-1] { | |
| 79 | 78 | now = now.Add(time.Millisecond) | |
| 80 | 79 | require.True(t, trace.Mark(name)) | |
| 81 | 80 | } | |
| @@ -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.Buffer | 70 | 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 { | |||
| 295 | 295 | // Initialize libadwaita once (required before using StyleManager). | |
| 296 | 296 | // This also initializes GTK implicitly. | |
| 297 | 297 | EnsureAdwaitaInitialized() | |
| 298 | - | logging.Trace().Mark("gtk_init") | |
| 299 | 298 | ||
| 300 | 299 | // Mark adwaita detector as available now that adw.Init() is complete. | |
| 301 | 300 | // This enables the highest-priority color scheme detector. | |
| @@ -317,7 +316,6 @@ func (a *App) Run(ctx context.Context, args []string) int { | |||
| 317 | 316 | return 1 | |
| 318 | 317 | } | |
| 319 | 318 | defer a.gtkApp.Unref() | |
| 320 | - | logging.Trace().Mark("gtk_app_created") | |
| 321 | 319 | a.dispatchOnMainThread = a.runOnMainThread | |
| 322 | 320 | ||
| 323 | 321 | // Connect activate signal | |
| @@ -353,7 +351,6 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 353 | 351 | log.Error().Err(err).Msg("failed to create main window") | |
| 354 | 352 | return | |
| 355 | 353 | } | |
| 356 | - | logging.Trace().Mark("window_created") | |
| 357 | 354 | ||
| 358 | 355 | a.installCrashReportNotifier(ctx) | |
| 359 | 356 | a.initFocusManager() | |
| @@ -362,7 +359,6 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 362 | 359 | ||
| 363 | 360 | a.initCoordinators(ctx) | |
| 364 | 361 | a.wireWebRTCPermissionIndicator() | |
| 365 | - | logging.Trace().Mark("coordinators_init") | |
| 366 | 362 | a.initKeyboardHandler(ctx) | |
| 367 | 363 | a.initOmniboxConfig(ctx) | |
| 368 | 364 | 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 1 | 316 | 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.runOnMainThread | 319 | a.dispatchOnMainThread = a.runOnMainThread | ||
| 322 | 320 | ||||
| 323 | // Connect activate signal | 321 | // 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 | return | 352 | 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) { | |||
| 314 | 314 | 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 | 317 | func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) port.WebView { | |
| 324 | 318 | c.webViewsMu.RLock() | |
| 325 | 319 | defer c.webViewsMu.RUnlock() | |
| @@ -314,12 +314,6 @@ func (c *Coordinator) activePaneOverrideID() (entity.PaneID, bool) { | |||||
| 314 | return c.activePaneOverride, true | 314 | 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) ( | |||
| 28 | 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 | 31 | wv, err := c.pool.Acquire(ctx) | |
| 37 | 32 | if err != nil { | |
| 38 | 33 | return nil, err | |
| 39 | 34 | } | |
| 40 | - | logging.Trace().Mark("webview_acquired") | |
| 41 | 35 | ||
| 42 | 36 | // setWebViewLocked atomically resets presentation state for the acquired | |
| 43 | 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 | 138 | log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to attach webview widget") | |
| 145 | 139 | continue | |
| 146 | 140 | } | |
| 147 | - | logging.Trace().Mark("webview_attached") | |
| 148 | 141 | } | |
| 149 | 142 | } | |
| 150 | 143 | ||
| @@ -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, err | 33 | return nil, err | ||
| 39 | } | 34 | } | ||
| 40 | - | logging.Trace().Mark("webview_acquired") | |||
| 41 | 35 | ||||
| 42 | // setWebViewLocked atomically resets presentation state for the acquired | 36 | // 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 | continue | 139 | 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 ( | |||
| 19 | 19 | // Also shows the WebView widget (it's hidden during creation to avoid white flash). | |
| 20 | 20 | func (c *Coordinator) onLoadCommitted(ctx context.Context, paneID entity.PaneID, wv port.WebView, identity webViewIdentity) { | |
| 21 | 21 | log := logging.FromContext(ctx) | |
| 22 | - | logging.Trace().Mark("load_committed") | |
| 23 | 22 | ||
| 24 | 23 | uri := wv.URI() | |
| 25 | 24 | if uri == "" { | |
| @@ -177,8 +176,6 @@ func (c *Coordinator) updatePaneURI(paneID entity.PaneID, uri string) { | |||
| 177 | 176 | ||
| 178 | 177 | // onLoadStarted shows the progress bar when page loading begins. | |
| 179 | 178 | func (c *Coordinator) onLoadStarted(paneID entity.PaneID) { | |
| 180 | - | logging.Trace().Mark("load_started") | |
| 181 | - | ||
| 182 | 179 | // Trigger deferred initialization on first load_started. | |
| 183 | 180 | // This ensures non-critical init runs after initial navigation starts. | |
| 184 | 181 | 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() { | ||