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 }