authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2020-08-07 23:26:58-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2020-08-07 23:26:58-07:00
log0347df82e8c821906ef0d07ec65fe4b3884c0212
tree20076f52a0024e2252eac24683e39d3ee25d4d28
parentcc17f84cccc540143f3fd19fe32218478d4a0c6f

improvements & fixes for general purpose allocator integration

* std.Mutex API is improved to not have init() deinit(). This API is designed to support static initialization and does not require any resource cleanup. This also happens to work around some kind of stage1 behavior that wasn't letting the new allocator mutex code get compiled. * the general purpose allocator now returns a bool from deinit() which tells if there were any leaks. This value is used by the test runner to fail the tests if there are any. * self-hosted compiler is updated to use the general purpose allocator when not linking against libc.

10 files changed, 142 insertions(+), 145 deletions(-)

lib/std/atomic/queue.zig+1-1
......@@ -22,7 +22,7 @@ pub fn Queue(comptime T: type) type {
2222 return Self{
2323 .head = null,
2424 .tail = null,
25 .mutex = std.Mutex.init(),
25 .mutex = std.Mutex{},
2626 };
2727 }
2828
lib/std/debug.zig+2-2
......@@ -47,7 +47,7 @@ pub const LineInfo = struct {
4747 }
4848};
4949
50var stderr_mutex = std.Mutex.init();
50var stderr_mutex = std.Mutex{};
5151
5252/// Deprecated. Use `std.log` functions for logging or `std.debug.print` for
5353/// "printf debugging".
......@@ -232,7 +232,7 @@ pub fn panic(comptime format: []const u8, args: anytype) noreturn {
232232var panicking: u8 = 0;
233233
234234// Locked to avoid interleaving panic messages from multiple threads.
235var panic_mutex = std.Mutex.init();
235var panic_mutex = std.Mutex{};
236236
237237/// Counts how many times the panic handler is invoked by this thread.
238238/// This is used to catch and handle panics triggered by the panic handler.
lib/std/heap.zig+7-1
......@@ -464,7 +464,13 @@ pub const HeapAllocator = switch (builtin.os.tag) {
464464 return buf;
465465 }
466466
467 fn resize(allocator: *Allocator, buf: []u8, new_size: usize, len_align: u29) error{OutOfMemory}!usize {
467 fn resize(
468 allocator: *Allocator,
469 buf: []u8,
470 buf_align: u29,
471 new_size: usize,
472 len_align: u29,
473 ) error{OutOfMemory}!usize {
468474 const self = @fieldParentPtr(HeapAllocator, "allocator", allocator);
469475 if (new_size == 0) {
470476 os.windows.HeapFree(self.heap_handle.?, 0, @intToPtr(*c_void, getRecordPtr(buf).*));
lib/std/heap/general_purpose_allocator.zig+26-19
......@@ -140,7 +140,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
140140 const total_requested_bytes_init = if (config.enable_memory_limit) @as(usize, 0) else {};
141141 const requested_memory_limit_init = if (config.enable_memory_limit) @as(usize, math.maxInt(usize)) else {};
142142
143 const mutex_init = if (config.thread_safe) std.Mutex.init() else std.Mutex.Dummy.init();
143 const mutex_init = if (config.thread_safe) std.Mutex{} else std.mutex.Dummy{};
144144
145145 const stack_n = config.stack_trace_frames;
146146 const one_trace_size = @sizeOf(usize) * stack_n;
......@@ -250,7 +250,8 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
250250 bucket: *BucketHeader,
251251 size_class: usize,
252252 used_bits_count: usize,
253 ) void {
253 ) bool {
254 var leaks = false;
254255 var used_bits_byte: usize = 0;
255256 while (used_bits_byte < used_bits_count) : (used_bits_byte += 1) {
256257 const used_byte = bucket.usedBits(used_bits_byte).*;
......@@ -268,22 +269,26 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
268269 .alloc,
269270 );
270271 std.debug.dumpStackTrace(stack_trace);
272 leaks = true;
271273 }
272274 if (bit_index == math.maxInt(u3))
273275 break;
274276 }
275277 }
276278 }
279 return leaks;
277280 }
278281
279 pub fn deinit(self: *Self) void {
282 /// Returns whether there were leaks.
283 pub fn deinit(self: *Self) bool {
284 var leaks = false;
280285 for (self.buckets) |optional_bucket, bucket_i| {
281286 const first_bucket = optional_bucket orelse continue;
282287 const size_class = @as(usize, 1) << @intCast(u6, bucket_i);
283288 const used_bits_count = usedBitsCount(size_class);
284289 var bucket = first_bucket;
285290 while (true) {
286 detectLeaksInBucket(bucket, size_class, used_bits_count);
291 leaks = detectLeaksInBucket(bucket, size_class, used_bits_count) or leaks;
287292 bucket = bucket.next;
288293 if (bucket == first_bucket)
289294 break;
......@@ -292,9 +297,11 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
292297 for (self.large_allocations.items()) |*large_alloc| {
293298 std.debug.print("\nMemory leak detected:\n", .{});
294299 large_alloc.value.dumpStackTrace();
300 leaks = true;
295301 }
296302 self.large_allocations.deinit(self.backing_allocator);
297303 self.* = undefined;
304 return leaks;
298305 }
299306
300307 fn collectStackTrace(first_trace_addr: usize, addresses: *[stack_n]usize) void {
......@@ -600,7 +607,7 @@ const test_config = Config{};
600607
601608test "small allocations - free in same order" {
602609 var gpda = GeneralPurposeAllocator(test_config){};
603 defer gpda.deinit();
610 defer std.testing.expect(!gpda.deinit());
604611 const allocator = &gpda.allocator;
605612
606613 var list = std.ArrayList(*u64).init(std.testing.allocator);
......@@ -619,7 +626,7 @@ test "small allocations - free in same order" {
619626
620627test "small allocations - free in reverse order" {
621628 var gpda = GeneralPurposeAllocator(test_config){};
622 defer gpda.deinit();
629 defer std.testing.expect(!gpda.deinit());
623630 const allocator = &gpda.allocator;
624631
625632 var list = std.ArrayList(*u64).init(std.testing.allocator);
......@@ -638,7 +645,7 @@ test "small allocations - free in reverse order" {
638645
639646test "large allocations" {
640647 var gpda = GeneralPurposeAllocator(test_config){};
641 defer gpda.deinit();
648 defer std.testing.expect(!gpda.deinit());
642649 const allocator = &gpda.allocator;
643650
644651 const ptr1 = try allocator.alloc(u64, 42768);
......@@ -651,7 +658,7 @@ test "large allocations" {
651658
652659test "realloc" {
653660 var gpda = GeneralPurposeAllocator(test_config){};
654 defer gpda.deinit();
661 defer std.testing.expect(!gpda.deinit());
655662 const allocator = &gpda.allocator;
656663
657664 var slice = try allocator.alignedAlloc(u8, @alignOf(u32), 1);
......@@ -673,7 +680,7 @@ test "realloc" {
673680
674681test "shrink" {
675682 var gpda = GeneralPurposeAllocator(test_config){};
676 defer gpda.deinit();
683 defer std.testing.expect(!gpda.deinit());
677684 const allocator = &gpda.allocator;
678685
679686 var slice = try allocator.alloc(u8, 20);
......@@ -696,7 +703,7 @@ test "shrink" {
696703
697704test "large object - grow" {
698705 var gpda = GeneralPurposeAllocator(test_config){};
699 defer gpda.deinit();
706 defer std.testing.expect(!gpda.deinit());
700707 const allocator = &gpda.allocator;
701708
702709 var slice1 = try allocator.alloc(u8, page_size * 2 - 20);
......@@ -714,7 +721,7 @@ test "large object - grow" {
714721
715722test "realloc small object to large object" {
716723 var gpda = GeneralPurposeAllocator(test_config){};
717 defer gpda.deinit();
724 defer std.testing.expect(!gpda.deinit());
718725 const allocator = &gpda.allocator;
719726
720727 var slice = try allocator.alloc(u8, 70);
......@@ -731,7 +738,7 @@ test "realloc small object to large object" {
731738
732739test "shrink large object to large object" {
733740 var gpda = GeneralPurposeAllocator(test_config){};
734 defer gpda.deinit();
741 defer std.testing.expect(!gpda.deinit());
735742 const allocator = &gpda.allocator;
736743
737744 var slice = try allocator.alloc(u8, page_size * 2 + 50);
......@@ -754,7 +761,7 @@ test "shrink large object to large object" {
754761
755762test "shrink large object to large object with larger alignment" {
756763 var gpda = GeneralPurposeAllocator(test_config){};
757 defer gpda.deinit();
764 defer std.testing.expect(!gpda.deinit());
758765 const allocator = &gpda.allocator;
759766
760767 var debug_buffer: [1000]u8 = undefined;
......@@ -782,7 +789,7 @@ test "shrink large object to large object with larger alignment" {
782789
783790test "realloc large object to small object" {
784791 var gpda = GeneralPurposeAllocator(test_config){};
785 defer gpda.deinit();
792 defer std.testing.expect(!gpda.deinit());
786793 const allocator = &gpda.allocator;
787794
788795 var slice = try allocator.alloc(u8, page_size * 2 + 50);
......@@ -797,7 +804,7 @@ test "realloc large object to small object" {
797804
798805test "non-page-allocator backing allocator" {
799806 var gpda = GeneralPurposeAllocator(.{}){ .backing_allocator = std.testing.allocator };
800 defer gpda.deinit();
807 defer std.testing.expect(!gpda.deinit());
801808 const allocator = &gpda.allocator;
802809
803810 const ptr = try allocator.create(i32);
......@@ -806,7 +813,7 @@ test "non-page-allocator backing allocator" {
806813
807814test "realloc large object to larger alignment" {
808815 var gpda = GeneralPurposeAllocator(test_config){};
809 defer gpda.deinit();
816 defer std.testing.expect(!gpda.deinit());
810817 const allocator = &gpda.allocator;
811818
812819 var debug_buffer: [1000]u8 = undefined;
......@@ -866,7 +873,7 @@ test "isAligned works" {
866873test "large object shrinks to small but allocation fails during shrink" {
867874 var failing_allocator = std.testing.FailingAllocator.init(std.heap.page_allocator, 3);
868875 var gpda = GeneralPurposeAllocator(.{}){ .backing_allocator = &failing_allocator.allocator };
869 defer gpda.deinit();
876 defer std.testing.expect(!gpda.deinit());
870877 const allocator = &gpda.allocator;
871878
872879 var slice = try allocator.alloc(u8, page_size * 2 + 50);
......@@ -883,7 +890,7 @@ test "large object shrinks to small but allocation fails during shrink" {
883890
884891test "objects of size 1024 and 2048" {
885892 var gpda = GeneralPurposeAllocator(test_config){};
886 defer gpda.deinit();
893 defer std.testing.expect(!gpda.deinit());
887894 const allocator = &gpda.allocator;
888895
889896 const slice = try allocator.alloc(u8, 1025);
......@@ -895,7 +902,7 @@ test "objects of size 1024 and 2048" {
895902
896903test "setting a memory cap" {
897904 var gpda = GeneralPurposeAllocator(.{ .enable_memory_limit = true }){};
898 defer gpda.deinit();
905 defer std.testing.expect(!gpda.deinit());
899906 const allocator = &gpda.allocator;
900907
901908 gpda.setRequestedMemoryLimit(1010);
lib/std/mutex.zig+90-114
......@@ -15,8 +15,7 @@ const ResetEvent = std.ResetEvent;
1515/// deadlock detection.
1616///
1717/// Example usage:
18/// var m = Mutex.init();
19/// defer m.deinit();
18/// var m = Mutex{};
2019///
2120/// const lock = m.acquire();
2221/// defer lock.release();
......@@ -32,101 +31,11 @@ const ResetEvent = std.ResetEvent;
3231pub const Mutex = if (builtin.single_threaded)
3332 Dummy
3433else if (builtin.os.tag == .windows)
35// https://locklessinc.com/articles/keyed_events/
36 extern union {
37 locked: u8,
38 waiters: u32,
39
40 const WAKE = 1 << 8;
41 const WAIT = 1 << 9;
42
43 pub const Dummy = Dummy;
44
45 pub fn init() Mutex {
46 return Mutex{ .waiters = 0 };
47 }
48
49 pub fn deinit(self: *Mutex) void {
50 self.* = undefined;
51 }
52
53 pub fn tryAcquire(self: *Mutex) ?Held {
54 if (@atomicRmw(u8, &self.locked, .Xchg, 1, .Acquire) != 0)
55 return null;
56 return Held{ .mutex = self };
57 }
58
59 pub fn acquire(self: *Mutex) Held {
60 return self.tryAcquire() orelse self.acquireSlow();
61 }
62
63 fn acquireSpinning(self: *Mutex) Held {
64 @setCold(true);
65 while (true) : (SpinLock.yield()) {
66 return self.tryAcquire() orelse continue;
67 }
68 }
69
70 fn acquireSlow(self: *Mutex) Held {
71 // try to use NT keyed events for blocking, falling back to spinlock if unavailable
72 @setCold(true);
73 const handle = ResetEvent.OsEvent.Futex.getEventHandle() orelse return self.acquireSpinning();
74 const key = @ptrCast(*const c_void, &self.waiters);
75
76 while (true) : (SpinLock.loopHint(1)) {
77 const waiters = @atomicLoad(u32, &self.waiters, .Monotonic);
78
79 // try and take lock if unlocked
80 if ((waiters & 1) == 0) {
81 if (@atomicRmw(u8, &self.locked, .Xchg, 1, .Acquire) == 0) {
82 return Held{ .mutex = self };
83 }
84
85 // otherwise, try and update the waiting count.
86 // then unset the WAKE bit so that another unlocker can wake up a thread.
87 } else if (@cmpxchgWeak(u32, &self.waiters, waiters, (waiters + WAIT) | 1, .Monotonic, .Monotonic) == null) {
88 const rc = windows.ntdll.NtWaitForKeyedEvent(handle, key, windows.FALSE, null);
89 assert(rc == .SUCCESS);
90 _ = @atomicRmw(u32, &self.waiters, .Sub, WAKE, .Monotonic);
91 }
92 }
93 }
94
95 pub const Held = struct {
96 mutex: *Mutex,
97
98 pub fn release(self: Held) void {
99 // unlock without a rmw/cmpxchg instruction
100 @atomicStore(u8, @ptrCast(*u8, &self.mutex.locked), 0, .Release);
101 const handle = ResetEvent.OsEvent.Futex.getEventHandle() orelse return;
102 const key = @ptrCast(*const c_void, &self.mutex.waiters);
103
104 while (true) : (SpinLock.loopHint(1)) {
105 const waiters = @atomicLoad(u32, &self.mutex.waiters, .Monotonic);
106
107 // no one is waiting
108 if (waiters < WAIT) return;
109 // someone grabbed the lock and will do the wake instead
110 if (waiters & 1 != 0) return;
111 // someone else is currently waking up
112 if (waiters & WAKE != 0) return;
113
114 // try to decrease the waiter count & set the WAKE bit meaning a thread is waking up
115 if (@cmpxchgWeak(u32, &self.mutex.waiters, waiters, waiters - WAIT + WAKE, .Release, .Monotonic) == null) {
116 const rc = windows.ntdll.NtReleaseKeyedEvent(handle, key, windows.FALSE, null);
117 assert(rc == .SUCCESS);
118 return;
119 }
120 }
121 }
122 };
123 }
34 WindowsMutex
12435else if (builtin.link_libc or builtin.os.tag == .linux)
12536// stack-based version of https://github.com/Amanieu/parking_lot/blob/master/core/src/word_lock.rs
12637 struct {
127 state: usize,
128
129 pub const Dummy = Dummy;
38 state: usize = 0,
13039
13140 /// number of times to spin trying to acquire the lock.
13241 /// https://webkit.org/blog/6161/locking-in-webkit/
......@@ -141,14 +50,6 @@ else if (builtin.link_libc or builtin.os.tag == .linux)
14150 event: ResetEvent,
14251 };
14352
144 pub fn init() Mutex {
145 return Mutex{ .state = 0 };
146 }
147
148 pub fn deinit(self: *Mutex) void {
149 self.* = undefined;
150 }
151
15253 pub fn tryAcquire(self: *Mutex) ?Held {
15354 if (@cmpxchgWeak(usize, &self.state, 0, MUTEX_LOCK, .Acquire, .Monotonic) != null)
15455 return null;
......@@ -263,7 +164,7 @@ else
263164/// This has the sematics as `Mutex`, however it does not actually do any
264165/// synchronization. Operations are safety-checked no-ops.
265166pub const Dummy = struct {
266 lock: @TypeOf(lock_init),
167 lock: @TypeOf(lock_init) = lock_init,
267168
268169 const lock_init = if (std.debug.runtime_safety) false else {};
269170
......@@ -278,15 +179,7 @@ pub const Dummy = struct {
278179 };
279180
280181 /// Create a new mutex in unlocked state.
281 pub fn init() Dummy {
282 return Dummy{ .lock = lock_init };
283 }
284
285 /// Free a mutex created with init. Calling this while the
286 /// mutex is held is illegal behavior.
287 pub fn deinit(self: *Dummy) void {
288 self.* = undefined;
289 }
182 pub const init = Dummy{};
290183
291184 /// Try to acquire the mutex without blocking. Returns null if
292185 /// the mutex is unavailable. Otherwise returns Held. Call
......@@ -306,6 +199,90 @@ pub const Dummy = struct {
306199 }
307200};
308201
202// https://locklessinc.com/articles/keyed_events/
203const WindowsMutex = struct {
204 state: State = State{ .waiters = 0 },
205
206 const State = extern union {
207 locked: u8,
208 waiters: u32,
209 };
210
211 const WAKE = 1 << 8;
212 const WAIT = 1 << 9;
213
214 pub fn tryAcquire(self: *WindowsMutex) ?Held {
215 if (@atomicRmw(u8, &self.state.locked, .Xchg, 1, .Acquire) != 0)
216 return null;
217 return Held{ .mutex = self };
218 }
219
220 pub fn acquire(self: *WindowsMutex) Held {
221 return self.tryAcquire() orelse self.acquireSlow();
222 }
223
224 fn acquireSpinning(self: *WindowsMutex) Held {
225 @setCold(true);
226 while (true) : (SpinLock.yield()) {
227 return self.tryAcquire() orelse continue;
228 }
229 }
230
231 fn acquireSlow(self: *WindowsMutex) Held {
232 // try to use NT keyed events for blocking, falling back to spinlock if unavailable
233 @setCold(true);
234 const handle = ResetEvent.OsEvent.Futex.getEventHandle() orelse return self.acquireSpinning();
235 const key = @ptrCast(*const c_void, &self.state.waiters);
236
237 while (true) : (SpinLock.loopHint(1)) {
238 const waiters = @atomicLoad(u32, &self.state.waiters, .Monotonic);
239
240 // try and take lock if unlocked
241 if ((waiters & 1) == 0) {
242 if (@atomicRmw(u8, &self.state.locked, .Xchg, 1, .Acquire) == 0) {
243 return Held{ .mutex = self };
244 }
245
246 // otherwise, try and update the waiting count.
247 // then unset the WAKE bit so that another unlocker can wake up a thread.
248 } else if (@cmpxchgWeak(u32, &self.state.waiters, waiters, (waiters + WAIT) | 1, .Monotonic, .Monotonic) == null) {
249 const rc = windows.ntdll.NtWaitForKeyedEvent(handle, key, windows.FALSE, null);
250 assert(rc == .SUCCESS);
251 _ = @atomicRmw(u32, &self.state.waiters, .Sub, WAKE, .Monotonic);
252 }
253 }
254 }
255
256 pub const Held = struct {
257 mutex: *WindowsMutex,
258
259 pub fn release(self: Held) void {
260 // unlock without a rmw/cmpxchg instruction
261 @atomicStore(u8, @ptrCast(*u8, &self.mutex.state.locked), 0, .Release);
262 const handle = ResetEvent.OsEvent.Futex.getEventHandle() orelse return;
263 const key = @ptrCast(*const c_void, &self.mutex.state.waiters);
264
265 while (true) : (SpinLock.loopHint(1)) {
266 const waiters = @atomicLoad(u32, &self.mutex.state.waiters, .Monotonic);
267
268 // no one is waiting
269 if (waiters < WAIT) return;
270 // someone grabbed the lock and will do the wake instead
271 if (waiters & 1 != 0) return;
272 // someone else is currently waking up
273 if (waiters & WAKE != 0) return;
274
275 // try to decrease the waiter count & set the WAKE bit meaning a thread is waking up
276 if (@cmpxchgWeak(u32, &self.mutex.state.waiters, waiters, waiters - WAIT + WAKE, .Release, .Monotonic) == null) {
277 const rc = windows.ntdll.NtReleaseKeyedEvent(handle, key, windows.FALSE, null);
278 assert(rc == .SUCCESS);
279 return;
280 }
281 }
282 }
283 };
284};
285
309286const TestContext = struct {
310287 mutex: *Mutex,
311288 data: i128,
......@@ -314,8 +291,7 @@ const TestContext = struct {
314291};
315292
316293test "std.Mutex" {
317 var mutex = Mutex.init();
318 defer mutex.deinit();
294 var mutex = Mutex{};
319295
320296 var context = TestContext{
321297 .mutex = &mutex,
lib/std/once.zig+1-1
......@@ -10,7 +10,7 @@ pub fn once(comptime f: fn () void) Once(f) {
1010pub fn Once(comptime f: fn () void) type {
1111 return struct {
1212 done: bool = false,
13 mutex: std.Mutex = std.Mutex.init(),
13 mutex: std.Mutex = std.Mutex{},
1414
1515 /// Call the function `f`.
1616 /// If `call` is invoked multiple times `f` will be executed only the
lib/std/special/test_runner.zig+10-1
......@@ -19,9 +19,14 @@ pub fn main() anyerror!void {
1919 // ignores the alignment of the slice.
2020 async_frame_buffer = &[_]u8{};
2121
22 var leaks: usize = 0;
2223 for (test_fn_list) |test_fn, i| {
2324 std.testing.allocator_instance = std.heap.GeneralPurposeAllocator(.{}){};
24 defer std.testing.allocator_instance.deinit();
25 defer {
26 if (std.testing.allocator_instance.deinit()) {
27 leaks += 1;
28 }
29 }
2530 std.testing.log_level = .warn;
2631
2732 var test_node = root_node.start(test_fn.name, null);
......@@ -70,6 +75,10 @@ pub fn main() anyerror!void {
7075 } else {
7176 std.debug.print("{} passed; {} skipped.\n", .{ ok_count, skip_count });
7277 }
78 if (leaks != 0) {
79 std.debug.print("{} tests leaked memory\n", .{ok_count});
80 std.process.exit(1);
81 }
7382}
7483
7584pub fn log(
lib/std/std.zig+2-1
......@@ -13,7 +13,8 @@ pub const ComptimeStringMap = @import("comptime_string_map.zig").ComptimeStringM
1313pub const DynLib = @import("dynamic_library.zig").DynLib;
1414pub const HashMap = hash_map.HashMap;
1515pub const HashMapUnmanaged = hash_map.HashMapUnmanaged;
16pub const Mutex = @import("mutex.zig").Mutex;
16pub const mutex = @import("mutex.zig");
17pub const Mutex = mutex.Mutex;
1718pub const PackedIntArray = @import("packed_int_array.zig").PackedIntArray;
1819pub const PackedIntArrayEndian = @import("packed_int_array.zig").PackedIntArrayEndian;
1920pub const PackedIntSlice = @import("packed_int_array.zig").PackedIntSlice;
src-self-hosted/main.zig+3-2
......@@ -60,9 +60,10 @@ pub fn log(
6060 std.debug.print(prefix ++ format, args);
6161}
6262
63var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
64
6365pub fn main() !void {
64 // TODO general purpose allocator in the zig std lib
65 const gpa = if (std.builtin.link_libc) std.heap.c_allocator else std.heap.page_allocator;
66 const gpa = if (std.builtin.link_libc) std.heap.c_allocator else &general_purpose_allocator.allocator;
6667 var arena_instance = std.heap.ArenaAllocator.init(gpa);
6768 defer arena_instance.deinit();
6869 const arena = &arena_instance.allocator;
src-self-hosted/test.zig-3
......@@ -407,8 +407,6 @@ pub const TestContext = struct {
407407 defer root_node.end();
408408
409409 for (self.cases.items) |case| {
410 std.testing.base_allocator_instance.reset();
411
412410 var prg_node = root_node.start(case.name, case.updates.items.len);
413411 prg_node.activate();
414412 defer prg_node.end();
......@@ -419,7 +417,6 @@ pub const TestContext = struct {
419417 progress.refresh_rate_ns = 0;
420418
421419 try self.runOneCase(std.testing.allocator, &prg_node, case);
422 try std.testing.allocator_instance.validate();
423420 }
424421 }
425422