Loop Abstraction Plan
Context
callbacks.run-loopinsrc/lua_callbacks.cppis a polling loop that dispatches jobs/http/callbacks.- Async producers (jobs/http) run in worker threads and enqueue results back to Lua.
- There is no shared event-loop abstraction today.
Goals
- Add a shared loop abstraction so Fennel can select a backend (default vs libuv).
- Improve latency and reduce busy-waiting by waking the loop when callbacks arrive.
- Keep main-thread callback dispatch as the single execution path for Lua.
Non-goals (initial)
- Full migration of HTTP/jobs/FS to libuv.
- Changing the frame update model of the engine.
Proposed Abstraction
Introduce a minimal Loop interface with:
run_once(max_work_ms)orpoll(max_work_ms)for stepping in the engine frame.wake()for cross-thread notification when new callbacks are enqueued.schedule_timer(delay_ms, repeat, fn)or a minimal timer API if needed.
The "default" backend keeps current behavior but uses wake() to avoid sleep polling.
Backend: Default (current loop)
- Keep current polling logic.
- Replace
sleep_mswith "wait until wake or timeout" using astd::condition_variable. lua_callbacks_enqueuecallsloop.wake()socallbacks.run-loopcan respond immediately.
Backend: Libuv (future)
- Own a
uv_loop_tplus:uv_async_tforwake()and cross-thread notifications.uv_timer_tfor timeouts and scheduled timers.
- Provide
poll(max_work_ms)viauv_run(loop, UV_RUN_NOWAIT)or boundedUV_RUN_ONCE. - Keep callbacks dispatched on the main thread; only wake/queue from worker threads.
Engine Integration
- Add a loop instance to the engine runtime (e.g.,
Engine.loopor runtime singleton). - In the main frame update, call
loop.poll(max_work_ms); budget can be small. - Keep the existing explicit
callbacks.run-loopfor tests and scripts.
Lua/Fennel API
- Add
callbacks.run-loopoption:loop(:defaultor:uv) or a separatecallbacks.set-loop :uvthat updates the runtime default. callbacks.run-loopcontinues to returntrue/falsebased on:until/timeout.
Migration Steps
- Add
Loopinterface and default backend backed by condition variable. - Store a loop instance in runtime/engine and expose selection to Lua.
- Update
lua_callbacks_enqueueto callloop.wake(). - Update tests (
assets/lua/tests/test-callbacks.fnl) to validate wake behavior. - (Optional) Add libuv backend and make selection available to scripts.
Risks
- Waking and dispatching from worker threads must remain thread-safe.
- Frame budget in
loop.pollmust be enforced to avoid spikes. - Lua callback dispatch must remain on the main thread.
Validation
- Existing
callbacks.run-looptests should pass. - Add a new test that enqueues a callback and ensures
run-loopreturns quickly withoutsleep-ms.
