Skip to content

Watcher full-scan loop causes sustained CPU use and repeated history rebuilds #3

Description

@VirtualPaul

Summary

The watcher performs full filesystem discovery every 500 ms and can rebuild all historical summaries whenever the active log changes. With a moderate corpus this consumes a sustained fraction of a CPU core and creates multi-second work on the request-serving process.

Tested against main at fb265549480ff0d3ad8b14b28007370539b70cf3 on macOS with 100 discovered sources.

Reproduction

  1. Start Token Meter with approximately 100 Claude/Codex JSONL files.

  2. Time source discovery:

    import meter, statistics, time
    samples=[]
    for _ in range(20):
        started=time.perf_counter(); meter.all_session_sources(); samples.append(time.perf_counter()-started)
    print(statistics.median(samples))
  3. Observe the server process while an agent is writing to the newest log.

  4. Invalidate _xsess and time one cold cross_session().

Observed:

  • Median all_session_sources() time: 0.1712 s.
  • The watcher calls it every 0.5 s, an intrinsic baseline near 34% of one core before other work.
  • Server CPU samples ranged from 19% to 90% while active.
  • A cold cross_session() took 2.717 s.

Root cause

  • meter.py:5539-5574 calls all_session_sources() on every 500 ms loop.
  • source_mtime_signature() treats any active-log mtime change as an aggregate change.
  • refresh_cross_session_state() invalidates the entire aggregate cache.
  • cross_session() walks and summarizes every source.

Suggested fix

  1. Cache the source inventory; update it from directory change notifications (FSEvents/kqueue) or a much slower adaptive scan.
  2. Poll only the selected/active log at the fast interval.
  3. Cache each session summary by stable source key plus (mtime, size) and recompute only changed files.
  4. Maintain aggregate totals incrementally by subtracting the prior summary and adding the new one.
  5. Back off the poll interval when idle and perform expensive work outside the HTTP request path.

Regression tests

  • Instrument discovery and assert an idle watcher does not run a full inventory scan every 500 ms.
  • With 100 fixture logs, modify one file and assert only that summary is recomputed.
  • Assert additions/removals still appear within a documented latency budget.
  • Add a benchmark/guardrail for idle CPU and refresh time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions