Census from a live daemon's module log — 110 perf ticks, ~2h uptime, several project roots bound by concurrent sessions:
| category |
runs |
total |
share |
p50 |
p95 |
max |
| dead_code |
34 |
52,586 ms |
83% |
326 ms |
19,087 ms |
19,655 ms |
| duplicates |
30 |
4,863 ms |
7% |
42 ms |
788 ms |
2,046 ms |
| unused_exports |
30 |
2,902 ms |
4% |
38 ms |
239 ms |
889 ms |
| cycles |
30 |
2,656 ms |
4% |
23 ms |
485 ms |
768 ms |
63 s of background scan time in the window, and dead_code is five sixths of it.
The interesting part isn't the total, it's the shape. p50 is 326 ms and p95 is 19 s — a 58× spread. Sorted run times:
19655 19087 2741 1167 771 637 597 466 ...
Two runs account for 74% of all dead_code time. This isn't a uniformly slow scanner; it's a fast one with a rare catastrophic mode, and that shape usually means a specific input class rather than general cost.
What I could rule out from the log: it isn't watcher volume, and it isn't callgraph churn.
13:11:11 2741ms watcher_paths= 25 callgraph_invalidations=3
14:18:21 19655ms watcher_paths= 52 callgraph_invalidations=0
14:21:21 19087ms watcher_paths=218 callgraph_invalidations=0
The 218-path tick ran faster than the 52-path one, and both 19 s runs had zero callgraph invalidations. Whatever drives the tail, it isn't how much changed.
Where I'd look, and why I'm asking rather than patching:
project_dead_code_snapshot opens the callgraph SQLite read-only and does what looks like a full projection — SELECT path FROM files ORDER BY path, then all nodes, then all refs — per scan. If that's per-root-per-scan with no incremental path, a large root would give exactly this bimodal profile: cheap on small roots, cliff on the big one. I haven't confirmed that's the code path taken in the slow runs, and the log doesn't carry the root, so I can't attribute it from here.
Two things that would make this diagnosable from a log without guessing:
- Tag tier2 timings with the root (or a stable root id). Right now a 19 s dead_code run is anonymous — you can't tell whether it's one pathological repo or a general regression, which is the first question anyone asks.
- A per-category soft deadline for tier2.
InspectManager::soft_deadline bounds a caller waiting on an outcome, but a background scan appears to run to completion regardless. A 19 s scan on a shared daemon competes with interactive work for the same process; a bounded scan that reports complete: false fits the honest-reporting convention better than an unbounded one that finishes eventually.
Happy to dig further on either if useful, but you'll know immediately whether the projection is the right suspect and whether the tail is expected on a large root.
Method note so the numbers can be discounted appropriately: single host, single daemon instance, ~2 h, mixed real workload from several concurrent sessions. Not a controlled benchmark — a census of what one production instance actually spent.
Census from a live daemon's module log — 110 perf ticks, ~2h uptime, several project roots bound by concurrent sessions:
63 s of background scan time in the window, and dead_code is five sixths of it.
The interesting part isn't the total, it's the shape. p50 is 326 ms and p95 is 19 s — a 58× spread. Sorted run times:
Two runs account for 74% of all dead_code time. This isn't a uniformly slow scanner; it's a fast one with a rare catastrophic mode, and that shape usually means a specific input class rather than general cost.
What I could rule out from the log: it isn't watcher volume, and it isn't callgraph churn.
The 218-path tick ran faster than the 52-path one, and both 19 s runs had zero callgraph invalidations. Whatever drives the tail, it isn't how much changed.
Where I'd look, and why I'm asking rather than patching:
project_dead_code_snapshotopens the callgraph SQLite read-only and does what looks like a full projection —SELECT path FROM files ORDER BY path, then all nodes, then all refs — per scan. If that's per-root-per-scan with no incremental path, a large root would give exactly this bimodal profile: cheap on small roots, cliff on the big one. I haven't confirmed that's the code path taken in the slow runs, and the log doesn't carry the root, so I can't attribute it from here.Two things that would make this diagnosable from a log without guessing:
InspectManager::soft_deadlinebounds a caller waiting on an outcome, but a background scan appears to run to completion regardless. A 19 s scan on a shared daemon competes with interactive work for the same process; a bounded scan that reportscomplete: falsefits the honest-reporting convention better than an unbounded one that finishes eventually.Happy to dig further on either if useful, but you'll know immediately whether the projection is the right suspect and whether the tail is expected on a large root.
Method note so the numbers can be discounted appropriately: single host, single daemon instance, ~2 h, mixed real workload from several concurrent sessions. Not a controlled benchmark — a census of what one production instance actually spent.