summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-25 17:10:23 -0300
committerGabriel Schneider <[email protected]>2026-09-25 17:52:33 -0300
commit73602127d15d10a1932b6fe916bd18a608054980 (patch)
tree0165c9a3638ea5c2031cad1189a9cd5e89a2dd77
parentdf20863879fe2d83077534f4726a985ffc239def (diff)
downloadcloud9-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]>
-rw-r--r--src/fs.zig68
-rw-r--r--src/serve.zig16
2 files changed, 79 insertions, 5 deletions
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 {
diff --git a/src/serve.zig b/src/serve.zig
index 10c8d01..0311f3d 100644
--- a/src/serve.zig
+++ b/src/serve.zig
@@ -178,7 +178,7 @@ pub fn Runner(comptime Backend: type, comptime opts: fs.Options, comptime limits
c.room = .unset;
// Under the lock: the slot is already visible as live.
c.lock();
- c.engine = .init(.{ .in = &c.in, .out = &c.out, .root = r.root, .seed = r.seed ^ c.index });
+ c.engine.initIn(.{ .in = &c.in, .out = &c.out, .root = r.root, .seed = r.seed ^ c.index });
c.unlock();
if (r.handler.opened) |f| f(r.handler.ctx, c);
}
@@ -308,7 +308,19 @@ pub fn Runner(comptime Backend: type, comptime opts: fs.Options, comptime limits
r.slots = .init;
r.stopping = .init(false);
r.active = .init(0);
- for (&r.conns, 0..) |*c, i| c.* = .{ .runner = r, .index = @intCast(i) };
+ // Field by field: a connection's engine and buffers are written
+ // when a client takes the slot (`start`), so a runner sized for
+ // many connections costs nothing for the ones never used.
+ for (&r.conns, 0..) |*c, i| {
+ c.runner = r;
+ c.index = @intCast(i);
+ c.user = null;
+ c.mutex = .init;
+ c.flags = .init(0);
+ c.signal = .unset;
+ c.room = .unset;
+ c.used = .init(false);
+ }
}
/// Binds `address` and starts accepting on it. Returns the bound