refactor(cef): route pump config through config system, remove dead external pump
Move CEF message pump settings (MultiThreadedMessageLoop, ManualPumpIntervalMs) into CEFEngineConfig instead of reading env vars directly. Multi-threaded message loop is now the default — no env vars needed to launch CEF. Remove ~250 lines of dead external pump code (wakeup pipe, signal/drain, arm/disarm timer, onPumpWake, performMessageLoopWork, diagnostics) since the external pump is broken in purego-cef (OnScheduleMessagePumpWork never fires after cef_initialize). - config/engine.go: add MultiThreadedMessageLoop (*bool, default true) and ManualPumpIntervalMs (int64, default 10) to CEFEngineConfig - engine_init.go: read from config, hardcode ExternalMessagePump=false with bug comment, remove parseBoolEnv/parseInt64Env helpers - engine.go: strip external pump fields/methods, simplify manual pump and stopMessagePump - main.go: replace multiThreadedCEFLoopEnabled() with resolveEngineType(cfg) - app.go: remove cefMultiThreadedLoopEnabled() and conditional color scheme skip
- commit
- a004a884cbf5c8381b1c0480f1e2d32b97a2abf3
- author
- brice · · 2026-03-20 14:46 UTC
- committer
- brice · · 2026-03-28 04:36 UTC
- parent
- ec11c42
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +61 -464
M cmd/dumber/main.go +10 -9
| @@ -7,7 +7,6 @@ import ( | |||
| 7 | 7 | "os" | |
| 8 | 8 | "os/signal" | |
| 9 | 9 | "runtime" | |
| 10 | - | "strconv" | |
| 11 | 10 | "syscall" | |
| 12 | 11 | ||
| 13 | 12 | "github.com/bnema/dumber/internal/application/port" | |
| @@ -114,7 +113,7 @@ func runGUI() int { | |||
| 114 | 113 | } | |
| 115 | 114 | timer.MarkDuration("parallel_phase", initResult.Duration) | |
| 116 | 115 | ||
| 117 | - | if multiThreadedCEFLoopEnabled() { | |
| 116 | + | if cfg.Engine.CEF.CEFMultiThreadedMessageLoop() && resolveEngineType(cfg) == "cef" { | |
| 118 | 117 | logging.FromContext(ctx).Info().Msg("pre-initializing libadwaita before CEF multi-threaded loop") | |
| 119 | 118 | ui.EnsureAdwaitaInitialized() | |
| 120 | 119 | if initResult.AdwaitaDetector != nil { | |
| @@ -253,14 +252,16 @@ func initStartupContext(cfg *config.Config) context.Context { | |||
| 253 | 252 | return ctx | |
| 254 | 253 | } | |
| 255 | 254 | ||
| 256 | - | func multiThreadedCEFLoopEnabled() bool { | |
| 257 | - | value, ok := os.LookupEnv("DUMBER_CEF_MULTI_THREADED_MESSAGE_LOOP") | |
| 258 | - | if !ok || value == "" { | |
| 259 | - | return false | |
| 255 | + | // resolveEngineType returns the effective engine type from config + env override. | |
| 256 | + | func resolveEngineType(cfg *config.Config) string { | |
| 257 | + | engineType := cfg.Engine.Type | |
| 258 | + | if engineType == "" { | |
| 259 | + | engineType = "webkit" | |
| 260 | 260 | } | |
| 261 | - | ||
| 262 | - | enabled, err := strconv.ParseBool(value) | |
| 263 | - | return err == nil && enabled | |
| 261 | + | if envEngine := os.Getenv("DUMBER_ENGINE"); envEngine != "" { | |
| 262 | + | engineType = envEngine | |
| 263 | + | } | |
| 264 | + | return engineType | |
| 264 | 265 | } | |
| 265 | 266 | ||
| 266 | 267 | func initStackAndRepos( | |
| @@ -7,7 +7,6 @@ import ( | |||||
| 7 | "os" | 7 | "os" | ||
| 8 | "os/signal" | 8 | "os/signal" | ||
| 9 | "runtime" | 9 | "runtime" | ||
| 10 | - | "strconv" | |||
| 11 | "syscall" | 10 | "syscall" | ||
| 12 | 11 | ||||
| 13 | "github.com/bnema/dumber/internal/application/port" | 12 | "github.com/bnema/dumber/internal/application/port" | ||
| @@ -114,7 +113,7 @@ func runGUI() int { | |||||
| 114 | } | 113 | } | ||
| 115 | timer.MarkDuration("parallel_phase", initResult.Duration) | 114 | timer.MarkDuration("parallel_phase", initResult.Duration) | ||
| 116 | 115 | ||||
| 117 | - | if multiThreadedCEFLoopEnabled() { | 116 | + | if cfg.Engine.CEF.CEFMultiThreadedMessageLoop() && resolveEngineType(cfg) == "cef" { |
| 118 | logging.FromContext(ctx).Info().Msg("pre-initializing libadwaita before CEF multi-threaded loop") | 117 | logging.FromContext(ctx).Info().Msg("pre-initializing libadwaita before CEF multi-threaded loop") | ||
| 119 | ui.EnsureAdwaitaInitialized() | 118 | ui.EnsureAdwaitaInitialized() | ||
| 120 | if initResult.AdwaitaDetector != nil { | 119 | if initResult.AdwaitaDetector != nil { | ||
| @@ -253,14 +252,16 @@ func initStartupContext(cfg *config.Config) context.Context { | |||||
| 253 | return ctx | 252 | return ctx | ||
| 254 | } | 253 | } | ||
| 255 | 254 | ||||
| 256 | - | func multiThreadedCEFLoopEnabled() bool { | 255 | + | // resolveEngineType returns the effective engine type from config + env override. |
| 257 | - | value, ok := os.LookupEnv("DUMBER_CEF_MULTI_THREADED_MESSAGE_LOOP") | 256 | + | func resolveEngineType(cfg *config.Config) string { |
| 258 | - | if !ok || value == "" { | 257 | + | engineType := cfg.Engine.Type |
| 259 | - | return false | 258 | + | if engineType == "" { |
| 259 | + | engineType = "webkit" | |||
| 260 | } | 260 | } | ||
| 261 | - | 261 | + | if envEngine := os.Getenv("DUMBER_ENGINE"); envEngine != "" { | |
| 262 | - | enabled, err := strconv.ParseBool(value) | 262 | + | engineType = envEngine |
| 263 | - | return err == nil && enabled | 263 | + | } |
| 264 | + | return engineType | |||
| 264 | } | 265 | } | ||
| 265 | 266 | ||||
| 266 | func initStackAndRepos( | 267 | func initStackAndRepos( | ||
M internal/infrastructure/cef/engine.go +13 -401
| @@ -2,7 +2,6 @@ package cef | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | - | "os" | |
| 6 | 5 | "sync/atomic" | |
| 7 | 6 | "time" | |
| 8 | 7 | ||
| @@ -16,11 +15,6 @@ import ( | |||
| 16 | 15 | // Compile-time interface check. | |
| 17 | 16 | var _ port.Engine = (*Engine)(nil) | |
| 18 | 17 | ||
| 19 | - | // cefFallbackPumpDelayMS mirrors the upstream external message pump fallback: | |
| 20 | - | // even when no new callback arrives we still re-enter CefDoMessageLoopWork | |
| 21 | - | // periodically so async browser creation and IPC continue to make progress. | |
| 22 | - | const cefFallbackPumpDelayMS int64 = 1000 / 30 | |
| 23 | - | ||
| 24 | 18 | // Engine implements port.Engine for the CEF browser backend. | |
| 25 | 19 | // It manages the CEF lifecycle and provides access to all engine subsystems. | |
| 26 | 20 | type Engine struct { | |
| @@ -30,44 +24,16 @@ type Engine struct { | |||
| 30 | 24 | pool *WebViewPool | |
| 31 | 25 | ||
| 32 | 26 | multiThreadedMessageLoop bool | |
| 33 | - | externalMessagePump bool | |
| 34 | 27 | manualPumpInterval int64 | |
| 35 | 28 | ||
| 36 | - | // nextWorkAtMs is the next deadline, in Unix milliseconds, when | |
| 37 | - | // CefDoMessageLoopWork should run. It is written from any Chromium thread | |
| 38 | - | // via OnScheduleMessagePumpWork and read on the GTK main thread. | |
| 39 | - | // Value: 0 means "nothing scheduled right now". | |
| 40 | - | nextWorkAtMs atomic.Int64 | |
| 41 | - | ||
| 42 | - | // Pump lifecycle. Scheduling callbacks can originate from any Chromium | |
| 43 | - | // thread, while source callbacks execute on the GTK main thread. | |
| 44 | - | pumpEnabled atomic.Bool | |
| 45 | - | pumpClosing atomic.Bool | |
| 46 | - | ||
| 47 | - | pumpSignalPending atomic.Bool | |
| 48 | - | pumpSignalSource atomic.Uint64 | |
| 49 | - | pumpSignalRead *os.File | |
| 50 | - | pumpSignalWrite *os.File | |
| 51 | - | ||
| 52 | - | pumpTimerDueAtMs atomic.Int64 | |
| 53 | - | pumpTimerSource atomic.Uint64 | |
| 54 | - | ||
| 55 | - | // Reentrancy guard for DoMessageLoopWork. | |
| 56 | - | pumpActive bool | |
| 57 | - | pumpReentry bool | |
| 58 | - | ||
| 59 | - | // Diagnostic counters (temporary). | |
| 60 | - | pumpWakeCount uint64 | |
| 61 | - | pumpWorkCount uint64 | |
| 62 | - | pumpWorkTotalNs uint64 | |
| 63 | - | pumpWorkMaxNs int64 | |
| 64 | - | pumpSlowCount uint64 | |
| 65 | - | scheduleCount atomic.Uint64 // incremented from any thread — read from main thread only | |
| 66 | - | ||
| 67 | - | scheduleImmediateCount atomic.Uint64 | |
| 68 | - | scheduleDelayedCount atomic.Uint64 | |
| 69 | - | scheduleLastDelayMs atomic.Int64 | |
| 70 | - | scheduleMaxDelayMs atomic.Int64 | |
| 29 | + | // Pump lifecycle (manual pump mode only). | |
| 30 | + | pumpEnabled atomic.Bool | |
| 31 | + | pumpClosing atomic.Bool | |
| 32 | + | pumpTimerSource atomic.Uint64 | |
| 33 | + | ||
| 34 | + | // Diagnostic counters. | |
| 35 | + | pumpWorkCount uint64 | |
| 36 | + | scheduleCount atomic.Uint64 // OnScheduleMessagePumpWork call count (currently always 0) | |
| 71 | 37 | ||
| 72 | 38 | contextInitializedCount atomic.Uint64 | |
| 73 | 39 | childLaunchRenderer atomic.Uint64 | |
| @@ -81,13 +47,6 @@ type Engine struct { | |||
| 81 | 47 | browserCreateLastWidth atomic.Int32 | |
| 82 | 48 | browserCreateLastHeight atomic.Int32 | |
| 83 | 49 | lastStallLoggedCreateSeq uint64 | |
| 84 | - | ||
| 85 | - | pumpDiagLastLogAt time.Time | |
| 86 | - | pumpDiagLastWakeCount uint64 | |
| 87 | - | pumpDiagLastWorkCount uint64 | |
| 88 | - | pumpDiagLastSchedule uint64 | |
| 89 | - | pumpDiagLastWorkTotalNs uint64 | |
| 90 | - | pumpDiagLastSlowCount uint64 | |
| 91 | 50 | } | |
| 92 | 51 | ||
| 93 | 52 | // Factory returns the WebViewFactory for creating new WebView instances. | |
| @@ -154,7 +113,6 @@ func (e *Engine) OnToolkitReady(_ context.Context) error { | |||
| 154 | 113 | log := logging.FromContext(e.ctx) | |
| 155 | 114 | log.Debug(). | |
| 156 | 115 | Bool("multi_threaded_message_loop", e.multiThreadedMessageLoop). | |
| 157 | - | Bool("external_message_pump", e.externalMessagePump). | |
| 158 | 116 | Int64("manual_pump_interval_ms", e.manualPumpInterval). | |
| 159 | 117 | Msg("cef: OnToolkitReady called, starting message pump") | |
| 160 | 118 | e.startMessagePump() | |
| @@ -268,14 +226,8 @@ func (e *Engine) maybeLogBrowserCreateStall() { | |||
| 268 | 226 | Int32("last_create_result", e.browserCreateLastResult.Load()). | |
| 269 | 227 | Int32("last_create_width", e.browserCreateLastWidth.Load()). | |
| 270 | 228 | Int32("last_create_height", e.browserCreateLastHeight.Load()). | |
| 271 | - | Uint64("pump_wakes", e.pumpWakeCount). | |
| 272 | 229 | Uint64("pump_work", e.pumpWorkCount). | |
| 273 | 230 | Uint64("schedule_calls", e.scheduleCount.Load()). | |
| 274 | - | Uint64("schedule_immediate", e.scheduleImmediateCount.Load()). | |
| 275 | - | Uint64("schedule_delayed", e.scheduleDelayedCount.Load()). | |
| 276 | - | Int64("last_schedule_delay_ms", e.scheduleLastDelayMs.Load()). | |
| 277 | - | Int64("max_schedule_delay_ms", e.scheduleMaxDelayMs.Load()). | |
| 278 | - | Int64("next_work_in_ms", e.nextWorkAtMs.Load()-time.Now().UnixMilli()). | |
| 279 | 231 | Uint64("context_initialized", e.contextInitializedCount.Load()). | |
| 280 | 232 | Uint64("renderer_launches", e.childLaunchRenderer.Load()). | |
| 281 | 233 | Uint64("gpu_launches", e.childLaunchGPU.Load()). | |
| @@ -284,125 +236,24 @@ func (e *Engine) maybeLogBrowserCreateStall() { | |||
| 284 | 236 | } | |
| 285 | 237 | ||
| 286 | 238 | // --------------------------------------------------------------------------- | |
| 287 | - | // CEF message pump — thread-safe via atomic deadlines | |
| 288 | - | // | |
| 289 | - | // OnScheduleMessagePumpWork can be called from ANY Chromium thread (UI, IO, | |
| 290 | - | // GPU compositor, etc.). It updates the next due time atomically and signals a | |
| 291 | - | // persistent GLib fd watch via a wakeup pipe. This closely follows the Linux | |
| 292 | - | // external-message-pump strategy used by upstream cefclient and avoids | |
| 293 | - | // creating/destroying GLib sources from Chromium threads. | |
| 239 | + | // CEF message pump | |
| 294 | 240 | // --------------------------------------------------------------------------- | |
| 295 | 241 | ||
| 296 | 242 | // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any | |
| 297 | - | // thread. It updates the earliest due deadline atomically and signals the GTK | |
| 298 | - | // main thread when it needs to re-evaluate timer state. | |
| 299 | - | func (e *Engine) scheduleMessagePumpWork(delayMs int64) { | |
| 300 | - | if !e.externalMessagePump { | |
| 301 | - | return | |
| 302 | - | } | |
| 303 | - | ||
| 304 | - | nowMs := time.Now().UnixMilli() | |
| 305 | - | dueAtMs := nowMs | |
| 306 | - | if delayMs > 0 { | |
| 307 | - | dueAtMs += delayMs | |
| 308 | - | } | |
| 309 | - | ||
| 310 | - | for { | |
| 311 | - | current := e.nextWorkAtMs.Load() | |
| 312 | - | if current != 0 && current <= dueAtMs { | |
| 313 | - | dueAtMs = current | |
| 314 | - | break | |
| 315 | - | } | |
| 316 | - | if e.nextWorkAtMs.CompareAndSwap(current, dueAtMs) { | |
| 317 | - | break | |
| 318 | - | } | |
| 319 | - | } | |
| 320 | - | ||
| 243 | + | // thread. External message pump is disabled (purego-cef bug), so this is a | |
| 244 | + | // no-op with a diagnostic counter. | |
| 245 | + | func (e *Engine) scheduleMessagePumpWork(_ int64) { | |
| 321 | 246 | e.scheduleCount.Add(1) | |
| 322 | - | e.scheduleLastDelayMs.Store(delayMs) | |
| 323 | - | if delayMs <= 0 { | |
| 324 | - | e.scheduleImmediateCount.Add(1) | |
| 325 | - | } else { | |
| 326 | - | e.scheduleDelayedCount.Add(1) | |
| 327 | - | for { | |
| 328 | - | current := e.scheduleMaxDelayMs.Load() | |
| 329 | - | if delayMs <= current { | |
| 330 | - | break | |
| 331 | - | } | |
| 332 | - | if e.scheduleMaxDelayMs.CompareAndSwap(current, delayMs) { | |
| 333 | - | break | |
| 334 | - | } | |
| 335 | - | } | |
| 336 | - | } | |
| 337 | - | ||
| 338 | - | if !e.pumpEnabled.Load() || e.pumpClosing.Load() { | |
| 339 | - | return | |
| 340 | - | } | |
| 341 | - | ||
| 342 | - | armedDueAtMs := e.pumpTimerDueAtMs.Load() | |
| 343 | - | if dueAtMs <= nowMs || armedDueAtMs == 0 || dueAtMs < armedDueAtMs { | |
| 344 | - | e.signalPump() | |
| 345 | - | } | |
| 346 | 247 | } | |
| 347 | 248 | ||
| 348 | 249 | func (e *Engine) startMessagePump() { | |
| 349 | 250 | if e.multiThreadedMessageLoop { | |
| 350 | - | logging.FromContext(e.ctx).Info().Msg("cef: multi-threaded message loop enabled, skipping external/manual pump") | |
| 351 | - | return | |
| 352 | - | } | |
| 353 | - | if e.externalMessagePump { | |
| 354 | - | e.startExternalMessagePump() | |
| 251 | + | logging.FromContext(e.ctx).Info().Msg("cef: multi-threaded message loop enabled, skipping manual pump") | |
| 355 | 252 | return | |
| 356 | 253 | } | |
| 357 | 254 | e.startManualMessagePump() | |
| 358 | 255 | } | |
| 359 | 256 | ||
| 360 | - | func (e *Engine) startExternalMessagePump() { | |
| 361 | - | if e.pumpEnabled.Load() { | |
| 362 | - | return | |
| 363 | - | } | |
| 364 | - | ||
| 365 | - | readFile, writeFile, err := os.Pipe() | |
| 366 | - | if err != nil { | |
| 367 | - | logging.FromContext(e.ctx).Error().Err(err).Msg("cef: failed to create wakeup pipe") | |
| 368 | - | return | |
| 369 | - | } | |
| 370 | - | ||
| 371 | - | cb := glib.UnixFDSourceFunc(func(_ int, _ glib.IOCondition, _ uintptr) bool { | |
| 372 | - | e.pumpSignalPending.Store(false) | |
| 373 | - | e.drainPumpSignal() | |
| 374 | - | if e.pumpClosing.Load() || !e.pumpEnabled.Load() { | |
| 375 | - | return glib.SOURCE_REMOVE | |
| 376 | - | } | |
| 377 | - | e.onPumpWake() | |
| 378 | - | return glib.SOURCE_CONTINUE | |
| 379 | - | }) | |
| 380 | - | ||
| 381 | - | sourceID := glib.UnixFdAddFull( | |
| 382 | - | glib.PRIORITY_DEFAULT_IDLE, | |
| 383 | - | int(readFile.Fd()), | |
| 384 | - | glib.GIoInValue|glib.GIoHupValue|glib.GIoErrValue, | |
| 385 | - | &cb, | |
| 386 | - | 0, | |
| 387 | - | nil, | |
| 388 | - | ) | |
| 389 | - | ||
| 390 | - | if sourceID == 0 { | |
| 391 | - | _ = readFile.Close() | |
| 392 | - | _ = writeFile.Close() | |
| 393 | - | logging.FromContext(e.ctx).Error().Msg("cef: failed to attach wakeup pipe source") | |
| 394 | - | return | |
| 395 | - | } | |
| 396 | - | ||
| 397 | - | e.pumpSignalRead = readFile | |
| 398 | - | e.pumpSignalWrite = writeFile | |
| 399 | - | e.pumpSignalSource.Store(uint64(sourceID)) | |
| 400 | - | e.pumpClosing.Store(false) | |
| 401 | - | e.pumpEnabled.Store(true) | |
| 402 | - | e.nextWorkAtMs.Store(time.Now().UnixMilli()) | |
| 403 | - | e.signalPump() | |
| 404 | - | } | |
| 405 | - | ||
| 406 | 257 | func (e *Engine) startManualMessagePump() { | |
| 407 | 258 | if e.pumpEnabled.Load() { | |
| 408 | 259 | return | |
| @@ -418,31 +269,16 @@ func (e *Engine) startManualMessagePump() { | |||
| 418 | 269 | return glib.SOURCE_REMOVE | |
| 419 | 270 | } | |
| 420 | 271 | ||
| 421 | - | nowMs := time.Now().UnixMilli() | |
| 422 | - | e.pumpWakeCount++ | |
| 423 | 272 | e.pumpWorkCount++ | |
| 424 | 273 | e.maybeLogBrowserCreateStall() | |
| 425 | 274 | if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 { | |
| 426 | 275 | logging.FromContext(e.ctx).Debug(). | |
| 427 | - | Uint64("wake", e.pumpWakeCount). | |
| 428 | 276 | Uint64("work", e.pumpWorkCount). | |
| 429 | 277 | Int64("interval_ms", intervalMs). | |
| 430 | - | Int64("now_ms", nowMs). | |
| 431 | 278 | Msg("cef: manual pump doing work") | |
| 432 | 279 | } | |
| 433 | 280 | ||
| 434 | - | workStartedAt := time.Now() | |
| 435 | 281 | purecef.DoMessageLoopWork() | |
| 436 | - | workDuration := time.Since(workStartedAt) | |
| 437 | - | e.pumpWorkTotalNs += uint64(workDuration) | |
| 438 | - | if workDuration.Nanoseconds() > e.pumpWorkMaxNs { | |
| 439 | - | e.pumpWorkMaxNs = workDuration.Nanoseconds() | |
| 440 | - | } | |
| 441 | - | if workDuration >= 2*time.Millisecond { | |
| 442 | - | e.pumpSlowCount++ | |
| 443 | - | } | |
| 444 | - | e.maybeLogPumpDiagnostics() | |
| 445 | - | ||
| 446 | 282 | return glib.SOURCE_CONTINUE | |
| 447 | 283 | }) | |
| 448 | 284 | ||
| @@ -457,7 +293,6 @@ func (e *Engine) startManualMessagePump() { | |||
| 457 | 293 | e.pumpClosing.Store(false) | |
| 458 | 294 | e.pumpEnabled.Store(true) | |
| 459 | 295 | e.pumpTimerSource.Store(uint64(sourceID)) | |
| 460 | - | e.pumpTimerDueAtMs.Store(time.Now().UnixMilli() + intervalMs) | |
| 461 | 296 | logging.FromContext(e.ctx).Info(). | |
| 462 | 297 | Int64("interval_ms", intervalMs). | |
| 463 | 298 | Msg("cef: started manual CEF pump") | |
| @@ -469,230 +304,7 @@ func (e *Engine) stopMessagePump() { | |||
| 469 | 304 | } | |
| 470 | 305 | e.pumpClosing.Store(true) | |
| 471 | 306 | e.pumpEnabled.Store(false) | |
| 472 | - | e.nextWorkAtMs.Store(0) | |
| 473 | - | e.pumpTimerDueAtMs.Store(0) | |
| 474 | 307 | if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 { | |
| 475 | 308 | glib.SourceRemove(sourceID) | |
| 476 | 309 | } | |
| 477 | - | if sourceID := uint(e.pumpSignalSource.Swap(0)); sourceID != 0 { | |
| 478 | - | glib.SourceRemove(sourceID) | |
| 479 | - | } | |
| 480 | - | if e.pumpSignalRead != nil { | |
| 481 | - | _ = e.pumpSignalRead.Close() | |
| 482 | - | e.pumpSignalRead = nil | |
| 483 | - | } | |
| 484 | - | if e.pumpSignalWrite != nil { | |
| 485 | - | _ = e.pumpSignalWrite.Close() | |
| 486 | - | e.pumpSignalWrite = nil | |
| 487 | - | } | |
| 488 | - | } | |
| 489 | - | ||
| 490 | - | func (e *Engine) scheduleFallbackPumpWork(nowMs int64) { | |
| 491 | - | if e.nextWorkAtMs.Load() != 0 { | |
| 492 | - | return | |
| 493 | - | } | |
| 494 | - | e.nextWorkAtMs.CompareAndSwap(0, nowMs+cefFallbackPumpDelayMS) | |
| 495 | - | } | |
| 496 | - | ||
| 497 | - | func (e *Engine) signalPump() { | |
| 498 | - | if e.pumpClosing.Load() || !e.pumpEnabled.Load() { | |
| 499 | - | return | |
| 500 | - | } | |
| 501 | - | if !e.pumpSignalPending.CompareAndSwap(false, true) { | |
| 502 | - | return | |
| 503 | - | } | |
| 504 | - | ||
| 505 | - | if e.pumpSignalWrite == nil { | |
| 506 | - | e.pumpSignalPending.Store(false) | |
| 507 | - | return | |
| 508 | - | } | |
| 509 | - | ||
| 510 | - | var wakeByte [1]byte | |
| 511 | - | wakeByte[0] = 1 | |
| 512 | - | if _, err := e.pumpSignalWrite.Write(wakeByte[:]); err != nil { | |
| 513 | - | e.pumpSignalPending.Store(false) | |
| 514 | - | } | |
| 515 | - | } | |
| 516 | - | ||
| 517 | - | func (e *Engine) drainPumpSignal() { | |
| 518 | - | if e.pumpSignalRead == nil { | |
| 519 | - | return | |
| 520 | - | } | |
| 521 | - | ||
| 522 | - | var buf [64]byte | |
| 523 | - | _, _ = e.pumpSignalRead.Read(buf[:]) | |
| 524 | - | } | |
| 525 | - | ||
| 526 | - | func (e *Engine) armPumpTimer(dueAtMs, nowMs int64) { | |
| 527 | - | if e.pumpClosing.Load() || !e.pumpEnabled.Load() { | |
| 528 | - | return | |
| 529 | - | } | |
| 530 | - | ||
| 531 | - | currentDueAtMs := e.pumpTimerDueAtMs.Load() | |
| 532 | - | if currentDueAtMs != 0 && currentDueAtMs <= dueAtMs { | |
| 533 | - | return | |
| 534 | - | } | |
| 535 | - | ||
| 536 | - | if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 { | |
| 537 | - | glib.SourceRemove(sourceID) | |
| 538 | - | } | |
| 539 | - | ||
| 540 | - | delayMs := dueAtMs - nowMs | |
| 541 | - | if delayMs <= 0 { | |
| 542 | - | delayMs = 1 | |
| 543 | - | } | |
| 544 | - | if delayMs > cefFallbackPumpDelayMS { | |
| 545 | - | delayMs = cefFallbackPumpDelayMS | |
| 546 | - | } | |
| 547 | - | ||
| 548 | - | cb := glib.SourceOnceFunc(func(_ uintptr) { | |
| 549 | - | e.pumpTimerSource.Store(0) | |
| 550 | - | e.pumpTimerDueAtMs.Store(0) | |
| 551 | - | if e.pumpClosing.Load() || !e.pumpEnabled.Load() { | |
| 552 | - | return | |
| 553 | - | } | |
| 554 | - | e.onPumpWake() | |
| 555 | - | }) | |
| 556 | - | ||
| 557 | - | sourceID := glib.TimeoutAddOnce(uint(delayMs), &cb, 0) | |
| 558 | - | e.pumpTimerSource.Store(uint64(sourceID)) | |
| 559 | - | e.pumpTimerDueAtMs.Store(nowMs + delayMs) | |
| 560 | - | } | |
| 561 | - | ||
| 562 | - | func (e *Engine) disarmPumpTimer() { | |
| 563 | - | e.pumpTimerDueAtMs.Store(0) | |
| 564 | - | if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 { | |
| 565 | - | glib.SourceRemove(sourceID) | |
| 566 | - | } | |
| 567 | - | } | |
| 568 | - | ||
| 569 | - | // onPumpWake is called on the GTK main thread when the message pump should | |
| 570 | - | // either process due CEF work immediately or arm a one-shot timer for the | |
| 571 | - | // next deadline. | |
| 572 | - | func (e *Engine) onPumpWake() { | |
| 573 | - | e.pumpWakeCount++ | |
| 574 | - | e.maybeLogBrowserCreateStall() | |
| 575 | - | ||
| 576 | - | dueAtMs := e.nextWorkAtMs.Load() | |
| 577 | - | if dueAtMs == 0 { | |
| 578 | - | e.disarmPumpTimer() | |
| 579 | - | return | |
| 580 | - | } | |
| 581 | - | ||
| 582 | - | nowMs := time.Now().UnixMilli() | |
| 583 | - | if nowMs < dueAtMs { | |
| 584 | - | e.armPumpTimer(dueAtMs, nowMs) | |
| 585 | - | return | |
| 586 | - | } | |
| 587 | - | ||
| 588 | - | e.disarmPumpTimer() | |
| 589 | - | e.nextWorkAtMs.Store(0) | |
| 590 | - | e.pumpWorkCount++ | |
| 591 | - | if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 { | |
| 592 | - | logging.FromContext(e.ctx).Debug(). | |
| 593 | - | Uint64("wake", e.pumpWakeCount). | |
| 594 | - | Uint64("work", e.pumpWorkCount). | |
| 595 | - | Uint64("schedule_calls", e.scheduleCount.Load()). | |
| 596 | - | Int64("due_at_ms", dueAtMs). | |
| 597 | - | Int64("now_ms", nowMs). | |
| 598 | - | Msg("cef: pump doing work") | |
| 599 | - | } | |
| 600 | - | workStartedAt := time.Now() | |
| 601 | - | wasReentrant := e.performMessageLoopWork() | |
| 602 | - | workDuration := time.Since(workStartedAt) | |
| 603 | - | e.pumpWorkTotalNs += uint64(workDuration) | |
| 604 | - | if workDuration.Nanoseconds() > e.pumpWorkMaxNs { | |
| 605 | - | e.pumpWorkMaxNs = workDuration.Nanoseconds() | |
| 606 | - | } | |
| 607 | - | if workDuration >= 2*time.Millisecond { | |
| 608 | - | e.pumpSlowCount++ | |
| 609 | - | } | |
| 610 | - | ||
| 611 | - | // Match the upstream external-pump reference behavior: if CEF did not | |
| 612 | - | // request another deadline, keep a placeholder wakeup so async work such as | |
| 613 | - | // browser creation can continue progressing. | |
| 614 | - | nowMs = time.Now().UnixMilli() | |
| 615 | - | if wasReentrant { | |
| 616 | - | e.signalPump() | |
| 617 | - | return | |
| 618 | - | } | |
| 619 | - | e.scheduleFallbackPumpWork(nowMs) | |
| 620 | - | ||
| 621 | - | nextDueAtMs := e.nextWorkAtMs.Load() | |
| 622 | - | if nextDueAtMs == 0 { | |
| 623 | - | return | |
| 624 | - | } | |
| 625 | - | if nextDueAtMs <= nowMs { | |
| 626 | - | e.signalPump() | |
| 627 | - | e.maybeLogPumpDiagnostics() | |
| 628 | - | return | |
| 629 | - | } | |
| 630 | - | e.armPumpTimer(nextDueAtMs, nowMs) | |
| 631 | - | e.maybeLogPumpDiagnostics() | |
| 632 | - | } | |
| 633 | - | ||
| 634 | - | // performMessageLoopWork calls DoMessageLoopWork with reentrancy protection. | |
| 635 | - | // It returns true when re-entrant work was detected and should be reposted. | |
| 636 | - | func (e *Engine) performMessageLoopWork() bool { | |
| 637 | - | if e.pumpActive { | |
| 638 | - | e.pumpReentry = true | |
| 639 | - | return false | |
| 640 | - | } | |
| 641 | - | ||
| 642 | - | e.pumpActive = true | |
| 643 | - | e.pumpReentry = false | |
| 644 | - | purecef.DoMessageLoopWork() | |
| 645 | - | e.pumpActive = false | |
| 646 | - | ||
| 647 | - | return e.pumpReentry | |
| 648 | - | } | |
| 649 | - | ||
| 650 | - | func (e *Engine) maybeLogPumpDiagnostics() { | |
| 651 | - | now := time.Now() | |
| 652 | - | if !e.pumpDiagLastLogAt.IsZero() && now.Sub(e.pumpDiagLastLogAt) < 3*time.Second { | |
| 653 | - | return | |
| 654 | - | } | |
| 655 | - | ||
| 656 | - | wakeDelta := e.pumpWakeCount - e.pumpDiagLastWakeCount | |
| 657 | - | workDelta := e.pumpWorkCount - e.pumpDiagLastWorkCount | |
| 658 | - | scheduleTotal := e.scheduleCount.Load() | |
| 659 | - | scheduleDelta := scheduleTotal - e.pumpDiagLastSchedule | |
| 660 | - | workTotalNsDelta := e.pumpWorkTotalNs - e.pumpDiagLastWorkTotalNs | |
| 661 | - | slowDelta := e.pumpSlowCount - e.pumpDiagLastSlowCount | |
| 662 | - | if wakeDelta == 0 && workDelta == 0 && scheduleDelta == 0 { | |
| 663 | - | return | |
| 664 | - | } | |
| 665 | - | ||
| 666 | - | elapsed := now.Sub(e.pumpDiagLastLogAt) | |
| 667 | - | if e.pumpDiagLastLogAt.IsZero() || elapsed <= 0 { | |
| 668 | - | elapsed = time.Second | |
| 669 | - | } | |
| 670 | - | elapsedSec := elapsed.Seconds() | |
| 671 | - | ||
| 672 | - | avgWorkUs := float64(0) | |
| 673 | - | if workDelta > 0 { | |
| 674 | - | avgWorkUs = float64(workTotalNsDelta) / float64(workDelta) / 1_000 | |
| 675 | - | } | |
| 676 | - | ||
| 677 | - | logging.FromContext(e.ctx).Debug(). | |
| 678 | - | Float64("wake_hz", float64(wakeDelta)/elapsedSec). | |
| 679 | - | Float64("work_hz", float64(workDelta)/elapsedSec). | |
| 680 | - | Float64("schedule_hz", float64(scheduleDelta)/elapsedSec). | |
| 681 | - | Uint64("wake_delta", wakeDelta). | |
| 682 | - | Uint64("work_delta", workDelta). | |
| 683 | - | Uint64("schedule_delta", scheduleDelta). | |
| 684 | - | Float64("avg_do_work_us", avgWorkUs). | |
| 685 | - | Float64("max_do_work_us", float64(e.pumpWorkMaxNs)/1_000). | |
| 686 | - | Uint64("slow_do_work_delta", slowDelta). | |
| 687 | - | Uint64("wake_total", e.pumpWakeCount). | |
| 688 | - | Uint64("work_total", e.pumpWorkCount). | |
| 689 | - | Uint64("schedule_total", scheduleTotal). | |
| 690 | - | Msg("cef: pump activity") | |
| 691 | - | ||
| 692 | - | e.pumpDiagLastLogAt = now | |
| 693 | - | e.pumpDiagLastWakeCount = e.pumpWakeCount | |
| 694 | - | e.pumpDiagLastWorkCount = e.pumpWorkCount | |
| 695 | - | e.pumpDiagLastSchedule = scheduleTotal | |
| 696 | - | e.pumpDiagLastWorkTotalNs = e.pumpWorkTotalNs | |
| 697 | - | e.pumpDiagLastSlowCount = e.pumpSlowCount | |
| 698 | 310 | } | |
| @@ -2,7 +2,6 @@ package cef | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | - | "os" | |||
| 6 | "sync/atomic" | 5 | "sync/atomic" | ||
| 7 | "time" | 6 | "time" | ||
| 8 | 7 | ||||
| @@ -16,11 +15,6 @@ import ( | |||||
| 16 | // Compile-time interface check. | 15 | // Compile-time interface check. | ||
| 17 | var _ port.Engine = (*Engine)(nil) | 16 | var _ port.Engine = (*Engine)(nil) | ||
| 18 | 17 | ||||
| 19 | - | // cefFallbackPumpDelayMS mirrors the upstream external message pump fallback: | |||
| 20 | - | // even when no new callback arrives we still re-enter CefDoMessageLoopWork | |||
| 21 | - | // periodically so async browser creation and IPC continue to make progress. | |||
| 22 | - | const cefFallbackPumpDelayMS int64 = 1000 / 30 | |||
| 23 | - | ||||
| 24 | // Engine implements port.Engine for the CEF browser backend. | 18 | // Engine implements port.Engine for the CEF browser backend. | ||
| 25 | // It manages the CEF lifecycle and provides access to all engine subsystems. | 19 | // It manages the CEF lifecycle and provides access to all engine subsystems. | ||
| 26 | type Engine struct { | 20 | type Engine struct { | ||
| @@ -30,44 +24,16 @@ type Engine struct { | |||||
| 30 | pool *WebViewPool | 24 | pool *WebViewPool | ||
| 31 | 25 | ||||
| 32 | multiThreadedMessageLoop bool | 26 | multiThreadedMessageLoop bool | ||
| 33 | - | externalMessagePump bool | |||
| 34 | manualPumpInterval int64 | 27 | manualPumpInterval int64 | ||
| 35 | 28 | ||||
| 36 | - | // nextWorkAtMs is the next deadline, in Unix milliseconds, when | 29 | + | // Pump lifecycle (manual pump mode only). |
| 37 | - | // CefDoMessageLoopWork should run. It is written from any Chromium thread | 30 | + | pumpEnabled atomic.Bool |
| 38 | - | // via OnScheduleMessagePumpWork and read on the GTK main thread. | 31 | + | pumpClosing atomic.Bool |
| 39 | - | // Value: 0 means "nothing scheduled right now". | 32 | + | pumpTimerSource atomic.Uint64 |
| 40 | - | nextWorkAtMs atomic.Int64 | 33 | + | |
| 41 | - | 34 | + | // Diagnostic counters. | |
| 42 | - | // Pump lifecycle. Scheduling callbacks can originate from any Chromium | 35 | + | pumpWorkCount uint64 |
| 43 | - | // thread, while source callbacks execute on the GTK main thread. | 36 | + | scheduleCount atomic.Uint64 // OnScheduleMessagePumpWork call count (currently always 0) |
| 44 | - | pumpEnabled atomic.Bool | |||
| 45 | - | pumpClosing atomic.Bool | |||
| 46 | - | ||||
| 47 | - | pumpSignalPending atomic.Bool | |||
| 48 | - | pumpSignalSource atomic.Uint64 | |||
| 49 | - | pumpSignalRead *os.File | |||
| 50 | - | pumpSignalWrite *os.File | |||
| 51 | - | ||||
| 52 | - | pumpTimerDueAtMs atomic.Int64 | |||
| 53 | - | pumpTimerSource atomic.Uint64 | |||
| 54 | - | ||||
| 55 | - | // Reentrancy guard for DoMessageLoopWork. | |||
| 56 | - | pumpActive bool | |||
| 57 | - | pumpReentry bool | |||
| 58 | - | ||||
| 59 | - | // Diagnostic counters (temporary). | |||
| 60 | - | pumpWakeCount uint64 | |||
| 61 | - | pumpWorkCount uint64 | |||
| 62 | - | pumpWorkTotalNs uint64 | |||
| 63 | - | pumpWorkMaxNs int64 | |||
| 64 | - | pumpSlowCount uint64 | |||
| 65 | - | scheduleCount atomic.Uint64 // incremented from any thread — read from main thread only | |||
| 66 | - | ||||
| 67 | - | scheduleImmediateCount atomic.Uint64 | |||
| 68 | - | scheduleDelayedCount atomic.Uint64 | |||
| 69 | - | scheduleLastDelayMs atomic.Int64 | |||
| 70 | - | scheduleMaxDelayMs atomic.Int64 | |||
| 71 | 37 | ||||
| 72 | contextInitializedCount atomic.Uint64 | 38 | contextInitializedCount atomic.Uint64 | ||
| 73 | childLaunchRenderer atomic.Uint64 | 39 | childLaunchRenderer atomic.Uint64 | ||
| @@ -81,13 +47,6 @@ type Engine struct { | |||||
| 81 | browserCreateLastWidth atomic.Int32 | 47 | browserCreateLastWidth atomic.Int32 | ||
| 82 | browserCreateLastHeight atomic.Int32 | 48 | browserCreateLastHeight atomic.Int32 | ||
| 83 | lastStallLoggedCreateSeq uint64 | 49 | lastStallLoggedCreateSeq uint64 | ||
| 84 | - | ||||
| 85 | - | pumpDiagLastLogAt time.Time | |||
| 86 | - | pumpDiagLastWakeCount uint64 | |||
| 87 | - | pumpDiagLastWorkCount uint64 | |||
| 88 | - | pumpDiagLastSchedule uint64 | |||
| 89 | - | pumpDiagLastWorkTotalNs uint64 | |||
| 90 | - | pumpDiagLastSlowCount uint64 | |||
| 91 | } | 50 | } | ||
| 92 | 51 | ||||
| 93 | // Factory returns the WebViewFactory for creating new WebView instances. | 52 | // Factory returns the WebViewFactory for creating new WebView instances. | ||
| @@ -154,7 +113,6 @@ func (e *Engine) OnToolkitReady(_ context.Context) error { | |||||
| 154 | log := logging.FromContext(e.ctx) | 113 | log := logging.FromContext(e.ctx) | ||
| 155 | log.Debug(). | 114 | log.Debug(). | ||
| 156 | Bool("multi_threaded_message_loop", e.multiThreadedMessageLoop). | 115 | Bool("multi_threaded_message_loop", e.multiThreadedMessageLoop). | ||
| 157 | - | Bool("external_message_pump", e.externalMessagePump). | |||
| 158 | Int64("manual_pump_interval_ms", e.manualPumpInterval). | 116 | Int64("manual_pump_interval_ms", e.manualPumpInterval). | ||
| 159 | Msg("cef: OnToolkitReady called, starting message pump") | 117 | Msg("cef: OnToolkitReady called, starting message pump") | ||
| 160 | e.startMessagePump() | 118 | e.startMessagePump() | ||
| @@ -268,14 +226,8 @@ func (e *Engine) maybeLogBrowserCreateStall() { | |||||
| 268 | Int32("last_create_result", e.browserCreateLastResult.Load()). | 226 | Int32("last_create_result", e.browserCreateLastResult.Load()). | ||
| 269 | Int32("last_create_width", e.browserCreateLastWidth.Load()). | 227 | Int32("last_create_width", e.browserCreateLastWidth.Load()). | ||
| 270 | Int32("last_create_height", e.browserCreateLastHeight.Load()). | 228 | Int32("last_create_height", e.browserCreateLastHeight.Load()). | ||
| 271 | - | Uint64("pump_wakes", e.pumpWakeCount). | |||
| 272 | Uint64("pump_work", e.pumpWorkCount). | 229 | Uint64("pump_work", e.pumpWorkCount). | ||
| 273 | Uint64("schedule_calls", e.scheduleCount.Load()). | 230 | Uint64("schedule_calls", e.scheduleCount.Load()). | ||
| 274 | - | Uint64("schedule_immediate", e.scheduleImmediateCount.Load()). | |||
| 275 | - | Uint64("schedule_delayed", e.scheduleDelayedCount.Load()). | |||
| 276 | - | Int64("last_schedule_delay_ms", e.scheduleLastDelayMs.Load()). | |||
| 277 | - | Int64("max_schedule_delay_ms", e.scheduleMaxDelayMs.Load()). | |||
| 278 | - | Int64("next_work_in_ms", e.nextWorkAtMs.Load()-time.Now().UnixMilli()). | |||
| 279 | Uint64("context_initialized", e.contextInitializedCount.Load()). | 231 | Uint64("context_initialized", e.contextInitializedCount.Load()). | ||
| 280 | Uint64("renderer_launches", e.childLaunchRenderer.Load()). | 232 | Uint64("renderer_launches", e.childLaunchRenderer.Load()). | ||
| 281 | Uint64("gpu_launches", e.childLaunchGPU.Load()). | 233 | Uint64("gpu_launches", e.childLaunchGPU.Load()). | ||
| @@ -284,125 +236,24 @@ func (e *Engine) maybeLogBrowserCreateStall() { | |||||
| 284 | } | 236 | } | ||
| 285 | 237 | ||||
| 286 | // --------------------------------------------------------------------------- | 238 | // --------------------------------------------------------------------------- | ||
| 287 | - | // CEF message pump — thread-safe via atomic deadlines | 239 | + | // CEF message pump |
| 288 | - | // | |||
| 289 | - | // OnScheduleMessagePumpWork can be called from ANY Chromium thread (UI, IO, | |||
| 290 | - | // GPU compositor, etc.). It updates the next due time atomically and signals a | |||
| 291 | - | // persistent GLib fd watch via a wakeup pipe. This closely follows the Linux | |||
| 292 | - | // external-message-pump strategy used by upstream cefclient and avoids | |||
| 293 | - | // creating/destroying GLib sources from Chromium threads. | |||
| 294 | // --------------------------------------------------------------------------- | 240 | // --------------------------------------------------------------------------- | ||
| 295 | 241 | ||||
| 296 | // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any | 242 | // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any | ||
| 297 | - | // thread. It updates the earliest due deadline atomically and signals the GTK | 243 | + | // thread. External message pump is disabled (purego-cef bug), so this is a |
| 298 | - | // main thread when it needs to re-evaluate timer state. | 244 | + | // no-op with a diagnostic counter. |
| 299 | - | func (e *Engine) scheduleMessagePumpWork(delayMs int64) { | 245 | + | func (e *Engine) scheduleMessagePumpWork(_ int64) { |
| 300 | - | if !e.externalMessagePump { | |||
| 301 | - | return | |||
| 302 | - | } | |||
| 303 | - | ||||
| 304 | - | nowMs := time.Now().UnixMilli() | |||
| 305 | - | dueAtMs := nowMs | |||
| 306 | - | if delayMs > 0 { | |||
| 307 | - | dueAtMs += delayMs | |||
| 308 | - | } | |||
| 309 | - | ||||
| 310 | - | for { | |||
| 311 | - | current := e.nextWorkAtMs.Load() | |||
| 312 | - | if current != 0 && current <= dueAtMs { | |||
| 313 | - | dueAtMs = current | |||
| 314 | - | break | |||
| 315 | - | } | |||
| 316 | - | if e.nextWorkAtMs.CompareAndSwap(current, dueAtMs) { | |||
| 317 | - | break | |||
| 318 | - | } | |||
| 319 | - | } | |||
| 320 | - | ||||
| 321 | e.scheduleCount.Add(1) | 246 | e.scheduleCount.Add(1) | ||
| 322 | - | e.scheduleLastDelayMs.Store(delayMs) | |||
| 323 | - | if delayMs <= 0 { | |||
| 324 | - | e.scheduleImmediateCount.Add(1) | |||
| 325 | - | } else { | |||
| 326 | - | e.scheduleDelayedCount.Add(1) | |||
| 327 | - | for { | |||
| 328 | - | current := e.scheduleMaxDelayMs.Load() | |||
| 329 | - | if delayMs <= current { | |||
| 330 | - | break | |||
| 331 | - | } | |||
| 332 | - | if e.scheduleMaxDelayMs.CompareAndSwap(current, delayMs) { | |||
| 333 | - | break | |||
| 334 | - | } | |||
| 335 | - | } | |||
| 336 | - | } | |||
| 337 | - | ||||
| 338 | - | if !e.pumpEnabled.Load() || e.pumpClosing.Load() { | |||
| 339 | - | return | |||
| 340 | - | } | |||
| 341 | - | ||||
| 342 | - | armedDueAtMs := e.pumpTimerDueAtMs.Load() | |||
| 343 | - | if dueAtMs <= nowMs || armedDueAtMs == 0 || dueAtMs < armedDueAtMs { | |||
| 344 | - | e.signalPump() | |||
| 345 | - | } | |||
| 346 | } | 247 | } | ||
| 347 | 248 | ||||
| 348 | func (e *Engine) startMessagePump() { | 249 | func (e *Engine) startMessagePump() { | ||
| 349 | if e.multiThreadedMessageLoop { | 250 | if e.multiThreadedMessageLoop { | ||
| 350 | - | logging.FromContext(e.ctx).Info().Msg("cef: multi-threaded message loop enabled, skipping external/manual pump") | 251 | + | logging.FromContext(e.ctx).Info().Msg("cef: multi-threaded message loop enabled, skipping manual pump") |
| 351 | - | return | |||
| 352 | - | } | |||
| 353 | - | if e.externalMessagePump { | |||
| 354 | - | e.startExternalMessagePump() | |||
| 355 | return | 252 | return | ||
| 356 | } | 253 | } | ||
| 357 | e.startManualMessagePump() | 254 | e.startManualMessagePump() | ||
| 358 | } | 255 | } | ||
| 359 | 256 | ||||
| 360 | - | func (e *Engine) startExternalMessagePump() { | |||
| 361 | - | if e.pumpEnabled.Load() { | |||
| 362 | - | return | |||
| 363 | - | } | |||
| 364 | - | ||||
| 365 | - | readFile, writeFile, err := os.Pipe() | |||
| 366 | - | if err != nil { | |||
| 367 | - | logging.FromContext(e.ctx).Error().Err(err).Msg("cef: failed to create wakeup pipe") | |||
| 368 | - | return | |||
| 369 | - | } | |||
| 370 | - | ||||
| 371 | - | cb := glib.UnixFDSourceFunc(func(_ int, _ glib.IOCondition, _ uintptr) bool { | |||
| 372 | - | e.pumpSignalPending.Store(false) | |||
| 373 | - | e.drainPumpSignal() | |||
| 374 | - | if e.pumpClosing.Load() || !e.pumpEnabled.Load() { | |||
| 375 | - | return glib.SOURCE_REMOVE | |||
| 376 | - | } | |||
| 377 | - | e.onPumpWake() | |||
| 378 | - | return glib.SOURCE_CONTINUE | |||
| 379 | - | }) | |||
| 380 | - | ||||
| 381 | - | sourceID := glib.UnixFdAddFull( | |||
| 382 | - | glib.PRIORITY_DEFAULT_IDLE, | |||
| 383 | - | int(readFile.Fd()), | |||
| 384 | - | glib.GIoInValue|glib.GIoHupValue|glib.GIoErrValue, | |||
| 385 | - | &cb, | |||
| 386 | - | 0, | |||
| 387 | - | nil, | |||
| 388 | - | ) | |||
| 389 | - | ||||
| 390 | - | if sourceID == 0 { | |||
| 391 | - | _ = readFile.Close() | |||
| 392 | - | _ = writeFile.Close() | |||
| 393 | - | logging.FromContext(e.ctx).Error().Msg("cef: failed to attach wakeup pipe source") | |||
| 394 | - | return | |||
| 395 | - | } | |||
| 396 | - | ||||
| 397 | - | e.pumpSignalRead = readFile | |||
| 398 | - | e.pumpSignalWrite = writeFile | |||
| 399 | - | e.pumpSignalSource.Store(uint64(sourceID)) | |||
| 400 | - | e.pumpClosing.Store(false) | |||
| 401 | - | e.pumpEnabled.Store(true) | |||
| 402 | - | e.nextWorkAtMs.Store(time.Now().UnixMilli()) | |||
| 403 | - | e.signalPump() | |||
| 404 | - | } | |||
| 405 | - | ||||
| 406 | func (e *Engine) startManualMessagePump() { | 257 | func (e *Engine) startManualMessagePump() { | ||
| 407 | if e.pumpEnabled.Load() { | 258 | if e.pumpEnabled.Load() { | ||
| 408 | return | 259 | return | ||
| @@ -418,31 +269,16 @@ func (e *Engine) startManualMessagePump() { | |||||
| 418 | return glib.SOURCE_REMOVE | 269 | return glib.SOURCE_REMOVE | ||
| 419 | } | 270 | } | ||
| 420 | 271 | ||||
| 421 | - | nowMs := time.Now().UnixMilli() | |||
| 422 | - | e.pumpWakeCount++ | |||
| 423 | e.pumpWorkCount++ | 272 | e.pumpWorkCount++ | ||
| 424 | e.maybeLogBrowserCreateStall() | 273 | e.maybeLogBrowserCreateStall() | ||
| 425 | if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 { | 274 | if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 { | ||
| 426 | logging.FromContext(e.ctx).Debug(). | 275 | logging.FromContext(e.ctx).Debug(). | ||
| 427 | - | Uint64("wake", e.pumpWakeCount). | |||
| 428 | Uint64("work", e.pumpWorkCount). | 276 | Uint64("work", e.pumpWorkCount). | ||
| 429 | Int64("interval_ms", intervalMs). | 277 | Int64("interval_ms", intervalMs). | ||
| 430 | - | Int64("now_ms", nowMs). | |||
| 431 | Msg("cef: manual pump doing work") | 278 | Msg("cef: manual pump doing work") | ||
| 432 | } | 279 | } | ||
| 433 | 280 | ||||
| 434 | - | workStartedAt := time.Now() | |||
| 435 | purecef.DoMessageLoopWork() | 281 | purecef.DoMessageLoopWork() | ||
| 436 | - | workDuration := time.Since(workStartedAt) | |||
| 437 | - | e.pumpWorkTotalNs += uint64(workDuration) | |||
| 438 | - | if workDuration.Nanoseconds() > e.pumpWorkMaxNs { | |||
| 439 | - | e.pumpWorkMaxNs = workDuration.Nanoseconds() | |||
| 440 | - | } | |||
| 441 | - | if workDuration >= 2*time.Millisecond { | |||
| 442 | - | e.pumpSlowCount++ | |||
| 443 | - | } | |||
| 444 | - | e.maybeLogPumpDiagnostics() | |||
| 445 | - | ||||
| 446 | return glib.SOURCE_CONTINUE | 282 | return glib.SOURCE_CONTINUE | ||
| 447 | }) | 283 | }) | ||
| 448 | 284 | ||||
| @@ -457,7 +293,6 @@ func (e *Engine) startManualMessagePump() { | |||||
| 457 | e.pumpClosing.Store(false) | 293 | e.pumpClosing.Store(false) | ||
| 458 | e.pumpEnabled.Store(true) | 294 | e.pumpEnabled.Store(true) | ||
| 459 | e.pumpTimerSource.Store(uint64(sourceID)) | 295 | e.pumpTimerSource.Store(uint64(sourceID)) | ||
| 460 | - | e.pumpTimerDueAtMs.Store(time.Now().UnixMilli() + intervalMs) | |||
| 461 | logging.FromContext(e.ctx).Info(). | 296 | logging.FromContext(e.ctx).Info(). | ||
| 462 | Int64("interval_ms", intervalMs). | 297 | Int64("interval_ms", intervalMs). | ||
| 463 | Msg("cef: started manual CEF pump") | 298 | Msg("cef: started manual CEF pump") | ||
| @@ -469,230 +304,7 @@ func (e *Engine) stopMessagePump() { | |||||
| 469 | } | 304 | } | ||
| 470 | e.pumpClosing.Store(true) | 305 | e.pumpClosing.Store(true) | ||
| 471 | e.pumpEnabled.Store(false) | 306 | e.pumpEnabled.Store(false) | ||
| 472 | - | e.nextWorkAtMs.Store(0) | |||
| 473 | - | e.pumpTimerDueAtMs.Store(0) | |||
| 474 | if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 { | 307 | if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 { | ||
| 475 | glib.SourceRemove(sourceID) | 308 | glib.SourceRemove(sourceID) | ||
| 476 | } | 309 | } | ||
| 477 | - | if sourceID := uint(e.pumpSignalSource.Swap(0)); sourceID != 0 { | |||
| 478 | - | glib.SourceRemove(sourceID) | |||
| 479 | - | } | |||
| 480 | - | if e.pumpSignalRead != nil { | |||
| 481 | - | _ = e.pumpSignalRead.Close() | |||
| 482 | - | e.pumpSignalRead = nil | |||
| 483 | - | } | |||
| 484 | - | if e.pumpSignalWrite != nil { | |||
| 485 | - | _ = e.pumpSignalWrite.Close() | |||
| 486 | - | e.pumpSignalWrite = nil | |||
| 487 | - | } | |||
| 488 | - | } | |||
| 489 | - | ||||
| 490 | - | func (e *Engine) scheduleFallbackPumpWork(nowMs int64) { | |||
| 491 | - | if e.nextWorkAtMs.Load() != 0 { | |||
| 492 | - | return | |||
| 493 | - | } | |||
| 494 | - | e.nextWorkAtMs.CompareAndSwap(0, nowMs+cefFallbackPumpDelayMS) | |||
| 495 | - | } | |||
| 496 | - | ||||
| 497 | - | func (e *Engine) signalPump() { | |||
| 498 | - | if e.pumpClosing.Load() || !e.pumpEnabled.Load() { | |||
| 499 | - | return | |||
| 500 | - | } | |||
| 501 | - | if !e.pumpSignalPending.CompareAndSwap(false, true) { | |||
| 502 | - | return | |||
| 503 | - | } | |||
| 504 | - | ||||
| 505 | - | if e.pumpSignalWrite == nil { | |||
| 506 | - | e.pumpSignalPending.Store(false) | |||
| 507 | - | return | |||
| 508 | - | } | |||
| 509 | - | ||||
| 510 | - | var wakeByte [1]byte | |||
| 511 | - | wakeByte[0] = 1 | |||
| 512 | - | if _, err := e.pumpSignalWrite.Write(wakeByte[:]); err != nil { | |||
| 513 | - | e.pumpSignalPending.Store(false) | |||
| 514 | - | } | |||
| 515 | - | } | |||
| 516 | - | ||||
| 517 | - | func (e *Engine) drainPumpSignal() { | |||
| 518 | - | if e.pumpSignalRead == nil { | |||
| 519 | - | return | |||
| 520 | - | } | |||
| 521 | - | ||||
| 522 | - | var buf [64]byte | |||
| 523 | - | _, _ = e.pumpSignalRead.Read(buf[:]) | |||
| 524 | - | } | |||
| 525 | - | ||||
| 526 | - | func (e *Engine) armPumpTimer(dueAtMs, nowMs int64) { | |||
| 527 | - | if e.pumpClosing.Load() || !e.pumpEnabled.Load() { | |||
| 528 | - | return | |||
| 529 | - | } | |||
| 530 | - | ||||
| 531 | - | currentDueAtMs := e.pumpTimerDueAtMs.Load() | |||
| 532 | - | if currentDueAtMs != 0 && currentDueAtMs <= dueAtMs { | |||
| 533 | - | return | |||
| 534 | - | } | |||
| 535 | - | ||||
| 536 | - | if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 { | |||
| 537 | - | glib.SourceRemove(sourceID) | |||
| 538 | - | } | |||
| 539 | - | ||||
| 540 | - | delayMs := dueAtMs - nowMs | |||
| 541 | - | if delayMs <= 0 { | |||
| 542 | - | delayMs = 1 | |||
| 543 | - | } | |||
| 544 | - | if delayMs > cefFallbackPumpDelayMS { | |||
| 545 | - | delayMs = cefFallbackPumpDelayMS | |||
| 546 | - | } | |||
| 547 | - | ||||
| 548 | - | cb := glib.SourceOnceFunc(func(_ uintptr) { | |||
| 549 | - | e.pumpTimerSource.Store(0) | |||
| 550 | - | e.pumpTimerDueAtMs.Store(0) | |||
| 551 | - | if e.pumpClosing.Load() || !e.pumpEnabled.Load() { | |||
| 552 | - | return | |||
| 553 | - | } | |||
| 554 | - | e.onPumpWake() | |||
| 555 | - | }) | |||
| 556 | - | ||||
| 557 | - | sourceID := glib.TimeoutAddOnce(uint(delayMs), &cb, 0) | |||
| 558 | - | e.pumpTimerSource.Store(uint64(sourceID)) | |||
| 559 | - | e.pumpTimerDueAtMs.Store(nowMs + delayMs) | |||
| 560 | - | } | |||
| 561 | - | ||||
| 562 | - | func (e *Engine) disarmPumpTimer() { | |||
| 563 | - | e.pumpTimerDueAtMs.Store(0) | |||
| 564 | - | if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 { | |||
| 565 | - | glib.SourceRemove(sourceID) | |||
| 566 | - | } | |||
| 567 | - | } | |||
| 568 | - | ||||
| 569 | - | // onPumpWake is called on the GTK main thread when the message pump should | |||
| 570 | - | // either process due CEF work immediately or arm a one-shot timer for the | |||
| 571 | - | // next deadline. | |||
| 572 | - | func (e *Engine) onPumpWake() { | |||
| 573 | - | e.pumpWakeCount++ | |||
| 574 | - | e.maybeLogBrowserCreateStall() | |||
| 575 | - | ||||
| 576 | - | dueAtMs := e.nextWorkAtMs.Load() | |||
| 577 | - | if dueAtMs == 0 { | |||
| 578 | - | e.disarmPumpTimer() | |||
| 579 | - | return | |||
| 580 | - | } | |||
| 581 | - | ||||
| 582 | - | nowMs := time.Now().UnixMilli() | |||
| 583 | - | if nowMs < dueAtMs { | |||
| 584 | - | e.armPumpTimer(dueAtMs, nowMs) | |||
| 585 | - | return | |||
| 586 | - | } | |||
| 587 | - | ||||
| 588 | - | e.disarmPumpTimer() | |||
| 589 | - | e.nextWorkAtMs.Store(0) | |||
| 590 | - | e.pumpWorkCount++ | |||
| 591 | - | if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 { | |||
| 592 | - | logging.FromContext(e.ctx).Debug(). | |||
| 593 | - | Uint64("wake", e.pumpWakeCount). | |||
| 594 | - | Uint64("work", e.pumpWorkCount). | |||
| 595 | - | Uint64("schedule_calls", e.scheduleCount.Load()). | |||
| 596 | - | Int64("due_at_ms", dueAtMs). | |||
| 597 | - | Int64("now_ms", nowMs). | |||
| 598 | - | Msg("cef: pump doing work") | |||
| 599 | - | } | |||
| 600 | - | workStartedAt := time.Now() | |||
| 601 | - | wasReentrant := e.performMessageLoopWork() | |||
| 602 | - | workDuration := time.Since(workStartedAt) | |||
| 603 | - | e.pumpWorkTotalNs += uint64(workDuration) | |||
| 604 | - | if workDuration.Nanoseconds() > e.pumpWorkMaxNs { | |||
| 605 | - | e.pumpWorkMaxNs = workDuration.Nanoseconds() | |||
| 606 | - | } | |||
| 607 | - | if workDuration >= 2*time.Millisecond { | |||
| 608 | - | e.pumpSlowCount++ | |||
| 609 | - | } | |||
| 610 | - | ||||
| 611 | - | // Match the upstream external-pump reference behavior: if CEF did not | |||
| 612 | - | // request another deadline, keep a placeholder wakeup so async work such as | |||
| 613 | - | // browser creation can continue progressing. | |||
| 614 | - | nowMs = time.Now().UnixMilli() | |||
| 615 | - | if wasReentrant { | |||
| 616 | - | e.signalPump() | |||
| 617 | - | return | |||
| 618 | - | } | |||
| 619 | - | e.scheduleFallbackPumpWork(nowMs) | |||
| 620 | - | ||||
| 621 | - | nextDueAtMs := e.nextWorkAtMs.Load() | |||
| 622 | - | if nextDueAtMs == 0 { | |||
| 623 | - | return | |||
| 624 | - | } | |||
| 625 | - | if nextDueAtMs <= nowMs { | |||
| 626 | - | e.signalPump() | |||
| 627 | - | e.maybeLogPumpDiagnostics() | |||
| 628 | - | return | |||
| 629 | - | } | |||
| 630 | - | e.armPumpTimer(nextDueAtMs, nowMs) | |||
| 631 | - | e.maybeLogPumpDiagnostics() | |||
| 632 | - | } | |||
| 633 | - | ||||
| 634 | - | // performMessageLoopWork calls DoMessageLoopWork with reentrancy protection. | |||
| 635 | - | // It returns true when re-entrant work was detected and should be reposted. | |||
| 636 | - | func (e *Engine) performMessageLoopWork() bool { | |||
| 637 | - | if e.pumpActive { | |||
| 638 | - | e.pumpReentry = true | |||
| 639 | - | return false | |||
| 640 | - | } | |||
| 641 | - | ||||
| 642 | - | e.pumpActive = true | |||
| 643 | - | e.pumpReentry = false | |||
| 644 | - | purecef.DoMessageLoopWork() | |||
| 645 | - | e.pumpActive = false | |||
| 646 | - | ||||
| 647 | - | return e.pumpReentry | |||
| 648 | - | } | |||
| 649 | - | ||||
| 650 | - | func (e *Engine) maybeLogPumpDiagnostics() { | |||
| 651 | - | now := time.Now() | |||
| 652 | - | if !e.pumpDiagLastLogAt.IsZero() && now.Sub(e.pumpDiagLastLogAt) < 3*time.Second { | |||
| 653 | - | return | |||
| 654 | - | } | |||
| 655 | - | ||||
| 656 | - | wakeDelta := e.pumpWakeCount - e.pumpDiagLastWakeCount | |||
| 657 | - | workDelta := e.pumpWorkCount - e.pumpDiagLastWorkCount | |||
| 658 | - | scheduleTotal := e.scheduleCount.Load() | |||
| 659 | - | scheduleDelta := scheduleTotal - e.pumpDiagLastSchedule | |||
| 660 | - | workTotalNsDelta := e.pumpWorkTotalNs - e.pumpDiagLastWorkTotalNs | |||
| 661 | - | slowDelta := e.pumpSlowCount - e.pumpDiagLastSlowCount | |||
| 662 | - | if wakeDelta == 0 && workDelta == 0 && scheduleDelta == 0 { | |||
| 663 | - | return | |||
| 664 | - | } | |||
| 665 | - | ||||
| 666 | - | elapsed := now.Sub(e.pumpDiagLastLogAt) | |||
| 667 | - | if e.pumpDiagLastLogAt.IsZero() || elapsed <= 0 { | |||
| 668 | - | elapsed = time.Second | |||
| 669 | - | } | |||
| 670 | - | elapsedSec := elapsed.Seconds() | |||
| 671 | - | ||||
| 672 | - | avgWorkUs := float64(0) | |||
| 673 | - | if workDelta > 0 { | |||
| 674 | - | avgWorkUs = float64(workTotalNsDelta) / float64(workDelta) / 1_000 | |||
| 675 | - | } | |||
| 676 | - | ||||
| 677 | - | logging.FromContext(e.ctx).Debug(). | |||
| 678 | - | Float64("wake_hz", float64(wakeDelta)/elapsedSec). | |||
| 679 | - | Float64("work_hz", float64(workDelta)/elapsedSec). | |||
| 680 | - | Float64("schedule_hz", float64(scheduleDelta)/elapsedSec). | |||
| 681 | - | Uint64("wake_delta", wakeDelta). | |||
| 682 | - | Uint64("work_delta", workDelta). | |||
| 683 | - | Uint64("schedule_delta", scheduleDelta). | |||
| 684 | - | Float64("avg_do_work_us", avgWorkUs). | |||
| 685 | - | Float64("max_do_work_us", float64(e.pumpWorkMaxNs)/1_000). | |||
| 686 | - | Uint64("slow_do_work_delta", slowDelta). | |||
| 687 | - | Uint64("wake_total", e.pumpWakeCount). | |||
| 688 | - | Uint64("work_total", e.pumpWorkCount). | |||
| 689 | - | Uint64("schedule_total", scheduleTotal). | |||
| 690 | - | Msg("cef: pump activity") | |||
| 691 | - | ||||
| 692 | - | e.pumpDiagLastLogAt = now | |||
| 693 | - | e.pumpDiagLastWakeCount = e.pumpWakeCount | |||
| 694 | - | e.pumpDiagLastWorkCount = e.pumpWorkCount | |||
| 695 | - | e.pumpDiagLastSchedule = scheduleTotal | |||
| 696 | - | e.pumpDiagLastWorkTotalNs = e.pumpWorkTotalNs | |||
| 697 | - | e.pumpDiagLastSlowCount = e.pumpSlowCount | |||
| 698 | } | 310 | } | ||
M internal/infrastructure/cef/engine_init.go +15 -38
| @@ -5,7 +5,6 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "os" | |
| 7 | 7 | "path/filepath" | |
| 8 | - | "strconv" | |
| 9 | 8 | ||
| 10 | 9 | purecef "github.com/bnema/purego-cef/cef" | |
| 11 | 10 | ||
| @@ -16,13 +15,19 @@ import ( | |||
| 16 | 15 | // NewEngine initializes the CEF runtime and returns a ready-to-use Engine. | |
| 17 | 16 | func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error) { | |
| 18 | 17 | logger := logging.FromContext(ctx) | |
| 18 | + | ||
| 19 | + | // Resolve pump mode from config (with defaults). | |
| 20 | + | multiThreaded := cfg.CEFMultiThreadedMessageLoop() | |
| 21 | + | manualPumpMs := cfg.CEFManualPumpIntervalMs() | |
| 22 | + | ||
| 19 | 23 | // 1. Initialize CEF. | |
| 20 | 24 | settings := purecef.DefaultSettings() | |
| 21 | - | settings.MultiThreadedMessageLoop = parseBoolEnv("DUMBER_CEF_MULTI_THREADED_MESSAGE_LOOP", false) | |
| 22 | - | settings.ExternalMessagePump = parseBoolEnv("DUMBER_CEF_EXTERNAL_PUMP", settings.ExternalMessagePump) | |
| 23 | - | if settings.MultiThreadedMessageLoop { | |
| 24 | - | settings.ExternalMessagePump = false | |
| 25 | - | } | |
| 25 | + | settings.MultiThreadedMessageLoop = multiThreaded | |
| 26 | + | // External message pump is disabled: purego-cef has a bug where | |
| 27 | + | // OnScheduleMessagePumpWork is never called after cef_initialize | |
| 28 | + | // (double-wrapping of BrowserProcessHandler in NewApp). | |
| 29 | + | // When multi-threaded is false, we use a manual glib timer instead. | |
| 30 | + | settings.ExternalMessagePump = false | |
| 26 | 31 | if cfg.CEFDir != "" { | |
| 27 | 32 | settings.CEFDir = cfg.CEFDir | |
| 28 | 33 | } | |
| @@ -57,15 +62,13 @@ func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error) | |||
| 57 | 62 | // 2. Build the engine early so the App can reference it for the pump callback. | |
| 58 | 63 | eng := &Engine{ | |
| 59 | 64 | ctx: ctx, | |
| 60 | - | multiThreadedMessageLoop: settings.MultiThreadedMessageLoop, | |
| 61 | - | externalMessagePump: settings.ExternalMessagePump, | |
| 62 | - | manualPumpInterval: parseInt64Env("DUMBER_CEF_MANUAL_PUMP_MS", 10), | |
| 65 | + | multiThreadedMessageLoop: multiThreaded, | |
| 66 | + | manualPumpInterval: manualPumpMs, | |
| 63 | 67 | } | |
| 64 | 68 | ||
| 65 | 69 | logger.Info(). | |
| 66 | - | Bool("multi_threaded_message_loop", settings.MultiThreadedMessageLoop). | |
| 67 | - | Bool("external_message_pump", settings.ExternalMessagePump). | |
| 68 | - | Int64("manual_pump_interval_ms", eng.manualPumpInterval). | |
| 70 | + | Bool("multi_threaded_message_loop", multiThreaded). | |
| 71 | + | Int64("manual_pump_interval_ms", manualPumpMs). | |
| 69 | 72 | Msg("cef: configured message pump mode") | |
| 70 | 73 | ||
| 71 | 74 | // Create the CEF App with a BrowserProcessHandler that drives the | |
| @@ -123,29 +126,3 @@ func findHelperBinary() string { | |||
| 123 | 126 | } | |
| 124 | 127 | return "" | |
| 125 | 128 | } | |
| 126 | - | ||
| 127 | - | func parseBoolEnv(name string, fallback bool) bool { | |
| 128 | - | value, ok := os.LookupEnv(name) | |
| 129 | - | if !ok || value == "" { | |
| 130 | - | return fallback | |
| 131 | - | } | |
| 132 | - | ||
| 133 | - | parsed, err := strconv.ParseBool(value) | |
| 134 | - | if err != nil { | |
| 135 | - | return fallback | |
| 136 | - | } | |
| 137 | - | return parsed | |
| 138 | - | } | |
| 139 | - | ||
| 140 | - | func parseInt64Env(name string, fallback int64) int64 { | |
| 141 | - | value, ok := os.LookupEnv(name) | |
| 142 | - | if !ok || value == "" { | |
| 143 | - | return fallback | |
| 144 | - | } | |
| 145 | - | ||
| 146 | - | parsed, err := strconv.ParseInt(value, 10, 64) | |
| 147 | - | if err != nil || parsed <= 0 { | |
| 148 | - | return fallback | |
| 149 | - | } | |
| 150 | - | return parsed | |
| 151 | - | } | |
| @@ -5,7 +5,6 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "os" | 6 | "os" | ||
| 7 | "path/filepath" | 7 | "path/filepath" | ||
| 8 | - | "strconv" | |||
| 9 | 8 | ||||
| 10 | purecef "github.com/bnema/purego-cef/cef" | 9 | purecef "github.com/bnema/purego-cef/cef" | ||
| 11 | 10 | ||||
| @@ -16,13 +15,19 @@ import ( | |||||
| 16 | // NewEngine initializes the CEF runtime and returns a ready-to-use Engine. | 15 | // NewEngine initializes the CEF runtime and returns a ready-to-use Engine. | ||
| 17 | func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error) { | 16 | func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error) { | ||
| 18 | logger := logging.FromContext(ctx) | 17 | logger := logging.FromContext(ctx) | ||
| 18 | + | ||||
| 19 | + | // Resolve pump mode from config (with defaults). | |||
| 20 | + | multiThreaded := cfg.CEFMultiThreadedMessageLoop() | |||
| 21 | + | manualPumpMs := cfg.CEFManualPumpIntervalMs() | |||
| 22 | + | ||||
| 19 | // 1. Initialize CEF. | 23 | // 1. Initialize CEF. | ||
| 20 | settings := purecef.DefaultSettings() | 24 | settings := purecef.DefaultSettings() | ||
| 21 | - | settings.MultiThreadedMessageLoop = parseBoolEnv("DUMBER_CEF_MULTI_THREADED_MESSAGE_LOOP", false) | 25 | + | settings.MultiThreadedMessageLoop = multiThreaded |
| 22 | - | settings.ExternalMessagePump = parseBoolEnv("DUMBER_CEF_EXTERNAL_PUMP", settings.ExternalMessagePump) | 26 | + | // External message pump is disabled: purego-cef has a bug where |
| 23 | - | if settings.MultiThreadedMessageLoop { | 27 | + | // OnScheduleMessagePumpWork is never called after cef_initialize |
| 24 | - | settings.ExternalMessagePump = false | 28 | + | // (double-wrapping of BrowserProcessHandler in NewApp). |
| 25 | - | } | 29 | + | // When multi-threaded is false, we use a manual glib timer instead. |
| 30 | + | settings.ExternalMessagePump = false | |||
| 26 | if cfg.CEFDir != "" { | 31 | if cfg.CEFDir != "" { | ||
| 27 | settings.CEFDir = cfg.CEFDir | 32 | settings.CEFDir = cfg.CEFDir | ||
| 28 | } | 33 | } | ||
| @@ -57,15 +62,13 @@ func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error) | |||||
| 57 | // 2. Build the engine early so the App can reference it for the pump callback. | 62 | // 2. Build the engine early so the App can reference it for the pump callback. | ||
| 58 | eng := &Engine{ | 63 | eng := &Engine{ | ||
| 59 | ctx: ctx, | 64 | ctx: ctx, | ||
| 60 | - | multiThreadedMessageLoop: settings.MultiThreadedMessageLoop, | 65 | + | multiThreadedMessageLoop: multiThreaded, |
| 61 | - | externalMessagePump: settings.ExternalMessagePump, | 66 | + | manualPumpInterval: manualPumpMs, |
| 62 | - | manualPumpInterval: parseInt64Env("DUMBER_CEF_MANUAL_PUMP_MS", 10), | |||
| 63 | } | 67 | } | ||
| 64 | 68 | ||||
| 65 | logger.Info(). | 69 | logger.Info(). | ||
| 66 | - | Bool("multi_threaded_message_loop", settings.MultiThreadedMessageLoop). | 70 | + | Bool("multi_threaded_message_loop", multiThreaded). |
| 67 | - | Bool("external_message_pump", settings.ExternalMessagePump). | 71 | + | Int64("manual_pump_interval_ms", manualPumpMs). |
| 68 | - | Int64("manual_pump_interval_ms", eng.manualPumpInterval). | |||
| 69 | Msg("cef: configured message pump mode") | 72 | Msg("cef: configured message pump mode") | ||
| 70 | 73 | ||||
| 71 | // Create the CEF App with a BrowserProcessHandler that drives the | 74 | // Create the CEF App with a BrowserProcessHandler that drives the | ||
| @@ -123,29 +126,3 @@ func findHelperBinary() string { | |||||
| 123 | } | 126 | } | ||
| 124 | return "" | 127 | return "" | ||
| 125 | } | 128 | } | ||
| 126 | - | ||||
| 127 | - | func parseBoolEnv(name string, fallback bool) bool { | |||
| 128 | - | value, ok := os.LookupEnv(name) | |||
| 129 | - | if !ok || value == "" { | |||
| 130 | - | return fallback | |||
| 131 | - | } | |||
| 132 | - | ||||
| 133 | - | parsed, err := strconv.ParseBool(value) | |||
| 134 | - | if err != nil { | |||
| 135 | - | return fallback | |||
| 136 | - | } | |||
| 137 | - | return parsed | |||
| 138 | - | } | |||
| 139 | - | ||||
| 140 | - | func parseInt64Env(name string, fallback int64) int64 { | |||
| 141 | - | value, ok := os.LookupEnv(name) | |||
| 142 | - | if !ok || value == "" { | |||
| 143 | - | return fallback | |||
| 144 | - | } | |||
| 145 | - | ||||
| 146 | - | parsed, err := strconv.ParseInt(value, 10, 64) | |||
| 147 | - | if err != nil || parsed <= 0 { | |||
| 148 | - | return fallback | |||
| 149 | - | } | |||
| 150 | - | return parsed | |||
| 151 | - | } | |||
M internal/infrastructure/config/engine.go +22 -0
| @@ -69,6 +69,28 @@ type CEFEngineConfig struct { | |||
| 69 | 69 | // WindowlessFrameRate is the maximum frame rate for off-screen rendering. | |
| 70 | 70 | // Default: 30. Higher values increase CPU usage. | |
| 71 | 71 | WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"` | |
| 72 | + | // MultiThreadedMessageLoop lets CEF run its own message loop thread. | |
| 73 | + | // Default: true. When false, the host drives the pump via a manual timer. | |
| 74 | + | MultiThreadedMessageLoop *bool `mapstructure:"multi_threaded_message_loop" toml:"multi_threaded_message_loop" yaml:"multi_threaded_message_loop"` //nolint:lll | |
| 75 | + | // ManualPumpIntervalMs is the polling interval (ms) for CefDoMessageLoopWork | |
| 76 | + | // when MultiThreadedMessageLoop is false. Default: 10. | |
| 77 | + | ManualPumpIntervalMs int64 `mapstructure:"manual_pump_interval_ms" toml:"manual_pump_interval_ms" yaml:"manual_pump_interval_ms"` | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // CEFMultiThreadedMessageLoop returns the effective value with default true. | |
| 81 | + | func (c CEFEngineConfig) CEFMultiThreadedMessageLoop() bool { | |
| 82 | + | if c.MultiThreadedMessageLoop != nil { | |
| 83 | + | return *c.MultiThreadedMessageLoop | |
| 84 | + | } | |
| 85 | + | return true | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // CEFManualPumpIntervalMs returns the effective value with default 10. | |
| 89 | + | func (c CEFEngineConfig) CEFManualPumpIntervalMs() int64 { | |
| 90 | + | if c.ManualPumpIntervalMs > 0 { | |
| 91 | + | return c.ManualPumpIntervalMs | |
| 92 | + | } | |
| 93 | + | return 10 | |
| 72 | 94 | } | |
| 73 | 95 | ||
| 74 | 96 | // PerformanceConfigFromEngine constructs a PerformanceConfig from the | |
| @@ -69,6 +69,28 @@ type CEFEngineConfig struct { | |||||
| 69 | // WindowlessFrameRate is the maximum frame rate for off-screen rendering. | 69 | // WindowlessFrameRate is the maximum frame rate for off-screen rendering. | ||
| 70 | // Default: 30. Higher values increase CPU usage. | 70 | // Default: 30. Higher values increase CPU usage. | ||
| 71 | WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"` | 71 | WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"` | ||
| 72 | + | // MultiThreadedMessageLoop lets CEF run its own message loop thread. | |||
| 73 | + | // Default: true. When false, the host drives the pump via a manual timer. | |||
| 74 | + | MultiThreadedMessageLoop *bool `mapstructure:"multi_threaded_message_loop" toml:"multi_threaded_message_loop" yaml:"multi_threaded_message_loop"` //nolint:lll | |||
| 75 | + | // ManualPumpIntervalMs is the polling interval (ms) for CefDoMessageLoopWork | |||
| 76 | + | // when MultiThreadedMessageLoop is false. Default: 10. | |||
| 77 | + | ManualPumpIntervalMs int64 `mapstructure:"manual_pump_interval_ms" toml:"manual_pump_interval_ms" yaml:"manual_pump_interval_ms"` | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // CEFMultiThreadedMessageLoop returns the effective value with default true. | |||
| 81 | + | func (c CEFEngineConfig) CEFMultiThreadedMessageLoop() bool { | |||
| 82 | + | if c.MultiThreadedMessageLoop != nil { | |||
| 83 | + | return *c.MultiThreadedMessageLoop | |||
| 84 | + | } | |||
| 85 | + | return true | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // CEFManualPumpIntervalMs returns the effective value with default 10. | |||
| 89 | + | func (c CEFEngineConfig) CEFManualPumpIntervalMs() int64 { | |||
| 90 | + | if c.ManualPumpIntervalMs > 0 { | |||
| 91 | + | return c.ManualPumpIntervalMs | |||
| 92 | + | } | |||
| 93 | + | return 10 | |||
| 72 | } | 94 | } | ||
| 73 | 95 | ||||
| 74 | // PerformanceConfigFromEngine constructs a PerformanceConfig from the | 96 | // PerformanceConfigFromEngine constructs a PerformanceConfig from the | ||
M internal/ui/app.go +1 -16
| @@ -6,7 +6,6 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "os" | |
| 8 | 8 | "path/filepath" | |
| 9 | - | "strconv" | |
| 10 | 9 | "strings" | |
| 11 | 10 | "sync" | |
| 12 | 11 | "unsafe" | |
| @@ -268,11 +267,7 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 268 | 267 | log := logging.FromContext(ctx) | |
| 269 | 268 | log.Debug().Msg("GTK application activated") | |
| 270 | 269 | ||
| 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 | - | } | |
| 270 | + | a.applyGTKColorSchemePreference(ctx) | |
| 276 | 271 | // Configure pool background color early (prevents white flash), but avoid | |
| 277 | 272 | // synchronous prewarming that delays the first navigation on cold start. | |
| 278 | 273 | a.setupPoolBackgroundColor(ctx) | |
| @@ -475,16 +470,6 @@ func (a *App) installCrashReportNotifier(ctx context.Context) { | |||
| 475 | 470 | } | |
| 476 | 471 | } | |
| 477 | 472 | ||
| 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 | - | ||
| 488 | 473 | func (a *App) initFocusAndBorderOverlay() { | |
| 489 | 474 | // Initialize focus manager for geometric navigation. | |
| 490 | 475 | a.focusMgr = focus.NewManager(a.panesUC) | |
| @@ -6,7 +6,6 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "os" | 7 | "os" | ||
| 8 | "path/filepath" | 8 | "path/filepath" | ||
| 9 | - | "strconv" | |||
| 10 | "strings" | 9 | "strings" | ||
| 11 | "sync" | 10 | "sync" | ||
| 12 | "unsafe" | 11 | "unsafe" | ||
| @@ -268,11 +267,7 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 268 | log := logging.FromContext(ctx) | 267 | log := logging.FromContext(ctx) | ||
| 269 | log.Debug().Msg("GTK application activated") | 268 | log.Debug().Msg("GTK application activated") | ||
| 270 | 269 | ||||
| 271 | - | if cefMultiThreadedLoopEnabled() { | 270 | + | a.applyGTKColorSchemePreference(ctx) |
| 272 | - | log.Warn().Msg("skipping GTK color scheme preference while testing CEF multi-threaded loop") | |||
| 273 | - | } else { | |||
| 274 | - | a.applyGTKColorSchemePreference(ctx) | |||
| 275 | - | } | |||
| 276 | // Configure pool background color early (prevents white flash), but avoid | 271 | // Configure pool background color early (prevents white flash), but avoid | ||
| 277 | // synchronous prewarming that delays the first navigation on cold start. | 272 | // synchronous prewarming that delays the first navigation on cold start. | ||
| 278 | a.setupPoolBackgroundColor(ctx) | 273 | a.setupPoolBackgroundColor(ctx) | ||
| @@ -475,16 +470,6 @@ func (a *App) installCrashReportNotifier(ctx context.Context) { | |||||
| 475 | } | 470 | } | ||
| 476 | } | 471 | } | ||
| 477 | 472 | ||||
| 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 | - | ||||
| 488 | func (a *App) initFocusAndBorderOverlay() { | 473 | func (a *App) initFocusAndBorderOverlay() { | ||
| 489 | // Initialize focus manager for geometric navigation. | 474 | // Initialize focus manager for geometric navigation. | ||
| 490 | a.focusMgr = focus.NewManager(a.panesUC) | 475 | a.focusMgr = focus.NewManager(a.panesUC) | ||