| author | |
| committer | |
| log | 8974cee5a1f67db42a83a0907d8b9e842b979072 |
| tree | 449a2d43b19eacf1f2535799f085195367adf548 |
| parent | 9e8298b864e076221ca9c487412209d8a08c43b2 |
| parent | 22720981ea50b1cee38d8309e0cd32df401b8156 |
| signature |
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 | 26 | .ReleaseFast, .ReleaseSmall => false, |
| 27 | 27 | }; |
| 28 | 28 | |
| 29 | pub 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 | ||
| 29 | 50 | pub const LineInfo = struct { |
| 30 | 51 | line: u64, |
| 31 | 52 | column: u64, |
lib/std/heap/general_purpose_allocator.zig+1-21| ... | ... | @@ -105,28 +105,8 @@ const StackTrace = std.builtin.StackTrace; |
| 105 | 105 | /// Integer type for pointing to slots in a small allocation |
| 106 | 106 | const SlotIndex = std.meta.Int(.unsigned, math.log2(page_size) + 1); |
| 107 | 107 | |
| 108 | const 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 | }; | |
| 128 | 108 | const default_test_stack_trace_frames: usize = if (builtin.is_test) 8 else 4; |
| 129 | const default_sys_stack_trace_frames: usize = if (sys_can_stack_trace) default_test_stack_trace_frames else 0; | |
| 109 | const default_sys_stack_trace_frames: usize = if (std.debug.sys_can_stack_trace) default_test_stack_trace_frames else 0; | |
| 130 | 110 | const default_stack_trace_frames: usize = switch (builtin.mode) { |
| 131 | 111 | .Debug => default_sys_stack_trace_frames, |
| 132 | 112 | else => 0, |
lib/std/testing.zig+20-6| ... | ... | @@ -534,18 +534,27 @@ test { |
| 534 | 534 | /// |
| 535 | 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 | 537 | /// The strategy employed is to: |
| 541 | 538 | /// - Run the test function once to get the total number of allocations. |
| 542 | 539 | /// - Then, iterate and run the function X more times, incrementing |
| 543 | 540 | /// the failing index each iteration (where X is the total number of |
| 544 | 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 the | |
| 557 | /// Here's an example using a simple test case that will cause a leak when the | |
| 549 | 558 | /// allocation of `bar` fails (but will pass normally): |
| 550 | 559 | /// |
| 551 | 560 | /// ```zig |
| ... | ... | @@ -645,12 +654,16 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime |
| 645 | 654 | args.@"0" = failing_allocator_inst.allocator(); |
| 646 | 655 | |
| 647 | 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 | 662 | } else |err| switch (err) { |
| 650 | 663 | error.OutOfMemory => { |
| 651 | 664 | if (failing_allocator_inst.allocated_bytes != failing_allocator_inst.freed_bytes) { |
| 652 | 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 | 668 | fail_index, |
| 656 | 669 | needed_alloc_count, |
| ... | ... | @@ -658,6 +671,7 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime |
| 658 | 671 | failing_allocator_inst.freed_bytes, |
| 659 | 672 | failing_allocator_inst.allocations, |
| 660 | 673 | failing_allocator_inst.deallocations, |
| 674 | failing_allocator_inst.getStackTrace(), | |
| 661 | 675 | }, |
| 662 | 676 | ); |
| 663 | 677 | return error.MemoryLeakDetected; |
lib/std/testing/failing_allocator.zig+28| ... | ... | @@ -19,6 +19,10 @@ pub const FailingAllocator = struct { |
| 19 | 19 | freed_bytes: usize, |
| 20 | 20 | allocations: usize, |
| 21 | 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; | |
| 22 | 26 | |
| 23 | 27 | /// `fail_index` is the number of successful allocations you can |
| 24 | 28 | /// expect from this allocator. The next allocation will fail. |
| ... | ... | @@ -37,6 +41,8 @@ pub const FailingAllocator = struct { |
| 37 | 41 | .freed_bytes = 0, |
| 38 | 42 | .allocations = 0, |
| 39 | 43 | .deallocations = 0, |
| 44 | .stack_addresses = undefined, | |
| 45 | .has_induced_failure = false, | |
| 40 | 46 | }; |
| 41 | 47 | } |
| 42 | 48 | |
| ... | ... | @@ -52,6 +58,15 @@ pub const FailingAllocator = struct { |
| 52 | 58 | return_address: usize, |
| 53 | 59 | ) error{OutOfMemory}![]u8 { |
| 54 | 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 | 70 | return error.OutOfMemory; |
| 56 | 71 | } |
| 57 | 72 | const result = try self.internal_allocator.rawAlloc(len, ptr_align, len_align, return_address); |
| ... | ... | @@ -88,4 +103,17 @@ pub const FailingAllocator = struct { |
| 88 | 103 | self.deallocations += 1; |
| 89 | 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 | }; |