detached ec11c42

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +45 -2
M cmd/dumber/main.go +19 -0
@@ -7,6 +7,7 @@ import (
77 "os"
88 "os/signal"
99 "runtime"
10+ "strconv"
1011 "syscall"
1112
1213 "github.com/bnema/dumber/internal/application/port"
@@ -113,6 +114,14 @@ func runGUI() int {
113114 }
114115 timer.MarkDuration("parallel_phase", initResult.Duration)
115116
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+
116125 needsEagerDB := restoreSessionID != "" || cfg.Session.AutoRestore
117126
118127 engine, repos, dbCleanup, err := initStackAndRepos(ctx, cfg, initResult, needsEagerDB)
@@ -244,6 +253,16 @@ func initStartupContext(cfg *config.Config) context.Context {
244253 return ctx
245254 }
246255
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+
247266 func initStackAndRepos(
248267 ctx context.Context,
249268 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.AutoRestore125 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 ctx253 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 (
66 "fmt"
77 "os"
88 "path/filepath"
9+ "strconv"
910 "strings"
1011 "sync"
1112 "unsafe"
@@ -50,6 +51,15 @@ func gtkApplicationFlags() gio.ApplicationFlags {
5051 return gio.GApplicationNonUniqueValue
5152 }
5253
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+
5363 // App wraps the GTK Application and manages the browser lifecycle.
5464 type App struct {
5565 deps *Dependencies
@@ -210,7 +220,7 @@ func (a *App) Run(ctx context.Context, args []string) int {
210220
211221 // Initialize libadwaita once (required before using StyleManager).
212222 // This also initializes GTK implicitly.
213- adw.Init()
223+ EnsureAdwaitaInitialized()
214224 logging.Trace().Mark("gtk_init")
215225
216226 // Mark adwaita detector as available now that adw.Init() is complete.
@@ -258,7 +268,11 @@ func (a *App) onActivate(ctx context.Context) {
258268 log := logging.FromContext(ctx)
259269 log.Debug().Msg("GTK application activated")
260270
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+ }
262276 // Configure pool background color early (prevents white flash), but avoid
263277 // synchronous prewarming that delays the first navigation on cold start.
264278 a.setupPoolBackgroundColor(ctx)
@@ -461,6 +475,16 @@ func (a *App) installCrashReportNotifier(ctx context.Context) {
461475 }
462476 }
463477
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+
464488 func (a *App) initFocusAndBorderOverlay() {
465489 // Initialize focus manager for geometric navigation.
466490 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.GApplicationNonUniqueValue51 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 *Dependencies65 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 avoid276 // 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)

Keyboard shortcuts

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

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

Branches 8

Tags 42

↑↓ moveenter openesc close