detached 52e0095

replace cef pump polling with glib one-shots

commit
52e00950cd473c45ad1dd4ea8435922c90515137
author
brice · · 2026-03-20 10:42 UTC
committer
brice · · 2026-03-28 04:36 UTC
parent
cff8190
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +139 -60
M internal/infrastructure/cef/engine.go +139 -60
@@ -15,10 +15,6 @@ import (
1515 // Compile-time interface check.
1616 var _ port.Engine = (*Engine)(nil)
1717
18-// pumpTickMS is the interval of the persistent glib timer that checks
19-// whether CEF work is due. This is a polling interval, NOT the frame rate.
20-const pumpTickMS uint = 4
21-
2218 // cefFallbackPumpDelayMS mirrors the upstream external message pump fallback:
2319 // even when no new callback arrives we still re-enter CefDoMessageLoopWork
2420 // periodically so async browser creation and IPC continue to make progress.
@@ -38,18 +34,23 @@ type Engine struct {
3834 // Value: 0 means "nothing scheduled right now".
3935 nextWorkAtMs atomic.Int64
4036
41- // Pump lifecycle managed only from the main GTK thread.
42- pumpCB glib.SourceFunc
43- pumpSourceID uint
44- pumpStarted bool
45- pumpClosing bool
37+ // Pump lifecycle. Scheduling callbacks can originate from any Chromium
38+ // thread, while source callbacks execute on the GTK main thread.
39+ pumpEnabled atomic.Bool
40+ pumpClosing atomic.Bool
41+
42+ pumpWakeQueued atomic.Bool
43+ pumpIdleSourceID atomic.Uint64
44+
45+ pumpTimerDueAtMs atomic.Int64
46+ pumpTimerSource atomic.Uint64
4647
4748 // Reentrancy guard for DoMessageLoopWork.
4849 pumpActive bool
4950 pumpReentry bool
5051
5152 // Diagnostic counters (temporary).
52- pumpTickCount uint64
53+ pumpWakeCount uint64
5354 pumpWorkCount uint64
5455 scheduleCount atomic.Uint64 // incremented from any thread — read from main thread only
5556
@@ -223,10 +224,6 @@ func (e *Engine) recordBrowserAfterCreated(browser purecef.Browser) {
223224 }
224225
225226 func (e *Engine) maybeLogBrowserCreateStall() {
226- if e.pumpTickCount%250 != 0 {
227- return
228- }
229-
230227 createCount := e.browserCreateRequests.Load()
231228 if createCount == 0 || createCount == e.browserAfterCreated.Load() || createCount == e.lastStallLoggedCreateSeq {
232229 return
@@ -250,7 +247,7 @@ func (e *Engine) maybeLogBrowserCreateStall() {
250247 Int32("last_create_result", e.browserCreateLastResult.Load()).
251248 Int32("last_create_width", e.browserCreateLastWidth.Load()).
252249 Int32("last_create_height", e.browserCreateLastHeight.Load()).
253- Uint64("pump_ticks", e.pumpTickCount).
250+ Uint64("pump_wakes", e.pumpWakeCount).
254251 Uint64("pump_work", e.pumpWorkCount).
255252 Uint64("schedule_calls", e.scheduleCount.Load()).
256253 Uint64("schedule_immediate", e.scheduleImmediateCount.Load()).
@@ -269,18 +266,14 @@ func (e *Engine) maybeLogBrowserCreateStall() {
269266 // CEF message pump — thread-safe via atomic deadlines
270267 //
271268 // OnScheduleMessagePumpWork can be called from ANY Chromium thread (UI, IO,
272-// GPU compositor, etc.). It MUST NOT call any glib/GTK functions. Instead it
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.
275-//
276-// CPU cost when idle: one atomic load and one clock read per tick.
277-// Latency: max 4ms before CEF work is processed.
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.
278272 // ---------------------------------------------------------------------------
279273
280274 // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any
281-// thread. It only updates the due deadline atomically.
282-// IMPORTANT: No logging, no context access, no allocations here — this can
283-// be called from native CEF threads (GPU, IO, renderer).
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.
284277 func (e *Engine) scheduleMessagePumpWork(delayMs int64) {
285278 nowMs := time.Now().UnixMilli()
286279 dueAtMs := nowMs
@@ -288,53 +281,65 @@ func (e *Engine) scheduleMessagePumpWork(delayMs int64) {
288281 dueAtMs += delayMs
289282 }
290283
291- e.nextWorkAtMs.Store(dueAtMs)
284+ for {
285+ current := e.nextWorkAtMs.Load()
286+ if current != 0 && current <= dueAtMs {
287+ dueAtMs = current
288+ break
289+ }
290+ if e.nextWorkAtMs.CompareAndSwap(current, dueAtMs) {
291+ break
292+ }
293+ }
294+
292295 e.scheduleCount.Add(1)
293296 e.scheduleLastDelayMs.Store(delayMs)
294297 if delayMs <= 0 {
295298 e.scheduleImmediateCount.Add(1)
299+ } else {
300+ e.scheduleDelayedCount.Add(1)
301+ for {
302+ current := e.scheduleMaxDelayMs.Load()
303+ if delayMs <= current {
304+ break
305+ }
306+ if e.scheduleMaxDelayMs.CompareAndSwap(current, delayMs) {
307+ break
308+ }
309+ }
310+ }
311+
312+ if !e.pumpEnabled.Load() || e.pumpClosing.Load() {
296313 return
297314 }
298315
299- e.scheduleDelayedCount.Add(1)
300- for {
301- current := e.scheduleMaxDelayMs.Load()
302- if delayMs <= current {
303- return
304- }
305- if e.scheduleMaxDelayMs.CompareAndSwap(current, delayMs) {
306- return
307- }
316+ armedDueAtMs := e.pumpTimerDueAtMs.Load()
317+ if dueAtMs <= nowMs || armedDueAtMs == 0 || dueAtMs < armedDueAtMs {
318+ e.queuePumpWake()
308319 }
309320 }
310321
311322 func (e *Engine) startMessagePump() {
312- if e.pumpStarted || e.pumpClosing {
323+ if e.pumpEnabled.Load() {
313324 return
314325 }
315326
316- // Ensure the first tick enters CEF even if the only startup callback
317- // happened before GTK finished activating.
327+ e.pumpClosing.Store(false)
328+ e.pumpEnabled.Store(true)
318329 e.nextWorkAtMs.Store(time.Now().UnixMilli())
319-
320- e.pumpCB = glib.SourceFunc(func(_ uintptr) bool {
321- if e.pumpClosing {
322- return false
323- }
324- e.pumpTick()
325- return true // keep the timer running
326- })
327-
328- e.pumpSourceID = glib.TimeoutAdd(pumpTickMS, &e.pumpCB, 0)
329- e.pumpStarted = true
330+ e.queuePumpWake()
330331 }
331332
332333 func (e *Engine) stopMessagePump() {
333- e.pumpClosing = true
334+ e.pumpClosing.Store(true)
335+ e.pumpEnabled.Store(false)
334336 e.nextWorkAtMs.Store(0)
335- if e.pumpSourceID != 0 {
336- glib.SourceRemove(e.pumpSourceID)
337- e.pumpSourceID = 0
337+ e.pumpTimerDueAtMs.Store(0)
338+ if sourceID := uint(e.pumpIdleSourceID.Swap(0)); sourceID != 0 {
339+ glib.SourceRemove(sourceID)
340+ }
341+ if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 {
342+ glib.SourceRemove(sourceID)
338343 }
339344 }
340345
@@ -345,29 +350,92 @@ func (e *Engine) scheduleFallbackPumpWork(nowMs int64) {
345350 e.nextWorkAtMs.CompareAndSwap(0, nowMs+cefFallbackPumpDelayMS)
346351 }
347352
348-// pumpTick is called on the GTK main thread by the persistent timer.
349-// It checks if CEF work is due and processes it.
350-func (e *Engine) pumpTick() {
351- e.pumpTickCount++
353+func (e *Engine) queuePumpWake() {
354+ if e.pumpClosing.Load() || !e.pumpEnabled.Load() {
355+ return
356+ }
357+ if !e.pumpWakeQueued.CompareAndSwap(false, true) {
358+ return
359+ }
360+
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+ })
369+
370+ sourceID := glib.IdleAddOnce(&cb, 0)
371+ e.pumpIdleSourceID.Store(uint64(sourceID))
372+}
373+
374+func (e *Engine) armPumpTimer(dueAtMs, nowMs int64) {
375+ if e.pumpClosing.Load() || !e.pumpEnabled.Load() {
376+ return
377+ }
378+
379+ currentDueAtMs := e.pumpTimerDueAtMs.Load()
380+ if currentDueAtMs != 0 && currentDueAtMs <= dueAtMs {
381+ return
382+ }
383+
384+ if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 {
385+ glib.SourceRemove(sourceID)
386+ }
387+
388+ delayMs := dueAtMs - nowMs
389+ if delayMs <= 0 {
390+ delayMs = 1
391+ }
392+
393+ cb := glib.SourceOnceFunc(func(_ uintptr) {
394+ e.pumpTimerSource.Store(0)
395+ e.pumpTimerDueAtMs.Store(0)
396+ if e.pumpClosing.Load() || !e.pumpEnabled.Load() {
397+ return
398+ }
399+ e.onPumpWake()
400+ })
401+
402+ sourceID := glib.TimeoutAddOnce(uint(delayMs), &cb, 0)
403+ e.pumpTimerSource.Store(uint64(sourceID))
404+ e.pumpTimerDueAtMs.Store(dueAtMs)
405+}
406+
407+func (e *Engine) disarmPumpTimer() {
408+ e.pumpTimerDueAtMs.Store(0)
409+ if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 {
410+ glib.SourceRemove(sourceID)
411+ }
412+}
413+
414+// onPumpWake is called on the GTK main thread when the message pump should
415+// either process due CEF work immediately or arm a one-shot timer for the
416+// next deadline.
417+func (e *Engine) onPumpWake() {
418+ e.pumpWakeCount++
352419 e.maybeLogBrowserCreateStall()
353420
354421 dueAtMs := e.nextWorkAtMs.Load()
355422 if dueAtMs == 0 {
423+ e.disarmPumpTimer()
356424 return
357425 }
358426
359427 nowMs := time.Now().UnixMilli()
360428 if nowMs < dueAtMs {
429+ e.armPumpTimer(dueAtMs, nowMs)
361430 return
362431 }
363432
364- // Consume the current deadline before entering CEF. Any new callback that
365- // happens during DoMessageLoopWork will publish a fresh deadline.
433+ e.disarmPumpTimer()
366434 e.nextWorkAtMs.Store(0)
367435 e.pumpWorkCount++
368436 if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 {
369437 logging.FromContext(e.ctx).Debug().
370- Uint64("tick", e.pumpTickCount).
438+ Uint64("wake", e.pumpWakeCount).
371439 Uint64("work", e.pumpWorkCount).
372440 Uint64("schedule_calls", e.scheduleCount.Load()).
373441 Int64("due_at_ms", dueAtMs).
@@ -379,7 +447,18 @@ func (e *Engine) pumpTick() {
379447 // Match the upstream external-pump reference behavior: if CEF did not
380448 // request another deadline, keep a placeholder wakeup so async work such as
381449 // browser creation can continue progressing.
382- e.scheduleFallbackPumpWork(time.Now().UnixMilli())
450+ nowMs = time.Now().UnixMilli()
451+ e.scheduleFallbackPumpWork(nowMs)
452+
453+ nextDueAtMs := e.nextWorkAtMs.Load()
454+ if nextDueAtMs == 0 {
455+ return
456+ }
457+ if nextDueAtMs <= nowMs {
458+ e.queuePumpWake()
459+ return
460+ }
461+ e.armPumpTimer(nextDueAtMs, nowMs)
383462 }
384463
385464 // performMessageLoopWork calls DoMessageLoopWork with reentrancy protection.
@@ -15,10 +15,6 @@ import (
15 // Compile-time interface check.15 // Compile-time interface check.
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 checks
19-// whether CEF work is due. This is a polling interval, NOT the frame rate.
20-const pumpTickMS uint = 4
21-
22 // cefFallbackPumpDelayMS mirrors the upstream external message pump fallback:18 // cefFallbackPumpDelayMS mirrors the upstream external message pump fallback:
23 // even when no new callback arrives we still re-enter CefDoMessageLoopWork19 // even when no new callback arrives we still re-enter CefDoMessageLoopWork
24 // periodically so async browser creation and IPC continue to make progress.20 // periodically so async browser creation and IPC continue to make progress.
@@ -38,18 +34,23 @@ type Engine struct {
38 // Value: 0 means "nothing scheduled right now".34 // Value: 0 means "nothing scheduled right now".
39 nextWorkAtMs atomic.Int6435 nextWorkAtMs atomic.Int64
40 36
41- // Pump lifecycle managed only from the main GTK thread.37+ // Pump lifecycle. Scheduling callbacks can originate from any Chromium
42- pumpCB glib.SourceFunc38+ // thread, while source callbacks execute on the GTK main thread.
43- pumpSourceID uint39+ pumpEnabled atomic.Bool
44- pumpStarted bool40+ pumpClosing atomic.Bool
45- pumpClosing bool41+
42+ pumpWakeQueued atomic.Bool
43+ pumpIdleSourceID atomic.Uint64
44+
45+ pumpTimerDueAtMs atomic.Int64
46+ pumpTimerSource atomic.Uint64
46 47
47 // Reentrancy guard for DoMessageLoopWork.48 // Reentrancy guard for DoMessageLoopWork.
48 pumpActive bool49 pumpActive bool
49 pumpReentry bool50 pumpReentry bool
50 51
51 // Diagnostic counters (temporary).52 // Diagnostic counters (temporary).
52- pumpTickCount uint6453+ pumpWakeCount uint64
53 pumpWorkCount uint6454 pumpWorkCount uint64
54 scheduleCount atomic.Uint64 // incremented from any thread — read from main thread only55 scheduleCount atomic.Uint64 // incremented from any thread — read from main thread only
55 56
@@ -223,10 +224,6 @@ func (e *Engine) recordBrowserAfterCreated(browser purecef.Browser) {
223 }224 }
224 225
225 func (e *Engine) maybeLogBrowserCreateStall() {226 func (e *Engine) maybeLogBrowserCreateStall() {
226- if e.pumpTickCount%250 != 0 {
227- return
228- }
229-
230 createCount := e.browserCreateRequests.Load()227 createCount := e.browserCreateRequests.Load()
231 if createCount == 0 || createCount == e.browserAfterCreated.Load() || createCount == e.lastStallLoggedCreateSeq {228 if createCount == 0 || createCount == e.browserAfterCreated.Load() || createCount == e.lastStallLoggedCreateSeq {
232 return229 return
@@ -250,7 +247,7 @@ func (e *Engine) maybeLogBrowserCreateStall() {
250 Int32("last_create_result", e.browserCreateLastResult.Load()).247 Int32("last_create_result", e.browserCreateLastResult.Load()).
251 Int32("last_create_width", e.browserCreateLastWidth.Load()).248 Int32("last_create_width", e.browserCreateLastWidth.Load()).
252 Int32("last_create_height", e.browserCreateLastHeight.Load()).249 Int32("last_create_height", e.browserCreateLastHeight.Load()).
253- Uint64("pump_ticks", e.pumpTickCount).250+ Uint64("pump_wakes", e.pumpWakeCount).
254 Uint64("pump_work", e.pumpWorkCount).251 Uint64("pump_work", e.pumpWorkCount).
255 Uint64("schedule_calls", e.scheduleCount.Load()).252 Uint64("schedule_calls", e.scheduleCount.Load()).
256 Uint64("schedule_immediate", e.scheduleImmediateCount.Load()).253 Uint64("schedule_immediate", e.scheduleImmediateCount.Load()).
@@ -269,18 +266,14 @@ func (e *Engine) maybeLogBrowserCreateStall() {
269 // CEF message pump — thread-safe via atomic deadlines266 // CEF message pump — thread-safe via atomic deadlines
270 //267 //
271 // OnScheduleMessagePumpWork can be called from ANY Chromium thread (UI, IO,268 // OnScheduleMessagePumpWork can be called from ANY Chromium thread (UI, IO,
272-// GPU compositor, etc.). It MUST NOT call any glib/GTK functions. Instead it269+// GPU compositor, etc.). It updates the next due time atomically and coalesces
273-// updates the next due time atomically. A persistent glib timer on the main270+// a wake-up on the GTK main loop. When work is not yet due, a one-shot GLib
274-// GTK thread polls that due time and calls DoMessageLoopWork when necessary.271+// timeout is armed for the exact deadline instead of polling every few ms.
275-//
276-// CPU cost when idle: one atomic load and one clock read per tick.
277-// Latency: max 4ms before CEF work is processed.
278 // ---------------------------------------------------------------------------272 // ---------------------------------------------------------------------------
279 273
280 // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any274 // scheduleMessagePumpWork is called by OnScheduleMessagePumpWork from any
281-// thread. It only updates the due deadline atomically.275+// thread. It updates the earliest due deadline atomically and queues a wake-up
282-// IMPORTANT: No logging, no context access, no allocations here — this can276+// if the main loop needs to re-evaluate timer state.
283-// be called from native CEF threads (GPU, IO, renderer).
284 func (e *Engine) scheduleMessagePumpWork(delayMs int64) {277 func (e *Engine) scheduleMessagePumpWork(delayMs int64) {
285 nowMs := time.Now().UnixMilli()278 nowMs := time.Now().UnixMilli()
286 dueAtMs := nowMs279 dueAtMs := nowMs
@@ -288,53 +281,65 @@ func (e *Engine) scheduleMessagePumpWork(delayMs int64) {
288 dueAtMs += delayMs281 dueAtMs += delayMs
289 }282 }
290 283
291- e.nextWorkAtMs.Store(dueAtMs)284+ for {
285+ current := e.nextWorkAtMs.Load()
286+ if current != 0 && current <= dueAtMs {
287+ dueAtMs = current
288+ break
289+ }
290+ if e.nextWorkAtMs.CompareAndSwap(current, dueAtMs) {
291+ break
292+ }
293+ }
294+
292 e.scheduleCount.Add(1)295 e.scheduleCount.Add(1)
293 e.scheduleLastDelayMs.Store(delayMs)296 e.scheduleLastDelayMs.Store(delayMs)
294 if delayMs <= 0 {297 if delayMs <= 0 {
295 e.scheduleImmediateCount.Add(1)298 e.scheduleImmediateCount.Add(1)
299+ } else {
300+ e.scheduleDelayedCount.Add(1)
301+ for {
302+ current := e.scheduleMaxDelayMs.Load()
303+ if delayMs <= current {
304+ break
305+ }
306+ if e.scheduleMaxDelayMs.CompareAndSwap(current, delayMs) {
307+ break
308+ }
309+ }
310+ }
311+
312+ if !e.pumpEnabled.Load() || e.pumpClosing.Load() {
296 return313 return
297 }314 }
298 315
299- e.scheduleDelayedCount.Add(1)316+ armedDueAtMs := e.pumpTimerDueAtMs.Load()
300- for {317+ if dueAtMs <= nowMs || armedDueAtMs == 0 || dueAtMs < armedDueAtMs {
301- current := e.scheduleMaxDelayMs.Load()318+ e.queuePumpWake()
302- if delayMs <= current {
303- return
304- }
305- if e.scheduleMaxDelayMs.CompareAndSwap(current, delayMs) {
306- return
307- }
308 }319 }
309 }320 }
310 321
311 func (e *Engine) startMessagePump() {322 func (e *Engine) startMessagePump() {
312- if e.pumpStarted || e.pumpClosing {323+ if e.pumpEnabled.Load() {
313 return324 return
314 }325 }
315 326
316- // Ensure the first tick enters CEF even if the only startup callback327+ e.pumpClosing.Store(false)
317- // happened before GTK finished activating.328+ e.pumpEnabled.Store(true)
318 e.nextWorkAtMs.Store(time.Now().UnixMilli())329 e.nextWorkAtMs.Store(time.Now().UnixMilli())
319-330+ e.queuePumpWake()
320- e.pumpCB = glib.SourceFunc(func(_ uintptr) bool {
321- if e.pumpClosing {
322- return false
323- }
324- e.pumpTick()
325- return true // keep the timer running
326- })
327-
328- e.pumpSourceID = glib.TimeoutAdd(pumpTickMS, &e.pumpCB, 0)
329- e.pumpStarted = true
330 }331 }
331 332
332 func (e *Engine) stopMessagePump() {333 func (e *Engine) stopMessagePump() {
333- e.pumpClosing = true334+ e.pumpClosing.Store(true)
335+ e.pumpEnabled.Store(false)
334 e.nextWorkAtMs.Store(0)336 e.nextWorkAtMs.Store(0)
335- if e.pumpSourceID != 0 {337+ e.pumpTimerDueAtMs.Store(0)
336- glib.SourceRemove(e.pumpSourceID)338+ if sourceID := uint(e.pumpIdleSourceID.Swap(0)); sourceID != 0 {
337- e.pumpSourceID = 0339+ glib.SourceRemove(sourceID)
340+ }
341+ if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 {
342+ glib.SourceRemove(sourceID)
338 }343 }
339 }344 }
340 345
@@ -345,29 +350,92 @@ func (e *Engine) scheduleFallbackPumpWork(nowMs int64) {
345 e.nextWorkAtMs.CompareAndSwap(0, nowMs+cefFallbackPumpDelayMS)350 e.nextWorkAtMs.CompareAndSwap(0, nowMs+cefFallbackPumpDelayMS)
346 }351 }
347 352
348-// pumpTick is called on the GTK main thread by the persistent timer.353+func (e *Engine) queuePumpWake() {
349-// It checks if CEF work is due and processes it.354+ if e.pumpClosing.Load() || !e.pumpEnabled.Load() {
350-func (e *Engine) pumpTick() {355+ return
351- e.pumpTickCount++356+ }
357+ if !e.pumpWakeQueued.CompareAndSwap(false, true) {
358+ return
359+ }
360+
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+ })
369+
370+ sourceID := glib.IdleAddOnce(&cb, 0)
371+ e.pumpIdleSourceID.Store(uint64(sourceID))
372+}
373+
374+func (e *Engine) armPumpTimer(dueAtMs, nowMs int64) {
375+ if e.pumpClosing.Load() || !e.pumpEnabled.Load() {
376+ return
377+ }
378+
379+ currentDueAtMs := e.pumpTimerDueAtMs.Load()
380+ if currentDueAtMs != 0 && currentDueAtMs <= dueAtMs {
381+ return
382+ }
383+
384+ if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 {
385+ glib.SourceRemove(sourceID)
386+ }
387+
388+ delayMs := dueAtMs - nowMs
389+ if delayMs <= 0 {
390+ delayMs = 1
391+ }
392+
393+ cb := glib.SourceOnceFunc(func(_ uintptr) {
394+ e.pumpTimerSource.Store(0)
395+ e.pumpTimerDueAtMs.Store(0)
396+ if e.pumpClosing.Load() || !e.pumpEnabled.Load() {
397+ return
398+ }
399+ e.onPumpWake()
400+ })
401+
402+ sourceID := glib.TimeoutAddOnce(uint(delayMs), &cb, 0)
403+ e.pumpTimerSource.Store(uint64(sourceID))
404+ e.pumpTimerDueAtMs.Store(dueAtMs)
405+}
406+
407+func (e *Engine) disarmPumpTimer() {
408+ e.pumpTimerDueAtMs.Store(0)
409+ if sourceID := uint(e.pumpTimerSource.Swap(0)); sourceID != 0 {
410+ glib.SourceRemove(sourceID)
411+ }
412+}
413+
414+// onPumpWake is called on the GTK main thread when the message pump should
415+// either process due CEF work immediately or arm a one-shot timer for the
416+// next deadline.
417+func (e *Engine) onPumpWake() {
418+ e.pumpWakeCount++
352 e.maybeLogBrowserCreateStall()419 e.maybeLogBrowserCreateStall()
353 420
354 dueAtMs := e.nextWorkAtMs.Load()421 dueAtMs := e.nextWorkAtMs.Load()
355 if dueAtMs == 0 {422 if dueAtMs == 0 {
423+ e.disarmPumpTimer()
356 return424 return
357 }425 }
358 426
359 nowMs := time.Now().UnixMilli()427 nowMs := time.Now().UnixMilli()
360 if nowMs < dueAtMs {428 if nowMs < dueAtMs {
429+ e.armPumpTimer(dueAtMs, nowMs)
361 return430 return
362 }431 }
363 432
364- // Consume the current deadline before entering CEF. Any new callback that433+ e.disarmPumpTimer()
365- // happens during DoMessageLoopWork will publish a fresh deadline.
366 e.nextWorkAtMs.Store(0)434 e.nextWorkAtMs.Store(0)
367 e.pumpWorkCount++435 e.pumpWorkCount++
368 if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 {436 if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 {
369 logging.FromContext(e.ctx).Debug().437 logging.FromContext(e.ctx).Debug().
370- Uint64("tick", e.pumpTickCount).438+ Uint64("wake", e.pumpWakeCount).
371 Uint64("work", e.pumpWorkCount).439 Uint64("work", e.pumpWorkCount).
372 Uint64("schedule_calls", e.scheduleCount.Load()).440 Uint64("schedule_calls", e.scheduleCount.Load()).
373 Int64("due_at_ms", dueAtMs).441 Int64("due_at_ms", dueAtMs).
@@ -379,7 +447,18 @@ func (e *Engine) pumpTick() {
379 // Match the upstream external-pump reference behavior: if CEF did not447 // Match the upstream external-pump reference behavior: if CEF did not
380 // request another deadline, keep a placeholder wakeup so async work such as448 // request another deadline, keep a placeholder wakeup so async work such as
381 // browser creation can continue progressing.449 // browser creation can continue progressing.
382- e.scheduleFallbackPumpWork(time.Now().UnixMilli())450+ nowMs = time.Now().UnixMilli()
451+ e.scheduleFallbackPumpWork(nowMs)
452+
453+ nextDueAtMs := e.nextWorkAtMs.Load()
454+ if nextDueAtMs == 0 {
455+ return
456+ }
457+ if nextDueAtMs <= nowMs {
458+ e.queuePumpWake()
459+ return
460+ }
461+ e.armPumpTimer(nextDueAtMs, nowMs)
383 }462 }
384 463
385 // performMessageLoopWork calls DoMessageLoopWork with reentrancy protection.464 // performMessageLoopWork calls DoMessageLoopWork with reentrancy protection.

Keyboard shortcuts

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

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

Branches 8

Tags 42

↑↓ moveenter openesc close