Skip to content

perf(codex): stream rollout metadata instead of parsing every line - #156

Open
od0 wants to merge 1 commit into
mainfrom
bryan/codex-metadata-streaming
Open

perf(codex): stream rollout metadata instead of parsing every line#156
od0 wants to merge 1 commit into
mainfrom
bryan/codex-metadata-streaming

Conversation

@od0

@od0 od0 commented Aug 4, 2026

Copy link
Copy Markdown

Found while dogfooding bare path resume (#154): the pre-picker gather stalled for 80 seconds with no output. /usr/bin/time pointed at codex — every session-listing surface (p list codex, the share picker, bare resume) calls read_metadata per rollout file, which full-parsed every JSONL line through serde. On a real tree (421 rollouts, 3.9 GB, 89 files >10 MB) that's ~80 s of pure parse CPU. The old code even predicted this: "If that becomes a bottleneck we'd peek the first line plus stat for mtime."

The fix

read_metadata is now a single streaming pass that exploits the rollout being an append-only log:

  • Head (bounded, ≤100 non-empty lines parsed): session_meta (id/cwd/cli_version/git), first timestamps, first user prompt — all live at the top.
  • Tail (1 line): the newest timestamp.
  • Everything between: counted, never JSON-parsed.

Measured on the tree above: p list codex 1m20s → 8.5 s in a debug build (release is faster); user-time drops from 72 s to ~6 s, i.e. it's raw I/O now.

Deliberate trades (documented on the method)

  • first_user_message is None if the first prompt appears after the head budget (real sessions surface it within a dozen lines).
  • line_count counts non-empty lines (unparseable ones included) instead of successfully parsed ones — it only ever approximated message_count.
  • started_at/last_activity come from head-min/tail-max rather than a global min/max scan — identical for append-only logs.

Three new tests pin the tail-timestamp path, tolerant counting over blank/junk lines, and the head-budget bound. Also picks up the pre-existing paths.rs rustfmt drift that fails cargo fmt --check on main under the pinned 1.94.0 toolchain.

toolpath-codex 0.6.1 → 0.6.2 (perf fix, patch bump) in the four conventional places.

Follow-up worth filing separately: the listing surfaces re-scan all files on every invocation; piggybacking session metadata on the sync manifest's stat stamps would make an unchanged tree instant.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Session listing (p list codex, the share picker, bare resume) called
read_metadata on every rollout, which serde-parsed every line of every
file — a minute-plus silent stall on a multi-gigabyte sessions tree.

read_metadata is now one streaming pass: JSON-parse a bounded head
(session_meta, first timestamps, first user prompt — all at the top of
this append-only log) plus the final line (newest timestamp), and
otherwise just count non-empty lines. 3.9 GB / 421 sessions: ~80 s ->
raw-I/O speed. Documented trades: a prompt buried past the head budget
reports None; line_count counts non-empty lines, not parsed ones.

toolpath-codex 0.6.2. Also picks up the pre-existing rustfmt drift in
paths.rs that fails cargo fmt --check on main under the pinned 1.94.0
toolchain.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

🔍 Preview deployed: https://009fd271.toolpath.pages.dev

@od0

od0 commented Aug 4, 2026

Copy link
Copy Markdown
Author

Folded into #157 — the codex metadata streaming commits are on that branch verbatim (759ced0), part of the single picker-performance story with the parallel gather and the listing cache (#158). No review needed here.

@od0 od0 closed this Aug 4, 2026
@od0 od0 reopened this Aug 4, 2026
@od0

od0 commented Aug 4, 2026

Copy link
Copy Markdown
Author

Reopened as the bottom of stack #160 (#156#157#159#154) — same commits as before the fold, now reviewable layer by layer. gh stack merge from the top handles the chain.

@od0 od0 assigned od0 and akesling and unassigned od0 Aug 4, 2026
let mut first_user: Option<String> = None;
let mut first_user_fallback: Option<String> = None;

loop {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be pulled out to a helper for readability.

let mut first_user_fallback: Option<String> = None;

loop {
raw.clear();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code looks rather fragile.

What performance do we get if we use serde over a partial struct with raw values / accepting "undefined" fields? If serde won't give us what we want out of the box, we may want to roll our own partial Visitor or a proper parser that skips things intentionally.

@akesling akesling assigned od0 and unassigned akesling Aug 4, 2026
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.

2 participants