Commit graph

1 commit

Author SHA1 Message Date
Ouroboros
bdab1d277a perf: cache ledger aggregation renders keyed by memo fingerprint
usage_projection/usage_breakdown re-aggregated every validated final row on
every display read (~120-150ms of CPU per /api/state poll on a grown ledger)
even though the PR-140 rows memo already made the rows themselves warm. The
renders are now cached inside the memo, keyed by every input that shapes the
output (function, root/task filters, resolved limit, integrity bit,
include_roots), so a warm hit does zero full replays AND zero re-aggregation:

- _LedgerRowsMemo gains a fingerprint-keyed `renders` dict, cleared on refold
  and on every non-empty advance (never TTL), plus a `generation` counter: a
  render computed outside the lock is published only when the memo object and
  generation are unchanged since the rows were read, so a concurrent append
  between read and publish returns the stale render to that caller without
  caching it (clear-then-publish race guard).
- The non-resumable crash-tail fingerprint (st_ino == -2) is never cached —
  every read of a torn tail stays a full replay; _memoized_final_rows now
  transports (rows, cacheable, memo, generation).
- TOTAL_BUDGET resolves BEFORE the key is built; the quarantine stat happens
  after the row read (which owns quarantine) and joins the key.
- Renders are served and stored as deep copies: callers mutate nested buckets
  in place, and the cached object is shared between requests.
- The cache seam stays inside the usage_projection/usage_breakdown bodies, so
  the 10+ monkeypatch-by-name pins and every existing call site inherit it.
- usage_projection(include_roots=True) is a new keyword: the default keeps
  the full contract; gateway/state.py passes include_roots=False because
  /api/state serializes named scalars only, so by_root was built per poll for
  zero readers. The slim projection still carries limit_usd and
  remaining_known_usd (the two fields budget_remaining consumes); the by_root
  loop itself went from O(N x roots) list scans to one-pass dict grouping.
- The memo + render-cache layer moves to ouroboros/_usage_rows_memo.py
  (usage_accounting sat at 1565 of the 1600-line module gate; same extraction
  precedent as _usage_rows.py). It resolves _locked/_read_records_locked back
  through the usage_accounting namespace at call time, so historical
  monkeypatch sites keep governing display reads; usage_ledger.py is
  untouched and stays cache-ignorant.
- New tests pin: warm hit = zero replays + zero re-aggregation, append and
  rotation invalidation, crash-tail never cached, include_roots=False omits
  only by_root while /api/state stays field-identical, nested-bucket deep-copy
  isolation, and the concurrent-append publish guard.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-08-09 01:47:51 +03:00