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 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-10 04:15:45 +02:00
commit bbb61c7405

View file

@ -80,10 +80,43 @@ schfs_namehash(const char *name, unsigned len)
/* Inodes */ /* 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 int
schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c, schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t ino, struct schfs_inode *out) 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; struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0); 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. // it rather than acting on a half-filled struct.
if (len != sizeof(*out)) if (len != sizeof(*out))
return -2; 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; 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 * 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 * 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 * 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. * 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 { static struct {
int valid; int valid;
const struct schfs_vol *vol; const struct schfs_vol *vol;
@ -326,7 +358,8 @@ static struct {
void void
schfs_cache_invalidate(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; ext_cache.valid = 0;
} }
@ -412,18 +445,12 @@ long
schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino, 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)
{ {
/* 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; struct schfs_inode in;
if (ino_cache.valid && ino_cache.vol == v && ino_cache.ino == ino) { int r = schfs_inode_get(v, c, ino, &in);
in = ino_cache.in; if (r != 0)
} else { return r;
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) if (off >= in.size)
return 0; return 0;