0052 pre sizing the artifact file buys nothing on a delayed allocation filesystem - CyrilB1531/lodestar GitHub Wiki
0052 โ Pre-sizing the artifact file buys nothing on a delayed-allocation filesystem
Status: accepted ยท Date: 2026-08-28
Context
0051 took step 1 of
#429 and left one item unbuilt.
#432 is it: the save writes ~20 MB through
an 80 KB buffer โ 252 write calls, each extending the file โ so setting the length up front
should let the filesystem allocate once.
It could not be shown against any published row, because every save row wrote to a
MemoryStream. So the row came first: embedding_index_save_file, with np.save to a path as
its Python pair, on the reasoning #336 used
for the load half โ the file path is the one a caller takes.
Decision
The artifact file is not pre-sized. JsonArtifact.OpenWrite keeps opening with
FileMode.Create and nothing sets a length.
Interleaved round-robin, ext4 on a block device โ checked, not a tmpfs. The hypothesis alone:
20 589 008 bytes through an 80 KB-buffered FileStream, no JSON and no base64, 25 rounds a run.
| run 1 | run 2 | run 3 | |
|---|---|---|---|
| plain | 5.149 ms | 4.998 ms | 5.135 ms |
SetLength first |
5.177 ms | 5.081 ms | 5.016 ms |
Under 2% apart and apart in both directions. On the real save path, pre-sizing came out slower in two runs of three.
The mechanism, because an unexplained null result is not evidence. ext4 defers allocation to
writeback and sizes it to what is there, so the per-write extension this would absorb never
happens; ftruncate up front only makes a sparse extent writeback still has to allocate. The
floor confirms it: File.WriteAllBytes of the finished artifact costs 4.86 ms against the whole
save's 7.67, and the 2.8 ms between them is the base64 encode 0051 prices at 3.211 on this
machine. Nothing is left over.
It was not free either. The estimate covers the vector block, and the head and the ids are
written past it, so pre-sizing obliges a truncation afterwards โ a crash between the two leaves a
file longer than its document. That is a new invariant on JsonArtifact for zero.
Consequences
- #432 closes as refused, not as done โ a roadmap item someone should see was measured and declined rather than skipped. 0051's refusal of parallel base64 is the same argument: a sound, cheap, obviously right design, refused because the measurement came back empty. There the memory subsystem was already doing the work; here the filesystem is.
embedding_index_save_filestays, and is not scaffolding for a refused change: it is the save row this project should have had since #336, written to a path of its own so neither direction measures a file the other just touched.- The reopening condition is a filesystem that charges per extension โ NTFS advances a valid-data-length and zero-fills rather than deferring. Nothing here was measured on Windows, so this covers ext4 rather than every filesystem, and the row is what would settle it there.