tripwire: change backing store from ArrayHashMap to EnumMap

This eliminates all allocation from Tripwire.
This commit is contained in:
Mitchell Hashimoto
2026-01-21 15:30:22 -08:00
parent fbc1e326d6
commit 01f1611c9f
7 changed files with 30 additions and 51 deletions

View File

@@ -829,7 +829,7 @@ test "init error" {
for (std.meta.tags(init_tw.FailPoint)) |tag| {
const tw = init_tw;
defer tw.end(.reset) catch unreachable;
try tw.errorAlways(tag, error.OutOfMemory);
tw.errorAlways(tag, error.OutOfMemory);
try testing.expectError(
error.OutOfMemory,
init(testing.allocator, 32, .grayscale),
@@ -847,7 +847,7 @@ test "reserve error" {
var atlas = try init(testing.allocator, 32, .grayscale);
defer atlas.deinit(testing.allocator);
try tw.errorAlways(tag, error.OutOfMemory);
tw.errorAlways(tag, error.OutOfMemory);
try testing.expectError(
error.OutOfMemory,
atlas.reserve(testing.allocator, 2, 2),
@@ -872,7 +872,7 @@ test "grow error" {
const old_modified = atlas.modified.load(.monotonic);
const old_resized = atlas.resized.load(.monotonic);
try tw.errorAlways(tag, error.OutOfMemory);
tw.errorAlways(tag, error.OutOfMemory);
try testing.expectError(
error.OutOfMemory,
atlas.grow(testing.allocator, atlas.size + 1),

View File

@@ -484,7 +484,7 @@ test "renderGlyph error after cache insert rolls back cache entry" {
// We use OutOfMemory as it's a valid error in the renderGlyph error set.
const tw = renderGlyph_tw;
defer tw.end(.reset) catch {};
try tw.errorAlways(.get_presentation, error.OutOfMemory);
tw.errorAlways(.get_presentation, error.OutOfMemory);
// This should fail due to the tripwire
try testing.expectError(
@@ -510,7 +510,7 @@ test "init error" {
for (std.meta.tags(init_tw.FailPoint)) |tag| {
const tw = init_tw;
defer tw.end(.reset) catch unreachable;
try tw.errorAlways(tag, error.OutOfMemory);
tw.errorAlways(tag, error.OutOfMemory);
// Create a resolver for testing - we need to set up a minimal one.
// The caller is responsible for cleaning up the resolver if init fails.

View File

@@ -5170,7 +5170,7 @@ test "PageList init error" {
for (std.meta.tags(init_tw.FailPoint)) |tag| {
const tw = init_tw;
defer tw.end(.reset) catch unreachable;
try tw.errorAlways(tag, error.OutOfMemory);
tw.errorAlways(tag, error.OutOfMemory);
try std.testing.expectError(
error.OutOfMemory,
init(
@@ -5187,7 +5187,7 @@ test "PageList init error" {
for (std.meta.tags(initPages_tw.FailPoint)) |tag| {
const tw = initPages_tw;
defer tw.end(.reset) catch unreachable;
try tw.errorAlways(tag, error.OutOfMemory);
tw.errorAlways(tag, error.OutOfMemory);
const cols: size.CellCountInt = if (tag == .page_buf_std) 80 else std_capacity.maxCols().? + 1;
try std.testing.expectError(
@@ -5207,7 +5207,7 @@ test "PageList init error" {
}) |tag| {
const tw = initPages_tw;
defer tw.end(.reset) catch unreachable;
try tw.errorAfter(tag, error.OutOfMemory, 1);
tw.errorAfter(tag, error.OutOfMemory, 1);
try std.testing.expectError(
error.OutOfMemory,
init(

View File

@@ -9493,7 +9493,7 @@ test "selectionString map allocation failure cleanup" {
// Trigger allocation failure on toOwnedSlice
var map: StringMap = undefined;
try selectionString_tw.errorAlways(.copy_map, error.OutOfMemory);
selectionString_tw.errorAlways(.copy_map, error.OutOfMemory);
const result = s.selectionString(alloc, .{
.sel = sel,
.map = &map,

View File

@@ -261,7 +261,7 @@ test "Tabstops: resize alloc failure preserves state" {
const original_cols = t.cols;
// Trigger allocation failure when resizing beyond prealloc
try resize_tw.errorAlways(.dynamic_alloc, error.OutOfMemory);
resize_tw.errorAlways(.dynamic_alloc, error.OutOfMemory);
const result = t.resize(testing.allocator, prealloc_columns * 2);
try testing.expectError(error.OutOfMemory, result);
try resize_tw.end(.reset);

View File

@@ -1460,7 +1460,7 @@ test "reloadActive partial history cleanup on appendSlice error" {
// that need cleanup.
const tw = reloadActive_tw;
defer tw.end(.reset) catch unreachable;
try tw.errorAlways(.history_append_existing, error.OutOfMemory);
tw.errorAlways(.history_append_existing, error.OutOfMemory);
// reloadActive is called by select(), which should trigger the error path.
// If the bug exists, testing.allocator will report a memory leak
@@ -1507,7 +1507,7 @@ test "reloadActive partial history cleanup on loop append error" {
// that needs cleanup.
const tw = reloadActive_tw;
defer tw.end(.reset) catch unreachable;
try tw.errorAfter(.history_append_new, error.OutOfMemory, 1);
tw.errorAfter(.history_append_new, error.OutOfMemory, 1);
// reloadActive is called by select(), which should trigger the error path.
// If the bug exists, testing.allocator will report a memory leak

View File

@@ -43,7 +43,7 @@
//! }
//!
//! test "myFunction fails on alloc" {
//! try tw.errorAlways(.alloc_buf, error.OutOfMemory);
//! tw.errorAlways(.alloc_buf, error.OutOfMemory);
//! try std.testing.expectError(error.OutOfMemory, myFunction());
//! try tw.end(.reset);
//! }
@@ -67,7 +67,6 @@ const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.tripwire);
@@ -134,8 +133,8 @@ pub fn module(
}
/// The configured tripwires for this module.
var tripwires: TripwireMap = .empty;
const TripwireMap = std.AutoArrayHashMapUnmanaged(FailPoint, Tripwire);
var tripwires: TripwireMap = .{};
const TripwireMap = std.EnumMap(FailPoint, Tripwire);
const Tripwire = struct {
/// Error to return when tripped
err: Error,
@@ -155,14 +154,6 @@ pub fn module(
tripped: bool = false,
};
/// For all allocations we use an allocator that can leak memory
/// without reporting it, since this is only used in tests. We don't
/// want to use a testing allocator here because that would report
/// leaks. Users are welcome to call `deinit` on the module to
/// free all memory.
const LeakyAllocator = std.heap.DebugAllocator(.{});
var alloc_state: LeakyAllocator = .init;
/// Check for a failure at the given failure point. These should
/// be placed directly before the `try` operation that may fail.
pub fn check(point: FailPoint) callconv(callingConvention()) Error!void {
@@ -187,42 +178,31 @@ pub fn module(
}
/// Mark a failure point to always trip with the given error.
pub fn errorAlways(
point: FailPoint,
err: Error,
) Allocator.Error!void {
try errorAfter(point, err, 0);
pub fn errorAlways(point: FailPoint, err: Error) void {
errorAfter(point, err, 0);
}
/// Mark a failure point to trip with the given error after
/// the failure point is reached at least `min` times. A value of
/// zero is equivalent to `errorAlways`.
pub fn errorAfter(
point: FailPoint,
err: Error,
min: usize,
) Allocator.Error!void {
try tripwires.put(
alloc_state.allocator(),
point,
.{ .err = err, .min = min },
);
pub fn errorAfter(point: FailPoint, err: Error, min: usize) void {
tripwires.put(point, .{ .err = err, .min = min });
}
/// Ends the tripwire session. This will raise an error if there
/// were untripped error expectations. The reset mode specifies
/// whether memory is reset too. Memory is always reset, even if
/// this returns an error.
/// whether expectations are reset too. Expectations are always reset,
/// even if this returns an error.
pub fn end(reset_mode: enum { reset, retain }) error{UntrippedError}!void {
var untripped: bool = false;
for (tripwires.keys(), tripwires.values()) |key, entry| {
if (!entry.tripped) {
log.warn("untripped point={t}", .{key});
var iter = tripwires.iterator();
while (iter.next()) |entry| {
if (!entry.value.tripped) {
log.warn("untripped point={s}", .{@tagName(entry.key)});
untripped = true;
}
}
// We always reset memory before failing
switch (reset_mode) {
.reset => reset(),
.retain => {},
@@ -231,10 +211,9 @@ pub fn module(
if (untripped) return error.UntrippedError;
}
/// Unset all the tripwires and free all allocated memory. You
/// should usually call `end` instead.
/// Unset all the tripwires. You should usually call `end` instead.
pub fn reset() void {
tripwires.clearAndFree(alloc_state.allocator());
tripwires = .{};
}
/// Our calling convention is inline if our tripwire module is
@@ -258,7 +237,7 @@ test {
try io.check(.read);
// Always trip
try io.errorAlways(.read, error.OutOfMemory);
io.errorAlways(.read, error.OutOfMemory);
try testing.expectError(
error.OutOfMemory,
io.check(.read),
@@ -283,7 +262,7 @@ test "module as error set" {
test "errorAfter" {
const io = module(enum { read, write }, anyerror);
// Trip after 2 calls (on the 3rd call)
try io.errorAfter(.read, error.OutOfMemory, 2);
io.errorAfter(.read, error.OutOfMemory, 2);
// First two calls succeed
try io.check(.read);
@@ -298,7 +277,7 @@ test "errorAfter" {
test "errorAfter untripped error if min not reached" {
const io = module(enum { read }, anyerror);
try io.errorAfter(.read, error.OutOfMemory, 2);
io.errorAfter(.read, error.OutOfMemory, 2);
// Only call once, not enough to trip
try io.check(.read);
// end should fail because tripwire was set but never tripped