|
| @@ -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 CefDoMessageLoopWork | 19 | | // 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.Int64 | 35 | | 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.SourceFunc | 38 | + | // thread, while source callbacks execute on the GTK main thread. |
| 43 | - | pumpSourceID uint | 39 | + | pumpEnabled atomic.Bool |
| 44 | - | pumpStarted bool | 40 | + | pumpClosing atomic.Bool |
| 45 | - | pumpClosing bool | 41 | + | |
| | | 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 bool | 49 | | pumpActive bool |
| 49 | | pumpReentry bool | 50 | | pumpReentry bool |
| 50 | | | 51 | | |
| 51 | | // Diagnostic counters (temporary). | 52 | | // Diagnostic counters (temporary). |
| 52 | - | pumpTickCount uint64 | 53 | + | pumpWakeCount uint64 |
| 53 | | pumpWorkCount uint64 | 54 | | pumpWorkCount uint64 |
| 54 | | scheduleCount atomic.Uint64 // incremented from any thread — read from main thread only | 55 | | scheduleCount atomic.Uint64 // incremented from any thread — read from main thread only |
| 55 | | | 56 | | |
|
| @@ -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 deadlines | 266 | | // 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 it | 269 | + | // 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 main | 270 | + | // 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 any | 274 | | // 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 can | 276 | + | // 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 := nowMs | 279 | | dueAtMs := nowMs |
|
| @@ -288,53 +281,65 @@ func (e *Engine) scheduleMessagePumpWork(delayMs int64) { |
| 288 | | dueAtMs += delayMs | 281 | | 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 | | return | 313 | | 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 | | return | 324 | | return |
| 314 | | } | 325 | | } |
| 315 | | | 326 | | |
| 316 | - | // Ensure the first tick enters CEF even if the only startup callback | 327 | + | 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 = true | 334 | + | 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 = 0 | 339 | + | 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 | | return | 424 | | 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 | | return | 430 | | return |
| 362 | | } | 431 | | } |
| 363 | | | 432 | | |
| 364 | - | // Consume the current deadline before entering CEF. Any new callback that | 433 | + | 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 not | 447 | | // Match the upstream external-pump reference behavior: if CEF did not |
| 380 | | // request another deadline, keep a placeholder wakeup so async work such as | 448 | | // 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. |