Skip to content

Commit 9ba9d45

Browse files
authored
Make token usage scans cached, incremental, and memoized (#63)
* Cache all usage scanners and memoize assembled scans The token usage scan re-parsed every source from scratch on each query: codex (multi-GB of session JSONL) had no cache at all and did a full serde_json::Value parse of every line, cursor re-read its multi-GB state.vscdb each scan (and then failed on NULL values, so it never reported anything), and opencode/pi/copilot were also uncached. Only claude was cached, via one SQLite point query per file (11k+ queries per scan on a large corpus). Borrowing the working architecture from ccusage and CodexBar: - Generalize the per-file cache to every source, keyed by (source, path, parser_version, size, mtime), loaded with one batched query per source and decoded in parallel; rows for vanished files are pruned. - Prefilter JSONL lines with memmem needles before any JSON parse and stream lines as bytes into typed serde structs (claude) or Value (codex survivors only); parse changed files in parallel. - Treat cursor/opencode databases as volatile: reuse cached rows for 60s even when metadata changed, since a running editor rewrites them continuously. - Memoize the assembled (pre-filter) event set in-process with an opt-in TTL; the TUI reuses one assembly across keystroke-driven queries, which only differ in post-assembly filters. - Fix the cursor scanner aborting on NULL cursorDiskKV values. - Dedupe cursor/copilot/opencode copies in a reconcile pass now that per-file results are cached independently. On a 26GB corpus (742k events) the cold scan takes ~32s once, warm full scans ~4s, and memoized TUI refreshes are effectively free. * Store usage cache blobs as postcard instead of JSON Decoding cached events dominated warm scans. Postcard's varint encoding suits the token-count-heavy CachedUsageEvent shape: per-source warm scans drop 2.5-3.5x (claude 2.2s -> 0.9s, codex 3.8s -> 1.1s on a 742k event corpus) and the cache file shrinks ~2.8x. Parser versions double as blob-format versions, so existing JSON rows re-parse once; the blob column rename drops pre-rename tables on open. * Limit OpenCode volatile cache reuse to databases OpenCode message JSON files are rewritten in place while a response streams, so the source-wide 60s volatile window could serve a stale or empty cached blob for a just-changed message. Make the volatile window a per-path decision: opencode applies it to its databases only, cursor keeps it for all its databases, and every other source always re-parses on metadata change.
1 parent 8cf6340 commit 9ba9d45

5 files changed

Lines changed: 851 additions & 304 deletions

File tree

Cargo.lock

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ort = "2.0.0-rc.10"
2323
crossbeam-channel = "0.5"
2424
once_cell = "1.19"
2525
memchr = "2.7"
26+
postcard = { version = "1.1", default-features = false, features = ["use-std"] }
2627
memmap2 = "0.9"
2728
rayon = "1.10"
2829
regex = "1.10"

src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,7 @@ fn run_usage(
14361436
cost_mode,
14371437
include_events,
14381438
cache_path: Some(paths.state.join("usage-cache.sqlite3")),
1439+
memo_ttl_ms: 0,
14391440
};
14401441
let report = scan_usage(&query)?;
14411442
if json {

src/tui.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3090,6 +3090,9 @@ fn home_token_usage_query(
30903090
cost_mode: CostMode::Source,
30913091
include_events: true,
30923092
cache_path: Some(cache_path),
3093+
// Keystrokes and result updates re-run this query with different post-assembly
3094+
// filters; reuse the assembled scan between them instead of re-reading logs.
3095+
memo_ttl_ms: 15_000,
30933096
}
30943097
}
30953098

0 commit comments

Comments
 (0)