summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/File.zig46
-rw-r--r--src/ninep/ctl.zig1
-rw-r--r--src/ninep/pane.zig80
-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.zig21
7 files changed, 135 insertions, 19 deletions
diff --git a/src/File.zig b/src/File.zig
index e8a9e2f8..43b921e3 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -57,6 +57,9 @@ pub const State = struct {
/// replaces it: an edit that brings the text back to it is clean again,
/// as an undo to the save point is in acme.
saved_hash: ?u64 = null,
+ /// The length of the text `saved_hash` is of, when it is of one: an
+ /// edit to another length cannot be back to it, and is not hashed.
+ saved_len: ?usize = null,
watch_after_save: bool = false,
/// Save makes the file's directory, and any above it, first: `Config`'s
/// pane for an init file that is not there yet, whose directory may not
@@ -617,14 +620,22 @@ pub fn lineIndex(gpa: std.mem.Allocator, f: *State) ![]const usize {
/// The line index of `new` from `old`'s: an edit replaces one span, so
/// line starts before it stand, starts after it shift, and only the span
/// is scanned. Rebuilding scanned the whole file on every keystroke.
-fn editedLineStarts(gpa: std.mem.Allocator, starts: []const usize, old: []const u8, new: []const u8) ![]usize {
+/// The bytes a splice left alone at either end, when its caller knows them.
+pub const Span = struct { head: usize, tail: usize };
+
+fn editedLineStarts(gpa: std.mem.Allocator, starts: []const usize, old: []const u8, new: []const u8, known: ?Span) ![]usize {
const n = @min(old.len, new.len);
var head: usize = 0;
- while (head + 64 <= n and std.mem.eql(u8, old[head..][0..64], new[head..][0..64])) head += 64;
- while (head < n and old[head] == new[head]) head += 1;
var tail: usize = 0;
- while (tail + 64 <= n - head and std.mem.eql(u8, old[old.len - tail - 64 ..][0..64], new[new.len - tail - 64 ..][0..64])) tail += 64;
- while (tail < n - head and old[old.len - tail - 1] == new[new.len - tail - 1]) tail += 1;
+ if (known) |span| {
+ head = span.head;
+ tail = span.tail;
+ } else {
+ while (head + 64 <= n and std.mem.eql(u8, old[head..][0..64], new[head..][0..64])) head += 64;
+ while (head < n and old[head] == new[head]) head += 1;
+ while (tail + 64 <= n - head and std.mem.eql(u8, old[old.len - tail - 64 ..][0..64], new[new.len - tail - 64 ..][0..64])) tail += 64;
+ while (tail < n - head and old[old.len - tail - 1] == new[new.len - tail - 1]) tail += 1;
+ }
// A start s follows the newline at s-1: kept while that newline is in
// the common head, shifted while it is in the common tail.
const kept = std.sort.upperBound(usize, starts, head, struct {
@@ -677,7 +688,12 @@ test "edited line index equals a rebuilt one" {
const old = try gpa.dupe(u8, text.items);
defer gpa.free(old);
try text.replaceRange(gpa, at, del, ins[0..ins_len]);
- const edited = try editedLineStarts(gpa, starts, old, text.items);
+ // As a splice that says what it kept, and as one that does not.
+ const known: Span = .{ .head = at, .tail = old.len - at - del };
+ const told = try editedLineStarts(gpa, starts, old, text.items, known);
+ defer gpa.free(told);
+ const edited = try editedLineStarts(gpa, starts, old, text.items, null);
+ try std.testing.expectEqualSlices(usize, edited, told);
defer gpa.free(edited);
var fresh_state: State = undefined;
fresh_state.content = text.items;
@@ -906,6 +922,12 @@ fn reportEdit(p: *Pardes, f: *State, new: []const u8) void {
}
pub fn setContent(p: *Pardes, f: *State, new: []u8) void {
+ setContentSpan(p, f, new, null);
+}
+
+/// `setContent` for a splice that knows what it left alone at either end,
+/// so the line index is not found again by comparing the whole text.
+pub fn setContentSpan(p: *Pardes, f: *State, new: []u8, span: ?Span) void {
locations.freeRows(p.gpa, f.location_rows);
f.location_rows = &.{};
for (p.panes) |slot| if (slot) |pane| {
@@ -919,13 +941,17 @@ pub fn setContent(p: *Pardes, f: *State, new: []u8) void {
reportEdit(p, f, new);
if (f.mini) |*mini| mini.deinit(p.gpa);
f.mini = null;
- const starts: []usize = if (f.line_starts.len > 0) editedLineStarts(p.gpa, f.line_starts, f.content, new) catch &.{} else &.{};
- // ponytail: a whole-text hash per edit; a rolling hash when files grow large.
- if (f.revision == f.saved_revision or f.saved_hash == null) f.saved_hash = std.hash.Wyhash.hash(0, f.content);
+ const starts: []usize = if (f.line_starts.len > 0) editedLineStarts(p.gpa, f.line_starts, f.content, new, span) catch &.{} else &.{};
+ // ponytail: a whole-text hash per edit that keeps the saved length; a
+ // rolling hash when that too matters.
+ if (f.revision == f.saved_revision or f.saved_hash == null) {
+ f.saved_hash = std.hash.Wyhash.hash(0, f.content);
+ f.saved_len = f.content.len;
+ }
p.gpa.free(f.content);
f.content = new;
f.revision +%= 1;
- if (f.saved_hash) |h| if (std.hash.Wyhash.hash(0, new) == h) {
+ if (f.saved_hash) |h| if ((f.saved_len orelse new.len) == new.len and std.hash.Wyhash.hash(0, new) == h) {
f.saved_revision = f.revision;
};
f.mtime = pardes.ctlfs.events.now();
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 5ed03c58..05f93ac5 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -983,6 +983,7 @@ fn get(p: *Pardes, pane: *Pane, failed: *anyerror) u16 {
panes.File.setContent(p, f, bytes);
f.saved_revision = f.revision;
f.saved_hash = std.hash.Wyhash.hash(0, f.content);
+ f.saved_len = f.content.len;
f.disk_newer = null;
f.disk_gone = false;
if (discarded) {
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 3fad1fae..af8bb52c 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -149,12 +149,62 @@ pub fn clip(n: usize) u32 {
return std.math.cast(u32, n) orelse std.math.maxInt(u32);
}
+/// The file's line index when it describes `text` (built by a render or a
+/// look), else null: with it a row is a binary search, not a count of every
+/// newline from the top, which a bulk write paid on each flush, twice over.
+fn lineIndex(pane: *const Pane, text: []const u8) ?[]const usize {
+ const f = if (pane.file) |*file| file else return null;
+ if (f.line_starts.len == 0 or f.content.ptr != text.ptr or f.content.len != text.len) return null;
+ return f.line_starts;
+}
+
+/// The line `off` is on: the last start at or before it.
+fn rowAt(starts: []const usize, off: usize) usize {
+ const after = std.sort.upperBound(usize, starts, off, struct {
+ fn order(key: usize, item: usize) std.math.Order {
+ return std.math.order(key, item);
+ }
+ }.order);
+ return after -| 1;
+}
+
+fn lineEnd(starts: []const usize, text: []const u8, row: usize) usize {
+ return if (row + 1 < starts.len) starts[row + 1] - 1 else text.len;
+}
+
+/// `modal.runeOffsetAt`, through the line index when there is one.
+fn runeOffsetAt(pane: *const Pane, text: []const u8, c: modal.Cursor) usize {
+ const starts = lineIndex(pane, text) orelse return modal.runeOffsetAt(text, c);
+ const row = @min(c.row, starts.len - 1);
+ const s = starts[row];
+ const e = lineEnd(starts, text, row);
+ return s + modal.runeStart(text[s..e], @min(c.col, e - s));
+}
+
+/// `modal.runePositionAt`, through the line index when there is one.
+fn runePositionAt(pane: *const Pane, text: []const u8, off: usize) modal.Cursor {
+ const starts = lineIndex(pane, text) orelse return modal.runePositionAt(text, off);
+ const bounded = modal.runeStart(text, off);
+ const row = rowAt(starts, bounded);
+ return .{ .row = row, .col = bounded - starts[row] };
+}
+
+/// `modal.positionAt`, through the line index when there is one.
+fn positionAt(pane: *const Pane, text: []const u8, off: usize) modal.Cursor {
+ const starts = lineIndex(pane, text) orelse return modal.positionAt(text, off);
+ const bounded = @min(off, text.len);
+ const row = rowAt(starts, bounded);
+ const s = starts[row];
+ const e = lineEnd(starts, text, row);
+ return .{ .row = row, .col = modal.graphemeStart(text[s..e], @min(bounded - s, e - s)) };
+}
+
pub fn dotOf(pane: *Pane) State.Range {
const text = bodyOf(pane);
// In runes, as every address is (modal.runeStart).
- const head = modal.runeOffsetAt(text, .{ .row = @intCast(@max(0, pane.body.cur_row)), .col = @intCast(@max(0, pane.body.cur_col)) });
+ const head = runeOffsetAt(pane, text, .{ .row = @intCast(@max(0, pane.body.cur_row)), .col = @intCast(@max(0, pane.body.cur_col)) });
if (!pane.body.vsel.active) return .{ .q0 = clip(head), .q1 = clip(head) };
- const anchor = modal.runeOffsetAt(text, .{ .row = @intCast(@max(0, pane.body.vsel.row)), .col = @intCast(@max(0, pane.body.vsel.col)) });
+ const anchor = runeOffsetAt(pane, text, .{ .row = @intCast(@max(0, pane.body.vsel.row)), .col = @intCast(@max(0, pane.body.vsel.col)) });
var hi = @max(head, anchor);
if (hi < text.len) hi = modal.nextRune(text, hi);
return .{ .q0 = clip(@min(head, anchor)), .q1 = clip(hi) };
@@ -164,9 +214,9 @@ pub fn setDot(pane: *Pane, r: State.Range) void {
const text = bodyOf(pane);
const q0 = @min(@as(usize, r.q0), text.len);
const q1 = @max(q0, @min(@as(usize, r.q1), text.len));
- const a = modal.runePositionAt(text, q0);
+ const a = runePositionAt(pane, text, q0);
pane.body.vsel = .{ .active = q1 > q0, .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true };
- const h = modal.runePositionAt(text, if (q1 > q0) modal.prevRune(text, q1) else q0);
+ const h = runePositionAt(pane, text, if (q1 > q0) modal.prevRune(text, q1) else q0);
pane.body.cur_row = @intCast(h.row);
pane.body.cur_col = @intCast(h.col);
pane.body.cur_pinned = true;
@@ -177,7 +227,7 @@ pub fn setDot(pane: *Pane, r: State.Range) void {
pub fn showOffset(pane: *Pane, off: usize) void {
const text = bodyOf(pane);
- const c = modal.positionAt(text, @min(off, text.len));
+ const c = positionAt(pane, text, @min(off, text.len));
pane.body.cur_row = @intCast(c.row);
pane.body.cur_col = @intCast(c.col);
pane.body.cur_pinned = true;
@@ -229,10 +279,28 @@ pub fn spliceBody(p: *Pardes, pane: *Pane, q0: usize, q1: usize, bytes: []const
const join = pane.fs.joined == f.revision;
pane.fs.joined = null;
if (!pane.fs.nomark and !join) panes.File.pushUndo(p, pane);
- panes.File.setContent(p, f, new);
+ panes.File.setContentSpan(p, f, new, .{ .head = lo, .tail = f.content.len - hi });
return take;
}
+test "rows through the line index are the ones counting newlines gives, at every offset" {
+ const p = try withFile(testing.allocator, "héllo\nwo\u{301}rld\n\n last line é\ntail");
+ defer p.deinit();
+ const pane = p.panes[0].?;
+ const f = &pane.file.?;
+ _ = try panes.File.lineIndex(p.gpa, f);
+ const text = bodyOf(pane);
+ try testing.expect(lineIndex(pane, text) != null);
+ for (0..text.len + 2) |off| {
+ try testing.expectEqual(modal.positionAt(text, off), positionAt(pane, text, off));
+ try testing.expectEqual(modal.runePositionAt(text, off), runePositionAt(pane, text, off));
+ }
+ for (0..8) |row| for (0..16) |col| {
+ const c: modal.Cursor = .{ .row = row, .col = col };
+ try testing.expectEqual(modal.runeOffsetAt(text, c), runeOffsetAt(pane, text, c));
+ };
+}
+
test "a terminal's body stats as long as it reads" {
const gpa = testing.allocator;
const p = try pardes.Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 });
diff --git a/test/perf-baseline-gui-Debug.json b/test/perf-baseline-gui-Debug.json
index 55bb2b70..8808dd55 100644
--- a/test/perf-baseline-gui-Debug.json
+++ b/test/perf-baseline-gui-Debug.json
@@ -1 +1 @@
-{"harness":"75a55b9f838d3f0","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":1970,"med_us":2159,"p90_us":2428,"max_us":2940},{"op":"render","fixture":"medium","min_us":75,"med_us":76,"p90_us":78,"max_us":79},{"op":"key-down","fixture":"medium","min_us":77,"med_us":81,"p90_us":83,"max_us":95},{"op":"key-right","fixture":"medium","min_us":77,"med_us":78,"p90_us":95,"max_us":107},{"op":"page-down","fixture":"medium","min_us":75,"med_us":78,"p90_us":81,"max_us":92},{"op":"wheel","fixture":"medium","min_us":75,"med_us":79,"p90_us":82,"max_us":83},{"op":"edit-char","fixture":"medium","min_us":1056,"med_us":1524,"p90_us":1648,"max_us":1751},{"op":"body-2m","fixture":"medium","min_us":8421,"med_us":15590,"p90_us":19804,"max_us":21613}]}
+{"harness":"f420fe5db60d6e97","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":1740,"med_us":2009,"p90_us":2077,"max_us":2079},{"op":"render","fixture":"medium","min_us":76,"med_us":80,"p90_us":90,"max_us":101},{"op":"key-down","fixture":"medium","min_us":77,"med_us":81,"p90_us":94,"max_us":95},{"op":"key-right","fixture":"medium","min_us":77,"med_us":82,"p90_us":86,"max_us":89},{"op":"page-down","fixture":"medium","min_us":77,"med_us":78,"p90_us":86,"max_us":99},{"op":"wheel","fixture":"medium","min_us":76,"med_us":76,"p90_us":80,"max_us":89},{"op":"edit-char","fixture":"medium","min_us":924,"med_us":1330,"p90_us":1522,"max_us":1566},{"op":"body-2m","fixture":"medium","min_us":4888,"med_us":8435,"p90_us":10635,"max_us":11460},{"op":"body-appends","fixture":"medium","min_us":119102,"med_us":187377,"p90_us":230392,"max_us":245721}]}
diff --git a/test/perf-baseline-tty-Debug.json b/test/perf-baseline-tty-Debug.json
index c4361c57..ef42f0ad 100644
--- a/test/perf-baseline-tty-Debug.json
+++ b/test/perf-baseline-tty-Debug.json
@@ -1 +1 @@
-{"harness":"75a55b9f838d3f0","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":1831,"med_us":2060,"p90_us":2194,"max_us":2252},{"op":"render","fixture":"medium","min_us":75,"med_us":79,"p90_us":79,"max_us":87},{"op":"key-down","fixture":"medium","min_us":77,"med_us":80,"p90_us":86,"max_us":91},{"op":"key-right","fixture":"medium","min_us":77,"med_us":81,"p90_us":89,"max_us":92},{"op":"page-down","fixture":"medium","min_us":77,"med_us":81,"p90_us":85,"max_us":86},{"op":"wheel","fixture":"medium","min_us":75,"med_us":77,"p90_us":83,"max_us":93},{"op":"edit-char","fixture":"medium","min_us":1071,"med_us":1446,"p90_us":1889,"max_us":2537},{"op":"body-2m","fixture":"medium","min_us":8740,"med_us":15875,"p90_us":19640,"max_us":21463}]}
+{"harness":"f420fe5db60d6e97","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":1743,"med_us":2011,"p90_us":2152,"max_us":2217},{"op":"render","fixture":"medium","min_us":76,"med_us":79,"p90_us":80,"max_us":84},{"op":"key-down","fixture":"medium","min_us":77,"med_us":78,"p90_us":85,"max_us":89},{"op":"key-right","fixture":"medium","min_us":77,"med_us":80,"p90_us":83,"max_us":95},{"op":"page-down","fixture":"medium","min_us":78,"med_us":78,"p90_us":87,"max_us":95},{"op":"wheel","fixture":"medium","min_us":76,"med_us":80,"p90_us":88,"max_us":90},{"op":"edit-char","fixture":"medium","min_us":1122,"med_us":1501,"p90_us":1812,"max_us":2440},{"op":"body-2m","fixture":"medium","min_us":4949,"med_us":8504,"p90_us":10909,"max_us":11452},{"op":"body-appends","fixture":"medium","min_us":115605,"med_us":190785,"p90_us":226244,"max_us":245258}]}
diff --git a/test/perf-baseline-tty-ReleaseFast.json b/test/perf-baseline-tty-ReleaseFast.json
index 96ea5801..66421ba3 100644
--- a/test/perf-baseline-tty-ReleaseFast.json
+++ b/test/perf-baseline-tty-ReleaseFast.json
@@ -1 +1 @@
-{"harness":"75a55b9f838d3f0","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":1760,"med_us":2041,"p90_us":2185,"max_us":2206},{"op":"render","fixture":"medium","min_us":76,"med_us":85,"p90_us":100,"max_us":102},{"op":"key-down","fixture":"medium","min_us":77,"med_us":78,"p90_us":85,"max_us":97},{"op":"key-right","fixture":"medium","min_us":76,"med_us":77,"p90_us":90,"max_us":92},{"op":"page-down","fixture":"medium","min_us":77,"med_us":80,"p90_us":85,"max_us":87},{"op":"wheel","fixture":"medium","min_us":75,"med_us":80,"p90_us":89,"max_us":97},{"op":"edit-char","fixture":"medium","min_us":1146,"med_us":1473,"p90_us":1659,"max_us":1796},{"op":"body-2m","fixture":"medium","min_us":8487,"med_us":15600,"p90_us":20312,"max_us":21859}]}
+{"harness":"f420fe5db60d6e97","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":1751,"med_us":2134,"p90_us":2183,"max_us":2415},{"op":"render","fixture":"medium","min_us":76,"med_us":77,"p90_us":91,"max_us":92},{"op":"key-down","fixture":"medium","min_us":77,"med_us":78,"p90_us":82,"max_us":83},{"op":"key-right","fixture":"medium","min_us":79,"med_us":85,"p90_us":100,"max_us":102},{"op":"page-down","fixture":"medium","min_us":78,"med_us":82,"p90_us":86,"max_us":95},{"op":"wheel","fixture":"medium","min_us":76,"med_us":79,"p90_us":83,"max_us":87},{"op":"edit-char","fixture":"medium","min_us":1063,"med_us":1290,"p90_us":1527,"max_us":1564},{"op":"body-2m","fixture":"medium","min_us":4950,"med_us":9414,"p90_us":10945,"max_us":13152},{"op":"body-appends","fixture":"medium","min_us":117839,"med_us":189253,"p90_us":232943,"max_us":247720}]}
diff --git a/test/perf.zig b/test/perf.zig
index fca4a3b0..95b55748 100644
--- a/test/perf.zig
+++ b/test/perf.zig
@@ -49,6 +49,10 @@ const Op = enum {
/// 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,
+ /// 64 appends to the file through 9P, each its own open, 8 KB write and
+ /// close, as a shell's `>>` loop makes: each costs its own bytes, not a
+ /// pass over the whole body for its cursor, line index and saved hash.
+ body_appends,
fn label(o: Op) []const u8 {
return switch (o) {
@@ -60,6 +64,7 @@ const Op = enum {
.wheel => "wheel",
.edit_char => "edit-char",
.body_write => "body-2m",
+ .body_appends => "body-appends",
};
}
};
@@ -78,6 +83,20 @@ fn bodyWrite(core: *pardes.Pardes, serial: u32) void {
_ = core.serveFs(.{ .tag = 4, .op = .release, .node = body, .handle = h, .opened = true });
}
+const append_chunk_len = 8 * 1024;
+const append_count = 64;
+
+fn bodyAppends(core: *pardes.Pardes, serial: u32) void {
+ const Node = pardes.ctlfs.Node;
+ const body = Node.of(serial, .body);
+ for (body_chunk[0..append_chunk_len], 0..) |*c, i| c.* = if (i % 64 == 63) '\n' else 'y';
+ for (0..append_count) |_| {
+ const h = core.serveFs(.{ .tag = 1, .op = .open, .node = body, .omode = 1 }).handle;
+ _ = core.serveFs(.{ .tag = 2, .op = .write, .node = body, .handle = h, .data = body_chunk[0..append_chunk_len] });
+ _ = core.serveFs(.{ .tag = 4, .op = .release, .node = body, .handle = h, .opened = true });
+ }
+}
+
const TermFixture = struct {
name: []const u8,
kb: usize,
@@ -533,6 +552,7 @@ fn measure(op: Op, fx: Fixture, path: []const u8, expected_bytes: usize, reps: u
.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),
+ .body_appends => bodyAppends(core, pane.serial),
}
pump(core);
_ = try frame(core);
@@ -540,6 +560,7 @@ fn measure(op: Op, fx: Fixture, path: []const u8, expected_bytes: usize, reps: u
switch (op) {
.open => unreachable,
.body_write => if (pane.file.?.content.len != before.len + 8 * body_chunk.len) return error.BodyWriteWasNotAppended,
+ .body_appends => if (pane.file.?.content.len != before.len + append_count * append_chunk_len) return error.BodyAppendsWereNotAppended,
.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,