8a3f8a6a
refactor: one counter struct, upgrade.Counters, on both sides of the exec
a73x 2026-08-29 10:01
Commit message
src/server/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -131,33 +131,6 @@ pub fn boundUdpPort(l: *quic_server.Listener) u16 { | |||
| 131 | return std.net.Address.initPosix(@ptrCast(@alignCast(&actual))).getPort(); | 131 | return std.net.Address.initPosix(@ptrCast(@alignCast(&actual))).getPort(); |
| 132 | } | 132 | } |
| 133 | 133 | ||
| 134 | const Stats = struct { | ||
| 135 | snapshots: u64 = 0, | ||
| 136 | snapshot_bytes: u64 = 0, | ||
| 137 | deltas: u64 = 0, | ||
| 138 | delta_bytes: u64 = 0, | ||
| 139 | /// What the same updates would have cost a snapshot-only daemon: | ||
| 140 | /// measured, not estimated — dumpState length at each delta send. | ||
| 141 | snapshot_equiv_bytes: u64 = 0, | ||
| 142 | /// Every `.attach` this daemon ACCEPTED, cumulative and monotonic. | ||
| 143 | /// | ||
| 144 | /// A counter, deliberately, where `clients=` is a gauge. The gauge | ||
| 145 | /// answers "who is watching right now" and cannot answer "did anyone | ||
| 146 | /// attach at all since I last looked": a client that attaches and | ||
| 147 | /// leaves between two samples is invisible to it, and so is one that | ||
| 148 | /// closes as another opens. The CLI wall's in-place zoom is exactly | ||
| 149 | /// the claim that needs the second question answered — it promotes a | ||
| 150 | /// connection it already holds, so the honest proof is that this | ||
| 151 | /// number did not move across a zoom, a skip and an unzoom. | ||
| 152 | /// | ||
| 153 | /// Counted where an attach SUCCEEDS (a session resolved and the client | ||
| 154 | /// is seated), not where the frame arrives: a refusal attached nobody. | ||
| 155 | /// Both arms count, because a socket client's first attach promotes an | ||
| 156 | /// observer and every later one — a QUIC first attach included — | ||
| 157 | /// arrives on an established connection. | ||
| 158 | attaches: u64 = 0, | ||
| 159 | }; | ||
| 160 | |||
| 161 | pub var shutdown_flag = std.atomic.Value(bool).init(false); | 134 | pub var shutdown_flag = std.atomic.Value(bool).init(false); |
| 162 | 135 | ||
| 163 | fn onShutdownSignal(_: c_int) callconv(.c) void { | 136 | fn onShutdownSignal(_: c_int) callconv(.c) void { |
| @@ -559,7 +532,7 @@ pub const Server = struct { | |||
| 559 | /// directory the per-session sockets are bound in. See server_agent.zig | 532 | /// directory the per-session sockets are bound in. See server_agent.zig |
| 560 | /// for why it is handed a `*Server` rather than holding one. | 533 | /// for why it is handed a `*Server` rather than holding one. |
| 561 | agents: AgentRelay = .{}, | 534 | agents: AgentRelay = .{}, |
| 562 | stats: Stats = .{}, | 535 | stats: upgrade.Counters = .{}, |
| 563 | // Set by the upgrade_req handler, checked by the run loop after each | 536 | // Set by the upgrade_req handler, checked by the run loop after each |
| 564 | // pump: the reply must drain before the exec, and the exec must happen | 537 | // pump: the reply must drain before the exec, and the exec must happen |
| 565 | // outside the frame handler (mid-handler exec would strand the observer | 538 | // outside the frame handler (mid-handler exec would strand the observer |
| @@ -745,14 +718,7 @@ pub const Server = struct { | |||
| 745 | // anything sampling `mux d stats`. Saturating rather than @intCast | 718 | // anything sampling `mux d stats`. Saturating rather than @intCast |
| 746 | // below: a manifest is bytes, and a corrupt counter must not panic a | 719 | // below: a manifest is bytes, and a corrupt counter must not panic a |
| 747 | // daemon that is otherwise able to serve. | 720 | // daemon that is otherwise able to serve. |
| 748 | srv.stats = .{ | 721 | srv.stats = d.counters; |
| 749 | .snapshots = d.counters.snapshots, | ||
| 750 | .snapshot_bytes = d.counters.snapshot_bytes, | ||
| 751 | .deltas = d.counters.deltas, | ||
| 752 | .delta_bytes = d.counters.delta_bytes, | ||
| 753 | .snapshot_equiv_bytes = d.counters.snapshot_equiv_bytes, | ||
| 754 | .attaches = d.counters.attaches, | ||
| 755 | }; | ||
| 756 | srv.agents.refused_no_offer = | 722 | srv.agents.refused_no_offer = |
| 757 | std.math.cast(u32, d.counters.agent_refused_no_offer) orelse std.math.maxInt(u32); | 723 | std.math.cast(u32, d.counters.agent_refused_no_offer) orelse std.math.maxInt(u32); |
| 758 | srv.agents.refused_full = | 724 | srv.agents.refused_full = |
| @@ -2061,7 +2027,7 @@ pub const Server = struct { | |||
| 2061 | return; | 2027 | return; |
| 2062 | }; | 2028 | }; |
| 2063 | // Past the refusal, so the counter never grows for an | 2029 | // Past the refusal, so the counter never grows for an |
| 2064 | // attach that attached nobody. See Stats.attaches. | 2030 | // attach that attached nobody. See upgrade.Counters.attaches. |
| 2065 | self.stats.attaches += 1; | 2031 | self.stats.attaches += 1; |
| 2066 | if (self.clients[i]) |*c| { | 2032 | if (self.clients[i]) |*c| { |
| 2067 | // An await's since_seq is a watermark in the OLD | 2033 | // An await's since_seq is a watermark in the OLD |
| @@ -2531,7 +2497,7 @@ pub const Server = struct { | |||
| 2531 | return self.refuseObserver(i); | 2497 | return self.refuseObserver(i); |
| 2532 | }; | 2498 | }; |
| 2533 | // Past both refusals above, so the counter only ever grows | 2499 | // Past both refusals above, so the counter only ever grows |
| 2534 | // for an attach that seated somebody. See Stats.attaches. | 2500 | // for an attach that seated somebody. See upgrade.Counters.attaches. |
| 2535 | self.stats.attaches += 1; | 2501 | self.stats.attaches += 1; |
| 2536 | // Promote: the buffer moves with the fd. | 2502 | // Promote: the buffer moves with the fd. |
| 2537 | const moved = self.observers[i].?.inbound; | 2503 | const moved = self.observers[i].?.inbound; |
| @@ -3259,6 +3225,12 @@ pub const Server = struct { | |||
| 3259 | // The relay hands over its counters as one value; nothing here reads | 3225 | // The relay hands over its counters as one value; nothing here reads |
| 3260 | // its fields, so the manifest cannot drift when the table's shape does. | 3226 | // its fields, so the manifest cannot drift when the table's shape does. |
| 3261 | const ac = self.agents.counters(); | 3227 | const ac = self.agents.counters(); |
| 3228 | // AgentRelay owns the two refusal counters; `self.stats` only ever | ||
| 3229 | // carried them, so they are stamped from the relay on the way out | ||
| 3230 | // and whatever adoption left in the carrier is overwritten here. | ||
| 3231 | var counters = self.stats; | ||
| 3232 | counters.agent_refused_no_offer = ac.refused_no_offer; | ||
| 3233 | counters.agent_refused_full = ac.refused_full; | ||
| 3262 | const daemon: upgrade.Daemon = .{ | 3234 | const daemon: upgrade.Daemon = .{ |
| 3263 | .writer_version = writer_version, | 3235 | .writer_version = writer_version, |
| 3264 | .writer_path = writer_path, | 3236 | .writer_path = writer_path, |
| @@ -3270,16 +3242,7 @@ pub const Server = struct { | |||
| 3270 | .shell_integration = self.spawn_shell_integration, | 3242 | .shell_integration = self.spawn_shell_integration, |
| 3271 | .extra_env = extra_env, | 3243 | .extra_env = extra_env, |
| 3272 | .quic = quic_state, | 3244 | .quic = quic_state, |
| 3273 | .counters = .{ | 3245 | .counters = counters, |
| 3274 | .snapshots = self.stats.snapshots, | ||
| 3275 | .snapshot_bytes = self.stats.snapshot_bytes, | ||
| 3276 | .deltas = self.stats.deltas, | ||
| 3277 | .delta_bytes = self.stats.delta_bytes, | ||
| 3278 | .snapshot_equiv_bytes = self.stats.snapshot_equiv_bytes, | ||
| 3279 | .attaches = self.stats.attaches, | ||
| 3280 | .agent_refused_no_offer = ac.refused_no_offer, | ||
| 3281 | .agent_refused_full = ac.refused_full, | ||
| 3282 | }, | ||
| 3283 | }; | 3246 | }; |
| 3284 | 3247 | ||
| 3285 | // One SessionRec per live session. dumpState is viewport-only by | 3248 | // One SessionRec per live session. dumpState is viewport-only by |
src/server/upgrade.zig
| Old | New | ||
|---|---|---|---|
| @@ -72,7 +72,25 @@ pub const Counters = struct { | |||
| 72 | snapshot_bytes: u64 = 0, | 72 | snapshot_bytes: u64 = 0, |
| 73 | deltas: u64 = 0, | 73 | deltas: u64 = 0, |
| 74 | delta_bytes: u64 = 0, | 74 | delta_bytes: u64 = 0, |
| 75 | /// What the same updates would have cost a snapshot-only daemon: | ||
| 76 | /// measured, not estimated — dumpState length at each delta send. | ||
| 75 | snapshot_equiv_bytes: u64 = 0, | 77 | snapshot_equiv_bytes: u64 = 0, |
| 78 | /// Every `.attach` this daemon ACCEPTED, cumulative and monotonic. | ||
| 79 | /// | ||
| 80 | /// A counter, deliberately, where `clients=` is a gauge. The gauge | ||
| 81 | /// answers "who is watching right now" and cannot answer "did anyone | ||
| 82 | /// attach at all since I last looked": a client that attaches and | ||
| 83 | /// leaves between two samples is invisible to it, and so is one that | ||
| 84 | /// closes as another opens. The CLI wall's in-place zoom is exactly | ||
| 85 | /// the claim that needs the second question answered — it promotes a | ||
| 86 | /// connection it already holds, so the honest proof is that this | ||
| 87 | /// number did not move across a zoom, a skip and an unzoom. | ||
| 88 | /// | ||
| 89 | /// Counted where an attach SUCCEEDS (a session resolved and the client | ||
| 90 | /// is seated), not where the frame arrives: a refusal attached nobody. | ||
| 91 | /// Both arms count, because a socket client's first attach promotes an | ||
| 92 | /// observer and every later one — a QUIC first attach included — | ||
| 93 | /// arrives on an established connection. | ||
| 76 | attaches: u64 = 0, | 94 | attaches: u64 = 0, |
| 77 | agent_refused_no_offer: u64 = 0, | 95 | agent_refused_no_offer: u64 = 0, |
| 78 | agent_refused_full: u64 = 0, | 96 | agent_refused_full: u64 = 0, |
| @@ -357,7 +375,16 @@ fn sampleDaemon() Daemon { | |||
| 357 | .shell_integration = true, | 375 | .shell_integration = true, |
| 358 | .extra_env = &.{ .{ .key = "FOO", .value = "bar" }, .{ .key = "UNSET_ME", .value = null } }, | 376 | .extra_env = &.{ .{ .key = "FOO", .value = "bar" }, .{ .key = "UNSET_ME", .value = null } }, |
| 359 | .quic = q, | 377 | .quic = q, |
| 360 | .counters = .{ .snapshots = 5, .attaches = 9 }, | 378 | .counters = .{ |
| 379 | .snapshots = 5, | ||
| 380 | .snapshot_bytes = 6, | ||
| 381 | .deltas = 7, | ||
| 382 | .delta_bytes = 8, | ||
| 383 | .snapshot_equiv_bytes = 10, | ||
| 384 | .attaches = 9, | ||
| 385 | .agent_refused_no_offer = 11, | ||
| 386 | .agent_refused_full = 12, | ||
| 387 | }, | ||
| 361 | }; | 388 | }; |
| 362 | } | 389 | } |
| 363 | 390 | ||
| @@ -428,8 +455,10 @@ test "manifest: a round-trip loses nothing a session needs" { | |||
| 428 | try std.testing.expectEqual(QuicArm.owned, p.daemon.quic.arm); | 455 | try std.testing.expectEqual(QuicArm.owned, p.daemon.quic.arm); |
| 429 | try std.testing.expectEqual(@as(u8, 0xCD), p.daemon.quic.key[31]); | 456 | try std.testing.expectEqual(@as(u8, 0xCD), p.daemon.quic.key[31]); |
| 430 | try std.testing.expectEqual(@as(u64, 30_000), p.daemon.quic.idle_ms); | 457 | try std.testing.expectEqual(@as(u64, 30_000), p.daemon.quic.idle_ms); |
| 431 | try std.testing.expectEqual(@as(u64, 5), p.daemon.counters.snapshots); | 458 | // Whole-struct, and every field distinct in the sample: a per-field |
| 432 | try std.testing.expectEqual(@as(u64, 9), p.daemon.counters.attaches); | 459 | // assertion is blind to the counter it forgot, and the `inline for` on |
| 460 | // both sides means the one it forgets is the one that was added. | ||
| 461 | try std.testing.expectEqual(d.counters, p.daemon.counters); | ||
| 433 | 462 | ||
| 434 | const ss = sampleSessions(); | 463 | const ss = sampleSessions(); |
| 435 | try std.testing.expectEqual(@as(usize, 2), p.sessions.len); | 464 | try std.testing.expectEqual(@as(usize, 2), p.sessions.len); |