authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-06-28 14:44:00-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2022-06-28 14:44:00-04:00
log8974cee5a1f67db42a83a0907d8b9e842b979072
tree449a2d43b19eacf1f2535799f085195367adf548
parent9e8298b864e076221ca9c487412209d8a08c43b2
parent22720981ea50b1cee38d8309e0cd32df401b8156
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #11919 from squeek502/failing-allocator-stacktrace

Add stack trace capturing to `FailingAllocator` and use it to improve `checkAllAllocationFailures`

4 files changed, 70 insertions(+), 27 deletions(-)

lib/std/debug.zig+21
...@@ -26,6 +26,27 @@ pub const runtime_safety = switch (builtin.mode) {...@@ -26,6 +26,27 @@ pub const runtime_safety = switch (builtin.mode) {
26 .ReleaseFast, .ReleaseSmall => false,26 .ReleaseFast, .ReleaseSmall => false,
27};27};
2828
29pub const sys_can_stack_trace = switch (builtin.cpu.arch) {
30 // Observed to go into an infinite loop.
31 // TODO: Make this work.
32 .mips,
33 .mipsel,
34 => false,
35
36 // `@returnAddress()` in LLVM 10 gives
37 // "Non-Emscripten WebAssembly hasn't implemented __builtin_return_address".
38 .wasm32,
39 .wasm64,
40 => builtin.os.tag == .emscripten,
41
42 // `@returnAddress()` is unsupported in LLVM 13.
43 .bpfel,
44 .bpfeb,
45 => false,
46
47 else => true,
48};
49
29pub const LineInfo = struct {50pub const LineInfo = struct {
30 line: u64,51 line: u64,
31 column: u64,52 column: u64,
lib/std/heap/general_purpose_allocator.zig+1-21
...@@ -105,28 +105,8 @@ const StackTrace = std.builtin.StackTrace;...@@ -105,28 +105,8 @@ const StackTrace = std.builtin.StackTrace;
105/// Integer type for pointing to slots in a small allocation105/// Integer type for pointing to slots in a small allocation
106const SlotIndex = std.meta.Int(.unsigned, math.log2(page_size) + 1);106const SlotIndex = std.meta.Int(.unsigned, math.log2(page_size) + 1);
107107
108const sys_can_stack_trace = switch (builtin.cpu.arch) {
109 // Observed to go into an infinite loop.
110 // TODO: Make this work.
111 .mips,
112 .mipsel,
113 => false,
114
115 // `@returnAddress()` in LLVM 10 gives
116 // "Non-Emscripten WebAssembly hasn't implemented __builtin_return_address".
117 .wasm32,
118 .wasm64,
119 => builtin.os.tag == .emscripten,
120
121 // `@returnAddress()` is unsupported in LLVM 13.
122 .bpfel,
123 .bpfeb,
124 => false,
125
126 else => true,
127};
128const default_test_stack_trace_frames: usize = if (builtin.is_test) 8 else 4;108const default_test_stack_trace_frames: usize = if (builtin.is_test) 8 else 4;
129const default_sys_stack_trace_frames: usize = if (sys_can_stack_trace) default_test_stack_trace_frames else 0;109const default_sys_stack_trace_frames: usize = if (std.debug.sys_can_stack_trace) default_test_stack_trace_frames else 0;
130const default_stack_trace_frames: usize = switch (builtin.mode) {110const default_stack_trace_frames: usize = switch (builtin.mode) {
131 .Debug => default_sys_stack_trace_frames,111 .Debug => default_sys_stack_trace_frames,
132 else => 0,112 else => 0,
lib/std/testing.zig+20-6
...@@ -534,18 +534,27 @@ test {...@@ -534,18 +534,27 @@ test {
534///534///
535/// Any relevant state shared between runs of `test_fn` *must* be reset within `test_fn`.535/// Any relevant state shared between runs of `test_fn` *must* be reset within `test_fn`.
536///536///
537/// Expects that the `test_fn` has a deterministic number of memory allocations
538/// (an error will be returned if non-deterministic allocations are detected).
539///
540/// The strategy employed is to:537/// The strategy employed is to:
541/// - Run the test function once to get the total number of allocations.538/// - Run the test function once to get the total number of allocations.
542/// - Then, iterate and run the function X more times, incrementing539/// - Then, iterate and run the function X more times, incrementing
543/// the failing index each iteration (where X is the total number of540/// the failing index each iteration (where X is the total number of
544/// allocations determined previously)541/// allocations determined previously)
545///542///
543/// Expects that `test_fn` has a deterministic number of memory allocations:
544/// - If an allocation was made to fail during a run of `test_fn`, but `test_fn`
545/// didn't return `error.OutOfMemory`, then `error.SwallowedOutOfMemoryError`
546/// is returned from `checkAllAllocationFailures`. You may want to ignore this
547/// depending on whether or not the code you're testing includes some strategies
548/// for recovering from `error.OutOfMemory`.
549/// - If a run of `test_fn` with an expected allocation failure executes without
550/// an allocation failure being induced, then `error.NondeterministicMemoryUsage`
551/// is returned. This error means that there are allocation points that won't be
552/// tested by the strategy this function employs (that is, there are sometimes more
553/// points of allocation than the initial run of `test_fn` detects).
554///
546/// ---555/// ---
547///556///
548/// Here's an example of using a simple test case that will cause a leak when the557/// Here's an example using a simple test case that will cause a leak when the
549/// allocation of `bar` fails (but will pass normally):558/// allocation of `bar` fails (but will pass normally):
550///559///
551/// ```zig560/// ```zig
...@@ -645,12 +654,16 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime...@@ -645,12 +654,16 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime
645 args.@"0" = failing_allocator_inst.allocator();654 args.@"0" = failing_allocator_inst.allocator();
646655
647 if (@call(.{}, test_fn, args)) |_| {656 if (@call(.{}, test_fn, args)) |_| {
648 return error.NondeterministicMemoryUsage;657 if (failing_allocator_inst.has_induced_failure) {
658 return error.SwallowedOutOfMemoryError;
659 } else {
660 return error.NondeterministicMemoryUsage;
661 }
649 } else |err| switch (err) {662 } else |err| switch (err) {
650 error.OutOfMemory => {663 error.OutOfMemory => {
651 if (failing_allocator_inst.allocated_bytes != failing_allocator_inst.freed_bytes) {664 if (failing_allocator_inst.allocated_bytes != failing_allocator_inst.freed_bytes) {
652 print(665 print(
653 "\nfail_index: {d}/{d}\nallocated bytes: {d}\nfreed bytes: {d}\nallocations: {d}\ndeallocations: {d}\n",666 "\nfail_index: {d}/{d}\nallocated bytes: {d}\nfreed bytes: {d}\nallocations: {d}\ndeallocations: {d}\nallocation that was made to fail: {s}",
654 .{667 .{
655 fail_index,668 fail_index,
656 needed_alloc_count,669 needed_alloc_count,
...@@ -658,6 +671,7 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime...@@ -658,6 +671,7 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime
658 failing_allocator_inst.freed_bytes,671 failing_allocator_inst.freed_bytes,
659 failing_allocator_inst.allocations,672 failing_allocator_inst.allocations,
660 failing_allocator_inst.deallocations,673 failing_allocator_inst.deallocations,
674 failing_allocator_inst.getStackTrace(),
661 },675 },
662 );676 );
663 return error.MemoryLeakDetected;677 return error.MemoryLeakDetected;
lib/std/testing/failing_allocator.zig+28
...@@ -19,6 +19,10 @@ pub const FailingAllocator = struct {...@@ -19,6 +19,10 @@ pub const FailingAllocator = struct {
19 freed_bytes: usize,19 freed_bytes: usize,
20 allocations: usize,20 allocations: usize,
21 deallocations: usize,21 deallocations: usize,
22 stack_addresses: [num_stack_frames]usize,
23 has_induced_failure: bool,
24
25 const num_stack_frames = if (std.debug.sys_can_stack_trace) 16 else 0;
2226
23 /// `fail_index` is the number of successful allocations you can27 /// `fail_index` is the number of successful allocations you can
24 /// expect from this allocator. The next allocation will fail.28 /// expect from this allocator. The next allocation will fail.
...@@ -37,6 +41,8 @@ pub const FailingAllocator = struct {...@@ -37,6 +41,8 @@ pub const FailingAllocator = struct {
37 .freed_bytes = 0,41 .freed_bytes = 0,
38 .allocations = 0,42 .allocations = 0,
39 .deallocations = 0,43 .deallocations = 0,
44 .stack_addresses = undefined,
45 .has_induced_failure = false,
40 };46 };
41 }47 }
4248
...@@ -52,6 +58,15 @@ pub const FailingAllocator = struct {...@@ -52,6 +58,15 @@ pub const FailingAllocator = struct {
52 return_address: usize,58 return_address: usize,
53 ) error{OutOfMemory}![]u8 {59 ) error{OutOfMemory}![]u8 {
54 if (self.index == self.fail_index) {60 if (self.index == self.fail_index) {
61 if (!self.has_induced_failure) {
62 mem.set(usize, &self.stack_addresses, 0);
63 var stack_trace = std.builtin.StackTrace{
64 .instruction_addresses = &self.stack_addresses,
65 .index = 0,
66 };
67 std.debug.captureStackTrace(return_address, &stack_trace);
68 self.has_induced_failure = true;
69 }
55 return error.OutOfMemory;70 return error.OutOfMemory;
56 }71 }
57 const result = try self.internal_allocator.rawAlloc(len, ptr_align, len_align, return_address);72 const result = try self.internal_allocator.rawAlloc(len, ptr_align, len_align, return_address);
...@@ -88,4 +103,17 @@ pub const FailingAllocator = struct {...@@ -88,4 +103,17 @@ pub const FailingAllocator = struct {
88 self.deallocations += 1;103 self.deallocations += 1;
89 self.freed_bytes += old_mem.len;104 self.freed_bytes += old_mem.len;
90 }105 }
106
107 /// Only valid once `has_induced_failure == true`
108 pub fn getStackTrace(self: *FailingAllocator) std.builtin.StackTrace {
109 std.debug.assert(self.has_induced_failure);
110 var len: usize = 0;
111 while (len < self.stack_addresses.len and self.stack_addresses[len] != 0) {
112 len += 1;
113 }
114 return .{
115 .instruction_addresses = &self.stack_addresses,
116 .index = len,
117 };
118 }
91};119};