Fix 100% CPU on idle pages: block in the GUI run loop, don't busy-spin
ober
eaff6739b911d68614e2f048dc261dc9d1c08df6
--- a/include/jerboa_browser.h +++ b/include/jerboa_browser.h @@ -113,6 +113,11 @@ JWB_API JwbStatus jwb_title(JwbHandle view, */ JWB_API JwbStatus jwb_pump_events(uint32_t max_ms); +/* Block until an event arrives or max_ms elapses (then dispatch the pending + * batch). For interactive run loops: unlike jwb_pump_events' fixed time budget, + * this sleeps when idle instead of busy-spinning, so it does not peg a core. */ +JWB_API JwbStatus jwb_pump_wait(uint32_t max_ms); + JWB_API JwbStatus jwb_load_url_sync(JwbHandle view, const uint8_t *url, uintptr_t len, --- a/qt-webengine/src/browser_page.cpp +++ b/qt-webengine/src/browser_page.cpp @@ -7,6 +7,7 @@ #include <QElapsedTimer> #include <QEventLoop> #include <QString> +#include <QTimer> #include <QUrl> #include <QVariant> #include <QWebEngineFindTextResult> @@ -72,6 +73,29 @@ JWB_API JwbStatus jwb_pump_events(uint32_t max_ms) { return JWB_OK; } +// Block until at least one event arrives or `max_ms` elapses, dispatch the +// pending batch, and return. Unlike jwb_pump_events (a fixed time budget that +// busy-spins on an idle page: the processEvents(flags, maxtime) overload +// IGNORES WaitForMoreEvents and returns instantly when the queue is empty), +// this calls the no-maxtime overload that actually honors WaitForMoreEvents, so +// an interactive run loop sleeps in the OS when idle (~0% CPU) yet wakes +// immediately on a key, paint, network reply, or window close. The one-shot +// heartbeat guarantees we still return at least every max_ms so the caller can +// re-check its quit/visibility flags even when no events are flowing. +JWB_API JwbStatus jwb_pump_wait(uint32_t max_ms) { + jwb::clear_last_error(); + if (!jwb::qapp()) { + jwb::set_last_error("runtime not initialized; call jwb_runtime_init first"); + return JWB_ERR_QT; + } + QTimer heartbeat; + heartbeat.setSingleShot(true); + heartbeat.start(max_ms > 0 ? static_cast<int>(max_ms) : 1); + QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents | + QEventLoop::AllEvents); + return JWB_OK; +} + JWB_API JwbStatus jwb_load_url_sync(JwbHandle view, const uint8_t *url, uintptr_t len, uint32_t timeout_ms) { jwb::clear_last_error(); --- a/scheme/browser.ss +++ b/scheme/browser.ss @@ -43,6 +43,7 @@ browser-focus browser-set-title browser-pump + browser-pump-wait browser-exec browser-quit browser-close-view @@ -183,6 +184,7 @@ (define-c-lambda %focus (uint64) int "jwb_view_focus") (define-c-lambda %set-title (uint64 char-string) int "jwb_view_set_title") (define-c-lambda %pump (uint32) int "jwb_pump_events") + (define-c-lambda %pump-wait (uint32) int "jwb_pump_wait") (define-c-lambda %exec () int "jwb_exec") (define-c-lambda %quit () int "jwb_quit") ;; automation / testing hooks (browser_automation.cpp) @@ -353,6 +355,7 @@ ;; Drive the shared Qt event loop for up to ms milliseconds (non-blocking ;; cooperative pump for the REPL between operations). (def (browser-pump ms) (status-result "pump" (%pump ms))) + (def (browser-pump-wait ms) (status-result "pump-wait" (%pump-wait ms))) ;; Run the shared Qt event loop until browser-quit (blocking GUI session). (def (browser-exec) (status-result "exec" (%exec))) --- a/scheme/browser/commands.ss +++ b/scheme/browser/commands.ss @@ -208,17 +208,20 @@ (when tok (app-feed-token! app tok))) (loop)))))) - ;; The interactive GUI loop: install key capture, then pump Qt events + drain - ;; keys until the quit command fires or the user closes the window. Replaces a - ;; bare browser-exec so keystrokes route through the keymap. `tick-ms` is the - ;; per-iteration Qt pump budget (default ~60fps). + ;; The interactive GUI loop: install key capture, then block on Qt events + + ;; drain keys until the quit command fires or the user closes the window. + ;; Replaces a bare browser-exec so keystrokes route through the keymap. + ;; browser-pump-wait sleeps until an event arrives (a key, paint, reply, or + ;; window close), so an idle page costs ~0% CPU instead of busy-spinning a + ;; core; `tick-ms` is only the idle-heartbeat cap — we still wake at least + ;; that often to re-check the quit/visibility flags when no events flow. (def (app-run-loop! app . opt) (let ((win (session-window (app-session app))) - (tick-ms (if (pair? opt) (car opt) 16))) + (tick-ms (if (pair? opt) (car opt) 250))) (browser-window-install-key-hook win) (app-render! app) (let loop () - (browser-pump tick-ms) + (browser-pump-wait tick-ms) (app-pump-keys! app) (unless (or (app-should-quit? app) (not (browser-window-visible? win))) (loop)))