summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/fs.md6
-rw-r--r--src/fs.zig14
-rw-r--r--src/ninep/pane.zig96
-rw-r--r--src/ninep/tree.zig3
-rw-r--r--src/pardes.zig5
-rw-r--r--test/perf-baseline-gui-Debug.json2
-rw-r--r--test/perf-baseline-tty-Debug.json2
-rw-r--r--test/perf-baseline-tty-ReleaseFast.json2
-rw-r--r--test/perf.zig20
9 files changed, 144 insertions, 6 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 5f3ef743..eab1b00d 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -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
diff --git a/src/fs.zig b/src/fs.zig
index 845e6424..2487ffee 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -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,