Skip to content

perf(core): setUsage has no batch variant — warming one agent's sessions rewrites the whole metadata file N times (~493 MB, ~9.6 s) #427

Description

@edspencer

autoName, preview and isSidechain all have batchSet* methods on SessionMetadataStore. usage does notbatchSetUsage does not exist.

Every setUsage call therefore runs the full cycle:

async setUsage(agentName, sessionId, usage, mtime) {
  let metadata = await this.loadMetadata(agentName);      // read whole file
  if (!metadata) metadata = this.createEmptyMetadata(agentName);
  metadata.sessions[sessionId] = { ...sessionEntry, usage, usageMtime: mtime };
  await this.saveMetadata(agentName, metadata);           // atomicWriteJson: rewrite WHOLE file
}

And it is called once per session, from getSessionUsage (session-discovery.ts:951), immediately after each extraction — reached per-session via FleetManager. So warming N cold sessions performs N whole-file reads and N whole-file writes: write volume is O(N x filesize), i.e. quadratic in session count.

Measured

Against a copy of a real session-metadata/<agent>.json from a production instance (293 KB, 1,677 sessions), simulating load -> mutate -> atomic write:

per setUsage 5.71 ms
to warm all 1,677 sessions ~9.6 s, ~493 MB written

An independent measurement during the same review, on the same file at an earlier size (261 KB / 1,520 sessions), got 2.99 ms per write / 389 MB / ~4.5 s. The two differ by roughly 2x on per-write cost — almost certainly methodology (whether the read is counted, and JSON.stringify indentation) — but agree on the shape and the order of magnitude. Either way it is hundreds of MB of writes to populate a cache that holds a few hundred KB.

Reads are not the problem: a full JSON.parse + Zod safeParse of that file is ~3 ms, and ~20 ms even at 3.5 MB. This is purely write amplification.

Why it matters now

  1. It is already happening in production on every cold start that enumerates sessions.
  2. It blocks the persisted-usage work. Paddock is about to stop maintaining its own in-memory usage cache and call getUsage/setUsage instead (its usage.ts currently duplicates this, and archive.ts:8 already notes core is "the natural home"). Routing Paddock's writes through setUsage as it stands inherits the quadratic behaviour at a larger scale. This is a prerequisite, not a follow-up.
  3. It multiplies the bug(core): SessionMetadataStore replaces the whole metadata file when a READ fails — an empty or unreadable file silently destroys every customName #419 blast radius. Every one of those N writes is another opportunity to hit the wholesale-file-replacement path when a read fails. Fewer, coalesced writes is a real mitigation as well as a speedup.

Ask

Either:

  • add batchSetUsage(agentName, entries) mirroring the existing batchSetAutoNames / batchSetPreviews / batchSetSidechains, and have the enrichment pass collect then flush once; or
  • give the store a write-coalescing queue so that N setters within one pass produce one file write, which fixes this for every field rather than just usage.

The second is more general and probably the better shape — the other three fields only avoid the problem because each grew a bespoke batch method. A single coalescing write path would make the batch variants redundant.

Ordering

Land #419 first (unreadable-vs-absent), then this. #426 (>= -> === plus size in the key) touches the same entry shape and can land alongside either.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions