diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-25 17:10:23 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-25 17:52:33 -0300 |
| commit | 73602127d15d10a1932b6fe916bd18a608054980 (patch) | |
| tree | 0165c9a3638ea5c2031cad1189a9cd5e89a2dd77 /src/fs.zig | |
| parent | df20863879fe2d83077534f4726a985ffc239def (diff) | |
| download | cloud9-73602127d15d10a1932b6fe916bd18a608054980.tar.gz cloud9-73602127d15d10a1932b6fe916bd18a608054980.zip | |
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) <[email protected]>
Diffstat (limited to 'src/fs.zig')
| -rw-r--r-- | src/fs.zig | 68 |
1 files changed, 65 insertions, 3 deletions
@@ -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 { |
