summaryrefslogtreecommitdiff
path: root/src/File.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 10:50:18 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit048e81e3471c73b9ac690901819976f689f6a4ce (patch)
treef2872bb0d64c25ff83c779785abe4119ff389ff6 /src/File.zig
parenta0ce72b3d02e836439cac52e8074f569a6b4ae85 (diff)
downloadpardes-048e81e3471c73b9ac690901819976f689f6a4ce.tar.gz
pardes-048e81e3471c73b9ac690901819976f689f6a4ce.zip
An append to a body through 9P costs its own bytes, not five passes over the whole body: 60 KB appends go from 35.7 to 2.1 ms each
Round 25 measured bulk body writes at about 8 ms a write. There is no frame wait in it: a profile of open, write, clunk in a loop found the flush of each close walking the whole body five times. dotOf, setDot and showOffset turned the cursor between offsets and rows by counting every newline from the top; setContent found the line index's changed span by comparing old and new byte for byte, and hashed the new text to see whether it was back to the saved one. The rows now come from the file's line index by binary search (checked against the counting at every offset), a splice tells setContent the span it changed, and the text is hashed only when its length is the saved text's. Measured over 9P on a Debug build: 60 KB appends 35.7 to 2.1 ms, 8 KB 5.3 to 0.8 ms, 1 KB 1.3 to 0.9 ms. What is left is two copies of the text an edit (the splice's and the undo snapshot's). The perf gate gains body-appends, 64 appends of 8 KB on the 50k-line file, and its three baselines are recorded again. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/File.zig')
-rw-r--r--src/File.zig46
1 files changed, 36 insertions, 10 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();