Summary
materialize_tracked reads and hashes EVERY present file on EVERY sync pass. It
never consults the scan cache. On the live Silber daemon this is the largest
single consumer of CPU.
Evidence
I measured the running daemon, PID 11655, version 0.2.0+fff659e.
I sampled the process CPU as a RATE, not as a level. I read the cumulative CPU
time twice over a known interval and divided.
- One 60 second window: 30.2 percent of one core.
- Twelve 10 second windows: mean 32.5 percent, range 19.7 to 50.8 percent.
- A 15 second
sample profile, apportioned by top of stack: 35.0 percent.
Three instruments agree. ps %CPU reported 45.9 to 89.4 percent at the same
time, so ps %CPU overstates this load. Do not use it here.
The profile apportions the busy time like this, as a share of one core:
| frame |
share of one core |
blake3_hash_many_neon |
14.22% |
__getdirentries64 |
6.30% |
__sendmsg |
2.69% |
__open_nocancel + __open |
2.85% |
sync::glob::matches_bytes |
1.63% |
Manifest::normalize_path |
0.77% |
Content hashing is the largest consumer. The call path is
entry_loop → sync_once → scan_folder → scan_into_node_observed →
materialize_tracked → content_hash.
The cause
materialize_tracked iterates every present path and does this per file:
let existing = std::fs::read(&path);
...
let existing_hash = content_hash(&existing);
The function holds no reference to scan_cache. I checked origin/main and the
function still has none. scan_folder uses the cache; materialize_tracked
bypasses it.
Why it costs so much here
The st2-declarations-default entry syncs 64 files. Those files hold
70,157,702 bytes. Three stale build artifacts hold 69,898,880 of those bytes,
which is 99.6 percent:
23,346,256 B plans/artifacts/fabric/pr25/f26618d/fabric-aarch64-apple-darwin
23,346,240 B plans/artifacts/fabric/pr24/0f19107/fabric-aarch64-apple-darwin
23,206,384 B plans/artifacts/fabric/pr24/5003cfd/fabric-aarch64-apple-darwin
I measured the pass cadence by watching the entry's state.json mtime. The
entry rewrote 64 times in 128 seconds, which is one pass about every 2 seconds.
That matches WATCH_MAX_COALESCE, so a continuous watch event stream drives it.
The mutation is real and external: st2 writes agent status files and appends
pty/*.events.jsonl inside the watched tree.
So fabric re-reads and re-hashes about 70 MB every 2 seconds. It does this
forever, for three binaries that no peer needs.
What this is NOT
This is not manifest decode. Manifest::diff_from took 1 sample. All
serde_json frames together took 12 samples. Issue #52 names "full decode per
pass" as cause 3. The profile does not support that as the live cost on this
daemon.
Suggested fix
Consult the scan cache in materialize_tracked, the same way scan_folder
does. A file whose size and mtime match the cache entry does not need a re-read
and does not need a re-hash.
I want a test that FAILS before the fix. The test should count the read or hash
calls across two passes over an unchanged tree. The count must not scale with
the tree.
Caveats
The profile is ONE 15 second sample. The apportionment above is that one sample.
I predicted blake3 would cost about 1.75 percent of a core, from 70 MB every 2
seconds at 2 GB/s. I measured 14.22 percent. My prediction was wrong by about
8x, so the per-byte cost, the real cadence, or the hashed volume is not what I
assumed. The direction is confirmed and the magnitude is not yet explained.
The st2-bus-default entry also re-reads 14,247 files per pass, holding
12.5 MB. That adds syscall load which I have not separated from the above.
Not actions I will take
The three artifacts live in the st2 catalog. That tree is not mine. I am not
deleting them and I am not editing that tree.
Summary
materialize_trackedreads and hashes EVERY present file on EVERY sync pass. Itnever consults the scan cache. On the live Silber daemon this is the largest
single consumer of CPU.
Evidence
I measured the running daemon, PID 11655, version 0.2.0+fff659e.
I sampled the process CPU as a RATE, not as a level. I read the cumulative CPU
time twice over a known interval and divided.
sampleprofile, apportioned by top of stack: 35.0 percent.Three instruments agree.
ps %CPUreported 45.9 to 89.4 percent at the sametime, so
ps %CPUoverstates this load. Do not use it here.The profile apportions the busy time like this, as a share of one core:
blake3_hash_many_neon__getdirentries64__sendmsg__open_nocancel+__opensync::glob::matches_bytesManifest::normalize_pathContent hashing is the largest consumer. The call path is
entry_loop→sync_once→scan_folder→scan_into_node_observed→materialize_tracked→content_hash.The cause
materialize_trackediterates every present path and does this per file:The function holds no reference to
scan_cache. I checkedorigin/mainand thefunction still has none.
scan_folderuses the cache;materialize_trackedbypasses it.
Why it costs so much here
The
st2-declarations-defaultentry syncs 64 files. Those files hold70,157,702 bytes. Three stale build artifacts hold 69,898,880 of those bytes,
which is 99.6 percent:
I measured the pass cadence by watching the entry's
state.jsonmtime. Theentry rewrote 64 times in 128 seconds, which is one pass about every 2 seconds.
That matches
WATCH_MAX_COALESCE, so a continuous watch event stream drives it.The mutation is real and external: st2 writes agent
statusfiles and appendspty/*.events.jsonlinside the watched tree.So fabric re-reads and re-hashes about 70 MB every 2 seconds. It does this
forever, for three binaries that no peer needs.
What this is NOT
This is not manifest decode.
Manifest::diff_fromtook 1 sample. Allserde_jsonframes together took 12 samples. Issue #52 names "full decode perpass" as cause 3. The profile does not support that as the live cost on this
daemon.
Suggested fix
Consult the scan cache in
materialize_tracked, the same wayscan_folderdoes. A file whose size and mtime match the cache entry does not need a re-read
and does not need a re-hash.
I want a test that FAILS before the fix. The test should count the read or hash
calls across two passes over an unchanged tree. The count must not scale with
the tree.
Caveats
The profile is ONE 15 second sample. The apportionment above is that one sample.
I predicted blake3 would cost about 1.75 percent of a core, from 70 MB every 2
seconds at 2 GB/s. I measured 14.22 percent. My prediction was wrong by about
8x, so the per-byte cost, the real cadence, or the hashed volume is not what I
assumed. The direction is confirmed and the magnitude is not yet explained.
The
st2-bus-defaultentry also re-reads 14,247 files per pass, holding12.5 MB. That adds syscall load which I have not separated from the above.
Not actions I will take
The three artifacts live in the st2 catalog. That tree is not mine. I am not
deleting them and I am not editing that tree.