detached cff8190

match cef external pump fallback scheduling

commit
cff8190cec6fe06f287270b8016b7833b198426e
author
brice · · 2026-03-20 09:58 UTC
committer
brice · · 2026-03-28 04:36 UTC
parent
4fd44b9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +57 -24
M internal/infrastructure/cef/engine.go +57 -24
@@ -16,11 +16,14 @@ import (
1616 var _ port.Engine = (*Engine)(nil)
1717
1818 // pumpTickMS is the interval of the persistent glib timer that checks
19-// for pending CEF work. This is a polling interval, NOT the frame rate —
20-// when there's no pending work, the tick is a single atomic load (near-zero
21-// CPU). When work is pending, it calls DoMessageLoopWork immediately.
19+// whether CEF work is due. This is a polling interval, NOT the frame rate.
2220 const pumpTickMS uint = 4
2321
22+// cefFallbackPumpDelayMS mirrors the upstream external message pump fallback:
23+// even when no new callback arrives we still re-enter CefDoMessageLoopWork
24+// periodically so async browser creation and IPC continue to make progress.
25+const cefFallbackPumpDelayMS int64 = 1000 / 30
26+
2427 // Engine implements port.Engine for the CEF browser backend.
2528 // It manages the CEF lifecycle and provides access to all engine subsystems.
2629 type Engine struct {
@@ -29,11 +32,11 @@ type Engine struct {
2932 factory *WebViewFactory
3033 pool *WebViewPool
3134
32- // pumpRequested is set atomically by OnScheduleMessagePumpWork (which
33- // can be called from ANY thread — GPU, IO, renderer, etc.). The
34- // persistent glib timer on the main thread reads and clears this flag.
35- // Value: 0 = no work, 1 = work requested.
36- pumpRequested atomic.Int32
35+ // nextWorkAtMs is the next deadline, in Unix milliseconds, when
36+ // CefDoMessageLoopWork should run. It is written from any Chromium thread
37+ // via OnScheduleMessagePumpWork and read on the GTK main thread.
38+ // Value: 0 means "nothing scheduled right now".
39+ nextWorkAtMs atomic.Int64
3740
3841 // Pump lifecycle managed only from the main GTK thread.
3942 pumpCB glib.SourceFunc
@@ -254,6 +257,7 @@ func (e *Engine) maybeLogBrowserCreateStall() {
254257 Uint64("schedule_delayed", e.scheduleDelayedCount.Load()).
255258 Int64("last_schedule_delay_ms", e.scheduleLastDelayMs.Load()).
256259 Int64("max_schedule_delay_ms", e.scheduleMaxDelayMs.Load()).
260+ Int64("next_work_in_ms", e.nextWorkAtMs.Load()-time.Now().UnixMilli()).
257261 Uint64("context_initialized", e.contextInitializedCount.Load()).
258262 Uint64("renderer_launches", e.childLaunchRenderer.Load()).
259263 Uint64("gpu_launches", e.childLaunchGPU.Load()).
@@ -262,23 +266,29 @@ func (e *Engine) maybeLogBrowserCreateStall() {
262266 }
263267
264268 // ---------------------------------------------------------------------------
265-// CEF message pump — thread-safe via atomic signaling
269+// CEF message pump — thread-safe via atomic deadlines
266270 //
267271 // OnScheduleMessagePumpWork can be called from ANY Chromium thread (UI, IO,
268272 // GPU compositor, etc.). It MUST NOT call any glib/GTK functions. Instead it
269-// sets an atomic flag. A persistent glib timer on the main GTK thread polls
270-// this flag and calls DoMessageLoopWork when work is pending.
273+// updates the next due time atomically. A persistent glib timer on the main
274+// GTK thread polls that due time and calls DoMessageLoopWork when necessary.
271275 //
272-// CPU cost when idle: one atomic load per tick (4ms) = negligible.
276+// CPU cost when idle: one atomic load and one clock read per tick.
273277 // Latency: max 4ms before CEF work is processed.
274278 // ---------------------------------------------------------------------------
275279
276280 // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any
277-// thread. It only sets an atomic flag — no glib calls, no allocations.
281+// thread. It only updates the due deadline atomically.
278282 // IMPORTANT: No logging, no context access, no allocations here — this can
279283 // be called from native CEF threads (GPU, IO, renderer).
280284 func (e *Engine) scheduleMessagePumpWork(delayMs int64) {
281- e.pumpRequested.Store(1)
285+ nowMs := time.Now().UnixMilli()
286+ dueAtMs := nowMs
287+ if delayMs > 0 {
288+ dueAtMs += delayMs
289+ }
290+
291+ e.nextWorkAtMs.Store(dueAtMs)
282292 e.scheduleCount.Add(1)
283293 e.scheduleLastDelayMs.Store(delayMs)
284294 if delayMs <= 0 {
@@ -303,8 +313,9 @@ func (e *Engine) startMessagePump() {
303313 return
304314 }
305315
306- // Ensure there's a pending request so the first tick does work.
307- e.pumpRequested.Store(1)
316+ // Ensure the first tick enters CEF even if the only startup callback
317+ // happened before GTK finished activating.
318+ e.nextWorkAtMs.Store(time.Now().UnixMilli())
308319
309320 e.pumpCB = glib.SourceFunc(func(_ uintptr) bool {
310321 if e.pumpClosing {
@@ -320,31 +331,55 @@ func (e *Engine) startMessagePump() {
320331
321332 func (e *Engine) stopMessagePump() {
322333 e.pumpClosing = true
334+ e.nextWorkAtMs.Store(0)
323335 if e.pumpSourceID != 0 {
324336 glib.SourceRemove(e.pumpSourceID)
325337 e.pumpSourceID = 0
326338 }
327339 }
328340
341+func (e *Engine) scheduleFallbackPumpWork(nowMs int64) {
342+ if e.nextWorkAtMs.Load() != 0 {
343+ return
344+ }
345+ e.nextWorkAtMs.CompareAndSwap(0, nowMs+cefFallbackPumpDelayMS)
346+}
347+
329348 // pumpTick is called on the GTK main thread by the persistent timer.
330-// It checks if CEF requested work and processes it.
349+// It checks if CEF work is due and processes it.
331350 func (e *Engine) pumpTick() {
332351 e.pumpTickCount++
333352 e.maybeLogBrowserCreateStall()
334- // Check if work was requested (from any thread).
335- if e.pumpRequested.Swap(0) == 0 {
336- return // nothing to do
353+
354+ dueAtMs := e.nextWorkAtMs.Load()
355+ if dueAtMs == 0 {
356+ return
357+ }
358+
359+ nowMs := time.Now().UnixMilli()
360+ if nowMs < dueAtMs {
361+ return
337362 }
338363
364+ // Consume the current deadline before entering CEF. Any new callback that
365+ // happens during DoMessageLoopWork will publish a fresh deadline.
366+ e.nextWorkAtMs.Store(0)
339367 e.pumpWorkCount++
340368 if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 {
341369 logging.FromContext(e.ctx).Debug().
342370 Uint64("tick", e.pumpTickCount).
343371 Uint64("work", e.pumpWorkCount).
344372 Uint64("schedule_calls", e.scheduleCount.Load()).
373+ Int64("due_at_ms", dueAtMs).
374+ Int64("now_ms", nowMs).
345375 Msg("cef: pump doing work")
346376 }
347377 e.performMessageLoopWork()
378+
379+ // Match the upstream external-pump reference behavior: if CEF did not
380+ // request another deadline, keep a placeholder wakeup so async work such as
381+ // browser creation can continue progressing.
382+ e.scheduleFallbackPumpWork(time.Now().UnixMilli())
348383 }
349384
350385 // performMessageLoopWork calls DoMessageLoopWork with reentrancy protection.
@@ -359,10 +394,8 @@ func (e *Engine) performMessageLoopWork() {
359394 purecef.DoMessageLoopWork()
360395 e.pumpActive = false
361396
362- // If CEF re-entered during the call (called OnScheduleMessagePumpWork
363- // from within DoMessageLoopWork), the atomic flag is already set.
364- // The next timer tick (within 4ms) will pick it up.
365- // For immediate re-entrant work, do one more pass now.
397+ // If CEF re-entered during the call, run one more pass immediately. Any
398+ // follow-up callback will have already published a fresh deadline.
366399 if e.pumpReentry {
367400 e.pumpActive = true
368401 e.pumpReentry = false
@@ -16,11 +16,14 @@ import (
16 var _ port.Engine = (*Engine)(nil)16 var _ port.Engine = (*Engine)(nil)
17 17
18 // pumpTickMS is the interval of the persistent glib timer that checks18 // pumpTickMS is the interval of the persistent glib timer that checks
19-// for pending CEF work. This is a polling interval, NOT the frame rate —19+// whether CEF work is due. This is a polling interval, NOT the frame rate.
20-// when there's no pending work, the tick is a single atomic load (near-zero
21-// CPU). When work is pending, it calls DoMessageLoopWork immediately.
22 const pumpTickMS uint = 420 const pumpTickMS uint = 4
23 21
22+// cefFallbackPumpDelayMS mirrors the upstream external message pump fallback:
23+// even when no new callback arrives we still re-enter CefDoMessageLoopWork
24+// periodically so async browser creation and IPC continue to make progress.
25+const cefFallbackPumpDelayMS int64 = 1000 / 30
26+
24 // Engine implements port.Engine for the CEF browser backend.27 // Engine implements port.Engine for the CEF browser backend.
25 // It manages the CEF lifecycle and provides access to all engine subsystems.28 // It manages the CEF lifecycle and provides access to all engine subsystems.
26 type Engine struct {29 type Engine struct {
@@ -29,11 +32,11 @@ type Engine struct {
29 factory *WebViewFactory32 factory *WebViewFactory
30 pool *WebViewPool33 pool *WebViewPool
31 34
32- // pumpRequested is set atomically by OnScheduleMessagePumpWork (which35+ // nextWorkAtMs is the next deadline, in Unix milliseconds, when
33- // can be called from ANY thread — GPU, IO, renderer, etc.). The36+ // CefDoMessageLoopWork should run. It is written from any Chromium thread
34- // persistent glib timer on the main thread reads and clears this flag.37+ // via OnScheduleMessagePumpWork and read on the GTK main thread.
35- // Value: 0 = no work, 1 = work requested.38+ // Value: 0 means "nothing scheduled right now".
36- pumpRequested atomic.Int3239+ nextWorkAtMs atomic.Int64
37 40
38 // Pump lifecycle managed only from the main GTK thread.41 // Pump lifecycle managed only from the main GTK thread.
39 pumpCB glib.SourceFunc42 pumpCB glib.SourceFunc
@@ -254,6 +257,7 @@ func (e *Engine) maybeLogBrowserCreateStall() {
254 Uint64("schedule_delayed", e.scheduleDelayedCount.Load()).257 Uint64("schedule_delayed", e.scheduleDelayedCount.Load()).
255 Int64("last_schedule_delay_ms", e.scheduleLastDelayMs.Load()).258 Int64("last_schedule_delay_ms", e.scheduleLastDelayMs.Load()).
256 Int64("max_schedule_delay_ms", e.scheduleMaxDelayMs.Load()).259 Int64("max_schedule_delay_ms", e.scheduleMaxDelayMs.Load()).
260+ Int64("next_work_in_ms", e.nextWorkAtMs.Load()-time.Now().UnixMilli()).
257 Uint64("context_initialized", e.contextInitializedCount.Load()).261 Uint64("context_initialized", e.contextInitializedCount.Load()).
258 Uint64("renderer_launches", e.childLaunchRenderer.Load()).262 Uint64("renderer_launches", e.childLaunchRenderer.Load()).
259 Uint64("gpu_launches", e.childLaunchGPU.Load()).263 Uint64("gpu_launches", e.childLaunchGPU.Load()).
@@ -262,23 +266,29 @@ func (e *Engine) maybeLogBrowserCreateStall() {
262 }266 }
263 267
264 // ---------------------------------------------------------------------------268 // ---------------------------------------------------------------------------
265-// CEF message pump — thread-safe via atomic signaling269+// CEF message pump — thread-safe via atomic deadlines
266 //270 //
267 // OnScheduleMessagePumpWork can be called from ANY Chromium thread (UI, IO,271 // OnScheduleMessagePumpWork can be called from ANY Chromium thread (UI, IO,
268 // GPU compositor, etc.). It MUST NOT call any glib/GTK functions. Instead it272 // GPU compositor, etc.). It MUST NOT call any glib/GTK functions. Instead it
269-// sets an atomic flag. A persistent glib timer on the main GTK thread polls273+// updates the next due time atomically. A persistent glib timer on the main
270-// this flag and calls DoMessageLoopWork when work is pending.274+// GTK thread polls that due time and calls DoMessageLoopWork when necessary.
271 //275 //
272-// CPU cost when idle: one atomic load per tick (4ms) = negligible.276+// CPU cost when idle: one atomic load and one clock read per tick.
273 // Latency: max 4ms before CEF work is processed.277 // Latency: max 4ms before CEF work is processed.
274 // ---------------------------------------------------------------------------278 // ---------------------------------------------------------------------------
275 279
276 // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any280 // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any
277-// thread. It only sets an atomic flag — no glib calls, no allocations.281+// thread. It only updates the due deadline atomically.
278 // IMPORTANT: No logging, no context access, no allocations here — this can282 // IMPORTANT: No logging, no context access, no allocations here — this can
279 // be called from native CEF threads (GPU, IO, renderer).283 // be called from native CEF threads (GPU, IO, renderer).
280 func (e *Engine) scheduleMessagePumpWork(delayMs int64) {284 func (e *Engine) scheduleMessagePumpWork(delayMs int64) {
281- e.pumpRequested.Store(1)285+ nowMs := time.Now().UnixMilli()
286+ dueAtMs := nowMs
287+ if delayMs > 0 {
288+ dueAtMs += delayMs
289+ }
290+
291+ e.nextWorkAtMs.Store(dueAtMs)
282 e.scheduleCount.Add(1)292 e.scheduleCount.Add(1)
283 e.scheduleLastDelayMs.Store(delayMs)293 e.scheduleLastDelayMs.Store(delayMs)
284 if delayMs <= 0 {294 if delayMs <= 0 {
@@ -303,8 +313,9 @@ func (e *Engine) startMessagePump() {
303 return313 return
304 }314 }
305 315
306- // Ensure there's a pending request so the first tick does work.316+ // Ensure the first tick enters CEF even if the only startup callback
307- e.pumpRequested.Store(1)317+ // happened before GTK finished activating.
318+ e.nextWorkAtMs.Store(time.Now().UnixMilli())
308 319
309 e.pumpCB = glib.SourceFunc(func(_ uintptr) bool {320 e.pumpCB = glib.SourceFunc(func(_ uintptr) bool {
310 if e.pumpClosing {321 if e.pumpClosing {
@@ -320,31 +331,55 @@ func (e *Engine) startMessagePump() {
320 331
321 func (e *Engine) stopMessagePump() {332 func (e *Engine) stopMessagePump() {
322 e.pumpClosing = true333 e.pumpClosing = true
334+ e.nextWorkAtMs.Store(0)
323 if e.pumpSourceID != 0 {335 if e.pumpSourceID != 0 {
324 glib.SourceRemove(e.pumpSourceID)336 glib.SourceRemove(e.pumpSourceID)
325 e.pumpSourceID = 0337 e.pumpSourceID = 0
326 }338 }
327 }339 }
328 340
341+func (e *Engine) scheduleFallbackPumpWork(nowMs int64) {
342+ if e.nextWorkAtMs.Load() != 0 {
343+ return
344+ }
345+ e.nextWorkAtMs.CompareAndSwap(0, nowMs+cefFallbackPumpDelayMS)
346+}
347+
329 // pumpTick is called on the GTK main thread by the persistent timer.348 // pumpTick is called on the GTK main thread by the persistent timer.
330-// It checks if CEF requested work and processes it.349+// It checks if CEF work is due and processes it.
331 func (e *Engine) pumpTick() {350 func (e *Engine) pumpTick() {
332 e.pumpTickCount++351 e.pumpTickCount++
333 e.maybeLogBrowserCreateStall()352 e.maybeLogBrowserCreateStall()
334- // Check if work was requested (from any thread).353+
335- if e.pumpRequested.Swap(0) == 0 {354+ dueAtMs := e.nextWorkAtMs.Load()
336- return // nothing to do355+ if dueAtMs == 0 {
356+ return
357+ }
358+
359+ nowMs := time.Now().UnixMilli()
360+ if nowMs < dueAtMs {
361+ return
337 }362 }
338 363
364+ // Consume the current deadline before entering CEF. Any new callback that
365+ // happens during DoMessageLoopWork will publish a fresh deadline.
366+ e.nextWorkAtMs.Store(0)
339 e.pumpWorkCount++367 e.pumpWorkCount++
340 if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 {368 if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 {
341 logging.FromContext(e.ctx).Debug().369 logging.FromContext(e.ctx).Debug().
342 Uint64("tick", e.pumpTickCount).370 Uint64("tick", e.pumpTickCount).
343 Uint64("work", e.pumpWorkCount).371 Uint64("work", e.pumpWorkCount).
344 Uint64("schedule_calls", e.scheduleCount.Load()).372 Uint64("schedule_calls", e.scheduleCount.Load()).
373+ Int64("due_at_ms", dueAtMs).
374+ Int64("now_ms", nowMs).
345 Msg("cef: pump doing work")375 Msg("cef: pump doing work")
346 }376 }
347 e.performMessageLoopWork()377 e.performMessageLoopWork()
378+
379+ // Match the upstream external-pump reference behavior: if CEF did not
380+ // request another deadline, keep a placeholder wakeup so async work such as
381+ // browser creation can continue progressing.
382+ e.scheduleFallbackPumpWork(time.Now().UnixMilli())
348 }383 }
349 384
350 // performMessageLoopWork calls DoMessageLoopWork with reentrancy protection.385 // performMessageLoopWork calls DoMessageLoopWork with reentrancy protection.
@@ -359,10 +394,8 @@ func (e *Engine) performMessageLoopWork() {
359 purecef.DoMessageLoopWork()394 purecef.DoMessageLoopWork()
360 e.pumpActive = false395 e.pumpActive = false
361 396
362- // If CEF re-entered during the call (called OnScheduleMessagePumpWork397+ // If CEF re-entered during the call, run one more pass immediately. Any
363- // from within DoMessageLoopWork), the atomic flag is already set.398+ // follow-up callback will have already published a fresh deadline.
364- // The next timer tick (within 4ms) will pick it up.
365- // For immediate re-entrant work, do one more pass now.
366 if e.pumpReentry {399 if e.pumpReentry {
367 e.pumpActive = true400 e.pumpActive = true
368 e.pumpReentry = false401 e.pumpReentry = false

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 9

Tags 42

↑↓ moveenter openesc close