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

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