lib/sys/src/host.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! Host-wide resource observations, each read from the operating system at the
2 //! moment of the call.
3 //!
4 //! `memory` reports available memory together with a pressure level, while
5 //! `availableMemoryBytes` reports the byte count alone. On Linux *available
6 //! memory*, the byte count the host reports as allocatable without swapping,
7 //! comes from `MemAvailable` in `/proc/meminfo`, and pressure always reports
8 //! normal. When the process belongs to a control group with a memory limit, the
9 //! call reports the smaller of the host number and the group's *headroom*, the
10 //! limit minus current usage, taken across every enclosing group and across
11 //! both the unified and the version one hierarchies. On Darwin available memory
12 //! comes from the percent-free level and the total system memory, and
13 //! *pressure*, the Darwin memory pressure level reported as normal or
14 //! pressured, comes from a sysctl.
15 //!
16 //! A declared control group membership whose mount is missing, and a mount
17 //! whose memory files cannot be read, both fail with
18 //! `CgroupTelemetryUnavailable`, so a limit that cannot be read never passes as
19 //! unlimited headroom. Every file is read into fixed storage, and a file that
20 //! fills its storage fails with `BufferTooSmall`. Every other platform returns
21 //! `UnsupportedPlatform`.
22
23 const std = @import("std");
24 const builtin = @import("builtin");
25 const capabilities = @import("capabilities.zig");
26
27 pub const required_capabilities = capabilities.host(&.{
28 .filesystem,
29 .platform_frameworks,
30 });
31
32 const meminfo_path = "/proc/meminfo";
33 const cgroup_path = "/proc/self/cgroup";
34 const mountinfo_path = "/proc/self/mountinfo";
35 const meminfo_bytes_max: usize = 16 * 1024;
36 const cgroup_bytes_max: usize = 16 * 1024;
37 const mountinfo_bytes_max: usize = 128 * 1024;
38 const cgroup_value_bytes_max: usize = 256;
39
40 pub const Pressure = enum {
41 normal,
42 pressured,
43 };
44
45 pub const Memory = struct {
46 available_bytes: u64,
47 pressure: Pressure,
48 };
49
50 const CgroupMembership = struct {
51 v2: ?[]const u8 = null,
52 v1_memory: ?[]const u8 = null,
53 };
54
55 const CgroupMount = struct {
56 root: []const u8,
57 point: []const u8,
58 };
59
60 const CgroupMounts = struct {
61 v2: ?CgroupMount = null,
62 v1_memory: ?CgroupMount = null,
63 };
64
65 const HierarchyHeadroom = struct {
66 bytes: ?u64 = null,
67 };
68
69 const MemoryLimit = union(enum) {
70 unlimited,
71 finite: u64,
72 };
73
74 extern "c" fn memorystatus_get_level(level_address: usize) c_int;
75
76 pub fn memory(io: std.Io) !Memory {
77 return switch (comptime builtin.os.tag) {
78 .linux => linuxMemory(io),
79 .driverkit,
80 .ios,
81 .maccatalyst,
82 .macos,
83 .tvos,
84 .visionos,
85 .watchos,
86 => darwinMemory(),
87 else => error.UnsupportedPlatform,
88 };
89 }
90
91 pub fn availableMemoryBytes(io: std.Io) !u64 {
92 return (try memory(io)).available_bytes;
93 }
94
95 fn linuxMemory(io: std.Io) !Memory {
96 const host_available = try linuxHostAvailableMemoryBytes(io);
97 const cgroup_available = try linuxCgroupHeadroom(io);
98 return .{
99 .available_bytes = if (cgroup_available) |available|
100 @min(host_available, available)
101 else
102 host_available,
103 .pressure = .normal,
104 };
105 }
106
107 fn linuxHostAvailableMemoryBytes(io: std.Io) !u64 {
108 var storage: [meminfo_bytes_max]u8 = undefined;
109 return parseLinuxAvailableMemory(try readBounded(io, meminfo_path, &storage));
110 }
111
112 fn linuxCgroupHeadroom(io: std.Io) !?u64 {
113 var membership_storage: [cgroup_bytes_max]u8 = undefined;
114 const membership = try parseCgroupMembership(try readBounded(
115 io,
116 cgroup_path,
117 &membership_storage,
118 ));
119 var mountinfo_storage: [mountinfo_bytes_max]u8 = undefined;
120 const mounts = try parseCgroupMounts(try readBounded(
121 io,
122 mountinfo_path,
123 &mountinfo_storage,
124 ));
125 return cgroupHeadroom(io, membership, mounts);
126 }
127
128 fn cgroupHeadroom(
129 io: std.Io,
130 membership: CgroupMembership,
131 mounts: CgroupMounts,
132 ) !?u64 {
133 var result: ?u64 = null;
134 if (membership.v2) |group| {
135 const mount = mounts.v2 orelse return error.CgroupTelemetryUnavailable;
136 if (try cgroupMountHeadroom(
137 io,
138 group,
139 mount,
140 "memory.max",
141 "memory.current",
142 true,
143 )) |headroom| result = @min(result orelse headroom, headroom);
144 }
145 if (membership.v1_memory) |group| {
146 const mount = mounts.v1_memory orelse return error.CgroupTelemetryUnavailable;
147 if (try cgroupMountHeadroom(
148 io,
149 group,
150 mount,
151 "memory.limit_in_bytes",
152 "memory.usage_in_bytes",
153 false,
154 )) |headroom| result = @min(result orelse headroom, headroom);
155 }
156 return result;
157 }
158
159 fn parseCgroupMounts(text: []const u8) !CgroupMounts {
160 var result: CgroupMounts = .{};
161 var lines = std.mem.splitScalar(u8, text, '\n');
162 while (lines.next()) |line| {
163 if (line.len == 0) continue;
164 const separator = std.mem.indexOf(u8, line, " - ") orelse
165 return error.InvalidData;
166 var mounted = std.mem.tokenizeScalar(u8, line[0..separator], ' ');
167 _ = mounted.next() orelse return error.InvalidData;
168 _ = mounted.next() orelse return error.InvalidData;
169 _ = mounted.next() orelse return error.InvalidData;
170 const root = mounted.next() orelse return error.InvalidData;
171 const mount_point = mounted.next() orelse return error.InvalidData;
172 var filesystem = std.mem.tokenizeScalar(u8, line[separator + 3 ..], ' ');
173 const kind = filesystem.next() orelse return error.InvalidData;
174 _ = filesystem.next() orelse return error.InvalidData;
175 const options = filesystem.next() orelse return error.InvalidData;
176 if (std.mem.eql(u8, kind, "cgroup2")) {
177 try validateCgroupMount(root, mount_point);
178 if (result.v2 != null) return error.InvalidData;
179 result.v2 = .{ .root = root, .point = mount_point };
180 } else if (std.mem.eql(u8, kind, "cgroup") and
181 controllerListed(options, "memory"))
182 {
183 try validateCgroupMount(root, mount_point);
184 if (result.v1_memory != null) return error.InvalidData;
185 result.v1_memory = .{ .root = root, .point = mount_point };
186 }
187 }
188 return result;
189 }
190
191 fn validateCgroupMount(root: []const u8, point: []const u8) !void {
192 if (!validCgroupGroup(root) or
193 !std.fs.path.isAbsolute(point) or
194 std.mem.indexOfScalar(u8, point, '\\') != null)
195 {
196 return error.InvalidData;
197 }
198 }
199
200 fn cgroupMountHeadroom(
201 io: std.Io,
202 group: []const u8,
203 mount: CgroupMount,
204 limit_name: []const u8,
205 current_name: []const u8,
206 allow_inactive_v2: bool,
207 ) !?u64 {
208 const relative_group = try mountedCgroup(mount.root, group);
209 const hierarchy = try cgroupHierarchyHeadroom(
210 io,
211 mount.point,
212 relative_group,
213 limit_name,
214 current_name,
215 );
216 if (hierarchy) |observed| return observed.bytes;
217 if (!allow_inactive_v2 or !std.mem.eql(u8, mount.root, "/")) {
218 return error.CgroupTelemetryUnavailable;
219 }
220 const inactive = v2MemoryControllerInactive(
221 io,
222 mount.point,
223 relative_group,
224 ) catch return error.CgroupTelemetryUnavailable;
225 if (!inactive) return error.CgroupTelemetryUnavailable;
226 return null;
227 }
228
229 fn mountedCgroup(root: []const u8, group: []const u8) ![]const u8 {
230 if (!validCgroupGroup(root) or !validCgroupGroup(group)) {
231 return error.InvalidData;
232 }
233 if (std.mem.eql(u8, root, "/")) return group;
234 if (!std.mem.startsWith(u8, group, root)) {
235 return error.CgroupTelemetryUnavailable;
236 }
237 if (group.len == root.len) return "/";
238 if (group[root.len] != '/') return error.CgroupTelemetryUnavailable;
239 return group[root.len..];
240 }
241
242 fn v2MemoryControllerInactive(
243 io: std.Io,
244 base: []const u8,
245 group: []const u8,
246 ) !bool {
247 if (!validCgroupGroup(group)) return error.InvalidData;
248 var controllers_path_storage: [std.fs.max_path_bytes]u8 = undefined;
249 const controllers_path = try hierarchyPath(
250 &controllers_path_storage,
251 base,
252 "",
253 "cgroup.controllers",
254 );
255 var controllers_storage: [cgroup_value_bytes_max]u8 = undefined;
256 if (!wordListed(
257 try readBounded(io, controllers_path, &controllers_storage),
258 "memory",
259 )) return true;
260 var child = std.mem.trim(u8, group, "/");
261 while (cgroupParent(child)) |parent| {
262 var path_storage: [std.fs.max_path_bytes]u8 = undefined;
263 const path = try hierarchyPath(
264 &path_storage,
265 base,
266 parent,
267 "cgroup.subtree_control",
268 );
269 var value_storage: [cgroup_value_bytes_max]u8 = undefined;
270 if (wordListed(try readBounded(io, path, &value_storage), "memory")) {
271 return false;
272 }
273 child = parent;
274 }
275 return true;
276 }
277
278 fn cgroupHierarchyHeadroom(
279 io: std.Io,
280 base: []const u8,
281 group: []const u8,
282 limit_name: []const u8,
283 current_name: []const u8,
284 ) !?HierarchyHeadroom {
285 if (!validCgroupGroup(group)) return error.InvalidData;
286 var result: HierarchyHeadroom = .{};
287 var observed = false;
288 var current = std.mem.trim(u8, group, "/");
289 while (true) {
290 var limit_path_storage: [std.fs.max_path_bytes]u8 = undefined;
291 const limit_path = try hierarchyPath(
292 &limit_path_storage,
293 base,
294 current,
295 limit_name,
296 );
297 var limit_storage: [cgroup_value_bytes_max]u8 = undefined;
298 const limit_text = readBounded(io, limit_path, &limit_storage) catch |err| switch (err) {
299 error.FileNotFound => {
300 current = cgroupParent(current) orelse break;
301 continue;
302 },
303 else => return err,
304 };
305 observed = true;
306 switch (try parseMemoryLimit(limit_text)) {
307 .unlimited => {},
308 .finite => |limit| {
309 var current_path_storage: [std.fs.max_path_bytes]u8 = undefined;
310 const current_path = try hierarchyPath(
311 ¤t_path_storage,
312 base,
313 current,
314 current_name,
315 );
316 var current_storage: [cgroup_value_bytes_max]u8 = undefined;
317 const usage = try parseMemoryCurrent(try readBounded(
318 io,
319 current_path,
320 ¤t_storage,
321 ));
322 const headroom = remainingMemory(limit, usage);
323 result.bytes = @min(result.bytes orelse headroom, headroom);
324 },
325 }
326 current = cgroupParent(current) orelse break;
327 }
328 return if (observed) result else null;
329 }
330
331 fn parseCgroupMembership(text: []const u8) !CgroupMembership {
332 var result: CgroupMembership = .{};
333 var lines = std.mem.splitScalar(u8, text, '\n');
334 while (lines.next()) |line| {
335 if (line.len == 0) continue;
336 var fields = std.mem.splitScalar(u8, line, ':');
337 const hierarchy = fields.next() orelse return error.InvalidData;
338 const controllers = fields.next() orelse return error.InvalidData;
339 const group = fields.next() orelse return error.InvalidData;
340 if (fields.next() != null or !validCgroupGroup(group)) return error.InvalidData;
341 if (std.mem.eql(u8, hierarchy, "0") and controllers.len == 0) {
342 if (result.v2 != null) return error.InvalidData;
343 result.v2 = group;
344 } else if (controllerListed(controllers, "memory")) {
345 if (result.v1_memory != null) return error.InvalidData;
346 result.v1_memory = group;
347 }
348 }
349 return result;
350 }
351
352 fn controllerListed(controllers: []const u8, expected: []const u8) bool {
353 var values = std.mem.splitScalar(u8, controllers, ',');
354 while (values.next()) |value| {
355 if (std.mem.eql(u8, value, expected)) return true;
356 }
357 return false;
358 }
359
360 fn wordListed(words: []const u8, expected: []const u8) bool {
361 var values = std.mem.tokenizeAny(u8, words, " \t\r\n");
362 while (values.next()) |value| {
363 if (std.mem.eql(u8, value, expected)) return true;
364 }
365 return false;
366 }
367
368 fn validCgroupGroup(group: []const u8) bool {
369 if (group.len == 0 or group[0] != '/') return false;
370 var components = std.mem.splitScalar(u8, group[1..], '/');
371 while (components.next()) |component| {
372 if (component.len == 0) return group.len == 1;
373 if (std.mem.eql(u8, component, ".") or std.mem.eql(u8, component, "..")) {
374 return false;
375 }
376 }
377 return true;
378 }
379
380 fn hierarchyPath(
381 storage: []u8,
382 base: []const u8,
383 group: []const u8,
384 name: []const u8,
385 ) ![]const u8 {
386 return if (group.len == 0)
387 std.fmt.bufPrint(storage, "{s}/{s}", .{ base, name })
388 else
389 std.fmt.bufPrint(storage, "{s}/{s}/{s}", .{ base, group, name });
390 }
391
392 fn cgroupParent(group: []const u8) ?[]const u8 {
393 if (group.len == 0) return null;
394 const separator = std.mem.lastIndexOfScalar(u8, group, '/') orelse return "";
395 return group[0..separator];
396 }
397
398 fn parseMemoryLimit(text: []const u8) !MemoryLimit {
399 const value = std.mem.trim(u8, text, " \t\r\n");
400 if (std.mem.eql(u8, value, "max")) return .unlimited;
401 if (value.len == 0) return error.InvalidData;
402 return .{ .finite = try std.fmt.parseUnsigned(u64, value, 10) };
403 }
404
405 fn parseMemoryCurrent(text: []const u8) !u64 {
406 const value = std.mem.trim(u8, text, " \t\r\n");
407 if (value.len == 0) return error.InvalidData;
408 return std.fmt.parseUnsigned(u64, value, 10);
409 }
410
411 fn remainingMemory(limit: u64, current: u64) u64 {
412 return limit -| current;
413 }
414
415 fn readBounded(io: std.Io, path: []const u8, storage: []u8) ![]const u8 {
416 var file = try std.Io.Dir.openFileAbsolute(io, path, .{});
417 defer file.close(io);
418 const count = try file.readPositionalAll(io, storage, 0);
419 if (count == storage.len) return error.BufferTooSmall;
420 return storage[0..count];
421 }
422
423 fn parseLinuxAvailableMemory(text: []const u8) !u64 {
424 var found: ?u64 = null;
425 var lines = std.mem.splitScalar(u8, text, '\n');
426 while (lines.next()) |line| {
427 var fields = std.mem.tokenizeAny(u8, line, " \t\r:");
428 const name = fields.next() orelse continue;
429 if (!std.mem.eql(u8, name, "MemAvailable")) continue;
430 if (found != null) return error.InvalidData;
431 const kib = try std.fmt.parseUnsigned(u64, fields.next() orelse
432 return error.InvalidData, 10);
433 if (!std.mem.eql(u8, fields.next() orelse return error.InvalidData, "kB") or
434 fields.next() != null)
435 {
436 return error.InvalidData;
437 }
438 found = try std.math.mul(u64, kib, 1024);
439 }
440 return found orelse error.InvalidData;
441 }
442
443 fn darwinMemory() !Memory {
444 var percent_free: c_uint = 0;
445 if (memorystatus_get_level(@intFromPtr(&percent_free)) != 0) {
446 return error.QueryFailed;
447 }
448 const total = try std.process.totalSystemMemory();
449 const available = try availableFromPercent(total, percent_free);
450 var pressure_level: c_uint = 0;
451 var pressure_size: usize = @sizeOf(c_uint);
452 if (std.c.sysctlbyname(
453 "kern.memorystatus_vm_pressure_level",
454 &pressure_level,
455 &pressure_size,
456 null,
457 0,
458 ) != 0 or pressure_size != @sizeOf(c_uint)) return error.QueryFailed;
459 return .{
460 .available_bytes = available,
461 .pressure = try decodeDarwinPressure(pressure_level),
462 };
463 }
464
465 fn availableFromPercent(total: u64, percent: c_uint) !u64 {
466 if (percent > 100) return error.InvalidData;
467 return @intCast(@as(u128, total) * percent / 100);
468 }
469
470 fn decodeDarwinPressure(level: c_uint) !Pressure {
471 return switch (level) {
472 1 => .normal,
473 2, 4 => .pressured,
474 else => error.InvalidData,
475 };
476 }
477
478 test "Linux available memory parser uses binary kibibytes" {
479 const text =
480 "MemTotal: 32768000 kB\n" ++
481 "MemFree: 1048576 kB\n" ++
482 "MemAvailable: 12582912 kB\n";
483 try std.testing.expectEqual(
484 @as(u64, 12 * 1024 * 1024 * 1024),
485 try parseLinuxAvailableMemory(text),
486 );
487 }
488
489 test "Linux available memory parser rejects missing duplicate and malformed values" {
490 try std.testing.expectError(
491 error.InvalidData,
492 parseLinuxAvailableMemory("MemFree: 1 kB\n"),
493 );
494 try std.testing.expectError(
495 error.InvalidData,
496 parseLinuxAvailableMemory("MemAvailable: 1 kB\nMemAvailable: 2 kB\n"),
497 );
498 try std.testing.expectError(
499 error.InvalidCharacter,
500 parseLinuxAvailableMemory("MemAvailable: unknown kB\n"),
501 );
502 try std.testing.expectError(
503 error.InvalidData,
504 parseLinuxAvailableMemory("MemAvailable: 1 MB\n"),
505 );
506 }
507
508 test "Linux cgroup membership distinguishes unified and memory controllers" {
509 const unified = try parseCgroupMembership("0::/executor/slot\n");
510 try std.testing.expectEqualStrings("/executor/slot", unified.v2.?);
511 const legacy = try parseCgroupMembership("4:cpu,memory:/executor/slot\n");
512 try std.testing.expectEqualStrings("/executor/slot", legacy.v1_memory.?);
513 try std.testing.expectError(
514 error.InvalidData,
515 parseCgroupMembership("0::/safe/../escape\n"),
516 );
517 }
518
519 test "Linux cgroup mount discovery accepts custom controller roots" {
520 const mounts = try parseCgroupMounts(
521 "36 25 0:32 /tenant /custom/unified rw,nosuid - cgroup2 cgroup rw\n" ++
522 "37 25 0:33 /legacy /custom/memory rw,nosuid - cgroup cgroup rw,memory\n",
523 );
524 try std.testing.expectEqualStrings("/tenant", mounts.v2.?.root);
525 try std.testing.expectEqualStrings("/custom/unified", mounts.v2.?.point);
526 try std.testing.expectEqualStrings("/legacy", mounts.v1_memory.?.root);
527 try std.testing.expectEqualStrings("/custom/memory", mounts.v1_memory.?.point);
528 }
529
530 test "Linux declared cgroup membership requires an observed mount" {
531 try std.testing.expectError(
532 error.CgroupTelemetryUnavailable,
533 cgroupHeadroom(
534 std.testing.io,
535 .{ .v2 = "/executor" },
536 .{},
537 ),
538 );
539 }
540
541 test "Linux cgroup headroom uses the tightest enclosing limit" {
542 var temporary = std.testing.tmpDir(.{});
543 defer temporary.cleanup();
544 try temporary.dir.createDirPath(std.testing.io, "parent/leaf");
545 try temporary.dir.writeFile(std.testing.io, .{
546 .sub_path = "memory.max",
547 .data = "max\n",
548 });
549 try temporary.dir.writeFile(std.testing.io, .{
550 .sub_path = "parent/memory.max",
551 .data = "800\n",
552 });
553 try temporary.dir.writeFile(std.testing.io, .{
554 .sub_path = "parent/memory.current",
555 .data = "300\n",
556 });
557 try temporary.dir.writeFile(std.testing.io, .{
558 .sub_path = "parent/leaf/memory.max",
559 .data = "1000\n",
560 });
561 try temporary.dir.writeFile(std.testing.io, .{
562 .sub_path = "parent/leaf/memory.current",
563 .data = "400\n",
564 });
565 const base = try temporary.dir.realPathFileAlloc(
566 std.testing.io,
567 ".",
568 std.testing.allocator,
569 );
570 defer std.testing.allocator.free(base);
571 const observed = (try cgroupHierarchyHeadroom(
572 std.testing.io,
573 base,
574 "/parent/leaf",
575 "memory.max",
576 "memory.current",
577 )).?;
578 try std.testing.expectEqual(@as(?u64, 500), observed.bytes);
579 }
580
581 test "Linux cgroup subtree mounts map membership relative to mount root" {
582 var temporary = std.testing.tmpDir(.{});
583 defer temporary.cleanup();
584 try temporary.dir.createDirPath(std.testing.io, "job");
585 try temporary.dir.writeFile(std.testing.io, .{
586 .sub_path = "memory.max",
587 .data = "max\n",
588 });
589 try temporary.dir.writeFile(std.testing.io, .{
590 .sub_path = "job/memory.max",
591 .data = "1000\n",
592 });
593 try temporary.dir.writeFile(std.testing.io, .{
594 .sub_path = "job/memory.current",
595 .data = "400\n",
596 });
597 const base = try temporary.dir.realPathFileAlloc(
598 std.testing.io,
599 ".",
600 std.testing.allocator,
601 );
602 defer std.testing.allocator.free(base);
603 const observed = try cgroupMountHeadroom(
604 std.testing.io,
605 "/tenant/job",
606 .{ .root = "/tenant", .point = base },
607 "memory.max",
608 "memory.current",
609 true,
610 );
611 try std.testing.expectEqual(@as(?u64, 600), observed);
612 }
613
614 test "Linux cgroup discovered mount without memory files fails closed" {
615 var temporary = std.testing.tmpDir(.{});
616 defer temporary.cleanup();
617 try temporary.dir.createDirPath(std.testing.io, "executor");
618 const base = try temporary.dir.realPathFileAlloc(
619 std.testing.io,
620 ".",
621 std.testing.allocator,
622 );
623 defer std.testing.allocator.free(base);
624 try std.testing.expectError(
625 error.CgroupTelemetryUnavailable,
626 cgroupMountHeadroom(
627 std.testing.io,
628 "/executor",
629 .{ .root = "/", .point = base },
630 "memory.max",
631 "memory.current",
632 true,
633 ),
634 );
635 }
636
637 test "Linux cgroup root without a v2 memory controller is unlimited" {
638 var temporary = std.testing.tmpDir(.{});
639 defer temporary.cleanup();
640 try temporary.dir.createDirPath(std.testing.io, "executor");
641 try temporary.dir.writeFile(std.testing.io, .{
642 .sub_path = "cgroup.controllers",
643 .data = "cpuset\n",
644 });
645 const base = try temporary.dir.realPathFileAlloc(
646 std.testing.io,
647 ".",
648 std.testing.allocator,
649 );
650 defer std.testing.allocator.free(base);
651 const observed = try cgroupMountHeadroom(
652 std.testing.io,
653 "/executor",
654 .{ .root = "/", .point = base },
655 "memory.max",
656 "memory.current",
657 true,
658 );
659 try std.testing.expectEqual(@as(?u64, null), observed);
660 }
661
662 test "Linux cgroup active v2 edge missing memory files fails closed" {
663 var temporary = std.testing.tmpDir(.{});
664 defer temporary.cleanup();
665 try temporary.dir.createDirPath(std.testing.io, "executor");
666 try temporary.dir.writeFile(std.testing.io, .{
667 .sub_path = "cgroup.controllers",
668 .data = "memory\n",
669 });
670 try temporary.dir.writeFile(std.testing.io, .{
671 .sub_path = "cgroup.subtree_control",
672 .data = "memory\n",
673 });
674 const base = try temporary.dir.realPathFileAlloc(
675 std.testing.io,
676 ".",
677 std.testing.allocator,
678 );
679 defer std.testing.allocator.free(base);
680 try std.testing.expectError(
681 error.CgroupTelemetryUnavailable,
682 cgroupMountHeadroom(
683 std.testing.io,
684 "/executor",
685 .{ .root = "/", .point = base },
686 "memory.max",
687 "memory.current",
688 true,
689 ),
690 );
691 }
692
693 test "Linux cgroup subtree mount without memory files fails closed" {
694 var temporary = std.testing.tmpDir(.{});
695 defer temporary.cleanup();
696 try temporary.dir.createDirPath(std.testing.io, "job");
697 const base = try temporary.dir.realPathFileAlloc(
698 std.testing.io,
699 ".",
700 std.testing.allocator,
701 );
702 defer std.testing.allocator.free(base);
703 try std.testing.expectError(
704 error.CgroupTelemetryUnavailable,
705 cgroupMountHeadroom(
706 std.testing.io,
707 "/tenant/job",
708 .{ .root = "/tenant", .point = base },
709 "memory.max",
710 "memory.current",
711 true,
712 ),
713 );
714 }
715
716 test "Linux cgroup values reject malformed data and saturate exhausted limits" {
717 try std.testing.expectError(error.InvalidData, parseMemoryLimit(""));
718 try std.testing.expectError(error.InvalidCharacter, parseMemoryLimit("unknown"));
719 try std.testing.expectError(error.InvalidData, parseMemoryCurrent(""));
720 try std.testing.expectError(error.InvalidCharacter, parseMemoryCurrent("unknown"));
721 try std.testing.expectEqual(@as(u64, 0), remainingMemory(100, 101));
722 try std.testing.expectEqual(@as(u64, 1), remainingMemory(100, 99));
723 }
724
725 test "Darwin pressure sysctl decodes dispatch note flags" {
726 try std.testing.expectEqual(Pressure.normal, try decodeDarwinPressure(1));
727 try std.testing.expectEqual(Pressure.pressured, try decodeDarwinPressure(2));
728 try std.testing.expectEqual(Pressure.pressured, try decodeDarwinPressure(4));
729 try std.testing.expectError(error.InvalidData, decodeDarwinPressure(0));
730 try std.testing.expectError(error.InvalidData, decodeDarwinPressure(3));
731 }
732
733 test "Darwin percent-free memory conversion is bounded and conservative" {
734 try std.testing.expectEqual(
735 @as(u64, 16 * 1024 * 1024 * 1024),
736 try availableFromPercent(64 * 1024 * 1024 * 1024, 25),
737 );
738 try std.testing.expectEqual(@as(u64, 1), try availableFromPercent(101, 1));
739 try std.testing.expectError(error.InvalidData, availableFromPercent(1, 101));
740 }
741
742 test "host memory is a bounded observation" {
743 if (comptime builtin.os.tag != .linux and !builtin.os.tag.isDarwin()) {
744 return error.SkipZigTest;
745 }
746 const observed = try memory(std.testing.io);
747 try std.testing.expect(observed.available_bytes > 0);
748 if (comptime builtin.os.tag == .linux) {
749 try std.testing.expectEqual(Pressure.normal, observed.pressure);
750 }
751 }