diff --git a/kernel/fs/schfs/obj.c b/kernel/fs/schfs/obj.c index 91bedd1..7cb74ae 100644 --- a/kernel/fs/schfs/obj.c +++ b/kernel/fs/schfs/obj.c @@ -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; diff --git a/kernel/fs/schfs/objw.c b/kernel/fs/schfs/objw.c index 72f8582..4a34545 100644 --- a/kernel/fs/schfs/objw.c +++ b/kernel/fs/schfs/objw.c @@ -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) diff --git a/kernel/fs/schfs/schfs_int.h b/kernel/fs/schfs/schfs_int.h index 5a94d66..985f8a0 100644 --- a/kernel/fs/schfs/schfs_int.h +++ b/kernel/fs/schfs/schfs_int.h @@ -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 // 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. +/* 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, uint64_t off, void *buf, unsigned n, void *dblk);