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 */ /* 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 // 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 // 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). // 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, extent_map(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t fblk, uint64_t *out) 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; struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk); 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) if (fblk >= got->offset + e->nblocks)
return 0; /* hole past the end of this extent */ 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); *out = e->disk_block + (fblk - got->offset);
return 1; 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) uint64_t off, void *buf, unsigned n, void *dblk)
{ {
struct schfs_inode in; struct schfs_inode in;
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); int r = schfs_inode_get(v, c, ino, &in);
if (r != 0) if (r != 0)
return r; return r;
ino_cache.valid = 1;
ino_cache.vol = v;
ino_cache.ino = ino;
ino_cache.in = in;
}
if (off >= in.size) if (off >= in.size)
return 0; return 0;

View file

@ -45,6 +45,7 @@ schfs_alloc_objectid(struct schfs_vol *v)
int int
schfs_inode_put(struct schfs_vol *v, uint64_t ino, const struct schfs_inode *in) schfs_inode_put(struct schfs_vol *v, uint64_t ino, const struct schfs_inode *in)
{ {
schfs_cache_invalidate(); /* any write can stale the read caches in obj.c */
struct schfs_key k; struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0); schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
return schfs_btree_insert(&v->tree, &v->ctx, &k, in, sizeof(*in)); return schfs_btree_insert(&v->tree, &v->ctx, &k, in, sizeof(*in));
@ -110,6 +111,7 @@ int
schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name, schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t child, uint16_t type) unsigned namelen, uint64_t child, uint16_t type)
{ {
schfs_cache_invalidate(); /* any write can stale the read caches in obj.c */
if (namelen == 0 || namelen > SCHFS_NAME_MAX) if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1; return -1;
@ -145,6 +147,7 @@ int
schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name, schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen) unsigned namelen)
{ {
schfs_cache_invalidate(); /* any write can stale the read caches in obj.c */
if (namelen == 0 || namelen > SCHFS_NAME_MAX) if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1; return -1;
@ -559,6 +562,7 @@ long
schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off, schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off,
const void *buf, unsigned n, void *dblk) const void *buf, unsigned n, void *dblk)
{ {
schfs_cache_invalidate(); /* any write can stale the read caches in obj.c */
struct schfs_inode in; struct schfs_inode in;
int r = inode_load(v, ino, &in); int r = inode_load(v, ino, &in);
if (r != 0) if (r != 0)
@ -635,6 +639,7 @@ int
schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size, schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size,
void *dblk) void *dblk)
{ {
schfs_cache_invalidate(); /* any write can stale the read caches in obj.c */
struct schfs_inode in; struct schfs_inode in;
int r = inode_load(v, ino, &in); int r = inode_load(v, ino, &in);
if (r != 0) if (r != 0)

View file

@ -184,6 +184,12 @@ int schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
// Read from a file. Short reads at end-of-file are normal; returns the byte // Read from a file. Short reads at end-of-file are normal; returns the byte
// count, or -1 on a missing object, -2 on corruption or a failed data checksum. // count, or -1 on a missing object, -2 on corruption or a failed data checksum.
// `dblk` is a caller-owned SCHFS_BLKSIZE scratch buffer for data blocks. // `dblk` is a caller-owned SCHFS_BLKSIZE scratch buffer for data blocks.
/* Drops the one-entry inode and extent read caches in obj.c. Every write
* entry point calls this before touching anything: the caches are only safe
* because invalidation is blunt, and one that is merely usually right hands
* back another file's data. */
void schfs_cache_invalidate(void);
long schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino, long schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t off, void *buf, unsigned n, void *dblk); uint64_t off, void *buf, unsigned n, void *dblk);