diff options
author | 2023-02-03 18:13:33 -0800 | |
---|---|---|
committer | 2023-02-03 18:13:33 -0800 | |
commit | a2fd20e5c02c8a47cbb8fce7852e03dcbaf69877 (patch) | |
tree | 9b128760098acc04e50c570f02855b393897508f | |
parent | de613baf81bfaf524818cb72811e13ad9a3765ff (diff) | |
download | bun-a2fd20e5c02c8a47cbb8fce7852e03dcbaf69877.tar.gz bun-a2fd20e5c02c8a47cbb8fce7852e03dcbaf69877.tar.zst bun-a2fd20e5c02c8a47cbb8fce7852e03dcbaf69877.zip |
Implement os.cpus() for Linux (#1977)
* Implement os.cpus() for Linux
`os.cpus()` currently returns an empty array for all platforms. This PR implements full functionality for Linux and has been tested on x86-64. Other OSes will continue to return an empty array.
Note that Linux on Arm64 may report the CPU model differently; if this is the case the CPU model will currently be reported as "unknown". As I do not have Arm64 hardware to verify and develop against, a todo has been left in the code.
* resolve issues from review
-rw-r--r-- | src/bun.js/node/node_os.zig | 140 |
1 files changed, 138 insertions, 2 deletions
diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index 525ee991b..f7a91393e 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -54,11 +54,147 @@ 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, + } = .{} + }; + pub fn cpus(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); - // TODO: - return JSC.JSArray.from(globalThis, &.{}); + 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) |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(); + } + } + + fn cpusImplLinux(cpu_buffer: []CPU) ![]CPU { + // 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; + + // Read /proc/stat to get number of CPUs and times + if (std.fs.openFileAbsolute("/proc/stat", .{})) |file| { + defer file.close(); + var reader = file.reader(); + + // Skip the first line (aggregate of all CPUs) + try reader.skipUntilDelimiterOrEof('\n'); + + // 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); + _ = 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); + + num_cpus += 1; + } + } else |_| { + return error.cannot_open_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(); + var reader = file.reader(); + const key_processor = "processor\t: "; + const key_model_name = "model name\t: "; + + var cpu_index: usize = 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; + + } 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); + } + //TODO: special handling for ARM64 (no model name)? + } + } else |_| { + // Do nothing: CPU default initializer has set model name to "unknown" + } + + // Read /sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq to get current frequency (optional) + for (slice) |*cpu, 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| { + defer file.close(); + + 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; + } else |_| { + // Do nothing: CPU default initializer has set speed to 0 + } + } + + return slice; } pub fn endianness(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { |