lib/quic/src/sim/link.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const quic = @import("../root.zig");
4
5 const Policy = quic.sim.Policy;
6 const Stats = quic.sim.Stats;
7
8 /// One of the link's two endpoints, `a` and `b`, so every call that sends, receives, or reads
9 /// counters names the endpoint it speaks for. Naming an endpoint picks the queue: a send goes into
10 /// the queue that endpoint owns, and a receive takes from the other one.
11 pub const End = enum(u1) {
12 a,
13 b,
14 };
15
16 /// The two capacities a caller chooses to set how much traffic the link must hold and derive its
17 /// storage size: the datagrams one direction may hold at once, and the bytes one datagram may
18 /// carry. Both directions get the same capacities.
19 pub const Limits = struct {
20 queue_capacity: u32,
21 datagram_capacity: u16,
22 };
23
24 pub const CapacityError = error{CapacityOverflow};
25
26 const Entry = struct {
27 delivery_at_ns: u64 = 0,
28 sequence: u64 = 0,
29 length: u16 = 0,
30 occupied: bool = false,
31 };
32
33 /// The exact storage layout and byte total derived from one set of limits, so a caller learns the
34 /// exact byte count its storage must have. The total covers queue records and payload bytes for
35 /// both directions. Limits whose product overflows give `CapacityOverflow`.
36 pub const Capacity = struct {
37 queue_capacity: u32,
38 datagram_capacity: u16,
39 entry_count: usize,
40 entry_bytes: usize,
41 direction_payload_bytes: usize,
42 payload_bytes: usize,
43 storage_bytes: usize,
44
45 pub const DeriveError: type = CapacityError;
46
47 pub fn derive(limits: Limits) DeriveError!Capacity {
48 const queue_count: usize = @intCast(limits.queue_capacity);
49 const datagram_bytes: usize = @intCast(limits.datagram_capacity);
50 const entry_count = try alloc_phase.capacity.mul(usize, queue_count, 2);
51 const entry_bytes = try alloc_phase.capacity.mul(
52 usize,
53 entry_count,
54 @sizeOf(Entry),
55 );
56 const direction_payload_bytes = try alloc_phase.capacity.mul(
57 usize,
58 queue_count,
59 datagram_bytes,
60 );
61 const payload_bytes = try alloc_phase.capacity.mul(
62 usize,
63 direction_payload_bytes,
64 2,
65 );
66 const storage_bytes = try alloc_phase.capacity.add(
67 usize,
68 entry_bytes,
69 payload_bytes,
70 );
71 return .{
72 .queue_capacity = limits.queue_capacity,
73 .datagram_capacity = limits.datagram_capacity,
74 .entry_count = entry_count,
75 .entry_bytes = entry_bytes,
76 .direction_payload_bytes = direction_payload_bytes,
77 .payload_bytes = payload_bytes,
78 .storage_bytes = storage_bytes,
79 };
80 }
81 };
82
83 const MemoryLimits = Limits;
84 const MemoryCapacity = Capacity;
85
86 const LinkInitSpecificError = error{
87 LimitsMismatch,
88 StorageNotReady,
89 };
90
91 const LinkInitError = Policy.ValidationError || LinkInitSpecificError;
92
93 const LinkSendError = error{
94 Oversize,
95 QueueFull,
96 };
97
98 const Direction = struct {
99 entries: []Entry,
100 payload: []u8,
101 used: u32 = 0,
102 };
103
104 const EntryRef = struct {
105 index: usize,
106 entry: *Entry,
107 payload: []u8,
108 };
109
110 /// The owner of the queue records and payload bytes a link runs on, so one aligned block handed
111 /// over by the caller becomes the only memory the link ever writes to. The owner takes one
112 /// caller-provided aligned byte block and partitions it into the two directions' records and
113 /// payload regions. A block whose length differs from the derived total gives
114 /// `StorageLengthMismatch`. The owner allocates nothing further, so the link's memory use is fixed
115 /// once the block is handed over. Teardown returns the block to the caller.
116 pub const Memory = struct {
117 phase: alloc_phase.capacity.Phase,
118 capacity: MemoryCapacity,
119 storage: Storage,
120 directions: [2]Direction,
121
122 pub const storage_alignment: usize = @alignOf(Entry);
123 pub const Storage = []align(storage_alignment) u8;
124 pub const Limits: type = MemoryLimits;
125 pub const Capacity: type = MemoryCapacity;
126 pub const Exhaustion = error{QueueFull};
127 pub const InitError = MemoryCapacity.DeriveError || error{StorageLengthMismatch};
128 pub const work_limits: alloc_phase.capacity.WorkLimits = .{
129 .transition_steps_max = 1,
130 .cleanup_steps_per_call_max = 0,
131 .cleanup_calls_at_capacity_max = 0,
132 };
133
134 pub const claim: alloc_phase.capacity.Declaration = .{
135 .source = .{
136 .id = "quic.sim_memory",
137 .kind = .phase_static,
138 .limit_source = .caller,
139 .storage = .{
140 .covered = &.{
141 .{
142 .id = "two_direction_queue_metadata",
143 .lifetime = .transferred,
144 .detail = "caller storage for two bounded datagram metadata queues",
145 },
146 .{
147 .id = "two_direction_datagram_payload_bytes",
148 .lifetime = .transferred,
149 .detail = "caller storage for two bounded datagram payload regions",
150 },
151 },
152 .excluded = &.{
153 "link policies, deterministic random state, statistics, and sequence counters",
154 "per-direction drop patterns",
155 "caller send slices and receive output slices",
156 "socket, thread, clock, and operating-system state",
157 },
158 },
159 .capacity = .{
160 .inputs = &.{
161 alloc_phase.capacity.bindInput(
162 MemoryLimits,
163 "queue_capacity",
164 "queue_capacity",
165 ),
166 alloc_phase.capacity.bindInput(
167 MemoryLimits,
168 "datagram_capacity",
169 "datagram_capacity",
170 ),
171 },
172 .type_selectors = &.{
173 alloc_phase.capacity.bindType(Entry, "entry"),
174 },
175 .nodes = &.{
176 .{ .input = 0 },
177 .{ .constant = 2 },
178 .{ .product = .{ .left = 0, .right = 1 } },
179 .{ .scale = .{
180 .node = 2,
181 .coefficient = .{ .size_of_concrete_type = 0 },
182 } },
183 .{ .input = 1 },
184 .{ .product = .{ .left = 2, .right = 4 } },
185 .{ .add = .{ .left = 3, .right = 5 } },
186 },
187 .assertions = &.{.{
188 .scope = .closure_total,
189 .measure = .retained,
190 .relation = .exact,
191 .expression = 6,
192 }},
193 },
194 .overload = .{
195 .kind = .reject_before_mutation,
196 .detail = "full direction admission preserves queue metadata and payload bytes",
197 },
198 .risks = .{
199 .transitive = .{
200 .status = .witnessed,
201 .detail = "link operations use only fixed scans and caller-owned byte slices",
202 },
203 .foreign = .{
204 .status = .excluded,
205 .detail = "simulator memory crosses no operating-system or foreign boundary",
206 },
207 },
208 .work = .{
209 .equation = "send and receive scan at most queue_capacity entries per direction",
210 },
211 .obligations = &.{
212 .{ .key = "quic_sim_memory_capacity", .role = .capacity_model },
213 .{ .key = "quic_sim_memory_overload", .role = .overload },
214 .{ .key = "quic_sim_memory_transitive", .role = .transitive_risk },
215 .{ .key = "quic_sim_memory_work", .role = .work_bound },
216 },
217 },
218 .bindings = .{
219 .owner = @This(),
220 .seal = .{
221 .family = alloc_phase.capacity.selector(@This().activate),
222 .premise = .{
223 .class = .checked_semantic_fact,
224 .authority = .checker,
225 },
226 },
227 .teardown = .{
228 .family = alloc_phase.capacity.selector(@This().deinit),
229 .premise = .{
230 .class = .checked_semantic_fact,
231 .authority = .checker,
232 },
233 },
234 },
235 };
236
237 pub fn init(storage: Storage, limits: MemoryLimits) InitError!Memory {
238 const capacity = try MemoryCapacity.derive(limits);
239 if (storage.len != capacity.storage_bytes) return error.StorageLengthMismatch;
240 const entries = std.mem.bytesAsSlice(Entry, storage[0..capacity.entry_bytes]);
241 for (entries) |*entry| entry.* = .{};
242 const queue_count: usize = @intCast(capacity.queue_capacity);
243 const payload = storage[capacity.entry_bytes..];
244 return .{
245 .phase = .initialization,
246 .capacity = capacity,
247 .storage = storage,
248 .directions = .{
249 .{
250 .entries = entries[0..queue_count],
251 .payload = payload[0..capacity.direction_payload_bytes],
252 },
253 .{
254 .entries = entries[queue_count..],
255 .payload = payload[capacity.direction_payload_bytes..],
256 },
257 },
258 };
259 }
260
261 pub fn activate(self: *Memory) void {
262 std.debug.assert(self.phase == .initialization);
263 std.debug.assert(self.storage.len == self.capacity.storage_bytes);
264 std.debug.assert(self.directions[0].used == 0);
265 std.debug.assert(self.directions[1].used == 0);
266 self.phase = .steady;
267 }
268
269 fn freeCount(self: *const Memory, direction_index: usize) u32 {
270 std.debug.assert(self.phase == .steady);
271 std.debug.assert(direction_index < self.directions.len);
272 const direction = &self.directions[direction_index];
273 std.debug.assert(direction.used <= self.capacity.queue_capacity);
274 return self.capacity.queue_capacity - direction.used;
275 }
276
277 /// Reports whether one direction has room for a given number of further datagrams before the
278 /// link queues anything, so a full direction is refused before any byte moves. Too little room
279 /// gives `QueueFull`. The check reads the queue and changes nothing, so a refusal leaves the
280 /// queued datagrams as they were.
281 pub fn requireFree(self: *const Memory, from: End, copies: u2) Exhaustion!void {
282 std.debug.assert(copies > 0);
283 if (self.freeCount(outgoingIndex(from)) < copies) return error.QueueFull;
284 }
285
286 fn acquire(self: *Memory, direction_index: usize) Exhaustion!EntryRef {
287 std.debug.assert(self.phase == .steady);
288 std.debug.assert(direction_index < self.directions.len);
289 var direction = &self.directions[direction_index];
290 if (direction.used == self.capacity.queue_capacity) return error.QueueFull;
291 for (direction.entries, 0..) |*entry, index| {
292 if (entry.occupied) continue;
293 entry.occupied = true;
294 direction.used += 1;
295 return .{
296 .index = index,
297 .entry = entry,
298 .payload = payloadAt(direction, index, self.capacity.datagram_capacity),
299 };
300 }
301 unreachable;
302 }
303
304 fn release(self: *Memory, direction_index: usize, entry_index: usize) void {
305 std.debug.assert(self.phase == .steady);
306 std.debug.assert(direction_index < self.directions.len);
307 var direction = &self.directions[direction_index];
308 std.debug.assert(entry_index < direction.entries.len);
309 std.debug.assert(direction.entries[entry_index].occupied);
310 std.debug.assert(direction.used > 0);
311 direction.entries[entry_index].occupied = false;
312 direction.used -= 1;
313 }
314
315 pub fn deinit(self: *Memory) Storage {
316 std.debug.assert(self.phase == .steady);
317 self.phase = .teardown;
318 const storage = self.storage;
319 self.* = undefined;
320 return storage;
321 }
322 };
323
324 comptime {
325 alloc_phase.capacity.requireProvisionedRejectingOwnerShape(Memory);
326 }
327
328 fn payloadAt(direction: *const Direction, index: usize, datagram_capacity: u16) []u8 {
329 std.debug.assert(index < direction.entries.len);
330 const width: usize = @intCast(datagram_capacity);
331 const start = index * width;
332 std.debug.assert(start <= direction.payload.len);
333 std.debug.assert(width <= direction.payload.len - start);
334 return direction.payload[start..][0..width];
335 }
336
337 /// A two-ended path that carries opaque datagrams between its endpoints over caller-owned memory,
338 /// on a schedule the caller can reproduce, so a test holds one and drives both endpoints of a
339 /// simulated path through it. The link holds one policy per direction, a generator started from the
340 /// caller's seed, the counters, the send order, and the pending drop patterns. The same seed and
341 /// the same sequence of calls give the same outcome every run, so a failing test can be replayed.
342 /// The bytes the link carries are opaque to it, so QUIC packets remain outside its view.
343 pub const Link = struct {
344 memory: *Memory,
345 policies: [2]Policy,
346 prng: std.Random.DefaultPrng,
347 counts: [2]Stats = .{ .{}, .{} },
348 next_sequence: [2]u64 = .{ 0, 0 },
349 drops: [2]u64 = .{ 0, 0 },
350
351 pub const InitError: type = LinkInitError;
352 pub const SendError: type = LinkSendError;
353
354 /// Starts a link once over memory the caller has already built, with one policy for each
355 /// direction and a seed for the delays and the chances, so the caller receives the value it
356 /// drives. The call moves the memory from its initialization phase into its steady phase, after
357 /// which the partition is fixed. Memory already past initialization gives `StorageNotReady`.
358 /// Limits that differ from the ones the memory was built with give `LimitsMismatch`. Each
359 /// policy is checked against the datagram capacity before anything is sealed, so an invalid
360 /// policy fails here.
361 pub fn init(
362 limits: Limits,
363 storage: *Memory,
364 seed: u64,
365 policy_a_to_b: Policy,
366 policy_b_to_a: Policy,
367 ) InitError!Link {
368 if (storage.phase != .initialization) return error.StorageNotReady;
369 if (storage.capacity.queue_capacity != limits.queue_capacity) {
370 return error.LimitsMismatch;
371 }
372 if (storage.capacity.datagram_capacity != limits.datagram_capacity) {
373 return error.LimitsMismatch;
374 }
375 try policy_a_to_b.validate(limits.datagram_capacity);
376 try policy_b_to_a.validate(limits.datagram_capacity);
377 storage.activate();
378 return .{
379 .memory = storage,
380 .policies = .{ policy_a_to_b, policy_b_to_a },
381 .prng = std.Random.DefaultPrng.init(seed),
382 };
383 }
384
385 /// Marks which of the next datagrams one endpoint sends are thrown away, by bit position,
386 /// starting from bit 0 and covering the next 64, so a test loses exactly the datagrams it
387 /// chooses at the moment it chooses. Only datagrams that pass the MTU check are counted against
388 /// the pattern, because the check comes first. A datagram dropped this way counts as sent and
389 /// lost. A marked datagram draws nothing from the seeded generator, so marking a drop leaves
390 /// the rest of the run unchanged. A subsequent call joins the new pattern with the bits still
391 /// pending, and both are counted from the next datagram.
392 pub fn dropNext(self: *Link, from: End, pattern: u64) void {
393 const direction_index = outgoingIndex(from);
394 std.debug.assert(direction_index < self.drops.len);
395 self.drops[direction_index] |= pattern;
396 std.debug.assert(self.drops[direction_index] & pattern == pattern);
397 }
398
399 /// Takes one datagram from an endpoint at a given instant and applies that direction's policy
400 /// to it, so the link decides what becomes of the datagram. A datagram above the MTU is refused
401 /// with `Oversize` and counted. A datagram the drop pattern names, or that the loss chance
402 /// catches, is counted as sent and lost and stops there. A direction without room for the
403 /// copies is refused with `QueueFull` before anything is queued. Otherwise the bytes are copied
404 /// into the queue with a delivery time drawn from the policy's delay range, and the duplication
405 /// chance may add a second copy with its own delay. The reordering chance trades the new copy's
406 /// delivery time and send order with the most recently queued one.
407 pub fn send(self: *Link, from: End, bytes: []const u8, now_ns: u64) SendError!void {
408 const direction_index = outgoingIndex(from);
409 const policy = self.policies[direction_index];
410 const mtu: usize = policy.effectiveMtu(self.memory.capacity.datagram_capacity);
411 if (bytes.len > mtu) {
412 increment(&self.counts[direction_index].oversize);
413 return error.Oversize;
414 }
415 const dropped = self.takeDrop(direction_index);
416 if (dropped or self.event(policy.loss_permille)) {
417 increment(&self.counts[direction_index].sent);
418 increment(&self.counts[direction_index].lost);
419 return;
420 }
421 const duplicate = self.event(policy.duplicate_permille);
422 const copies: u2 = if (duplicate) 2 else 1;
423 self.memory.requireFree(from, copies) catch {
424 increment(&self.counts[direction_index].queue_full);
425 return error.QueueFull;
426 };
427 const reorder = self.event(policy.reorder_permille);
428 const previous = if (reorder) self.previousIndex(direction_index) else null;
429 const first_delay = self.delay(policy);
430 const second_delay = if (duplicate) self.delay(policy) else 0;
431 increment(&self.counts[direction_index].sent);
432 const first = self.enqueue(direction_index, bytes, deliveryAt(now_ns, first_delay));
433 if (duplicate) {
434 _ = self.enqueue(direction_index, bytes, deliveryAt(now_ns, second_delay));
435 increment(&self.counts[direction_index].duplicated);
436 }
437 if (previous) |previous_index| {
438 self.swapDelivery(direction_index, first.index, previous_index);
439 increment(&self.counts[direction_index].reordered);
440 }
441 }
442
443 /// Copies the earliest datagram due at one endpoint by a given instant into the caller's buffer
444 /// and returns its length, so the destination endpoint receives whatever has arrived. The call
445 /// yields null before any datagram becomes due. Ties between equal delivery times go to the
446 /// earlier send order, so a reordered pair keeps a definite order. The caller's buffer holds at
447 /// least the datagram capacity, which debug builds check. The buffer lies outside the link's
448 /// own bytes, because the copy assumes they do not overlap. Taking a datagram frees its queue
449 /// slot for the next send.
450 pub fn receive(self: *Link, at: End, now_ns: u64, out: []u8) ?usize {
451 const direction_index = incomingIndex(at);
452 const entry_index = self.earliestIndex(direction_index) orelse return null;
453 const direction = &self.memory.directions[direction_index];
454 const entry = &direction.entries[entry_index];
455 if (entry.delivery_at_ns > now_ns) return null;
456 const length: usize = @intCast(entry.length);
457 std.debug.assert(out.len >= self.memory.capacity.datagram_capacity);
458 const payload = payloadAt(
459 direction,
460 entry_index,
461 self.memory.capacity.datagram_capacity,
462 );
463 @memcpy(out[0..length], payload[0..length]);
464 self.memory.release(direction_index, entry_index);
465 increment(&self.counts[direction_index].delivered);
466 return length;
467 }
468
469 /// Returns the instant at which the earliest pending datagram becomes due at one endpoint, so a
470 /// test driving a manual clock learns how far time may move before something arrives. An empty
471 /// queue gives null.
472 pub fn nextDeliveryAt(self: *const Link, at: End) ?u64 {
473 const direction_index = incomingIndex(at);
474 const entry_index = self.earliestIndex(direction_index) orelse return null;
475 return self.memory.directions[direction_index].entries[entry_index].delivery_at_ns;
476 }
477
478 /// Returns a copy of the counters for the datagrams one endpoint sent, so a test reads what the
479 /// path did to those datagrams after a run. The delivery count belongs to the sending direction
480 /// too, because the receiving endpoint's take is counted against the queue it came from.
481 pub fn stats(self: *const Link, from: End) Stats {
482 return self.counts[outgoingIndex(from)];
483 }
484
485 fn event(self: *Link, permille: u16) bool {
486 std.debug.assert(permille <= 1000);
487 if (permille == 0) return false;
488 if (permille == 1000) return true;
489 return self.prng.random().uintLessThan(u16, 1000) < permille;
490 }
491
492 fn takeDrop(self: *Link, direction_index: usize) bool {
493 std.debug.assert(direction_index < self.drops.len);
494 const pattern = self.drops[direction_index];
495 self.drops[direction_index] = pattern >> 1;
496 const dropped = pattern & 1 != 0;
497 const remaining = @popCount(self.drops[direction_index]);
498 std.debug.assert(remaining + @intFromBool(dropped) == @popCount(pattern));
499 return dropped;
500 }
501
502 fn delay(self: *Link, policy: Policy) u64 {
503 std.debug.assert(policy.delay_min_ns <= policy.delay_max_ns);
504 if (policy.delay_min_ns == policy.delay_max_ns) return policy.delay_min_ns;
505 const span = policy.delay_max_ns - policy.delay_min_ns;
506 const offset = if (span == std.math.maxInt(u64))
507 self.prng.random().int(u64)
508 else
509 self.prng.random().uintLessThan(u64, span + 1);
510 return policy.delay_min_ns + offset;
511 }
512
513 fn enqueue(
514 self: *Link,
515 direction_index: usize,
516 bytes: []const u8,
517 delivery_at_ns: u64,
518 ) EntryRef {
519 const acquired = self.memory.acquire(direction_index) catch unreachable;
520 std.debug.assert(bytes.len <= acquired.payload.len);
521 const sequence = self.takeSequence(direction_index);
522 @memcpy(acquired.payload[0..bytes.len], bytes);
523 acquired.entry.* = .{
524 .delivery_at_ns = delivery_at_ns,
525 .sequence = sequence,
526 .length = @intCast(bytes.len),
527 .occupied = true,
528 };
529 return acquired;
530 }
531
532 fn takeSequence(self: *Link, direction_index: usize) u64 {
533 std.debug.assert(direction_index < self.next_sequence.len);
534 std.debug.assert(self.next_sequence[direction_index] < std.math.maxInt(u64));
535 const sequence = self.next_sequence[direction_index];
536 self.next_sequence[direction_index] += 1;
537 return sequence;
538 }
539
540 fn previousIndex(self: *const Link, direction_index: usize) ?usize {
541 const entries = self.memory.directions[direction_index].entries;
542 var selected: ?usize = null;
543 for (entries, 0..) |entry, index| {
544 if (!entry.occupied) continue;
545 if (selected == null) selected = index;
546 if (selected) |current| {
547 if (entry.sequence > entries[current].sequence) selected = index;
548 }
549 }
550 return selected;
551 }
552
553 fn earliestIndex(self: *const Link, direction_index: usize) ?usize {
554 const entries = self.memory.directions[direction_index].entries;
555 var selected: ?usize = null;
556 for (entries, 0..) |entry, index| {
557 if (!entry.occupied) continue;
558 if (selected == null) selected = index;
559 if (selected) |current| {
560 if (entry.delivery_at_ns < entries[current].delivery_at_ns) selected = index;
561 if (entry.delivery_at_ns != entries[current].delivery_at_ns) continue;
562 if (entry.sequence < entries[current].sequence) selected = index;
563 }
564 }
565 return selected;
566 }
567
568 fn swapDelivery(self: *Link, direction_index: usize, first: usize, second: usize) void {
569 const entries = self.memory.directions[direction_index].entries;
570 std.debug.assert(first < entries.len);
571 std.debug.assert(second < entries.len);
572 std.mem.swap(u64, &entries[first].delivery_at_ns, &entries[second].delivery_at_ns);
573 std.mem.swap(u64, &entries[first].sequence, &entries[second].sequence);
574 }
575 };
576
577 fn outgoingIndex(from: End) usize {
578 return @backingInt(from);
579 }
580
581 fn incomingIndex(at: End) usize {
582 return switch (at) {
583 .a => outgoingIndex(.b),
584 .b => outgoingIndex(.a),
585 };
586 }
587
588 fn deliveryAt(now_ns: u64, delay_ns: u64) u64 {
589 return std.math.add(u64, now_ns, delay_ns) catch std.math.maxInt(u64);
590 }
591
592 fn increment(counter: *u64) void {
593 std.debug.assert(counter.* < std.math.maxInt(u64));
594 counter.* += 1;
595 }
596
597 test "simulator memory capacity matches its independent byte model" {
598 comptime {
599 @stardustClaim(
600 alloc_phase.capacity.witness(Memory, "quic_sim_memory_capacity"),
601 null,
602 null,
603 null,
604 null,
605 null,
606 null,
607 );
608 }
609 const limits = Limits{ .queue_capacity = 3, .datagram_capacity = 7 };
610 const capacity = try Capacity.derive(limits);
611 try std.testing.expectEqual(@as(usize, 6), capacity.entry_count);
612 try std.testing.expectEqual(6 * @sizeOf(Entry), capacity.entry_bytes);
613 try std.testing.expectEqual(@as(usize, 21), capacity.direction_payload_bytes);
614 try std.testing.expectEqual(@as(usize, 42), capacity.payload_bytes);
615 try std.testing.expectEqual(
616 6 * @sizeOf(Entry) + 42,
617 capacity.storage_bytes,
618 );
619 }
620
621 test "simulator memory requires the exact derived caller storage" {
622 comptime {
623 @stardustClaim(
624 alloc_phase.capacity.witness(Memory, "quic_sim_memory_transitive"),
625 null,
626 null,
627 null,
628 null,
629 null,
630 null,
631 );
632 }
633 const limits = Limits{ .queue_capacity = 2, .datagram_capacity = 4 };
634 const capacity = comptime Capacity.derive(limits) catch unreachable;
635 var short: [capacity.storage_bytes - 1]u8 align(Memory.storage_alignment) = undefined;
636 try std.testing.expectError(
637 error.StorageLengthMismatch,
638 Memory.init(&short, limits),
639 );
640 var exact: [capacity.storage_bytes]u8 align(Memory.storage_alignment) = undefined;
641 var memory = try Memory.init(&exact, limits);
642 memory.activate();
643 const returned = memory.deinit();
644 try std.testing.expectEqual(@as(usize, capacity.storage_bytes), returned.len);
645 }
646
647 test "simulator memory admits its queue capacity and rejects maximum plus one" {
648 comptime {
649 @stardustClaim(
650 alloc_phase.capacity.witness(Memory, "quic_sim_memory_overload"),
651 null,
652 null,
653 null,
654 null,
655 null,
656 null,
657 );
658 }
659 comptime {
660 @stardustClaim(
661 alloc_phase.capacity.witness(Memory, "quic_sim_memory_work"),
662 null,
663 null,
664 null,
665 null,
666 null,
667 null,
668 );
669 }
670 const limits = Limits{ .queue_capacity = 2, .datagram_capacity = 1 };
671 const capacity = comptime Capacity.derive(limits) catch unreachable;
672 var bytes: [capacity.storage_bytes]u8 align(Memory.storage_alignment) = undefined;
673 var memory = try Memory.init(&bytes, limits);
674 memory.activate();
675 _ = try memory.acquire(0);
676 _ = try memory.acquire(0);
677 try std.testing.expectError(error.QueueFull, memory.acquire(0));
678 _ = memory.deinit();
679 }