Commit3b76062aRecorded21 Jul 2026Repositorylantern

loop: block on the GMainContext poll fds instead of a 5ms poll

Message

The run-loop pumped GLib every 5ms (~200Hz) even when idle. It now drains non-blocking, then blocks on the context's poll fds via await-readable-fds (sigil 0.17.18+) until an event arrives or GLib's requested timeout elapses, falling back to the interval poll on a backend that cannot report its fds.

gtk backend: bind gmaincontext_acquire/release/prepare/query and expose query-fds, which reports the default GMainContext's poll fds and timeout (GPollFD read GC-safely out of a bytevector). webview: webview-query-fds dispatches to it (optional op).

Measured (lantern run, 8s idle, this Mesa/sway box): 49.0% -> 39.8% on a blank page, 49.6% -> 39.9% on the full Slate web build. Blank ~= Slate, so the remaining ~40% is the WebKitGTK/GTK4/Mesa webview compositing (it signals the main loop at ~140Hz regardless of content), not the pump: this fix removes the forced 200Hz poll but cannot go below WebKit's own event rate.

Changed
 lantern/src/lantern/backend/gtk.sgl | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++
 lantern/src/lantern/loop.sgl        | 28 ++++++++++++++++++++++++++--
 lantern/src/lantern/webview.sgl     |  8 ++++++++
 3 files changed, 85 insertions(+), 2 deletions(-)
Diff
lantern/src/lantern/backend/gtk.sglmodified
@@ -111,6 +111,12 @@
111
112
(define jsc-value-to-string #f)
113
(define g-main-context-iteration #f)
+114
;; Idle-block support: query the default GMainContext's poll fds so the
+115
;; run-loop can sleep on them (await-readable-fds) instead of polling.
+116
(define g-main-context-acquire #f)
+117
(define g-main-context-release #f)
+118
(define g-main-context-prepare #f)
+119
(define g-main-context-query #f)
120
(define g-memory-input-stream-new-from-data #f)
121
(define g-app-info-launch-default-for-uri #f)
122
@@ -383,6 +389,10 @@
389
390
(set! jsc-value-to-string (c-function libjsc "jsc_value_to_string" (list ffi/pointer) ffi/string))
391
(set! g-main-context-iteration (c-function libglib "g_main_context_iteration" (list ffi/pointer ffi/int) ffi/int))
+392
(set! g-main-context-acquire (c-function libglib "g_main_context_acquire" (list ffi/pointer) ffi/int))
+393
(set! g-main-context-release (c-function libglib "g_main_context_release" (list ffi/pointer) ffi/void))
+394
(set! g-main-context-prepare (c-function libglib "g_main_context_prepare" (list ffi/pointer ffi/pointer) ffi/int))
+395
(set! g-main-context-query (c-function libglib "g_main_context_query" (list ffi/pointer ffi/int ffi/pointer ffi/pointer ffi/int) ffi/int))
396
(set! g-memory-input-stream-new-from-data
397
(c-function libgio "g_memory_input_stream_new_from_data" (list ffi/pointer ffi/int64 ffi/pointer) ffi/pointer))
398
(set! g-app-info-launch-default-for-uri
@@ -523,6 +533,46 @@
533
534
(define (quit) (set! *quit-requested* #t))
535
+536
;; Read a little-endian signed 32-bit int out of a bytevector. GC-safe:
+537
;; reads the live bytevector object (whose contents the GC relocates with
+538
;; it), never a captured C pointer that a moving collection could stale.
+539
(define (bv-s32 bv off)
+540
(let ((v (+ (bytevector-u8-ref bv off)
+541
(* 256 (bytevector-u8-ref bv (+ off 1)))
+542
(* 65536 (bytevector-u8-ref bv (+ off 2)))
+543
(* 16777216 (bytevector-u8-ref bv (+ off 3))))))
+544
(if (>= v 2147483648) (- v 4294967296) v)))
+545
+546
;; Query the default GMainContext for the fds it polls plus the timeout it
+547
;; wants, so the run-loop can BLOCK on them (await-readable-fds) instead of
+548
;; polling on a fixed interval. Returns (fds . timeout-ms): the list of raw
+549
;; fds and GLib's requested timeout in milliseconds (-1 = wait until an fd
+550
;; fires; 0 = work is already pending). GLib fills a GPollFD[] — each is
+551
;; { gint fd; gushort events; gushort revents } = 8 bytes, fd at offset 0.
+552
;; We only READ the fds to wait on; pump-once (g_main_context_iteration)
+553
;; still runs the real prepare/check/dispatch, so no revents marshaling is
+554
;; needed here. The context is acquired around prepare/query as GLib
+555
;; requires (trivially satisfied on the single-threaded VM).
+556
(define max-poll-fds 32)
+557
(define (query-fds)
+558
(let ((prio (make-bytevector 4 0))
+559
(timeout (make-bytevector 4 0))
+560
(fds-buf (make-bytevector (* max-poll-fds 8) 0)))
+561
(g-main-context-acquire NULL)
+562
(g-main-context-prepare NULL (bytevector->pointer prio))
+563
(let ((n (g-main-context-query NULL (bv-s32 prio 0)
+564
(bytevector->pointer timeout)
+565
(bytevector->pointer fds-buf)
+566
max-poll-fds)))
+567
(g-main-context-release NULL)
+568
(let ((count (if (> n max-poll-fds) max-poll-fds n)))
+569
(cons
+570
(let loop ((i 0) (acc '()))
+571
(if (>= i count)
+572
acc
+573
(loop (+ i 1) (cons (bv-s32 fds-buf (* i 8)) acc))))
+574
(bv-s32 timeout 0))))))
+575
576
;; Bind the FFI, then init GTK. All GTK calls must come from the thread
577
;; that initialized it; the factory runs once at startup on that thread.
578
;; (Every internal define above must precede these first expressions.)
@@ -542,6 +592,7 @@
592
set-close-handler: set-close-handler
593
window-op: window-op
594
pump-once: pump-once
+595
query-fds: query-fds
596
get-title: get-title
597
should-quit?: should-quit?
598
quit: quit)))
lantern/src/lantern/loop.sglmodified
@@ -23,19 +23,43 @@
23
default-pump-interval)
24
(begin
25
26
;; ~200Hz pump ceiling; bounds worst-case input-servicing latency.
+26
;; Fallback pump interval for a backend that cannot report its poll fds
+27
;; (webview-query-fds returns #f): ~200Hz, bounds worst-case input latency.
28
(define default-pump-interval 0.005)
29
+30
;; When GLib reports no timer deadline (timeout -1: purely fd-driven), block
+31
;; on the fds but re-poll at least this often as a safety net against a non-fd
+32
;; source we do not model. 1s => ~1Hz idle, negligible CPU. An event on any
+33
;; GLib fd still wakes the loop immediately, so this is not input latency.
+34
(define idle-cap-ms 1000)
+35
36
;; Run the app's event loop. `goroutines` are zero-argument thunks started
37
;; alongside the pump inside the same async scope (e.g. the bridge
38
;; dispatcher). Returns 0 when a quit is requested.
+39
;;
+40
;; The pump drains GLib non-blocking, then BLOCKS on the GMainContext's poll
+41
;; fds (await-readable-fds) until an event arrives or GLib's requested timeout
+42
;; elapses, instead of waking every 5ms. Idle CPU drops from a constant
+43
;; ~200Hz poll to ~0, while input still wakes the loop instantly. A backend
+44
;; that cannot report its fds falls back to the interval poll.
45
(define (run-loop wv . goroutines)
46
(with-async
47
(for-each (lambda (thunk) (go (thunk))) goroutines)
48
;; The GLib pump.
49
(go (let loop ()
50
(webview-pump-once wv)
38
(sleep default-pump-interval)
+51
(let ((q (webview-query-fds wv)))
+52
(if (and q (pair? (car q)))
+53
(let* ((fds (car q))
+54
(glib-ms (cdr q))
+55
;; -1 (no timer deadline) => idle cap; else GLib's own
+56
;; requested timeout, capped so we never oversleep it.
+57
(cap (cond ((< glib-ms 0) idle-cap-ms)
+58
((< glib-ms idle-cap-ms) glib-ms)
+59
(else idle-cap-ms))))
+60
(await-readable-fds fds timeout-ms: cap))
+61
;; No fds reported (or backend can't): interval poll.
+62
(sleep default-pump-interval)))
63
(unless (webview-should-quit? wv) (loop))))
64
;; Main task: idle until quit, then fall out of with-async.
65
(let wait ()
lantern/src/lantern/webview.sglmodified
@@ -49,6 +49,7 @@
49
webview-set-close-handler
50
webview-window-op
51
webview-pump-once
+52
webview-query-fds
53
webview-get-title
54
webview-should-quit?
55
webview-quit)
@@ -116,6 +117,13 @@
117
(apply (op wv window-op:) window op-name args))
118
(define (webview-pump-once wv)
119
((op wv pump-once:)))
+120
;; Optional op: a backend that can expose its event-loop poll fds returns
+121
;; (fds . timeout-ms) so the run-loop can BLOCK on them (await-readable-fds)
+122
;; instead of polling. Returns #f when the backend does not implement it, so
+123
;; the run-loop falls back to interval polling.
+124
(define (webview-query-fds wv)
+125
(let ((f (dict-ref wv query-fds: #f)))
+126
(and f (f))))
127
(define (webview-get-title wv window)
128
((op wv get-title:) window))
129
(define (webview-should-quit? wv)