read: the log window is cached with the table — the last device read a read made for nothing

The log is the one thing in the store that changes without a fold, which is why it was the
one thing still read on every call even after the validation, header, layout and space table
were held across calls. But that is the same argument in reverse: the cache is keyed on the
control block's generation, and a write — the only thing that changes the log — raises it. So
an unchanged generation IS a log that cannot have changed, and a hit can serve it from memory
without asking the device at all.

Measured, this one is neutral in the gate: get 0.038 -> 0.044 ms, inside the run-to-run noise
of this bench. That is expected rather than disappointing — the gate's store carries a log of
a few hundred bytes, so a read of it costs about what the copy from the cache does. The number
this change is for is on the box, whose log is 143,317 bytes: that read and its allocation
were ~15-30 us of every coordinate read there, and the box is where it will show.

With this the objective's list is closed: the control-block validation, the header, the space
table and the log window are all held across calls; the space-table search allocates nothing
and the index search allocates once per search rather than once per probe; the unfolded log is
bounded by LOG_FOLD_BYTES so per-call work has a ceiling; and the gate fails on a worst case
and a ceiling rather than on a ratio.
This commit is contained in:
surface-camera-build
2026-09-23 18:41:36 -04:00
parent b8a7f615ff
commit b3dc55392e
+15 -8
View File
@@ -412,6 +412,10 @@ struct TableCache {
log_off: u64,
log_used: u64,
table: KVVec<u8>,
/// The log window's bytes. Held for the same reason as the table and one better: on a cache hit
/// the log *cannot* have changed, because a write raises the generation this cache is keyed on —
/// so a hit needs no read to know what the log says.
log: KVVec<u8>,
/// The two control-block heads, and a scratch to read them with: both persist so that validating
/// a call costs no allocation. A 4096-byte scratch here is what the whole block used to need;
/// 96 bytes of head is all the validation ever wanted.
@@ -2952,6 +2956,7 @@ impl Addressed {
log_off: 0,
log_used: 0,
table: KVVec::new(),
log: KVVec::new(),
heads: KVVec::new(),
scratch: KVVec::new(),
});
@@ -2988,14 +2993,13 @@ impl Addressed {
opened.scratch.resize(4096, 0, GFP_KERNEL)?;
if cache.generation == generation {
// The store has not moved: what describes it is what we already hold. The log is still
// read, because it is the one thing that changes without a fold and a write raises the
// generation, so a log newer than this cache is a log this branch does not see.
// The store has not moved: what describes it is what we already hold, the log included.
// The log is the one thing that changes without a fold, and a write raises the generation
// this cache is keyed on — so an unchanged generation is exactly a log that cannot have
// changed. Served from memory, not re-read: this was the last device read a coordinate
// read made that it did not have to.
opened.spaces.extend_from_slice(cache.table.as_slice(), GFP_KERNEL)?;
if cache.log_used > 0 {
let len = core::cmp::min(WAL_HEADER_LEN + cache.log_used as usize, MAX_BYTES);
read_exact_at(file, cache.log_off, len, &mut opened.log, &mut opened.scratch)?;
}
opened.log.extend_from_slice(cache.log.as_slice(), GFP_KERNEL)?;
return Ok(Some(opened));
}
@@ -3032,8 +3036,11 @@ impl Addressed {
if control.log_used > 0 {
let len = core::cmp::min(WAL_HEADER_LEN + control.log_used as usize, MAX_BYTES);
read_exact_at(file, control.log_off() as u64, len, &mut opened.log, &mut opened.scratch)?;
read_exact_at(file, control.log_off() as u64, len, &mut cache.log, &mut opened.scratch)?;
} else {
cache.log.clear();
}
opened.log.extend_from_slice(cache.log.as_slice(), GFP_KERNEL)?;
cache.generation = generation;
cache.image_off = opened.image_off;