diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 16:17:26 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | eb47ded95f43e50dc24b841289b68814105dcfb7 (patch) | |
| tree | 74a1f34024ecbb54eff0b0114aad73df41cfe90a | |
| parent | 1e5904677535d219e9bba93964e531c786870539 (diff) | |
| download | pardes-eb47ded95f43e50dc24b841289b68814105dcfb7.tar.gz pardes-eb47ded95f43e50dc24b841289b68814105dcfb7.zip | |
An open's writes in a row to one place go in as one edit: a 10 MB body write is linear
Each body or data write copied and hashed the whole buffer, so a write the mount cut in 8 KB pieces was quadratic: 10 MB took 50 s and a 1 MB insert into 10 MB 11 s, holding the editor's turn. An open's appends to body, or inserts going on at data's address, are now held and put in as one splice (one copy, one undo step, one line-starts pass) before any other request, the close, or the editor's step once the writes pause 20 ms. fs.py-driven: 2 MB 2.19 -> 0.16 s, 10 MB 50.17 -> 0.78 s, 1 MB data into 10 MB 11.49 -> 0.26 s. A body-2m case (2 MB in 256 KB writes on one open) joins the perf gate: 84023 -> 15785 us; baselines re-recorded.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | docs/fs.md | 6 | ||||
| -rw-r--r-- | src/fs.zig | 14 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 96 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 3 | ||||
| -rw-r--r-- | src/pardes.zig | 5 | ||||
| -rw-r--r-- | test/perf-baseline-gui-Debug.json | 2 | ||||
| -rw-r--r-- | test/perf-baseline-tty-Debug.json | 2 | ||||
| -rw-r--r-- | test/perf-baseline-tty-ReleaseFast.json | 2 | ||||
| -rw-r--r-- | test/perf.zig | 20 |
9 files changed, 144 insertions, 6 deletions
@@ -768,7 +768,11 @@ shell writes it a line at a time), and the next open starts another. The pane has one history, so two opens writing at once take turns in it: each open's first write after the other's starts a step of its own; to make a loop's writes one step, write `1` (an undo point here), then `0`, -the writes, then `1` again. +the writes, then `1` again. An open's writes in a row to one place -- an +append to `body`, inserts going on at `data`'s address -- are held and go +in as one edit when anything else comes (another request, the close, or +the editor's step once they pause 20 ms), so a 10 MB write is one copy, not +one per 8 KB piece; any read sees them. One rule for what fails: a write fails whenever what it asked for fails, whether it came to a `ctl` (the root's, a pane's, a column's, a pane's @@ -1330,6 +1330,20 @@ pub const Namespace = struct { /// its header), 0 when unknown: a write shorter than it is the whole of /// what its client wrote, one that fills it may be cut and go on. write_room: u32 = 0, + /// Writes one open made in a row into one place of a pane's body (an + /// append to `body`, an insert at `data`'s address), held and put in + /// as one edit when anything else comes (ninep/pane.zig flushBatch): a + /// 10 MB write in 8 KB pieces is one copy and one undo step, not 1280. + batch: struct { + serial: u32 = 0, + node: u64 = 0, + handle: u32 = 0, + at: usize = 0, + bytes: std.ArrayList(u8) = .empty, + /// The editor's clock at the last write held: its step puts them in + /// once the writes have paused (`batch_pause_ns`). + last_ns: u64 = 0, + } = .{}, /// The serial of the pane the root's exec or look last ran a line at /// (the keyboard's), 0 before one: its err record names that pane. ran_at: u32 = 0, diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index d760bebd..ec6e8551 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -475,6 +475,55 @@ fn joinOnly(pane: *Pane, handle: u32) void { if (pane.fs.joined_by != 0 and pane.fs.joined_by != handle) pane.fs.joined = null; } +/// How long writes in a row may pause before the editor's own step puts +/// them in: long enough that a stream of 8 KB writes is one edit. +pub const batch_pause_ns: u64 = 20 * std.time.ns_per_ms; + +/// The editor's step: the held writes go in once they have paused. +pub fn flushPausedBatch(p: *Pardes) void { + const b = &p.fs.batch; + if (b.bytes.items.len == 0) return; + if (p.now_ns != 0 and p.now_ns -| b.last_ns < batch_pause_ns) return; + flushBatch(p); +} + +/// Puts the held writes (`fs.batch`) in as one edit: before any other +/// request, the open's release, or the editor's own step. +pub fn flushBatch(p: *Pardes) void { + const b = &p.fs.batch; + if (b.bytes.items.len == 0) return; + defer b.bytes.clearRetainingCapacity(); + const id = p.paneBySerial(b.serial) orelse return; + const pane = p.panes[id].?; + if (fileOf(pane) == null) return; + const before = dotOf(pane); + joinOnly(pane, b.handle); + const take = spliceBody(p, pane, b.at, b.at, b.bytes.items) orelse return; + pane.fs.joined = fileOf(pane).?.revision; + pane.fs.joined_by = b.handle; + setDot(pane, .{ + .q0 = shiftOne(before.q0, clip(b.at), 0, clip(take)), + .q1 = shiftOne(before.q1, clip(b.at), 0, clip(take)), + }); + if (!pane.fs.noscroll) showOffset(pane, b.at + take); +} + +/// The held bytes a write at `at` on this open can go on from: its own +/// batch, when it ends right at `at`; any other is put in first. +fn batchFor(p: *Pardes, req: Req, pane: *Pane, at: usize) ?*@TypeOf(p.fs.batch) { + const b = &p.fs.batch; + const mine = b.serial == pane.serial and b.node == req.node and b.handle == req.handle; + if (b.bytes.items.len > 0 and !(mine and b.at + b.bytes.items.len == at)) flushBatch(p); + // Only an open's writes are held: one with no record (a test's, handle + // 0) is put in at once. A hold past 64 MiB is put in first. + if (req.handle == 0 or b.bytes.items.len > 64 << 20) { + flushBatch(p); + return null; + } + if (b.bytes.items.len == 0) b.* = .{ .serial = pane.serial, .node = req.node, .handle = req.handle, .at = at, .bytes = b.bytes }; + return b; +} + fn writeBody(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 }; if (pane.file == null) { @@ -484,7 +533,14 @@ fn writeBody(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { p.emitWrite(id, req.data[0..take]); return .{ .tag = req.tag, .written = @intCast(take) }; } - const at = bodyOf(pane).len; + const held = if (p.fs.batch.serial == pane.serial and p.fs.batch.node == req.node and p.fs.batch.handle == req.handle) p.fs.batch.bytes.items.len else 0; + const at = bodyOf(pane).len + held; + if (batchFor(p, req, pane, at)) |b| { + const take = wholeUtf8(req.data); + b.bytes.appendSlice(p.gpa, req.data[0..take]) catch return Reply.fail(req.tag, E.NOMEM); + b.last_ns = p.now_ns; + return .{ .tag = req.tag, .written = @intCast(take) }; + } joinOnly(pane, req.handle); const take = spliceBody(p, pane, at, at, req.data) orelse return Reply.fail(req.tag, E.NOMEM); @@ -538,9 +594,19 @@ fn writeData(p: *Pardes, req: Req, pane: *Pane) Reply { if (fileOf(pane) == null) return tree.failText(req.tag, E.INVAL, e_no_text); const pf = &pane.fs; if (pf.addr_failed) return tree.failText(req.tag, E.INVAL, e_addr_failed); - clampAddr(pf, bodyOf(pane).len); + const held = if (p.fs.batch.serial == pane.serial and p.fs.batch.node == req.node and p.fs.batch.handle == req.handle) p.fs.batch.bytes.items.len else 0; + clampAddr(pf, bodyOf(pane).len + held); const q0: usize = pf.addr.q0; const q1: usize = @max(q0, @as(usize, pf.addr.q1)); + // An insert (an empty address) goes on from the open's held bytes. + if (q1 == q0) if (batchFor(p, req, pane, q0)) |b| { + const take = wholeUtf8(req.data); + b.bytes.appendSlice(p.gpa, req.data[0..take]) catch return Reply.fail(req.tag, E.NOMEM); + b.last_ns = p.now_ns; + pf.addr = .{ .q0 = clip(q0 + take), .q1 = clip(q0 + take) }; + return .{ .tag = req.tag, .written = @intCast(take) }; + }; + flushBatch(p); const before = dotOf(pane); joinOnly(pane, req.handle); const take = spliceBody(p, pane, q0, q1, req.data) orelse @@ -1097,6 +1163,32 @@ test "the writes of one open are one undo step, as bash's line-buffered printf m } } +test "an open's writes in a row are held and go in as one edit, seen by the next request" { + const p = try withFile(testing.allocator, "one\n"); + defer p.deinit(); + const serial = serialOf(p); + const body = Node.of(serial, .body); + const h = call(p, .{ .tag = 1, .op = .open, .node = body, .omode = 1 }).reply.handle; + const revision = p.panes[0].?.file.?.revision; + for (0..100) |_| _ = call(p, .{ .tag = 2, .op = .write, .node = body, .handle = h, .data = "0123456789\n" }); + // Held: nothing edited yet. + try testing.expectEqual(revision, p.panes[0].?.file.?.revision); + // Any other request puts them in, once. + try testing.expectEqual(@as(usize, 4 + 1100), rd(p, body, 0, 4096).bytes.len); + try testing.expectEqual(revision + 1, p.panes[0].?.file.?.revision); + _ = call(p, .{ .tag = 3, .op = .release, .node = body, .handle = h, .opened = true }); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status); + try testing.expectEqualStrings("one\n", rd(p, body, 0, 64).bytes); + // data inserts at its address go on the same way. + const data = Node.of(serial, .data); + _ = wr(p, Node.of(serial, .addr), "#0"); + const d = call(p, .{ .tag = 4, .op = .open, .node = data, .omode = 1 }).reply.handle; + _ = call(p, .{ .tag = 5, .op = .write, .node = data, .handle = d, .data = "a" }); + _ = call(p, .{ .tag = 5, .op = .write, .node = data, .handle = d, .data = "b" }); + _ = call(p, .{ .tag = 6, .op = .release, .node = data, .handle = d, .opened = true }); + try testing.expectEqualStrings("abone\n", rd(p, body, 0, 64).bytes); +} + test "two opens writing one body are two undo steps, however their writes interleave" { const p = try withFile(testing.allocator, "one\n"); defer p.deinit(); diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 0960f00b..6d09b9a6 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -454,6 +454,9 @@ pub fn stagedReply(p: *Pardes, req: Req) Reply { // ---- dispatch ---- pub fn handle(p: *Pardes, req: Req) Reply { + // Held writes go in before anything but the next write of their open. + if (p.fs.batch.bytes.items.len > 0 and !(req.op == .write and req.node == p.fs.batch.node and req.handle == p.fs.batch.handle)) + pane.flushBatch(p); if (req.op == .open and req.create) { if (Node.target(req.node)) |t| if (t == .pane and p.paneBySerial(t.pane.serial) == null) return Reply.fail(req.tag, E.NOENT); diff --git a/src/pardes.zig b/src/pardes.zig index dc479909..391fa23d 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -4792,6 +4792,7 @@ pub const Pardes = struct { for (p.panes) |slot| if (slot) |pane| p.noteClosed(pane); p.saveRecent(); p.recent.deinit(p.recentGpa()); + p.fs.batch.bytes.deinit(p.gpa); if (p.stand_in) |pane| p.teardownPane(pane); look.cancelLookHover(p); if (p.repl_ask) |ask| p.gpa.free(ask.text); @@ -5686,6 +5687,8 @@ pub const Pardes = struct { f(h.ctx, timeout); } if (v.now) |clock| p.advance(clock(h.ctx)); + // What a 9P open wrote in a row goes in once the writes pause. + ctlfs.pane.flushPausedBatch(p); while (p.nextQueued()) |ev| p.update(ev); while (p.nextEffect()) |e| p.perform(e); turn.settle(); @@ -7123,6 +7126,8 @@ pub const Pardes = struct { /// sleep exactly as long as nothing changes. pub fn nextWake(p: *const Pardes) ?u64 { const next = p.stepped_ns + animation.frame_ns; + // Held 9P writes go in when they pause: wake for it. + if (p.fs.batch.bytes.items.len > 0) return p.fs.batch.last_ns + ctlfs.pane.batch_pause_ns; if (p.chrome_animation.isActive() or p.presentation.animating()) return next; var frames: ?u64 = null; if (p.look_hover_wait) |waiting| { diff --git a/test/perf-baseline-gui-Debug.json b/test/perf-baseline-gui-Debug.json index 6bfe2c90..659e49d1 100644 --- a/test/perf-baseline-gui-Debug.json +++ b/test/perf-baseline-gui-Debug.json @@ -1 +1 @@ -{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"gui","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1887,"med_us":2147,"p90_us":2277,"max_us":2326},{"op":"render","fixture":"medium","min_us":73,"med_us":76,"p90_us":84,"max_us":96},{"op":"key-down","fixture":"medium","min_us":73,"med_us":81,"p90_us":84,"max_us":86},{"op":"key-right","fixture":"medium","min_us":73,"med_us":77,"p90_us":91,"max_us":173},{"op":"page-down","fixture":"medium","min_us":75,"med_us":78,"p90_us":80,"max_us":84},{"op":"wheel","fixture":"medium","min_us":72,"med_us":76,"p90_us":79,"max_us":83},{"op":"edit-char","fixture":"medium","min_us":1269,"med_us":1437,"p90_us":1680,"max_us":1697}]} +{"harness":"b96624a78789195f","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"gui","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1841,"med_us":2197,"p90_us":2308,"max_us":2328},{"op":"render","fixture":"medium","min_us":72,"med_us":81,"p90_us":84,"max_us":86},{"op":"key-down","fixture":"medium","min_us":74,"med_us":74,"p90_us":80,"max_us":81},{"op":"key-right","fixture":"medium","min_us":74,"med_us":80,"p90_us":85,"max_us":100},{"op":"page-down","fixture":"medium","min_us":74,"med_us":81,"p90_us":84,"max_us":176},{"op":"wheel","fixture":"medium","min_us":71,"med_us":75,"p90_us":79,"max_us":93},{"op":"edit-char","fixture":"medium","min_us":1086,"med_us":1515,"p90_us":1692,"max_us":1822},{"op":"body-2m","fixture":"medium","min_us":9044,"med_us":16111,"p90_us":19782,"max_us":21405}]} diff --git a/test/perf-baseline-tty-Debug.json b/test/perf-baseline-tty-Debug.json index 6ffd034c..bed59c8c 100644 --- a/test/perf-baseline-tty-Debug.json +++ b/test/perf-baseline-tty-Debug.json @@ -1 +1 @@ -{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1907,"med_us":2037,"p90_us":2283,"max_us":2371},{"op":"render","fixture":"medium","min_us":72,"med_us":80,"p90_us":87,"max_us":87},{"op":"key-down","fixture":"medium","min_us":73,"med_us":78,"p90_us":82,"max_us":88},{"op":"key-right","fixture":"medium","min_us":72,"med_us":75,"p90_us":87,"max_us":87},{"op":"page-down","fixture":"medium","min_us":73,"med_us":81,"p90_us":93,"max_us":94},{"op":"wheel","fixture":"medium","min_us":73,"med_us":75,"p90_us":84,"max_us":88},{"op":"edit-char","fixture":"medium","min_us":1333,"med_us":1712,"p90_us":1956,"max_us":2636}]} +{"harness":"b96624a78789195f","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1800,"med_us":2113,"p90_us":2193,"max_us":2326},{"op":"render","fixture":"medium","min_us":71,"med_us":76,"p90_us":85,"max_us":97},{"op":"key-down","fixture":"medium","min_us":73,"med_us":76,"p90_us":82,"max_us":84},{"op":"key-right","fixture":"medium","min_us":72,"med_us":76,"p90_us":81,"max_us":95},{"op":"page-down","fixture":"medium","min_us":74,"med_us":78,"p90_us":83,"max_us":83},{"op":"wheel","fixture":"medium","min_us":72,"med_us":75,"p90_us":82,"max_us":83},{"op":"edit-char","fixture":"medium","min_us":1027,"med_us":1418,"p90_us":1699,"max_us":1834},{"op":"body-2m","fixture":"medium","min_us":8357,"med_us":17722,"p90_us":20080,"max_us":21699}]} diff --git a/test/perf-baseline-tty-ReleaseFast.json b/test/perf-baseline-tty-ReleaseFast.json index 5d68e6b2..d13442a5 100644 --- a/test/perf-baseline-tty-ReleaseFast.json +++ b/test/perf-baseline-tty-ReleaseFast.json @@ -1 +1 @@ -{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"ReleaseFast","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseFast","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1771,"med_us":2022,"p90_us":2188,"max_us":2228},{"op":"render","fixture":"medium","min_us":75,"med_us":76,"p90_us":76,"max_us":86},{"op":"key-down","fixture":"medium","min_us":73,"med_us":79,"p90_us":80,"max_us":80},{"op":"key-right","fixture":"medium","min_us":72,"med_us":78,"p90_us":81,"max_us":82},{"op":"page-down","fixture":"medium","min_us":72,"med_us":76,"p90_us":80,"max_us":84},{"op":"wheel","fixture":"medium","min_us":75,"med_us":76,"p90_us":78,"max_us":79},{"op":"edit-char","fixture":"medium","min_us":1106,"med_us":1436,"p90_us":1692,"max_us":1721}]} +{"harness":"b96624a78789195f","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"ReleaseFast","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseFast","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1819,"med_us":2002,"p90_us":2068,"max_us":2122},{"op":"render","fixture":"medium","min_us":71,"med_us":76,"p90_us":88,"max_us":89},{"op":"key-down","fixture":"medium","min_us":73,"med_us":75,"p90_us":76,"max_us":82},{"op":"key-right","fixture":"medium","min_us":73,"med_us":77,"p90_us":86,"max_us":126},{"op":"page-down","fixture":"medium","min_us":71,"med_us":75,"p90_us":81,"max_us":83},{"op":"wheel","fixture":"medium","min_us":71,"med_us":74,"p90_us":76,"max_us":97},{"op":"edit-char","fixture":"medium","min_us":1036,"med_us":1493,"p90_us":1805,"max_us":1967},{"op":"body-2m","fixture":"medium","min_us":8482,"med_us":17227,"p90_us":20296,"max_us":23040}]} diff --git a/test/perf.zig b/test/perf.zig index e244091c..c639178f 100644 --- a/test/perf.zig +++ b/test/perf.zig @@ -46,6 +46,9 @@ const Op = enum { page_down, wheel, edit_char, + /// 2 MB appended to the file through 9P, on one open, in 256 KB + /// writes: linear, not a copy of the whole buffer per write. + body_write, fn label(o: Op) []const u8 { return switch (o) { @@ -56,10 +59,25 @@ const Op = enum { .page_down => "page-down", .wheel => "wheel", .edit_char => "edit-char", + .body_write => "body-2m", }; } }; +var body_chunk: [256 * 1024]u8 = @splat('y'); + +/// The body_write gesture: an open of the pane's body, eight 256 KB writes, +/// a read (which the held writes go in before), the close. +fn bodyWrite(core: *pardes.Pardes, serial: u32) void { + const Node = pardes.ctlfs.Node; + const body = Node.of(serial, .body); + for (&body_chunk, 0..) |*c, i| c.* = if (i % 64 == 63) '\n' else 'y'; + const h = core.serveFs(.{ .tag = 1, .op = .open, .node = body, .omode = 1 }).handle; + for (0..8) |_| _ = core.serveFs(.{ .tag = 2, .op = .write, .node = body, .handle = h, .data = &body_chunk }); + _ = core.serveFs(.{ .tag = 3, .op = .getattr, .node = body, .handle = h }); + _ = core.serveFs(.{ .tag = 4, .op = .release, .node = body, .handle = h, .opened = true }); +} + const TermFixture = struct { name: []const u8, kb: usize, @@ -514,12 +532,14 @@ fn measure(op: Op, fx: Fixture, path: []const u8, expected_bytes: usize, reps: u .page_down => core.update(.{ .key = .{ .cp = pardes.Key.page_down } }), .wheel => core.update(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = wheel_col, .row = wheel_row } }), .edit_char => core.update(.{ .key = .{ .cp = 'x', .text = "x" } }), + .body_write => bodyWrite(core, pane.serial), } pump(core); _ = try frame(core); const dt = nowNs() -| t0; switch (op) { .open => unreachable, + .body_write => if (pane.file.?.content.len != before.len + 8 * body_chunk.len) return error.BodyWriteWasNotAppended, .render => if (pane.body.cur_row != before.row or pane.body.cur_col != before.col or pane.file.?.content.len != before.len) return error.RenderChangedDocument, .key_down => if (pane.body.cur_row != before.row + 1) return error.DownDidNotMove, |
