Commit 7da7d5c
Skip reading source files on a warm cache hit via mtime + length
`process_files_with_cache` is the largest phase of `pks check`, and half of it is
work we can avoid: for every file we open it, read it in full, and MD5 it, purely
to compare a digest against the cache entry.
Record (mtime_ns, len) alongside the digest and settle the common case -- nothing
changed since the last run -- with one `stat`. The digest remains the authority:
if no stat is recorded, or the stat moved, we fall back to reading and hashing
exactly as before. An entry whose contents match but whose stat moved (a git
checkout, a `touch`) is repaired in place so the next run takes the fast path.
MEASURED on a 51,513-file application, A/B against main in one hyperfine run:
main 4.945s +/- 0.118
this branch 3.450s +/- 0.117 1.43x faster
user time 7.534s -> 6.877s
system time 12.927s -> 8.495s (-34%)
An earlier batch on a busier machine measured the same pair at 1.30x. Both are
valid single-batch comparisons; the ratio moves with load because the phases this
does not touch are a larger share when the machine is quiet. The system-time drop
is the stable signal, and it is the mechanism: this phase is syscall-bound, not
CPU-bound. An earlier attempt to speed the same phase up by parsing JSON faster
(from_reader -> from_slice) changed nothing measurable, which is what pointed at
the syscalls.
## Coarse filesystems are detected, not assumed away
Trusting (mtime, len) is only sound where the filesystem timestamps finely enough
to notice a write. At one-second granularity -- some Docker bind mounts on macOS,
NFS, SMB, FAT -- a same-length edit inside the same second keeps both fields, and
a stat-only check would serve the stale entry.
Rather than probe or assume a platform, `SourceStat::of` reads the value it
already has: a non-zero sub-second component proves the filesystem tracks
sub-second time, so an edit at any other instant would have moved the mtime. A
zero component means it cannot tell us, so the stat is discarded and the digest
carries the entry. This needed no new branches at the call sites -- `None`
already meant "no usable stat" -- and both ways of being wrong fail safe:
- Coarse filesystem: nothing is trusted, the fast path never engages. Correct,
just not faster.
- Fine filesystem, mtime landing exactly on a second boundary: a 1-in-10^9
coincidence costing one extra hash. Measured across 20,003 files of a real
Rails application: zero occurrences.
Cost of the check itself: 1.00x +/- 0.02 against the same branch without it.
It narrows rather than closes the window, and the type's docs say so. A
millisecond-granularity filesystem is trusted, so two same-length writes inside
one millisecond would still be missed -- six orders of magnitude tighter, and
needing machine-speed edits to reach. Also documented: mtimes that are copied
rather than set by writing (rsync -t, tar -p, cp -p) can carry a timestamp from
elsewhere; every mtime-driven cache shares that hole, which is why they all
document `touch` as the way to force a rebuild.
## packwerk compatibility
Verified against packwerk 3.3.0, and the concern turned out to be misplaced:
- Its `Cache::CacheContents.deserialize` uses plain hash access and never
enumerates keys, so an unknown key is invisible to it. Ran its logic against a
packwerk-format entry carrying `source_stat`: reads fine.
- The tools do not share a directory. packwerk reads `tmp/cache/packwerk/<md5>`;
pks writes `tmp/cache/packwerk/zeitwerk/<md5>`.
- The formats were never interchangeable. Feeding packwerk what pks writes today
raises `NoMethodError: undefined method 'map' for nil`. That predates this
change, so `test_compatible_with_packwerk` does not test what its name claims;
it round-trips pks's own format. Left alone, but it is not a guarantee.
Regardless, `source_stat` is `#[serde(default, skip_serializing_if)]`, so an
entry without one still deserializes and is still honored via the digest.
## Failure modes closed by construction
- `EmptyCacheEntry` holds `Option<String>` rather than an empty string meaning
"not computed", private behind `digest()`. `write` errors instead of persisting
a placeholder, which would have produced an entry that never matches -- making
that file permanently uncacheable and silently slow.
- The in-place repair warns on failure rather than discarding the error. The
result stays correct either way, but a persistent failure (unwritable cache
dir, full disk) would otherwise leave every run re-hashing with no clue why.
- That repair only fires when there is a stat worth recording. Without the guard,
a filesystem yielding `None` every run would never match and would rewrite the
entire cache every time.
## Tests
tests/cache_stat_fastpath_test.rs, ten cases. Note that before this change *no
test in the repo exercised a warm cache at all* -- every fixture ships
`cache: false` -- so these paths were untested rather than under-tested.
The fast path: stats are recorded; warm output matches cold; an edit invalidates;
a *same-length* edit invalidates (the case a length-only check would miss); a
whole-second mtime is not trusted.
Fallback and repair: a stat-less packwerk-style entry is honored then upgraded; a
stale stat with a matching digest is repaired in place; a malformed `source_stat`
(five shapes) degrades to the digest without panicking.
Other commands: `pks update` on a warm cache -- the highest-consequence path,
since update *writes* package_todo.yml and a stale entry persists a wrong answer
rather than printing one -- and the experimental parser, which uses a different
cache subdirectory but shares this implementation.
Each was verified to fail rather than assumed to pass: injecting a bug that makes
the cache always hit fails 7 of the 10, and the granularity and repair guards
were separately confirmed to fail with their own checks removed.
Uses `common::Fixture` from #57 rather than a local copy helper.
Verified: `check` and `check --no-cache` produce identical output on the 51k-file
application, as do this branch and main. Across the 30 fixture apps with a
packwerk.yml, 29 are byte-identical; the 30th is app_with_monkey_patches, which
trips the pre-existing nondeterministic duplicate-constant panic in both binaries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>1 parent 54014ec commit 7da7d5c
3 files changed
Lines changed: 736 additions & 22 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
| |||
10 | 12 | | |
11 | 13 | | |
12 | 14 | | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
13 | 110 | | |
14 | 111 | | |
15 | 112 | | |
16 | 113 | | |
17 | | - | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
18 | 119 | | |
19 | 120 | | |
20 | 121 | | |
| 122 | + | |
21 | 123 | | |
22 | 124 | | |
23 | 125 | | |
24 | | - | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
25 | 130 | | |
26 | 131 | | |
27 | 132 | | |
28 | 133 | | |
29 | 134 | | |
30 | 135 | | |
31 | 136 | | |
32 | | - | |
33 | | - | |
34 | 137 | | |
35 | 138 | | |
36 | | - | |
| 139 | + | |
37 | 140 | | |
38 | 141 | | |
| 142 | + | |
39 | 143 | | |
40 | 144 | | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
41 | 164 | | |
42 | 165 | | |
43 | 166 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
33 | 87 | | |
34 | | - | |
35 | | - | |
36 | 88 | | |
| 89 | + | |
| 90 | + | |
37 | 91 | | |
38 | 92 | | |
39 | 93 | | |
40 | 94 | | |
41 | 95 | | |
42 | 96 | | |
43 | 97 | | |
44 | | - | |
45 | | - | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
46 | 111 | | |
47 | 112 | | |
48 | 113 | | |
| 114 | + | |
49 | 115 | | |
50 | 116 | | |
51 | 117 | | |
| |||
81 | 147 | | |
82 | 148 | | |
83 | 149 | | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
84 | 155 | | |
85 | 156 | | |
86 | 157 | | |
| |||
169 | 240 | | |
170 | 241 | | |
171 | 242 | | |
| 243 | + | |
| 244 | + | |
172 | 245 | | |
173 | 246 | | |
174 | 247 | | |
| |||
210 | 283 | | |
211 | 284 | | |
212 | 285 | | |
213 | | - | |
| 286 | + | |
214 | 287 | | |
215 | 288 | | |
216 | 289 | | |
| |||
0 commit comments