lib/alloc/src/owner.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const builtin = @import("builtin");
  3 const deadalloc = @import("deadalloc");
  4 const sys = @import("sys");
  5 const diagnostics = @import("diagnostics.zig");
  6 const page = @import("page/root.zig");
  7 const policy = @import("policy.zig");
  8 const process = @import("process.zig");
  9 
 10 const Allocator = std.mem.Allocator;
 11 
 12 pub const ProcessAllocatorFinish = union(enum) {
 13     clean,
 14     leak,
 15     diagnostics: deadalloc.Report,
 16 
 17     pub fn exitCode(self: ProcessAllocatorFinish) u8 {
 18         return switch (self) {
 19             .clean => 0,
 20             .leak, .diagnostics => 1,
 21         };
 22     }
 23 
 24     pub fn diagnosticReport(self: ProcessAllocatorFinish) ?deadalloc.Report {
 25         return switch (self) {
 26             .diagnostics => |report| report,
 27             .clean, .leak => null,
 28         };
 29     }
 30 };
 31 
 32 pub const ProcessAllocatorOwner = if (diagnostics.mode != null or process.mode != .off)
 33     DiagnosticProcessAllocatorOwner
 34 else if (retainsPages(builtin.mode, builtin.is_test, std.heap.page_size_max))
 35     RetainingProcessAllocatorOwner
 36 else
 37     DebugProcessAllocatorOwner;
 38 
 39 fn retainsPages(mode: std.builtin.OptimizeMode, is_test: bool, page_size_max: usize) bool {
 40     if (is_test) return false;
 41     if (page_size_max > page.page_bytes) return false;
 42     return switch (mode) {
 43         .ReleaseFast, .ReleaseSmall => true,
 44         .Debug, .ReleaseSafe => false,
 45     };
 46 }
 47 
 48 /// The largest number of bucket pages live at one time, taken over every input
 49 /// of the start harness, is the measurement the retained page count is derived
 50 /// from. The measurement was traced on the baseline `chic` binary the
 51 /// start-latency harness measures against, the *ruler*.
 52 const measured_peak_pages = 298;
 53 /// The count of bucket pages the process allocator keeps mapped for reuse, and
 54 /// so the memory the process holds on to in a release build: it takes the
 55 /// measured peak, adds a quarter of it again, and rounds up to the next
 56 /// multiple of 8.
 57 const retained_pages = std.mem.alignForward(u16, (measured_peak_pages * 5 + 3) / 4, 8);
 58 
 59 const DebugProcessAllocatorOwner = struct {
 60     gpa: std.heap.DebugAllocator(.{}) = .{
 61         .backing_allocator = sys.memory.page_allocator,
 62     },
 63 
 64     pub fn init() @This() {
 65         return .{};
 66     }
 67 
 68     pub fn allocator(self: *@This()) Allocator {
 69         return self.gpa.allocator();
 70     }
 71 
 72     pub fn finish(self: *@This()) ProcessAllocatorFinish {
 73         return if (self.gpa.deinit() == .leak) .leak else .clean;
 74     }
 75 };
 76 
 77 const RetainingProcessAllocatorOwner = struct {
 78     gpa: std.heap.DebugAllocator(.{
 79         .backing_allocator_zeroes = false,
 80         .page_size = page.page_bytes,
 81     }) = .{},
 82     pages: page.Router = .init(sys.memory.page_allocator, .{ .pages = retained_pages }),
 83 
 84     pub fn init() @This() {
 85         return .{};
 86     }
 87 
 88     pub fn allocator(self: *@This()) Allocator {
 89         self.gpa.backing_allocator = self.pages.allocator();
 90         return self.gpa.allocator();
 91     }
 92 
 93     pub fn finish(self: *@This()) ProcessAllocatorFinish {
 94         const check = self.gpa.deinit();
 95         self.pages.finish();
 96         return if (check == .leak) .leak else .clean;
 97     }
 98 };
 99 
100 const DiagnosticProcessAllocatorOwner = struct {
101     pub fn init() @This() {
102         return .{};
103     }
104 
105     pub fn allocator(_: *@This()) Allocator {
106         return policy.standaloneProcessAllocator();
107     }
108 
109     pub fn finish(_: *@This()) ProcessAllocatorFinish {
110         const report = diagnostics.report() orelse return .clean;
111         return if (report.isClean()) .clean else .{ .diagnostics = report };
112     }
113 };
114 
115 test "process allocator owner retains pages only in release builds outside tests on small pages" {
116     const small = 4 * 1024;
117     try std.testing.expect(retainsPages(.ReleaseFast, false, small));
118     try std.testing.expect(retainsPages(.ReleaseSmall, false, page.page_bytes));
119     try std.testing.expect(!retainsPages(.Debug, false, small));
120     try std.testing.expect(!retainsPages(.ReleaseSafe, false, small));
121     try std.testing.expect(!retainsPages(.ReleaseSmall, true, small));
122     try std.testing.expect(!retainsPages(.ReleaseSmall, false, 2 * page.page_bytes));
123 }
124 
125 test "retained page count is the smallest multiple of 8 at or above 1.25 times the peak" {
126     try std.testing.expectEqual(@as(u16, 0), retained_pages % 8);
127     try std.testing.expect(@as(u32, retained_pages) * 4 >= measured_peak_pages * 5);
128     try std.testing.expect(@as(u32, retained_pages - 8) * 4 < measured_peak_pages * 5);
129 }
130 
131 test "retaining process allocator owner reuses bucket pages and finishes clean" {
132     var owner = RetainingProcessAllocatorOwner.init();
133     const allocator = owner.allocator();
134     for (0..64) |_| {
135         const objects = try allocator.alloc(*[4096]u8, 256);
136         for (objects) |*object| object.* = try allocator.create([4096]u8);
137         try std.testing.expectEqual(@as(u64, 0), owner.pages.reserve.occupied[1]);
138         for (objects) |object| allocator.destroy(object);
139         allocator.free(objects);
140     }
141     try std.testing.expectEqual(page.Router.State.active, owner.pages.state);
142     try std.testing.expectEqual(@as(u16, 0), owner.pages.reserve.held);
143     try std.testing.expectEqual(ProcessAllocatorFinish.clean, owner.finish());
144 }