[ Architecture ]
Event Loop Architecture
Overview
Section titled “Overview”The libkoto event subsystem spans koto::App (src/app.cpp) and koto::EventLoop (src/event.cpp). It combines kernel ::poll() demultiplexing across STDIN_FILENO and a non-blocking POSIX self-pipe (m_pipe_fds), ioctl(STDIN_FILENO, FIONREAD) burst draining, stateful deadline tracking (m_escape_deadline and m_paste_deadline), and pre-render event batching.
Mermaid diagram
Section titled “Mermaid diagram”sequenceDiagram
participant AppRun as App::run
participant LoopPoll as EventLoop::poll
participant ParseStep as parse_step
participant Dispatch as App::dispatch_event
participant DrawFrame as App::draw_frame
AppRun->>LoopPoll: poll(wait_ms)
LoopPoll->>ParseStep: parse_step(m_pending_input)
alt m_pending_input empty or incomplete
LoopPoll->>LoopPoll: ::poll(pfds, 2, eff_timeout)
LoopPoll->>LoopPoll: ioctl(STDIN_FILENO, FIONREAD) and read()
LoopPoll->>ParseStep: parse_stdin() -> parse_step(m_pending_input)
end
LoopPoll-->>AppRun: Event
AppRun->>Dispatch: dispatch_event(ev)
loop while m_running and m_loop.has_pending_event()
AppRun->>LoopPoll: poll(0)
LoopPoll-->>AppRun: pending_ev
AppRun->>Dispatch: dispatch_event(pending_ev)
end
opt m_tick_handler or m_tick_dt_handler due (now >= next_frame)
AppRun->>AppRun: m_tick_handler() / m_tick_dt_handler(dt)
end
opt m_needs_draw is true
AppRun->>DrawFrame: draw_frame(true)
end
Walkthrough
Section titled “Walkthrough”- Wait timeout computation (
src/app.cpp:237-269):- At the top of each iteration in
App::run(), ifm_loop.has_pending_event() || m_needs_drawistrue,wait_msis set to0so the loop does not block. - Otherwise, if
m_tick_handlerorm_tick_dt_handleris registered,frame_waitis clamped to[0, 16]ms based onnext_frame - clock::now(). - If
m_loop.has_pending()istrue(an incomplete escape or UTF-8 sequence is buffered),wait_msis capped at35ms (std::min(35, frame_wait)). - If neither a tick handler nor pending bytes exist,
wait_msis-1(infinite kernel sleep).
- At the top of each iteration in
- Self-pipe and stdin polling (
src/event.cpp:837-1029):EventLoop::poll(timeout_ms)first inspectsm_pending_inputwithparse_step(). If a complete event is already in memory, it erasesres.consumedbytes and returns immediately without a syscall.- Otherwise, it tightens
eff_timeoutagainstm_paste_deadline(500ms) orm_escape_deadline(35ms) and calls::poll(pfds, 2, eff_timeout). - If
m_pipe_fds[0]is readable (pfds[1].revents & POLLIN), it drains up to 64 bytes; if any byte equals'R'(written byEventLoop::on_sigwinch), it callson_resize()(ioctl(STDOUT_FILENO, TIOCGWINSZ)) and returnsResizeEvent; otherwise (byte'W'fromwakeup()) it returnsTickEvent{}. - If
STDIN_FILENOis readable, it drains all available kernel bytes viaioctl(STDIN_FILENO, FIONREAD, &available)(falling back to a single 512-byteread()ifFIONREADreturns<= 0), refreshes the active deadline, and callsparse_stdin().
- User-space batch draining and single render (
src/app.cpp:277-313):- After dispatching the first event,
App::run()loopswhile (m_running && m_loop.has_pending_event())callingm_loop.poll(0)anddispatch_event(pending_ev). - Once all buffered events are dispatched,
App::run()fires tick callbacks ifclock::now() >= next_frame(advancingnext_frame = now + 16ms) and executesdraw_frame()at most once for the entire input burst.
- After dispatching the first event,
Invariants
Section titled “Invariants”- No busy-spin on partial sequences:
has_pending()istruewheneverm_pending_inputis non-empty, whereashas_pending_event()istrueonly whenparse_step()returnsCompleteorInvalid, or whenm_escape_deadline(35ms),m_paste_deadline(500ms), or the4 MBpaste cap has expired (src/event.cpp:629-669).App::run()drains onhas_pending_event(), never onhas_pending(). - Async-signal-safe
SIGWINCHdelivery:EventLoop::on_sigwinchsaveserrno, performs a 1-bytewrite(m_pipe_write_fd, "R", 1), and restoreserrno(src/event.cpp:817-826).ioctl(TIOCGWINSZ)and buffer reallocation happen synchronously on the main thread insidepoll()anddispatch_event(). - Forward progress on malformed input: When
parse_step()returnsParseStatus::Invalid(or when a non-ESC orphan byte times out inflush_incomplete_escape()),EventLooperases at least1byte (size_t to_drop = res.consumed > 0 ? res.consumed : 1) so a corrupted byte can never stall the input buffer (src/event.cpp:716-721, 806-814).