Skip to content

perf: load cold thread logs off the host mailbox - #590

Closed
Tryanks wants to merge 1 commit into
feat/client-history-memoryfrom
feat/async-cold-hydration
Closed

Tryanks wants to merge 1 commit into
feat/client-history-memoryfrom
feat/async-cold-hydration

Conversation

@Tryanks

@Tryanks Tryanks commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

First slice of #524, stacked on #589. Storage format, fold, compaction and protocol are unchanged.

Loading a cold thread's log (read, parse every record, fold) no longer runs on the host mailbox, so other sessions keep responding while a large thread opens.

 on(subscribe / select cold thread, first event with no cached log, page or ReadItemOutput of an unopened thread)
-  read + parse + fold on the mailbox           # every other session waits
-  reply
+  start or join the session's Hydration        # crates/runtime/src/app/history.rs
+    store writer takes a LogSnapshot in queue order   # open file + length, no parse
+    parse + fold on HostCx::unblock
+  meanwhile: appends keep their order; records for the session accumulate in `pending`
+  on completion
+    send every waiting window, ending at the snapshot's end
+    then the pending records at consecutive positions
+    acknowledge the subscription last           # the mux drops a request's route at its ack
  • Exactly once. For the event that starts a hydration, the snapshot is queued before the event's append, so the record is in pending and not in the snapshot. Two clients opening the same cold thread share one read.
  • Nothing else waits. Resident timeline, turn-end handling, status, queue, title generation and orchestrate callbacks run in the same mailbox turn as before. Status and git subscriptions never wait on history.
  • A client that leaves before the read completes gets no window, and the log is dropped unless the session is resident.
  • An event for a thread that is neither resident nor subscribed is now only appended; before, it loaded, cached and released the whole log.

Evidence

Release build, synthetic compacted logs, real spawn_host driven over its protocol channel. Round trips of an unrelated session while a cold open is in progress, median / max:

Log Request Before After
124.7 MB ping 347 / 771 ms 0.03 / 0.16 ms
subscribe 342 / 764 ms 0.05 / 0.20 ms
live event 342 / 769 ms 0.03 / 0.13 ms
16.1 MB ping 14.3 / 25.4 ms 0.04 / 5.19 ms

End-to-end open latency is unchanged (large log: 666–720 ms before, 653–689 ms after); the work is the same and the fold dominates (read ≈ 100 ms, fold ≈ 520 ms). Peak RSS unchanged. The machine was under other load during the runs.

  • Tests added, all through production entry points, each waiting for the state it asserts: two subscriptions and a first live event before the mailbox runs (both windows end at the snapshot end, the record arrives once, one parse); the event that starts a read follows an empty window once; a client that leaves early gets no window and no log is kept; a page and a full output of an unopened thread are answered without keeping the log; a child completing a turn while its log is read reports to status, result and the parent's callback; two clients through the mux each get one window and every record once.
  • Answering replies after pending records, queueing the append before the snapshot, and acknowledging before the window each fail one of these.
  • Changed drivers, assertions kept: tests that called the synchronous snapshot in the selection's turn now subscribe through the pipe and wait for the reply, as the app does. Two tests that used a later ack as a fence now wait for the windows. Resident-log unit tests call the window logic on SessionLog, where it moved.
cargo fmt / clippy --workspace -D warnings / machete   ✓
cargo nextest run --workspace --locked                 908 passed, 6 skipped (run three times on the final tree)
Web and iOS checks with -D warnings                    ✓

Merge Danger

Door: two-way

Runtime scheduling only; nothing on disk or on the wire changes.

Blast Radius: host-runtime

  • A cold open now waits behind whatever is already in the store writer's queue, such as a turn-end compaction of a large log; the old synchronous load did not.
  • The ordering between a hydration's windows, its pending records and the ack is the correctness surface; the tests above pin each of the three.
  • Not run: history_paging_bench (ignored, needs a log path), Android, Windows, Linux.

Opening a cold thread, the first event of an uncached thread, and history
pages or full outputs of a thread nobody has open each parsed and folded
the whole log on the host mailbox, stalling every other session for as
long as that took.

A session now has at most one hydration. The store writer opens the log
in its queue order and records its length, so the read holds exactly the
appends queued before it; the parse and fold run on the blocking pool.
Records accepted meanwhile are held in order and take the positions after
the snapshot's end. When the log is resident the waiting subscription
windows go out first, each ending where the snapshot ends, then the held
records, so a subscriber sees every record once. A subscription's
acknowledgement waits for its window. The log is kept only if the
session is still resident.

Part of #524
@Tryanks Tryanks changed the title perf: read a cold thread's log off the host mailbox perf: load cold thread logs off the host mailbox Oct 6, 2026
@Tryanks

Tryanks commented Oct 6, 2026

Copy link
Copy Markdown
Owner Author

Folded into #591, which carries this commit unchanged as one PR for the whole storage phase.

@Tryanks Tryanks closed this Oct 6, 2026
@Tryanks
Tryanks deleted the feat/async-cold-hydration branch October 6, 2026 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant