summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
4 files changed, 116 insertions, 2 deletions
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| {