|
| @@ -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 checks | 18 | | // 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 = 4 | 20 | | 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 *WebViewFactory | 32 | | factory *WebViewFactory |
| 30 | | pool *WebViewPool | 33 | | pool *WebViewPool |
| 31 | | | 34 | | |
| 32 | - | // pumpRequested is set atomically by OnScheduleMessagePumpWork (which | 35 | + | // nextWorkAtMs is the next deadline, in Unix milliseconds, when |
| 33 | - | // can be called from ANY thread — GPU, IO, renderer, etc.). The | 36 | + | // 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.Int32 | 39 | + | 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.SourceFunc | 42 | | 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 signaling | 269 | + | // 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 it | 272 | | // 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 | 273 | + | // 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 any | 280 | | // 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 can | 282 | | // 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 { |
|
| @@ -320,31 +331,55 @@ func (e *Engine) startMessagePump() { |
| 320 | | | 331 | | |
| 321 | | func (e *Engine) stopMessagePump() { | 332 | | func (e *Engine) stopMessagePump() { |
| 322 | | e.pumpClosing = true | 333 | | 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 = 0 | 337 | | 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 do | 355 | + | 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 = false | 395 | | e.pumpActive = false |
| 361 | | | 396 | | |
| 362 | - | // If CEF re-entered during the call (called OnScheduleMessagePumpWork | 397 | + | // 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 = true | 400 | | e.pumpActive = true |
| 368 | | e.pumpReentry = false | 401 | | e.pumpReentry = false |