summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 02:33:14 -0300
committerGabriel Schneider <[email protected]>2026-10-01 02:33:14 -0300
commit803f2a2678a8aa8f89b0663a84a60721d36f5831 (patch)
treed0e6cecbf41baeb2790cacde42ee74b393462d03
parent62ed30e879693ece34c517f238fd6ee1ec3a114b (diff)
downloadpardes-803f2a2678a8aa8f89b0663a84a60721d36f5831.tar.gz
pardes-803f2a2678a8aa8f89b0663a84a60721d36f5831.zip
pardes - writes the whole text through one open of the body, so a page over 8 MiB arrives whole, not as its last chunk
Each 8 MiB chunk went in by its own Client.write, which opens a body with OTRUNC, so every chunk replaced the one before and a long page kept only its tail, with exit 0. The text now goes in through one open, truncated once and written at rising offsets; a long write is timed by its progress, each 9P write answered within 30 s rather than the whole transfer within 2. fs.py pages 11 MiB of numbered lines and compares the body byte for byte, and pages past the 256 MiB cap and checks the first 256 MiB and the cut note. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/9p_io.zig8
-rw-r--r--src/main.zig24
-rw-r--r--test/fs.py23
3 files changed, 36 insertions, 19 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig
index 6cf75c99..d80d7b30 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -1937,6 +1937,8 @@ test "9P shell environment states being inside pardes apart from how to reach it
pub const Client = struct {
pub const budget_ms: i64 = 2000;
+ /// Each write of a long one has this long to be answered (transact).
+ pub const write_budget_ms: i64 = 30_000;
pub const max_depth: usize = 2 * ninep.max_welem;
@@ -2211,6 +2213,12 @@ pub const Client = struct {
const count = (try s.ask(.{ .write = .{ .fid = cur, .offset = written, .data = chunk } }, remote)).write;
if (count == 0 or count > chunk.len) return Error.Botch;
written += count;
+ // A long write is timed by its progress: each answer gives
+ // the next its own budget, so a large body (`pardes -`) goes
+ // in through one open, whole, however long it takes. The
+ // budget is a write's: a server putting in tens of MiB it
+ // held (pane.zig batchFor) answers that one write late.
+ s.deadline = nowMs() +| write_budget_ms;
}
return;
}
diff --git a/src/main.zig b/src/main.zig
index 6627bece..1075b596 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -101,8 +101,8 @@ const help_text =
;
/// `pardes -` inside a session: `text` into the directory's one +Pager
-/// (root ctl `pager <dir>`, refilled when it is there), streamed into its
-/// body a bounded chunk at a time as one undo step, and left clean. A text
+/// (root ctl `pager <dir>`, refilled when it is there), written into its
+/// body through one open, as one undo step, and left clean. A text
/// the session would not take is never lost: it goes to stderr, with why,
/// and the exit is 1.
fn pageInto(io: std.Io, gpa: std.mem.Allocator, dial: []const u8, text: []const u8) void {
@@ -129,25 +129,11 @@ fn pageInto(io: std.Io, gpa: std.mem.Allocator, dial: []const u8, text: []const
ninep_io.Client.write(gpa, dial, "/ctl", ctl_line) catch |err| Fail.with(io, text, "no +Pager pane", err);
const answer = ninep_io.Client.read(gpa, dial, "/exec", "/exec") catch |err| Fail.with(io, text, "no +Pager pane", err);
const serial = std.fmt.parseInt(u32, std.mem.trim(u8, answer, " \n"), 10) catch |err| Fail.with(io, text, "no +Pager pane", err);
- var path_buf: [64]u8 = undefined;
- const mark = std.fmt.bufPrint(&path_buf, "/pane/{d}/mark", .{serial}) catch unreachable;
- // One undo step for the whole text, however many writes carry it.
- ninep_io.Client.write(gpa, dial, mark, "0\n") catch |err| Fail.with(io, text, "the +Pager pane went", err);
+ // The whole text through one open of the body (truncated once, then
+ // written at rising offsets): one undo step, and every byte of it.
var body_buf: [64]u8 = undefined;
const body = std.fmt.bufPrint(&body_buf, "/pane/{d}/body", .{serial}) catch unreachable;
- // A chunk ends on a line's end (never inside a character), each its own
- // write with its own deadline, so a large text arrives whole.
- const chunk_max = 8 * 1024 * 1024;
- var at: usize = 0;
- while (at < text.len) {
- var end = @min(text.len, at + chunk_max);
- if (end < text.len) if (std.mem.lastIndexOfScalar(u8, text[at..end], '\n')) |nl| {
- end = at + nl + 1;
- };
- ninep_io.Client.write(gpa, dial, body, text[at..end]) catch |err| Fail.with(io, text[at..], "the +Pager pane would not take it", err);
- at = end;
- }
- ninep_io.Client.write(gpa, dial, mark, "1\n") catch {};
+ ninep_io.Client.write(gpa, dial, body, text) catch |err| Fail.with(io, text, "the +Pager pane would not take it", err);
var ctl_buf: [64]u8 = undefined;
ninep_io.Client.write(gpa, dial, std.fmt.bufPrint(&ctl_buf, "/pane/{d}/ctl", .{serial}) catch unreachable, "clean\n") catch {};
}
diff --git a/test/fs.py b/test/fs.py
index b2f52c3e..789021bd 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -1467,6 +1467,29 @@ def test(binary, quic=False):
assert serials() - before == {made_now}, (serials(), before)
assert client.read(f'/pane/{made_now}/body') == b'again\nprogress 100%\n'
client.remove(f'/pane/{made_now}')
+ # More than one 8 MiB write's worth arrives whole, byte for byte,
+ # in the one +Pager (each write once replaced the last).
+ big = b''.join(b'line %08d of the paged text\n' % i for i in range(400000)) # ~11 MiB
+ assert len(big) > 8 * 1024 * 1024
+ fed = subprocess.run([binary, '-'], cwd=root, env=env, input=big,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
+ assert fed.returncode == 0, fed.stderr
+ big_pane, = serials() - before
+ assert client.read(f'/pane/{big_pane}/body') == big, 'the paged body is not the text'
+ # Past the 256 MiB cap: the first 256 MiB, then the cut note.
+ cap = 256 * 1024 * 1024
+ huge = subprocess.Popen(['sh', '-c', f'yes 0123456789abcdef | head -c {cap + 4096}'], stdout=subprocess.PIPE)
+ fed = subprocess.run([binary, '-'], cwd=root, env=env, stdin=huge.stdout,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=600)
+ huge.wait()
+ assert fed.returncode == 0, fed.stderr
+ assert serials() - before == {big_pane}
+ got = client.read(f'/pane/{big_pane}/body')
+ note = b'\n[pardes: the rest of stdin is cut: a pane holds at most a file\'s limit]\n'
+ assert got.endswith(note) and len(got) == cap + len(note), (len(got), got[-120:])
+ assert got[:cap] == (b'0123456789abcdef\n' * (cap // 17 + 1))[:cap]
+ del got, big
+ client.remove(f'/pane/{big_pane}')
# From a directory whose name could not be a ctl line: the
# session's directory's +Pager, the text whole, no stray +New.
odd = root / 'two\nlines'