Serve history pages of a resident session from its cached log - #457
Merged
Merged
Conversation
A thread that was only opened in this process re-read and re-parsed its whole JSONL for the initial snapshot and for every history page, and each window start was found by folding the log from record 0: 25 MB / 203k records cost ~0.6 s (debug) or ~65 ms (release) per page, for ~16 pages. The existing per-session record cache (`AppState::event_records`, until now filled only by in-process appends and never emptied) becomes `SessionLog`: the records, the pure fold of them, and the indices of the records that open turns, extended by each append. A session opened for a client loads its log once on the mailbox, in the same turn as the snapshot that needs it; the timeline load clones the cached fold instead of parsing and folding again, and a background load caches its result on completion. Pages align their start by binary search over the turn-start indices, and the snapshot reports the fold's turn count instead of a resident timeline that may still be loading. Memory policy: a log exists for live and parked sessions only. When a session leaves residency, a store-writer barrier is queued and the log is dropped once it echoes, so a later cold read sees every append the log accepted; appends racing the barrier re-arm it. Paging a non-resident session reads cold and retains nothing. Snapshot + 16 pages of the 203k-record thread: debug 11.6 s -> 0.52 s, release 1.08 s -> 24 ms (`history_paging_bench`, ignored, driven by TCODE_HISTORY_BENCH_LOG). `SessionStore` counts `read_events` calls under the new `test-support` feature so a test can prove the single parse.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
For a long thread that was only opened (never appended to in this process), every history snapshot and page re-read and re-parsed the whole JSONL, and
turn_aligned_startre-folded from record 0 each call. After #454 a 203k-record thread needs 16 pages instead of ~1,000, but each still paid the full read.Change
AppState::event_records(the existing per-session record cache) now holds aSessionLog: the records, the pure fold, and the indices of turn-opening records.turn_aligned_startis a binary search over those indices. Envelope,SESSION_HISTORY_RECORDSand paging semantics are unchanged.TimelineLoadTarget::Active) loads the log on the mailbox in the same turn the snapshot needs it; the background timeline load clones the cached fold instead of re-reading. Background (orchestrate child) loads keep parsing off the mailbox and install the log on completion.record_event_atstill lazily loads on first append.release_stale_session_logsruns at every residency exit; it queues a store-writer flush barrier and drops the log when the barrier echoes, unless the session became resident again or appended meanwhile (re-armed). A cold read after the drop therefore sees every append the log accepted, so the JSONL lagging the writer can never truncate a reopened thread.total_turnsnow comes from the cached fold rather than the resident timeline, which was stillTimeline::default()while the async load was pending.SessionStore::event_reads()counter behind a newtcode-services/test-supportfeature (resolver 3; dev-dep only, zero cost in production).Timings (real 25.2 MB / 202,941-record log, snapshot + 16 pages)
select_session(73 ms release, 676 ms debug) and the second parse for the timeline load is gone.Tests
history_pages_of_an_opened_session_parse_the_log_once(old code: one read per page → fails).session_log_follows_residency_and_flushes_before_release(old code never dropped entries → fails).history_paging_bench(#[ignore],TCODE_HISTORY_BENCH_LOG=<path>).parked_readopt_refolds_events_appended_while_parkednow appends throughrecord_event(the production writer) instead of writing to the JSONL behind the runtime; the contract it protects is unchanged.cargo fmt --all --check,cargo clippy --workspace --all-targets --locked -- -D warnings,cargo test --workspace --locked,cargo machetegreen.