aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Justin Whear <justin.whear@gmail.com> 2023-02-24 12:24:04 -0800
committerGravatar GitHub <noreply@github.com> 2023-02-24 14:24:04 -0600
commitf0e5d5457922a2cbdff954b4dc01c879f8e39b3c (patch)
tree1f6e4a97308a160976f2e00a53ab98f4b903ed98
parent1c531472c93f9d3a7b491b100803b8c0ad42d0e7 (diff)
downloadbun-f0e5d5457922a2cbdff954b4dc01c879f8e39b3c.tar.gz
bun-f0e5d5457922a2cbdff954b4dc01c879f8e39b3c.tar.zst
bun-f0e5d5457922a2cbdff954b4dc01c879f8e39b3c.zip
Implement `os.cpus` for Darwin (OS X) (#2115)
* adding experimental Mac implementation os os.cpus * Simplify cpus interfaces * remove support for osx 10 * Refactor os.cpus implementation This commit substantially refactors how the Linux and Darwin implementations of `os.cpus`. The goal is to avoid unnecessary copying and allow broader latitude in responding to errors per implementation. * improved comments * ensure no buffer overrun * use PROCESSOR_CPU_LOAD_INFO_COUNT; not sure if this is correct * oh teh noes * use sliceTo instead of span * cpu_ticks are uints
-rw-r--r--build.zig6
-rw-r--r--src/bun.js/node/node_os.zig226
-rw-r--r--src/darwin_c.zig17
3 files changed, 174 insertions, 75 deletions
diff --git a/build.zig b/build.zig
index 33e7ad12a..fe8aa6a53 100644
--- a/build.zig
+++ b/build.zig
@@ -170,11 +170,7 @@ pub fn build(b: *Build) !void {
if (std.mem.eql(u8, os_tagname, "macos")) {
os_tagname = "darwin";
- if (arch.isAARCH64()) {
- target.os_version_min = std.zig.CrossTarget.OsVersion{ .semver = .{ .major = 11, .minor = 0, .patch = 0 } };
- } else if (arch.isX86()) {
- target.os_version_min = std.zig.CrossTarget.OsVersion{ .semver = .{ .major = 10, .minor = 14, .patch = 0 } };
- }
+ target.os_version_min = std.zig.CrossTarget.OsVersion{ .semver = .{ .major = 11, .minor = 0, .patch = 0 } };
} else if (target.isLinux()) {
target.setGnuLibCVersion(2, 27, 0);
}
diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig
index ffe35ecd0..eb7ae3c2d 100644
--- a/src/bun.js/node/node_os.zig
+++ b/src/bun.js/node/node_os.zig
@@ -56,63 +56,58 @@ pub const Os = struct {
return JSC.ZigString.init(Global.arch_name).withEncoding().toValue(globalThis);
}
- const CPU = struct {
- model: JSC.ZigString = JSC.ZigString.init("unknown"),
- speed: u64 = 0,
- times: struct {
- user: u64 = 0,
- nice: u64 = 0,
- sys: u64 = 0,
- idle: u64 = 0,
- irq: u64 = 0,
- } = .{},
+ const CPUTimes = struct {
+ user: u64 = 0,
+ nice: u64 = 0,
+ sys: u64 = 0,
+ idle: u64 = 0,
+ irq: u64 = 0,
+
+ pub fn toValue(self: CPUTimes, globalThis: *JSC.JSGlobalObject) JSC.JSValue {
+ const fields = comptime std.meta.fieldNames(CPUTimes);
+ const ret = JSC.JSValue.createEmptyObject(globalThis, fields.len);
+ inline for (fields) |fieldName| {
+ ret.put(globalThis, JSC.ZigString.static(fieldName),
+ JSC.JSValue.jsNumberFromUint64(@field(self, fieldName)));
+ }
+ return ret;
+ }
};
pub fn cpus(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
JSC.markBinding(@src());
- var cpu_buffer: [8192]CPU = undefined;
- const cpus_or_error = if (comptime Environment.isLinux)
- cpusImplLinux(&cpu_buffer)
- else
- @as(anyerror![]CPU, cpu_buffer[0..0]); // unsupported platform -> empty array
-
- if (cpus_or_error) |list| {
- // Convert the CPU list to a JS Array
- const values = JSC.JSValue.createEmptyArray(globalThis, list.len);
- for (list, 0..) |cpu, cpu_index| {
- const obj = JSC.JSValue.createEmptyObject(globalThis, 3);
- obj.put(globalThis, JSC.ZigString.static("model"), cpu.model.withEncoding().toValueGC(globalThis));
- obj.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumberFromUint64(cpu.speed));
-
- const timesFields = comptime std.meta.fieldNames(@TypeOf(cpu.times));
- const times = JSC.JSValue.createEmptyObject(globalThis, 5);
- inline for (timesFields) |fieldName| {
- times.put(globalThis, JSC.ZigString.static(fieldName), JSC.JSValue.jsNumberFromUint64(@field(cpu.times, fieldName)));
- }
- obj.put(globalThis, JSC.ZigString.static("times"), times);
- values.putIndex(globalThis, @intCast(u32, cpu_index), obj);
- }
- return values;
- } else |zig_err| {
- const msg = switch (zig_err) {
- error.too_many_cpus => "Too many CPUs or malformed /proc/cpuinfo file",
- error.eol => "Malformed /proc/stat file",
- else => "An error occurred while fetching cpu information",
- };
- //TODO more suitable error type?
- const err = JSC.SystemError{
- .message = JSC.ZigString.init(msg),
- };
- globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis));
- return JSC.JSValue.jsUndefined();
- }
+ return if (comptime Environment.isLinux)
+ cpusImplLinux(globalThis) catch {
+ const err = JSC.SystemError{
+ .message = JSC.ZigString.init("Failed to get cpu information"),
+ .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))),
+ };
+
+ globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis));
+ return JSC.JSValue.jsUndefined();
+ }
+ else if (comptime Environment.isMac)
+ cpusImplDarwin(globalThis) catch {
+ const err = JSC.SystemError{
+ .message = JSC.ZigString.init("Failed to get cpu information"),
+ .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))),
+ };
+
+ globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis));
+ return JSC.JSValue.jsUndefined();
+ }
+ else
+ JSC.JSValue.createEmptyArray(globalThis, 0);
}
- fn cpusImplLinux(cpu_buffer: []CPU) ![]CPU {
+ fn cpusImplLinux(globalThis: *JSC.JSGlobalObject) !JSC.JSValue {
+ // Create the return array
+ const values = JSC.JSValue.createEmptyArray(globalThis, 0);
+ var num_cpus: u32 = 0;
+
// Use a large line buffer because the /proc/stat file can have a very long list of interrupts
- var line_buffer: [1024 * 8]u8 = undefined;
- var num_cpus: usize = 0;
+ var line_buffer: [1024*8]u8 = undefined;
// Read /proc/stat to get number of CPUs and times
if (std.fs.openFileAbsolute("/proc/stat", .{})) |file| {
@@ -124,33 +119,34 @@ pub const Os = struct {
// Read each CPU line
while (try reader.readUntilDelimiterOrEof(&line_buffer, '\n')) |line| {
- if (num_cpus >= cpu_buffer.len) return error.too_many_cpus;
// CPU lines are formatted as `cpu0 user nice sys idle iowait irq softirq`
var toks = std.mem.tokenize(u8, line, " \t");
const cpu_name = toks.next();
if (cpu_name == null or !std.mem.startsWith(u8, cpu_name.?, "cpu")) break; // done with CPUs
- // Default initialize the CPU to ensure that we never return uninitialized fields
- cpu_buffer[num_cpus] = CPU{};
-
//NOTE: libuv assumes this is fixed on Linux, not sure that's actually the case
const scale = 10;
- cpu_buffer[num_cpus].times.user = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10);
- cpu_buffer[num_cpus].times.nice = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10);
- cpu_buffer[num_cpus].times.sys = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10);
- cpu_buffer[num_cpus].times.idle = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10);
+
+ var times = CPUTimes{};
+ times.user = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10);
+ times.nice = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10);
+ times.sys = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10);
+ times.idle = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10);
_ = try (toks.next() orelse error.eol); // skip iowait
- cpu_buffer[num_cpus].times.irq = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10);
+ times.irq = scale * try std.fmt.parseInt(u64, toks.next() orelse return error.eol, 10);
+
+ // Actually create the JS object representing the CPU
+ const cpu = JSC.JSValue.createEmptyObject(globalThis, 3);
+ cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis));
+ values.putIndex(globalThis, num_cpus, cpu);
num_cpus += 1;
}
} else |_| {
- return error.cannot_open_proc_stat;
+ return error.no_proc_stat;
}
- const slice = cpu_buffer[0..num_cpus];
-
// Read /proc/cpuinfo to get model information (optional)
if (std.fs.openFileAbsolute("/proc/cpuinfo", .{})) |file| {
defer file.close();
@@ -158,26 +154,37 @@ pub const Os = struct {
const key_processor = "processor\t: ";
const key_model_name = "model name\t: ";
- var cpu_index: usize = 0;
+ var cpu_index: u32 = 0;
while (try reader.readUntilDelimiterOrEof(&line_buffer, '\n')) |line| {
if (std.mem.startsWith(u8, line, key_processor)) {
// If this line starts a new processor, parse the index from the line
const digits = std.mem.trim(u8, line[key_processor.len..], " \t\n");
- cpu_index = try std.fmt.parseInt(usize, digits, 10);
- if (cpu_index >= slice.len) return error.too_may_cpus;
+ cpu_index = try std.fmt.parseInt(u32, digits, 10);
+ if (cpu_index >= num_cpus) return error.too_may_cpus;
+
} else if (std.mem.startsWith(u8, line, key_model_name)) {
// If this is the model name, extract it and store on the current cpu
const model_name = line[key_model_name.len..];
- slice[cpu_index].model = JSC.ZigString.init(model_name);
+ const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index);
+ cpu.put(globalThis, JSC.ZigString.static("model"),
+ JSC.ZigString.init(model_name).withEncoding().toValueGC(globalThis));
}
//TODO: special handling for ARM64 (no model name)?
}
} else |_| {
- // Do nothing: CPU default initializer has set model name to "unknown"
+ // Initialize model name to "unknown"
+ var it = values.arrayIterator(globalThis);
+ while (it.next()) |cpu| {
+ cpu.put(globalThis, JSC.ZigString.static("model"),
+ JSC.ZigString.static("unknown").withEncoding().toValue(globalThis));
+ }
}
// Read /sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq to get current frequency (optional)
- for (slice, 0..) |*cpu, cpu_index| {
+ var cpu_index: u32 = 0;
+ while (cpu_index < num_cpus) : (cpu_index += 1) {
+ const cpu = JSC.JSObject.getIndex(values, globalThis, cpu_index);
+
var path_buf: [128]u8 = undefined;
const path = try std.fmt.bufPrint(&path_buf, "/sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq", .{cpu_index});
if (std.fs.openFileAbsolute(path, .{})) |file| {
@@ -185,13 +192,92 @@ pub const Os = struct {
const bytes_read = try file.readAll(&line_buffer);
const digits = std.mem.trim(u8, line_buffer[0..bytes_read], " \n");
- cpu.speed = try std.fmt.parseInt(u64, digits, 10) / 1000;
+ const speed = (std.fmt.parseInt(u64, digits, 10) catch 0) / 1000;
+
+ cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(speed));
} else |_| {
- // Do nothing: CPU default initializer has set speed to 0
+ // Initialize CPU speed to 0
+ cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(0));
}
}
- return slice;
+ return values;
+ }
+
+ fn cpusImplDarwin(globalThis: *JSC.JSGlobalObject) !JSC.JSValue {
+ const local_bindings = @import("../../darwin_c.zig");
+ const c = std.c;
+
+ // Fetch the CPU info structure
+ var num_cpus: c.natural_t = 0;
+ var info: [*]local_bindings.processor_cpu_load_info = undefined;
+ var info_size: std.c.mach_msg_type_number_t = 0;
+ if (local_bindings.host_processor_info(
+ std.c.mach_host_self(),
+ local_bindings.PROCESSOR_CPU_LOAD_INFO,
+ &num_cpus, @ptrCast(*local_bindings.processor_info_array_t, &info),
+ &info_size) != .SUCCESS) {
+ return error.no_processor_info;
+ }
+ defer _ = std.c.vm_deallocate(std.c.mach_task_self(), @ptrToInt(info), info_size);
+
+ // Ensure we got the amount of data we expected to guard against buffer overruns
+ if (info_size != C.PROCESSOR_CPU_LOAD_INFO_COUNT * num_cpus) {
+ return error.broken_process_info;
+ }
+
+
+ // Get CPU model name
+ var model_name_buf: [512]u8 = undefined;
+ var len: usize = model_name_buf.len;
+ // Try brand_string first and if it fails try hw.model
+ if (!(std.c.sysctlbyname("machdep.cpu.brand_string", &model_name_buf, &len, null, 0) == 0 or
+ std.c.sysctlbyname("hw.model", &model_name_buf, &len, null, 0) == 0)) {
+ return error.no_processor_info;
+ }
+ //NOTE: sysctlbyname doesn't update len if it was large enough, so we
+ // still have to find the null terminator. All cpus can share the same
+ // model name.
+ const model_name = JSC.ZigString.init(std.mem.sliceTo(&model_name_buf, 0)).withEncoding().toValueGC(globalThis);
+
+
+ // Get CPU speed
+ var speed: u64 = 0;
+ len = @sizeOf(@TypeOf(speed));
+ _ = std.c.sysctlbyname("hw.cpufrequency", &speed, &len, null, 0);
+ if (speed == 0) {
+ // Suggested by Node implementation:
+ // If sysctl hw.cputype == CPU_TYPE_ARM64, the correct value is unavailable
+ // from Apple, but we can hard-code it here to a plausible value.
+ speed = 2_400_000_000;
+ }
+
+
+ // Get the multiplier; this is the number of ms/tick
+ const unistd = @cImport({@cInclude("unistd.h");});
+ const ticks: i64 = unistd.sysconf(unistd._SC_CLK_TCK);
+ const multiplier = 1000 / @intCast(u64, ticks);
+
+ // Set up each CPU value in the return
+ const values = JSC.JSValue.createEmptyArray(globalThis, @intCast(u32, num_cpus));
+ var cpu_index: u32 = 0;
+ while (cpu_index < num_cpus) : (cpu_index += 1) {
+ const times = CPUTimes{
+ .user = info[cpu_index].cpu_ticks[0] * multiplier,
+ .nice = info[cpu_index].cpu_ticks[3] * multiplier,
+ .sys = info[cpu_index].cpu_ticks[1] * multiplier,
+ .idle = info[cpu_index].cpu_ticks[2] * multiplier,
+ .irq = 0, // not available
+ };
+
+ const cpu = JSC.JSValue.createEmptyObject(globalThis, 3);
+ cpu.put(globalThis, JSC.ZigString.static("speed"), JSC.JSValue.jsNumber(speed / 1_000_000));
+ cpu.put(globalThis, JSC.ZigString.static("model"), model_name);
+ cpu.put(globalThis, JSC.ZigString.static("times"), times.toValue(globalThis));
+
+ values.putIndex(globalThis, cpu_index, cpu);
+ }
+ return values;
}
pub fn endianness(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
diff --git a/src/darwin_c.zig b/src/darwin_c.zig
index 474e3374f..f2e22c85e 100644
--- a/src/darwin_c.zig
+++ b/src/darwin_c.zig
@@ -550,6 +550,23 @@ pub fn get_system_loadavg() [3]f64 {
};
}
+pub const processor_flavor_t = c_int;
+
+// https://opensource.apple.com/source/xnu/xnu-792/osfmk/mach/processor_info.h.auto.html
+pub const PROCESSOR_CPU_LOAD_INFO: processor_flavor_t = 2;
+// https://opensource.apple.com/source/xnu/xnu-792/osfmk/mach/machine.h.auto.html
+pub const CPU_STATE_MAX = 4;
+pub const processor_cpu_load_info = extern struct {
+ cpu_ticks: [CPU_STATE_MAX]c_uint,
+};
+pub const PROCESSOR_CPU_LOAD_INFO_COUNT = @as(std.c.mach_msg_type_number_t,
+ @sizeOf(processor_cpu_load_info)/@sizeOf(std.c.natural_t));
+pub const processor_info_array_t = [*]c_int;
+pub const PROCESSOR_INFO_MAX = 1024;
+
+pub extern fn host_processor_info(host: std.c.host_t , flavor: processor_flavor_t , out_processor_count: *std.c.natural_t , out_processor_info: *processor_info_array_t, out_processor_infoCnt: *std.c.mach_msg_type_number_t) std.c.E;
+
+
pub extern fn getuid(...) std.os.uid_t;
pub extern fn getgid(...) std.os.gid_t;