From 73602127d15d10a1932b6fe916bd18a608054980 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Fri, 25 Sep 2026 17:10:23 -0300 Subject: fs, serve: set an engine up in place, so a large fid table costs only the fids used A kernel mount (9ns) holds a fid for every inode the kernel caches, so a server that wants `ls -l` of a directory of thousands of files to work needs a fid table of tens of thousands. Server.initIn sets an engine up in place and, with fid_index, never writes a fid slot at or past high_water; the walks over the table (references, reset, orphan) stop there. Runner.init sets each connection's small fields one by one and leaves the engine to start(), so connection slots nobody uses are never written. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/fs.zig | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 3 deletions(-) (limited to 'src/fs.zig') diff --git a/src/fs.zig b/src/fs.zig index 83cea68..ef18a9d 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -467,8 +467,40 @@ pub fn Server(comptime Backend: type, comptime opts: Options) type { return .{ .protocol = .init(.{ .in = o.in, .out = o.out }), .root = o.root, .hash_seed = fmix32(o.seed) }; } + /// `init`, in place. With `Options.fid_index` the fid table is + /// bump-allocated and nothing reads a slot at or past `high_water`, + /// so those slots are left unwritten: a large `fid_capacity` then + /// costs memory only for the fids a client actually holds, where + /// `init` writes the whole table on every connection. + pub fn initIn(s: *Self, o: InitOptions) void { + assert(o.in.len >= msize_min); + assert(o.out.len >= 2 * msize_min); + assert(o.root != 0); + s.protocol = .init(.{ .in = o.in, .out = o.out }); + s.root = o.root; + s.uname = @splat(0); + s.uname_len = 0; + if (!opts.fid_index) s.fids = @splat(.{}); + s.slots = @splat(.{}); + s.job = .{}; + s.seq = 0; + s.nfids = 0; + s.norphans = 0; + s.index = @splat(no_slot); + s.hash_seed = fmix32(o.seed); + s.free_head = no_slot; + s.high_water = 0; + } + + /// The fid slots that may hold anything: all of them, or with + /// `Options.fid_index` the bump-allocated prefix. + fn touched(s: *Self) []Fid { + return if (opts.fid_index) s.fids[0..s.high_water] else s.fids[0..]; + } + pub fn references(s: *const Self, node: u64) bool { - for (s.fids) |fid| if (fid.used and fid.node == node) return true; + const held = if (opts.fid_index) s.fids[0..s.high_water] else s.fids[0..]; + for (held) |fid| if (fid.used and fid.node == node) return true; if (s.job.kind != .none and s.job.node == node) return true; for (s.slots) |slot| if (slot.used and slot.req.node == node) return true; return false; @@ -485,7 +517,7 @@ pub fn Server(comptime Backend: type, comptime opts: Options) type { } fn reset(s: *Self) void { - for (&s.fids, 0..) |*f, i| { + for (s.touched(), 0..) |*f, i| { if (!f.used or f.orphan) continue; if (f.open or refs) s.orphanFid(i) else s.releaseSlot(i); } @@ -754,7 +786,7 @@ pub fn Server(comptime Backend: type, comptime opts: Options) type { fn orphan(s: *Self) ?Backend.Req { if (s.norphans == 0) return null; - for (&s.fids, 0..) |*f, i| { + for (s.touched(), 0..) |*f, i| { if (!f.used or !f.orphan) continue; assert(f.open or refs); s.norphans -= 1; @@ -3358,6 +3390,36 @@ fn checkFidIndex(s: *const IndexRig.S) !void { try testing.expectEqual(used, s.fidCount()); } +test "fs server: an engine set up in place never writes the fid slots it has not used" { + // What a runner's connection slot holds before a client takes it: + // memory nobody wrote, here made loud with a pattern. + const r = try testing.allocator.create(IndexRig); + defer testing.allocator.destroy(r); + r.* = .{ .fsys = .{} }; + @memset(std.mem.asBytes(&r.srv), 0xA5); + r.srv.initIn(.{ .in = &r.in, .out = &r.out, .root = 1, .seed = 0x4242 }); + _ = try r.call(notag, .{ .tversion = .{ .msize = 4096, .version = "9P2000" } }); + _ = try r.call(0, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } }); + // Walk a few hundred fids, clunk half, walk again (the free list), and + // hang up (the reset walks every used slot). + var i: u32 = 1; + while (i <= 300) : (i += 1) _ = try r.walkTo(0, i, &.{ "1", "body" }); + i = 1; + while (i <= 300) : (i += 2) _ = try r.call(2, .{ .tclunk = .{ .fid = i } }); + i = 1; + while (i <= 300) : (i += 2) _ = try r.walkTo(0, 1000 + i, &.{"1"}); + try testing.expectEqual(@as(u16, 301), r.srv.high_water); + try testing.expectEqual(@as(usize, 301), r.srv.fidCount()); + for (r.srv.fids[0..r.srv.high_water], 0..) |*f, k| { + if (f.used and !f.orphan) try testing.expectEqual(k, r.srv.findFid(f.fid).?); + } + // Every slot past the high-water mark is still the pattern. + for (std.mem.sliceAsBytes(r.srv.fids[r.srv.high_water..])) |b| try testing.expectEqual(@as(u8, 0xA5), b); + r.srv.hangup(); + try testing.expectEqual(@as(usize, 0), r.srv.fidCount()); + for (std.mem.sliceAsBytes(r.srv.fids[301..])) |b| try testing.expectEqual(@as(u8, 0xA5), b); +} + /// Fid numbers chosen to stress the index: dense low ids, ids with only high /// bits set, and ids counting down from 2^32-1 (all distinct for i < 2^20). fn adversarialId(i: u32) u32 { -- cgit v1.3