detached 0569599

align cef linux pump with wakeup pipe scheduling

commit
05695999b1addbefa007658cbc748fa1a0c39825
author
brice · · 2026-03-20 10:58 UTC
committer
brice · · 2026-03-28 04:36 UTC
parent
52e0095
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +92 -36
M internal/infrastructure/cef/engine.go +92 -36
@@ -2,6 +2,7 @@ package cef
22
33 import (
44 "context"
5+ "os"
56 "sync/atomic"
67 "time"
78
@@ -39,8 +40,10 @@ type Engine struct {
3940 pumpEnabled atomic.Bool
4041 pumpClosing atomic.Bool
4142
42- pumpWakeQueued atomic.Bool
43- pumpIdleSourceID atomic.Uint64
43+ pumpSignalPending atomic.Bool
44+ pumpSignalSource atomic.Uint64
45+ pumpSignalRead *os.File
46+ pumpSignalWrite *os.File
4447
4548 pumpTimerDueAtMs atomic.Int64
4649 pumpTimerSource atomic.Uint64
@@ -266,14 +269,15 @@ func (e *Engine) maybeLogBrowserCreateStall() {
266269 // CEF message pump — thread-safe via atomic deadlines
267270 //
268271 // OnScheduleMessagePumpWork can be called from ANY Chromium thread (UI, IO,
269-// GPU compositor, etc.). It updates the next due time atomically and coalesces
270-// a wake-up on the GTK main loop. When work is not yet due, a one-shot GLib
271-// timeout is armed for the exact deadline instead of polling every few ms.
272+// GPU compositor, etc.). It updates the next due time atomically and signals a
273+// persistent GLib fd watch via a wakeup pipe. This closely follows the Linux
274+// external-message-pump strategy used by upstream cefclient and avoids
275+// creating/destroying GLib sources from Chromium threads.
272276 // ---------------------------------------------------------------------------
273277
274278 // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any
275-// thread. It updates the earliest due deadline atomically and queues a wake-up
276-// if the main loop needs to re-evaluate timer state.
279+// thread. It updates the earliest due deadline atomically and signals the GTK
280+// main thread when it needs to re-evaluate timer state.
277281 func (e *Engine) scheduleMessagePumpWork(delayMs int64) {
278282 nowMs := time.Now().UnixMilli()
279283 dueAtMs := nowMs
@@ -315,7 +319,7 @@ func (e *Engine) scheduleMessagePumpWork(delayMs int64) {
315319
316320 armedDueAtMs := e.pumpTimerDueAtMs.Load()
317321 if dueAtMs <= nowMs || armedDueAtMs == 0 || dueAtMs < armedDueAtMs {
318- e.queuePumpWake()
322+ e.signalPump()
319323 }
320324 }
321325
@@ -324,10 +328,45 @@ func (e *Engine) startMessagePump() {
324328 return
325329 }
326330
331+ readFile, writeFile, err := os.Pipe()
332+ if err != nil {
333+ logging.FromContext(e.ctx).Error().Err(err).Msg("cef: failed to create wakeup pipe")
334+ return
335+ }
336+
337+ cb := glib.UnixFDSourceFunc(func(_ int, _ glib.IOCondition, _ uintptr) bool {
338+ e.pumpSignalPending.Store(false)
339+ e.drainPumpSignal()
340+ if e.pumpClosing.Load() || !e.pumpEnabled.Load() {
341+ return glib.SOURCE_REMOVE
342+ }
343+ e.onPumpWake()
344+ return glib.SOURCE_CONTINUE
345+ })
346+
347+ sourceID := glib.UnixFdAddFull(
348+ glib.PRIORITY_DEFAULT_IDLE,
349+ int(readFile.Fd()),
350+ glib.GIoInValue|glib.GIoHupValue|glib.GIoErrValue,
351+ &cb,
352+ 0,
353+ nil,
354+ )
355+
356+ if sourceID == 0 {
357+ _ = readFile.Close()
358+ _ = writeFile.Close()
359+ logging.FromContext(e.ctx).Error().Msg("cef: failed to attach wakeup pipe source")
360+ return
361+ }
362+
363+ e.pumpSignalRead = readFile
364+ e.pumpSignalWrite = writeFile
365+ e.pumpSignalSource.Store(uint64(sourceID))
327366 e.pumpClosing.Store(false)
328367 e.pumpEnabled.Store(true)
329368 e.nextWorkAtMs.Store(time.Now().UnixMilli())
330- e.queuePumpWake()
369+ e.signalPump()
331370 }
332371
333372 func (e *Engine) stopMessagePump() {
@@ -335,12 +374,20 @@ func (e *Engine) stopMessagePump() {
335374 e.pumpEnabled.Store(false)
336375 e.nextWorkAtMs.Store(0)
337376 e.pumpTimerDueAtMs.Store(0)
338- if sourceID := uint(e.pumpIdleSourceID.Swap(0)); sourceID != 0 {
377+ if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 {
339378 glib.SourceRemove(sourceID)
340379 }
341- if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 {
380+ if sourceID := uint(e.pumpSignalSource.Swap(0)); sourceID != 0 {
342381 glib.SourceRemove(sourceID)
343382 }
383+ if e.pumpSignalRead != nil {
384+ _ = e.pumpSignalRead.Close()
385+ e.pumpSignalRead = nil
386+ }
387+ if e.pumpSignalWrite != nil {
388+ _ = e.pumpSignalWrite.Close()
389+ e.pumpSignalWrite = nil
390+ }
344391 }
345392
346393 func (e *Engine) scheduleFallbackPumpWork(nowMs int64) {
@@ -350,25 +397,33 @@ func (e *Engine) scheduleFallbackPumpWork(nowMs int64) {
350397 e.nextWorkAtMs.CompareAndSwap(0, nowMs+cefFallbackPumpDelayMS)
351398 }
352399
353-func (e *Engine) queuePumpWake() {
400+func (e *Engine) signalPump() {
354401 if e.pumpClosing.Load() || !e.pumpEnabled.Load() {
355402 return
356403 }
357- if !e.pumpWakeQueued.CompareAndSwap(false, true) {
404+ if !e.pumpSignalPending.CompareAndSwap(false, true) {
358405 return
359406 }
360407
361- cb := glib.SourceOnceFunc(func(_ uintptr) {
362- e.pumpIdleSourceID.Store(0)
363- e.pumpWakeQueued.Store(false)
364- if e.pumpClosing.Load() || !e.pumpEnabled.Load() {
365- return
366- }
367- e.onPumpWake()
368- })
408+ if e.pumpSignalWrite == nil {
409+ e.pumpSignalPending.Store(false)
410+ return
411+ }
369412
370- sourceID := glib.IdleAddOnce(&cb, 0)
371- e.pumpIdleSourceID.Store(uint64(sourceID))
413+ var wakeByte [1]byte
414+ wakeByte[0] = 1
415+ if _, err := e.pumpSignalWrite.Write(wakeByte[:]); err != nil {
416+ e.pumpSignalPending.Store(false)
417+ }
418+}
419+
420+func (e *Engine) drainPumpSignal() {
421+ if e.pumpSignalRead == nil {
422+ return
423+ }
424+
425+ var buf [64]byte
426+ _, _ = e.pumpSignalRead.Read(buf[:])
372427 }
373428
374429 func (e *Engine) armPumpTimer(dueAtMs, nowMs int64) {
@@ -389,6 +444,9 @@ func (e *Engine) armPumpTimer(dueAtMs, nowMs int64) {
389444 if delayMs <= 0 {
390445 delayMs = 1
391446 }
447+ if delayMs > cefFallbackPumpDelayMS {
448+ delayMs = cefFallbackPumpDelayMS
449+ }
392450
393451 cb := glib.SourceOnceFunc(func(_ uintptr) {
394452 e.pumpTimerSource.Store(0)
@@ -401,7 +459,7 @@ func (e *Engine) armPumpTimer(dueAtMs, nowMs int64) {
401459
402460 sourceID := glib.TimeoutAddOnce(uint(delayMs), &cb, 0)
403461 e.pumpTimerSource.Store(uint64(sourceID))
404- e.pumpTimerDueAtMs.Store(dueAtMs)
462+ e.pumpTimerDueAtMs.Store(nowMs + delayMs)
405463 }
406464
407465 func (e *Engine) disarmPumpTimer() {
@@ -442,12 +500,16 @@ func (e *Engine) onPumpWake() {
442500 Int64("now_ms", nowMs).
443501 Msg("cef: pump doing work")
444502 }
445- e.performMessageLoopWork()
503+ wasReentrant := e.performMessageLoopWork()
446504
447505 // Match the upstream external-pump reference behavior: if CEF did not
448506 // request another deadline, keep a placeholder wakeup so async work such as
449507 // browser creation can continue progressing.
450508 nowMs = time.Now().UnixMilli()
509+ if wasReentrant {
510+ e.signalPump()
511+ return
512+ }
451513 e.scheduleFallbackPumpWork(nowMs)
452514
453515 nextDueAtMs := e.nextWorkAtMs.Load()
@@ -455,17 +517,18 @@ func (e *Engine) onPumpWake() {
455517 return
456518 }
457519 if nextDueAtMs <= nowMs {
458- e.queuePumpWake()
520+ e.signalPump()
459521 return
460522 }
461523 e.armPumpTimer(nextDueAtMs, nowMs)
462524 }
463525
464526 // performMessageLoopWork calls DoMessageLoopWork with reentrancy protection.
465-func (e *Engine) performMessageLoopWork() {
527+// It returns true when re-entrant work was detected and should be reposted.
528+func (e *Engine) performMessageLoopWork() bool {
466529 if e.pumpActive {
467530 e.pumpReentry = true
468- return
531+ return false
469532 }
470533
471534 e.pumpActive = true
@@ -473,12 +536,5 @@ func (e *Engine) performMessageLoopWork() {
473536 purecef.DoMessageLoopWork()
474537 e.pumpActive = false
475538
476- // If CEF re-entered during the call, run one more pass immediately. Any
477- // follow-up callback will have already published a fresh deadline.
478- if e.pumpReentry {
479- e.pumpActive = true
480- e.pumpReentry = false
481- purecef.DoMessageLoopWork()
482- e.pumpActive = false
483- }
539+ return e.pumpReentry
484540 }
@@ -2,6 +2,7 @@ package cef
2 2
3 import (3 import (
4 "context"4 "context"
5+ "os"
5 "sync/atomic"6 "sync/atomic"
6 "time"7 "time"
7 8
@@ -39,8 +40,10 @@ type Engine struct {
39 pumpEnabled atomic.Bool40 pumpEnabled atomic.Bool
40 pumpClosing atomic.Bool41 pumpClosing atomic.Bool
41 42
42- pumpWakeQueued atomic.Bool43+ pumpSignalPending atomic.Bool
43- pumpIdleSourceID atomic.Uint6444+ pumpSignalSource atomic.Uint64
45+ pumpSignalRead *os.File
46+ pumpSignalWrite *os.File
44 47
45 pumpTimerDueAtMs atomic.Int6448 pumpTimerDueAtMs atomic.Int64
46 pumpTimerSource atomic.Uint6449 pumpTimerSource atomic.Uint64
@@ -266,14 +269,15 @@ func (e *Engine) maybeLogBrowserCreateStall() {
266 // CEF message pump — thread-safe via atomic deadlines269 // CEF message pump — thread-safe via atomic deadlines
267 //270 //
268 // OnScheduleMessagePumpWork can be called from ANY Chromium thread (UI, IO,271 // OnScheduleMessagePumpWork can be called from ANY Chromium thread (UI, IO,
269-// GPU compositor, etc.). It updates the next due time atomically and coalesces272+// GPU compositor, etc.). It updates the next due time atomically and signals a
270-// a wake-up on the GTK main loop. When work is not yet due, a one-shot GLib273+// persistent GLib fd watch via a wakeup pipe. This closely follows the Linux
271-// timeout is armed for the exact deadline instead of polling every few ms.274+// external-message-pump strategy used by upstream cefclient and avoids
275+// creating/destroying GLib sources from Chromium threads.
272 // ---------------------------------------------------------------------------276 // ---------------------------------------------------------------------------
273 277
274 // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any278 // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any
275-// thread. It updates the earliest due deadline atomically and queues a wake-up279+// thread. It updates the earliest due deadline atomically and signals the GTK
276-// if the main loop needs to re-evaluate timer state.280+// main thread when it needs to re-evaluate timer state.
277 func (e *Engine) scheduleMessagePumpWork(delayMs int64) {281 func (e *Engine) scheduleMessagePumpWork(delayMs int64) {
278 nowMs := time.Now().UnixMilli()282 nowMs := time.Now().UnixMilli()
279 dueAtMs := nowMs283 dueAtMs := nowMs
@@ -315,7 +319,7 @@ func (e *Engine) scheduleMessagePumpWork(delayMs int64) {
315 319
316 armedDueAtMs := e.pumpTimerDueAtMs.Load()320 armedDueAtMs := e.pumpTimerDueAtMs.Load()
317 if dueAtMs <= nowMs || armedDueAtMs == 0 || dueAtMs < armedDueAtMs {321 if dueAtMs <= nowMs || armedDueAtMs == 0 || dueAtMs < armedDueAtMs {
318- e.queuePumpWake()322+ e.signalPump()
319 }323 }
320 }324 }
321 325
@@ -324,10 +328,45 @@ func (e *Engine) startMessagePump() {
324 return328 return
325 }329 }
326 330
331+ readFile, writeFile, err := os.Pipe()
332+ if err != nil {
333+ logging.FromContext(e.ctx).Error().Err(err).Msg("cef: failed to create wakeup pipe")
334+ return
335+ }
336+
337+ cb := glib.UnixFDSourceFunc(func(_ int, _ glib.IOCondition, _ uintptr) bool {
338+ e.pumpSignalPending.Store(false)
339+ e.drainPumpSignal()
340+ if e.pumpClosing.Load() || !e.pumpEnabled.Load() {
341+ return glib.SOURCE_REMOVE
342+ }
343+ e.onPumpWake()
344+ return glib.SOURCE_CONTINUE
345+ })
346+
347+ sourceID := glib.UnixFdAddFull(
348+ glib.PRIORITY_DEFAULT_IDLE,
349+ int(readFile.Fd()),
350+ glib.GIoInValue|glib.GIoHupValue|glib.GIoErrValue,
351+ &cb,
352+ 0,
353+ nil,
354+ )
355+
356+ if sourceID == 0 {
357+ _ = readFile.Close()
358+ _ = writeFile.Close()
359+ logging.FromContext(e.ctx).Error().Msg("cef: failed to attach wakeup pipe source")
360+ return
361+ }
362+
363+ e.pumpSignalRead = readFile
364+ e.pumpSignalWrite = writeFile
365+ e.pumpSignalSource.Store(uint64(sourceID))
327 e.pumpClosing.Store(false)366 e.pumpClosing.Store(false)
328 e.pumpEnabled.Store(true)367 e.pumpEnabled.Store(true)
329 e.nextWorkAtMs.Store(time.Now().UnixMilli())368 e.nextWorkAtMs.Store(time.Now().UnixMilli())
330- e.queuePumpWake()369+ e.signalPump()
331 }370 }
332 371
333 func (e *Engine) stopMessagePump() {372 func (e *Engine) stopMessagePump() {
@@ -335,12 +374,20 @@ func (e *Engine) stopMessagePump() {
335 e.pumpEnabled.Store(false)374 e.pumpEnabled.Store(false)
336 e.nextWorkAtMs.Store(0)375 e.nextWorkAtMs.Store(0)
337 e.pumpTimerDueAtMs.Store(0)376 e.pumpTimerDueAtMs.Store(0)
338- if sourceID := uint(e.pumpIdleSourceID.Swap(0)); sourceID != 0 {377+ if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 {
339 glib.SourceRemove(sourceID)378 glib.SourceRemove(sourceID)
340 }379 }
341- if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 {380+ if sourceID := uint(e.pumpSignalSource.Swap(0)); sourceID != 0 {
342 glib.SourceRemove(sourceID)381 glib.SourceRemove(sourceID)
343 }382 }
383+ if e.pumpSignalRead != nil {
384+ _ = e.pumpSignalRead.Close()
385+ e.pumpSignalRead = nil
386+ }
387+ if e.pumpSignalWrite != nil {
388+ _ = e.pumpSignalWrite.Close()
389+ e.pumpSignalWrite = nil
390+ }
344 }391 }
345 392
346 func (e *Engine) scheduleFallbackPumpWork(nowMs int64) {393 func (e *Engine) scheduleFallbackPumpWork(nowMs int64) {
@@ -350,25 +397,33 @@ func (e *Engine) scheduleFallbackPumpWork(nowMs int64) {
350 e.nextWorkAtMs.CompareAndSwap(0, nowMs+cefFallbackPumpDelayMS)397 e.nextWorkAtMs.CompareAndSwap(0, nowMs+cefFallbackPumpDelayMS)
351 }398 }
352 399
353-func (e *Engine) queuePumpWake() {400+func (e *Engine) signalPump() {
354 if e.pumpClosing.Load() || !e.pumpEnabled.Load() {401 if e.pumpClosing.Load() || !e.pumpEnabled.Load() {
355 return402 return
356 }403 }
357- if !e.pumpWakeQueued.CompareAndSwap(false, true) {404+ if !e.pumpSignalPending.CompareAndSwap(false, true) {
358 return405 return
359 }406 }
360 407
361- cb := glib.SourceOnceFunc(func(_ uintptr) {408+ if e.pumpSignalWrite == nil {
362- e.pumpIdleSourceID.Store(0)409+ e.pumpSignalPending.Store(false)
363- e.pumpWakeQueued.Store(false)410+ return
364- if e.pumpClosing.Load() || !e.pumpEnabled.Load() {411+ }
365- return
366- }
367- e.onPumpWake()
368- })
369 412
370- sourceID := glib.IdleAddOnce(&cb, 0)413+ var wakeByte [1]byte
371- e.pumpIdleSourceID.Store(uint64(sourceID))414+ wakeByte[0] = 1
415+ if _, err := e.pumpSignalWrite.Write(wakeByte[:]); err != nil {
416+ e.pumpSignalPending.Store(false)
417+ }
418+}
419+
420+func (e *Engine) drainPumpSignal() {
421+ if e.pumpSignalRead == nil {
422+ return
423+ }
424+
425+ var buf [64]byte
426+ _, _ = e.pumpSignalRead.Read(buf[:])
372 }427 }
373 428
374 func (e *Engine) armPumpTimer(dueAtMs, nowMs int64) {429 func (e *Engine) armPumpTimer(dueAtMs, nowMs int64) {
@@ -389,6 +444,9 @@ func (e *Engine) armPumpTimer(dueAtMs, nowMs int64) {
389 if delayMs <= 0 {444 if delayMs <= 0 {
390 delayMs = 1445 delayMs = 1
391 }446 }
447+ if delayMs > cefFallbackPumpDelayMS {
448+ delayMs = cefFallbackPumpDelayMS
449+ }
392 450
393 cb := glib.SourceOnceFunc(func(_ uintptr) {451 cb := glib.SourceOnceFunc(func(_ uintptr) {
394 e.pumpTimerSource.Store(0)452 e.pumpTimerSource.Store(0)
@@ -401,7 +459,7 @@ func (e *Engine) armPumpTimer(dueAtMs, nowMs int64) {
401 459
402 sourceID := glib.TimeoutAddOnce(uint(delayMs), &cb, 0)460 sourceID := glib.TimeoutAddOnce(uint(delayMs), &cb, 0)
403 e.pumpTimerSource.Store(uint64(sourceID))461 e.pumpTimerSource.Store(uint64(sourceID))
404- e.pumpTimerDueAtMs.Store(dueAtMs)462+ e.pumpTimerDueAtMs.Store(nowMs + delayMs)
405 }463 }
406 464
407 func (e *Engine) disarmPumpTimer() {465 func (e *Engine) disarmPumpTimer() {
@@ -442,12 +500,16 @@ func (e *Engine) onPumpWake() {
442 Int64("now_ms", nowMs).500 Int64("now_ms", nowMs).
443 Msg("cef: pump doing work")501 Msg("cef: pump doing work")
444 }502 }
445- e.performMessageLoopWork()503+ wasReentrant := e.performMessageLoopWork()
446 504
447 // Match the upstream external-pump reference behavior: if CEF did not505 // Match the upstream external-pump reference behavior: if CEF did not
448 // request another deadline, keep a placeholder wakeup so async work such as506 // request another deadline, keep a placeholder wakeup so async work such as
449 // browser creation can continue progressing.507 // browser creation can continue progressing.
450 nowMs = time.Now().UnixMilli()508 nowMs = time.Now().UnixMilli()
509+ if wasReentrant {
510+ e.signalPump()
511+ return
512+ }
451 e.scheduleFallbackPumpWork(nowMs)513 e.scheduleFallbackPumpWork(nowMs)
452 514
453 nextDueAtMs := e.nextWorkAtMs.Load()515 nextDueAtMs := e.nextWorkAtMs.Load()
@@ -455,17 +517,18 @@ func (e *Engine) onPumpWake() {
455 return517 return
456 }518 }
457 if nextDueAtMs <= nowMs {519 if nextDueAtMs <= nowMs {
458- e.queuePumpWake()520+ e.signalPump()
459 return521 return
460 }522 }
461 e.armPumpTimer(nextDueAtMs, nowMs)523 e.armPumpTimer(nextDueAtMs, nowMs)
462 }524 }
463 525
464 // performMessageLoopWork calls DoMessageLoopWork with reentrancy protection.526 // performMessageLoopWork calls DoMessageLoopWork with reentrancy protection.
465-func (e *Engine) performMessageLoopWork() {527+// It returns true when re-entrant work was detected and should be reposted.
528+func (e *Engine) performMessageLoopWork() bool {
466 if e.pumpActive {529 if e.pumpActive {
467 e.pumpReentry = true530 e.pumpReentry = true
468- return531+ return false
469 }532 }
470 533
471 e.pumpActive = true534 e.pumpActive = true
@@ -473,12 +536,5 @@ func (e *Engine) performMessageLoopWork() {
473 purecef.DoMessageLoopWork()536 purecef.DoMessageLoopWork()
474 e.pumpActive = false537 e.pumpActive = false
475 538
476- // If CEF re-entered during the call, run one more pass immediately. Any539+ return e.pumpReentry
477- // follow-up callback will have already published a fresh deadline.
478- if e.pumpReentry {
479- e.pumpActive = true
480- e.pumpReentry = false
481- purecef.DoMessageLoopWork()
482- e.pumpActive = false
483- }
484 }540 }

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