From bbb61c7405c08c3f59dcf507acbd09c156fe38a2 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Mon, 10 Aug 2026 04:15:45 +0200 Subject: [PATCH] schfs: cache sixteen inodes, not one The one-entry cache lived inside schfs_file_read, so stat() never touched it -- which is why `ls` barely moved. Moved into schfs_inode_get, where every caller benefits, and widened to sixteen entries. `ls` is the shape that matters: it stats every name in a directory, so a single entry never hits and each file costs two descents, one to find the name and one to read the inode. Sixteen keeps hitting through a directory walk without a second reader evicting everything. Round-robin rather than LRU: the pattern is a walk, not a working set, so the bookkeeping on every hit would buy nothing. Measured in the guest, TCG, against the original baseline: ls /bin warm 0.70s -> 0.43s ls /bin cold 2.80s -> 1.32s read 834 KB 1.73s -> 0.35s schfs-check passes including the round-trip; bootcheck 108 [ok]. Co-Authored-By: Claude Opus 5 --- kernel/fs/schfs/obj.c | 67 ++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/kernel/fs/schfs/obj.c b/kernel/fs/schfs/obj.c index 7cb74ae..1fa45b1 100644 --- a/kernel/fs/schfs/obj.c +++ b/kernel/fs/schfs/obj.c @@ -80,10 +80,43 @@ schfs_namehash(const char *name, unsigned len) /* Inodes */ /* ------------------------------------------------------------------ */ +/* Inode cache. + * + * Sixteen entries, not one. `ls` is the shape that matters: it stats every name + * in a directory, so a single entry never hits and the whole directory costs + * two tree descents per file -- one to find the name, one to read the inode. + * Sixteen is enough that a directory walk keeps hitting while a second reader + * elsewhere does not evict everything. + * + * Round-robin replacement, deliberately: an LRU needs bookkeeping on every hit, + * and the access pattern here is a walk rather than a working set, so the + * cleverer policy buys nothing. + * + * Invalidation is blunt and total -- see schfs_cache_invalidate(). + */ +#define INO_CACHE_N 16 + +static struct { + int valid; + const struct schfs_vol *vol; + uint64_t ino; + struct schfs_inode in; +} ino_cache[INO_CACHE_N]; + +static unsigned ino_cache_next; + int schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino, struct schfs_inode *out) { + for (unsigned i = 0; i < INO_CACHE_N; i++) { + if (ino_cache[i].valid && ino_cache[i].vol == v && + ino_cache[i].ino == ino) { + *out = ino_cache[i].in; + return 0; + } + } + struct schfs_key k; schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0); @@ -96,6 +129,12 @@ schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c, // it rather than acting on a half-filled struct. if (len != sizeof(*out)) return -2; + + unsigned slot = ino_cache_next++ % INO_CACHE_N; + ino_cache[slot].valid = 1; + ino_cache[slot].vol = v; + ino_cache[slot].ino = ino; + ino_cache[slot].in = *out; return 0; } @@ -287,7 +326,7 @@ schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir, /* ------------------------------------------------------------------ */ /* -------------------------------------------------------------------------- - * Read caches: the last inode, and the last extent. + * Read caches: recently-read inodes (see schfs_inode_get) 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 @@ -307,13 +346,6 @@ schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir, * 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; @@ -326,7 +358,8 @@ static struct { void schfs_cache_invalidate(void) { - ino_cache.valid = 0; + for (unsigned i = 0; i < INO_CACHE_N; i++) + ino_cache[i].valid = 0; ext_cache.valid = 0; } @@ -412,18 +445,12 @@ long schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino, uint64_t off, void *buf, unsigned n, void *dblk) { + /* schfs_inode_get caches for itself now, so this is a hit on every chunk + * after the first without a second cache here to keep coherent. */ 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); - if (r != 0) - return r; - ino_cache.valid = 1; - ino_cache.vol = v; - ino_cache.ino = ino; - ino_cache.in = in; - } + int r = schfs_inode_get(v, c, ino, &in); + if (r != 0) + return r; if (off >= in.size) return 0;