perf: make validate <files> fast (22.7s -> 15ms on a 2000-file changeset) - #123
perf: make validate <files> fast (22.7s -> 15ms on a 2000-file changeset)#123perryqh wants to merge 2 commits into
validate <files> fast (22.7s -> 15ms on a 2000-file changeset)#123Conversation
validate <files> resolves ownership from the config and the CODEOWNERS file only — it never touches Runner's Project. But it went through Runner::new, which walks every tracked file in the repo first. On a 130k-file monorepo that is a ~2.1s fixed cost paid for nothing, on the code path documented as "fast mode for git hooks". Extracts validate_files into a free function and routes api::validate to it directly when paths are given, mirroring the bypass for_file already uses for single-file queries. Tradeoff, deliberately not hidden: the fast path no longer creates or persists the cache, so it does not warm it for a following command, and it cannot surface project-build IO errors. --no-cache is a no-op there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Combines O1 (one batched CODEOWNERS query instead of one per path) with O2 (skip the project build for validate <files>). The two wins are independent: O2 removes the ~2.1s fixed project build, O1 removes the ~9.5ms/file linear term. Together they take validate on 1000 files from 13.1s to 0.17s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Closing: this optimizes a path that is not semantically equivalent to
The existing test Background: the file-list path came from #82. #89 patched the unowned-file false positive by adding the The useful half survives separately. The two optimizations here split along exactly the correctness line:
Follow-ups, in order:
Branches |
Summary
validate <files>— the path documented as "fast mode for git hooks" — was the slowest way to use the tool on any real changeset. On a 130k-file monorepo a 2000-file run took 22.7s. It now takes 15ms.The two causes
1. The whole project was built for nothing.
validate <files>resolves ownership from the config and the CODEOWNERS file alone — it never touchesRunner'sProject. But it went throughRunner::new, which walks every tracked file first. That is a ~2.1s fixed cost on every invocation, including a single-file pre-commit hook.2. The CODEOWNERS file was re-parsed once per path. The loop called
team_for_file_from_codeownersper file. That helper wraps the path in a one-element slice and hands it to the batch query, which reloads the config and re-reads and re-parses the entire 17,981-line CODEOWNERS every time —parse_codeowners_entriesis not memoized, unliketeams_by_github_team_namedirectly beside it. That is ~9.5ms per path, linear: ~20s of the 22.7s was re-parsing the same file 2000 times.The fix for (2) is not a new abstraction. The batch function already accepts a slice and already parallelizes across it; it was simply being called N times with one path instead of once with N.
Measured
Corpus: 130,934 tracked files, 91,206 owned, 17,981-line CODEOWNERS. macOS/aarch64, 11 cpus, best of 3, warm cache.
validate1 filevalidate100 filesvalidate1,000 filesvalidate2,000 filesThe result is now flat in the number of files, which is the real point — the linear term is gone, not just reduced.
Measured independently on their own branches, so the two causes are attributable:
perf/o1-batch-codeowners-queryperf/o2-skip-project-buildCorrectness
This is the main risk in a change like this, so it was checked directly rather than inferred:
unowned_globs, absolute paths, and paths absent from CODEOWNERS. stdout, stderr and exit codes byte-identical.validate_filestests and the absolute-path case.Unowned files are still reported using the caller's original path string and in input order, so absolute paths render exactly as before. Note the query is keyed by project-relative path, so the original string is carried alongside rather than recomputed.
Two behavior changes, deliberately not hidden
path/x.rb: <error>). A batched read succeeds or fails for the whole set, so the error is no longer per-path. This was open question [Experiment] Integrate error-stack for better error and stacktrace reporting #2 in the plan; if per-path attribution matters to a caller, the batch signature needs to be fallible per item.--no-cacheis a no-op there. This was open question feature parity: owner metadata key in package.json #1, and it is the one thing here I would not merge without your call.Not included, and why
Measured and rejected — recording these so nobody re-attempts them:
mapper_buildis 0ms; the noise floor on this machine is ±2%. Kept on a separate branch as a simplification, not a perf change.gvvalidate_codeowners_fileis 82ms — the entire available prize, not the ~900ms originally assumed.🤖 Generated with Claude Code