038a91b6
perf: first reconnect attempt is immediate — the 200ms was our own
a73x 2026-08-08 14:08
Commit message
src/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -628,10 +628,25 @@ fn reconnect( | |||
| 628 | // retry loop from closing the same fd twice. | 628 | // retry loop from closing the same fd twice. |
| 629 | transport.close(); | 629 | transport.close(); |
| 630 | 630 | ||
| 631 | var backoff_ms: u64 = 200; | 631 | // Iteration zero waits not at all. M7 measured resume over the WAN at |
| 632 | // ~255ms, of which ~200ms was this backoff and only ~54ms was the link | ||
| 633 | // and the protocol: the wait was nearly the whole cost, and it bought | ||
| 634 | // nothing. A transport that died a moment ago is overwhelmingly likely | ||
| 635 | // to accept a new connection right now — daemon restarts, link blips | ||
| 636 | // and killed proxies all reconnect immediately — so the first attempt | ||
| 637 | // is free to be optimistic. A genuinely down link pays one wasted | ||
| 638 | // syscall for it, and then the backoff below does its real job, which | ||
| 639 | // is pacing a link that is flapping rather than merely gone. | ||
| 640 | // | ||
| 641 | // This is the same single iteration as every other one, with a | ||
| 642 | // zero-length wait — not a second code path. Everything that guards a | ||
| 643 | // retry (the abort drain, one transport owned per iteration, the | ||
| 644 | // explicit close of a `fresh` that failed) therefore guards the | ||
| 645 | // immediate attempt too, for free. | ||
| 646 | var backoff_ms: u64 = 0; | ||
| 632 | while (true) { | 647 | while (true) { |
| 633 | if (drainStdinForQuit(stdin_fd, backoff_ms)) return false; | 648 | if (drainStdinForQuit(stdin_fd, backoff_ms)) return false; |
| 634 | backoff_ms = @min(backoff_ms * 2, 2000); | 649 | backoff_ms = if (backoff_ms == 0) 200 else @min(backoff_ms * 2, 2000); |
| 635 | 650 | ||
| 636 | var fresh = Transport.open(alloc, sock_path, via) catch continue; | 651 | var fresh = Transport.open(alloc, sock_path, via) catch continue; |
| 637 | proto.writeFrame( | 652 | proto.writeFrame( |
| @@ -763,6 +778,26 @@ test "drainStdinForQuit: the quit byte is seen, other input is dropped" { | |||
| 763 | } | 778 | } |
| 764 | } | 779 | } |
| 765 | 780 | ||
| 781 | test "drainStdinForQuit: a zero wait returns at once and consumes nothing" { | ||
| 782 | const pipe = try std.posix.pipe(); | ||
| 783 | defer std.posix.close(pipe[0]); | ||
| 784 | defer std.posix.close(pipe[1]); | ||
| 785 | // A quit byte is already queued, which makes this the interesting case: | ||
| 786 | // reconnect's iteration zero passes a zero-length wait, and it must | ||
| 787 | // neither block nor eat the byte. Leaving it unread is what keeps | ||
| 788 | // Ctrl-\ working — the main loop reads it as input a moment later and | ||
| 789 | // detaches — while eating it here would silently drop the user's abort. | ||
| 790 | _ = try std.posix.write(pipe[1], "\x1c"); | ||
| 791 | |||
| 792 | var timer = try std.time.Timer.start(); | ||
| 793 | try std.testing.expect(!drainStdinForQuit(pipe[0], 0)); | ||
| 794 | try std.testing.expect(timer.read() / std.time.ns_per_ms < 50); | ||
| 795 | |||
| 796 | var buf: [4]u8 = undefined; | ||
| 797 | const n = try std.posix.read(pipe[0], &buf); | ||
| 798 | try std.testing.expectEqualSlices(u8, "\x1c", buf[0..n]); | ||
| 799 | } | ||
| 800 | |||
| 766 | test "drainStdinForQuit: silence waits out the timeout and gives up" { | 801 | test "drainStdinForQuit: silence waits out the timeout and gives up" { |
| 767 | const pipe = try std.posix.pipe(); | 802 | const pipe = try std.posix.pipe(); |
| 768 | defer std.posix.close(pipe[0]); | 803 | defer std.posix.close(pipe[0]); |
test/wan.sh
| Old | New | ||
|---|---|---|---|
| @@ -881,9 +881,11 @@ m7_block() { | |||
| 881 | printf ' %-34s %8s %8s %8s %5s\n' "first frame after tear" \ | 881 | printf ' %-34s %8s %8s %8s %5s\n' "first frame after tear" \ |
| 882 | "$(val clean reconnect min)" "$(val clean reconnect med)" \ | 882 | "$(val clean reconnect min)" "$(val clean reconnect med)" \ |
| 883 | "$(val clean reconnect max)" "$(val clean reconnect n)" | 883 | "$(val clean reconnect max)" "$(val clean reconnect n)" |
| 884 | echo " (milliseconds; includes the ~200ms first backoff and one ssh" | 884 | echo " (milliseconds; one ssh channel open — measured just above as" |
| 885 | echo " channel open, measured just above as viafloor med ${floor}ms —" | 885 | echo " viafloor med ${floor}ms — plus the protocol's own share. The" |
| 886 | echo " printed, not gated, per the reattach ruling above)" | 886 | echo " first attempt is immediate since M8 Task 0; before that this" |
| 887 | echo " number also carried a 200ms backoff that bought nothing." | ||
| 888 | echo " Printed, not gated, per the reattach ruling above)" | ||
| 887 | printf ' resumed hands-off: %s of %s\n' "$resumed" "$reps" | 889 | printf ' resumed hands-off: %s of %s\n' "$resumed" "$reps" |
| 888 | printf ' daemon snapshots across all tears: %s -> %s (%s)\n' \ | 890 | printf ' daemon snapshots across all tears: %s -> %s (%s)\n' \ |
| 889 | "$snaps_b" "$snaps_a" \ | 891 | "$snaps_b" "$snaps_a" \ |