Stabilize CEF multi-threaded GTK startup
- commit
- ec11c42c78347946753e3d7cddb6bae7773b2ab4
- author
- brice · · 2026-03-20 14:01 UTC
- committer
- brice · · 2026-03-28 04:36 UTC
- parent
- 3f8c6a5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +45 -2
M cmd/dumber/main.go +19 -0
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "os" | |
| 8 | 8 | "os/signal" | |
| 9 | 9 | "runtime" | |
| 10 | + | "strconv" | |
| 10 | 11 | "syscall" | |
| 11 | 12 | ||
| 12 | 13 | "github.com/bnema/dumber/internal/application/port" | |
| @@ -113,6 +114,14 @@ func runGUI() int { | |||
| 113 | 114 | } | |
| 114 | 115 | timer.MarkDuration("parallel_phase", initResult.Duration) | |
| 115 | 116 | ||
| 117 | + | if multiThreadedCEFLoopEnabled() { | |
| 118 | + | logging.FromContext(ctx).Info().Msg("pre-initializing libadwaita before CEF multi-threaded loop") | |
| 119 | + | ui.EnsureAdwaitaInitialized() | |
| 120 | + | if initResult.AdwaitaDetector != nil { | |
| 121 | + | initResult.AdwaitaDetector.MarkAvailable() | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | ||
| 116 | 125 | needsEagerDB := restoreSessionID != "" || cfg.Session.AutoRestore | |
| 117 | 126 | ||
| 118 | 127 | engine, repos, dbCleanup, err := initStackAndRepos(ctx, cfg, initResult, needsEagerDB) | |
| @@ -244,6 +253,16 @@ func initStartupContext(cfg *config.Config) context.Context { | |||
| 244 | 253 | return ctx | |
| 245 | 254 | } | |
| 246 | 255 | ||
| 256 | + | func multiThreadedCEFLoopEnabled() bool { | |
| 257 | + | value, ok := os.LookupEnv("DUMBER_CEF_MULTI_THREADED_MESSAGE_LOOP") | |
| 258 | + | if !ok || value == "" { | |
| 259 | + | return false | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | enabled, err := strconv.ParseBool(value) | |
| 263 | + | return err == nil && enabled | |
| 264 | + | } | |
| 265 | + | ||
| 247 | 266 | func initStackAndRepos( | |
| 248 | 267 | ctx context.Context, | |
| 249 | 268 | cfg *config.Config, | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "os" | 7 | "os" | ||
| 8 | "os/signal" | 8 | "os/signal" | ||
| 9 | "runtime" | 9 | "runtime" | ||
| 10 | + | "strconv" | |||
| 10 | "syscall" | 11 | "syscall" | ||
| 11 | 12 | ||||
| 12 | "github.com/bnema/dumber/internal/application/port" | 13 | "github.com/bnema/dumber/internal/application/port" | ||
| @@ -113,6 +114,14 @@ func runGUI() int { | |||||
| 113 | } | 114 | } | ||
| 114 | timer.MarkDuration("parallel_phase", initResult.Duration) | 115 | timer.MarkDuration("parallel_phase", initResult.Duration) | ||
| 115 | 116 | ||||
| 117 | + | if multiThreadedCEFLoopEnabled() { | |||
| 118 | + | logging.FromContext(ctx).Info().Msg("pre-initializing libadwaita before CEF multi-threaded loop") | |||
| 119 | + | ui.EnsureAdwaitaInitialized() | |||
| 120 | + | if initResult.AdwaitaDetector != nil { | |||
| 121 | + | initResult.AdwaitaDetector.MarkAvailable() | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | ||||
| 116 | needsEagerDB := restoreSessionID != "" || cfg.Session.AutoRestore | 125 | needsEagerDB := restoreSessionID != "" || cfg.Session.AutoRestore | ||
| 117 | 126 | ||||
| 118 | engine, repos, dbCleanup, err := initStackAndRepos(ctx, cfg, initResult, needsEagerDB) | 127 | engine, repos, dbCleanup, err := initStackAndRepos(ctx, cfg, initResult, needsEagerDB) | ||
| @@ -244,6 +253,16 @@ func initStartupContext(cfg *config.Config) context.Context { | |||||
| 244 | return ctx | 253 | return ctx | ||
| 245 | } | 254 | } | ||
| 246 | 255 | ||||
| 256 | + | func multiThreadedCEFLoopEnabled() bool { | |||
| 257 | + | value, ok := os.LookupEnv("DUMBER_CEF_MULTI_THREADED_MESSAGE_LOOP") | |||
| 258 | + | if !ok || value == "" { | |||
| 259 | + | return false | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | enabled, err := strconv.ParseBool(value) | |||
| 263 | + | return err == nil && enabled | |||
| 264 | + | } | |||
| 265 | + | ||||
| 247 | func initStackAndRepos( | 266 | func initStackAndRepos( | ||
| 248 | ctx context.Context, | 267 | ctx context.Context, | ||
| 249 | cfg *config.Config, | 268 | cfg *config.Config, | ||
M internal/ui/app.go +26 -2
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "os" | |
| 8 | 8 | "path/filepath" | |
| 9 | + | "strconv" | |
| 9 | 10 | "strings" | |
| 10 | 11 | "sync" | |
| 11 | 12 | "unsafe" | |
| @@ -50,6 +51,15 @@ func gtkApplicationFlags() gio.ApplicationFlags { | |||
| 50 | 51 | return gio.GApplicationNonUniqueValue | |
| 51 | 52 | } | |
| 52 | 53 | ||
| 54 | + | var adwaitaInitOnce sync.Once | |
| 55 | + | ||
| 56 | + | // EnsureAdwaitaInitialized initializes libadwaita and GTK exactly once. | |
| 57 | + | func EnsureAdwaitaInitialized() { | |
| 58 | + | adwaitaInitOnce.Do(func() { | |
| 59 | + | adw.Init() | |
| 60 | + | }) | |
| 61 | + | } | |
| 62 | + | ||
| 53 | 63 | // App wraps the GTK Application and manages the browser lifecycle. | |
| 54 | 64 | type App struct { | |
| 55 | 65 | deps *Dependencies | |
| @@ -210,7 +220,7 @@ func (a *App) Run(ctx context.Context, args []string) int { | |||
| 210 | 220 | ||
| 211 | 221 | // Initialize libadwaita once (required before using StyleManager). | |
| 212 | 222 | // This also initializes GTK implicitly. | |
| 213 | - | adw.Init() | |
| 223 | + | EnsureAdwaitaInitialized() | |
| 214 | 224 | logging.Trace().Mark("gtk_init") | |
| 215 | 225 | ||
| 216 | 226 | // Mark adwaita detector as available now that adw.Init() is complete. | |
| @@ -258,7 +268,11 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 258 | 268 | log := logging.FromContext(ctx) | |
| 259 | 269 | log.Debug().Msg("GTK application activated") | |
| 260 | 270 | ||
| 261 | - | a.applyGTKColorSchemePreference(ctx) | |
| 271 | + | if cefMultiThreadedLoopEnabled() { | |
| 272 | + | log.Warn().Msg("skipping GTK color scheme preference while testing CEF multi-threaded loop") | |
| 273 | + | } else { | |
| 274 | + | a.applyGTKColorSchemePreference(ctx) | |
| 275 | + | } | |
| 262 | 276 | // Configure pool background color early (prevents white flash), but avoid | |
| 263 | 277 | // synchronous prewarming that delays the first navigation on cold start. | |
| 264 | 278 | a.setupPoolBackgroundColor(ctx) | |
| @@ -461,6 +475,16 @@ func (a *App) installCrashReportNotifier(ctx context.Context) { | |||
| 461 | 475 | } | |
| 462 | 476 | } | |
| 463 | 477 | ||
| 478 | + | func cefMultiThreadedLoopEnabled() bool { | |
| 479 | + | value, ok := os.LookupEnv("DUMBER_CEF_MULTI_THREADED_MESSAGE_LOOP") | |
| 480 | + | if !ok || value == "" { | |
| 481 | + | return false | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | enabled, err := strconv.ParseBool(value) | |
| 485 | + | return err == nil && enabled | |
| 486 | + | } | |
| 487 | + | ||
| 464 | 488 | func (a *App) initFocusAndBorderOverlay() { | |
| 465 | 489 | // Initialize focus manager for geometric navigation. | |
| 466 | 490 | a.focusMgr = focus.NewManager(a.panesUC) | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "os" | 7 | "os" | ||
| 8 | "path/filepath" | 8 | "path/filepath" | ||
| 9 | + | "strconv" | |||
| 9 | "strings" | 10 | "strings" | ||
| 10 | "sync" | 11 | "sync" | ||
| 11 | "unsafe" | 12 | "unsafe" | ||
| @@ -50,6 +51,15 @@ func gtkApplicationFlags() gio.ApplicationFlags { | |||||
| 50 | return gio.GApplicationNonUniqueValue | 51 | return gio.GApplicationNonUniqueValue | ||
| 51 | } | 52 | } | ||
| 52 | 53 | ||||
| 54 | + | var adwaitaInitOnce sync.Once | |||
| 55 | + | ||||
| 56 | + | // EnsureAdwaitaInitialized initializes libadwaita and GTK exactly once. | |||
| 57 | + | func EnsureAdwaitaInitialized() { | |||
| 58 | + | adwaitaInitOnce.Do(func() { | |||
| 59 | + | adw.Init() | |||
| 60 | + | }) | |||
| 61 | + | } | |||
| 62 | + | ||||
| 53 | // App wraps the GTK Application and manages the browser lifecycle. | 63 | // App wraps the GTK Application and manages the browser lifecycle. | ||
| 54 | type App struct { | 64 | type App struct { | ||
| 55 | deps *Dependencies | 65 | deps *Dependencies | ||
| @@ -210,7 +220,7 @@ func (a *App) Run(ctx context.Context, args []string) int { | |||||
| 210 | 220 | ||||
| 211 | // Initialize libadwaita once (required before using StyleManager). | 221 | // Initialize libadwaita once (required before using StyleManager). | ||
| 212 | // This also initializes GTK implicitly. | 222 | // This also initializes GTK implicitly. | ||
| 213 | - | adw.Init() | 223 | + | EnsureAdwaitaInitialized() |
| 214 | logging.Trace().Mark("gtk_init") | 224 | logging.Trace().Mark("gtk_init") | ||
| 215 | 225 | ||||
| 216 | // Mark adwaita detector as available now that adw.Init() is complete. | 226 | // Mark adwaita detector as available now that adw.Init() is complete. | ||
| @@ -258,7 +268,11 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 258 | log := logging.FromContext(ctx) | 268 | log := logging.FromContext(ctx) | ||
| 259 | log.Debug().Msg("GTK application activated") | 269 | log.Debug().Msg("GTK application activated") | ||
| 260 | 270 | ||||
| 261 | - | a.applyGTKColorSchemePreference(ctx) | 271 | + | if cefMultiThreadedLoopEnabled() { |
| 272 | + | log.Warn().Msg("skipping GTK color scheme preference while testing CEF multi-threaded loop") | |||
| 273 | + | } else { | |||
| 274 | + | a.applyGTKColorSchemePreference(ctx) | |||
| 275 | + | } | |||
| 262 | // Configure pool background color early (prevents white flash), but avoid | 276 | // Configure pool background color early (prevents white flash), but avoid | ||
| 263 | // synchronous prewarming that delays the first navigation on cold start. | 277 | // synchronous prewarming that delays the first navigation on cold start. | ||
| 264 | a.setupPoolBackgroundColor(ctx) | 278 | a.setupPoolBackgroundColor(ctx) | ||
| @@ -461,6 +475,16 @@ func (a *App) installCrashReportNotifier(ctx context.Context) { | |||||
| 461 | } | 475 | } | ||
| 462 | } | 476 | } | ||
| 463 | 477 | ||||
| 478 | + | func cefMultiThreadedLoopEnabled() bool { | |||
| 479 | + | value, ok := os.LookupEnv("DUMBER_CEF_MULTI_THREADED_MESSAGE_LOOP") | |||
| 480 | + | if !ok || value == "" { | |||
| 481 | + | return false | |||
| 482 | + | } | |||
| 483 | + | ||||
| 484 | + | enabled, err := strconv.ParseBool(value) | |||
| 485 | + | return err == nil && enabled | |||
| 486 | + | } | |||
| 487 | + | ||||
| 464 | func (a *App) initFocusAndBorderOverlay() { | 488 | func (a *App) initFocusAndBorderOverlay() { | ||
| 465 | // Initialize focus manager for geometric navigation. | 489 | // Initialize focus manager for geometric navigation. | ||
| 466 | a.focusMgr = focus.NewManager(a.panesUC) | 490 | a.focusMgr = focus.NewManager(a.panesUC) | ||