| 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,6 +26,27 @@ pub const runtime_safety = switch (builtin.mode) { |
| 26 | .ReleaseFast, .ReleaseSmall => false, | 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 | pub const LineInfo = struct { | 50 | pub 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 allocation | 105 | /// Integer type for pointing to slots in a small allocation |
| 106 | const SlotIndex = std.meta.Int(.unsigned, math.log2(page_size) + 1); | 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 | const default_test_stack_trace_frames: usize = if (builtin.is_test) 8 else 4; | 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 | const default_stack_trace_frames: usize = switch (builtin.mode) { | 110 | const 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, incrementing | 539 | /// - Then, iterate and run the function X more times, incrementing |
| 543 | /// the failing index each iteration (where X is the total number of | 540 | /// 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 the | 557 | /// 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 | /// ```zig | 560 | /// ```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(); |
| 646 | 655 | ||
| 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; | ||
| 22 | 26 | ||
| 23 | /// `fail_index` is the number of successful allocations you can | 27 | /// `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 | } |
| 42 | 48 | ||
| ... | @@ -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 | }; |