schfs: cache the last inode and the last extent

Every read descended the B-tree twice per block -- once for the inode and
once for the extent -- so an 834 KB file cost 418 descents, each a tree
walk plus a 4 KB copy out of the buffer cache per node visited. A
sequential read asks for the same inode every time and usually stays
inside one extent, so both answers were already known and thrown away.

One entry each, not a table: the pattern being optimised is a single file
read straight through, and one entry catches all of it without needing an
eviction policy. A second reader simply misses.

Correctness rests entirely on invalidation, so it is blunt: all five
write entry points in objw.c call schfs_cache_invalidate() before
touching anything. A cache that is merely usually right would hand back
another file's data. The volume pointer is part of the key because the
host tools can hold two images open at once.

Measured in the guest, bracketed inside one command line, TCG:

  read 834 KB   1.73s -> 0.31s   (5.6x, together with the BOUNCE fix)
  ls /bin cold  2.80s -> 1.40s   (2x)
  ls /bin warm  0.70s -> 0.70s   (unchanged)

Warm ls is untouched on purpose-by-consequence: it is 241 separate
inodes, so a one-entry cache never hits. That cost is the per-entry
lookup path -- dirlookup plus iload, each its own descent -- and it is
where the next order of magnitude is, not here.

schfs-check passes, including the round-trip; bootcheck 108 [ok].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-09 23:52:11 +02:00
commit ddf1e41177
3 changed files with 80 additions and 3 deletions

View file

@ -286,6 +286,50 @@ schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
/* File data */
/* ------------------------------------------------------------------ */
/* --------------------------------------------------------------------------
* Read caches: the last inode, and the last extent.
*
* Every read used to descend the B-tree twice per block -- once for the inode
* and once for the extent -- so an 834 KB file cost 418 descents, each a tree
* walk plus a 4 KB copy out of the buffer cache per node visited. Sequential
* reads ask for the same inode every time and usually stay inside one extent,
* so both answers are already known.
*
* One entry each, not a table: the access pattern being optimised is one file
* read straight through, and a single entry catches all of it without needing
* eviction policy or a hash. A second reader simply misses.
*
* Correctness rests entirely on invalidation, so it is deliberately blunt:
* every write entry point in objw.c calls schfs_cache_invalidate() before it
* touches anything. A cache that is merely usually right is worse than none at
* all here -- a stale extent hands back another file's data.
*
* The volume pointer is part of the key because the host tools can hold more
* than one volume open, and two images would otherwise share these.
* ------------------------------------------------------------------------ */
static struct {
int valid;
const struct schfs_vol *vol;
uint64_t ino;
struct schfs_inode in;
} ino_cache;
static struct {
int valid;
const struct schfs_vol *vol;
uint64_t ino;
uint64_t first; /* first file block covered */
uint64_t nblocks; /* how many, from `first` */
uint64_t disk; /* disk block backing `first` */
} ext_cache;
void
schfs_cache_invalidate(void)
{
ino_cache.valid = 0;
ext_cache.valid = 0;
}
// Map a file block to a disk block. Returns 1 if mapped, 0 for a hole, -2 on
// corruption. Extents are keyed by the file block they start at, so the record
// governing block B is the last one at or before (ino, EXTENT, B).
@ -293,6 +337,12 @@ static int
extent_map(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t fblk, uint64_t *out)
{
if (ext_cache.valid && ext_cache.vol == v && ext_cache.ino == ino &&
fblk >= ext_cache.first && fblk < ext_cache.first + ext_cache.nblocks) {
*out = ext_cache.disk + (fblk - ext_cache.first);
return 1;
}
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
@ -319,6 +369,14 @@ extent_map(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
if (fblk >= got->offset + e->nblocks)
return 0; /* hole past the end of this extent */
ext_cache.valid = 1;
ext_cache.vol = v;
ext_cache.ino = ino;
ext_cache.first = got->offset;
ext_cache.nblocks = e->nblocks;
ext_cache.disk = e->disk_block;
*out = e->disk_block + (fblk - got->offset);
return 1;
}
@ -355,9 +413,17 @@ schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t off, void *buf, unsigned n, void *dblk)
{
struct schfs_inode in;
int r = schfs_inode_get(v, c, ino, &in);
if (r != 0)
return r;
if (ino_cache.valid && ino_cache.vol == v && ino_cache.ino == ino) {
in = ino_cache.in;
} else {
int r = schfs_inode_get(v, c, ino, &in);
if (r != 0)
return r;
ino_cache.valid = 1;
ino_cache.vol = v;
ino_cache.ino = ino;
ino_cache.in = in;
}
if (off >= in.size)
return 0;