Switch gostat with zigstat
All checks were successful
PikaOS Package Build & Release (amd64-v3) / build (push) Successful in 40s

This commit is contained in:
ferreo 2024-12-02 14:27:56 +00:00
parent e381297455
commit 1e056c7491
8 changed files with 357 additions and 4 deletions

View File

@ -1 +1 @@
2
1

View File

@ -1,3 +1,9 @@
pikabar (1.0.0-101pika18) pika; urgency=medium
* gostat replaced with zigstat
-- ferrreo <harderthanfire@gmail.com> Sat, 01 Oct 2022 14:50:00 +0300
pikabar (1.0.0-101pika17) pika; urgency=medium
* Less of a chonker

Binary file not shown.

View File

@ -17,9 +17,9 @@ Item {
property string memoryUsage: ""
Process {
id: gostatProcess
id: zigstatProcess
running: true
command: [Quickshell.shellRoot + "/programs/gostat", root.updateInterval]
command: [Quickshell.shellRoot + "/programs/zigstat", root.updateInterval]
stdout: SplitParser {
onRead: function (line) {
try {
@ -28,7 +28,7 @@ Item {
root.cpuTemp = data.cputemp + "°C";
root.memoryUsage = data.mem + "G";
} catch (e) {
console.error("Failed to parse gostat output:", e);
console.error("Failed to parse zigstat output:", e);
}
}
}

15
src/zigstat/build.zig Normal file
View File

@ -0,0 +1,15 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "zigstat",
.root_source_file = .{ .cwd_relative = "src/main.zig" },
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
}

72
src/zigstat/build.zig.zon Normal file
View File

@ -0,0 +1,72 @@
.{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = "zigstat",
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.1.0",
// This field is optional.
// This is currently advisory only; Zig does not yet do anything
// with this value.
//.minimum_zig_version = "0.11.0",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `hash`, otherwise you are communicating that you expect to find the old hash at
// // the new URL.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
},
// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

260
src/zigstat/src/main.zig Normal file
View File

@ -0,0 +1,260 @@
const std = @import("std");
const fs = std.fs;
const mem = std.mem;
const time = std.time;
const json = std.json;
const math = std.math;
// Shared static buffers
const SharedBuffers = struct {
path_buf: [256]u8 = undefined,
read_buf: [256]u8 = undefined,
};
// Cache for temperature sensor paths
const TempSensorCache = struct {
initialized: bool = false,
sensor_type: enum { none, coretemp, k10temp } = .none,
path: [256]u8 = undefined,
path_len: usize = 0,
fn init(self: *TempSensorCache, allocator: std.mem.Allocator, bufs: *SharedBuffers) !void {
if (self.initialized) return;
var hwmon_dir = try fs.openDirAbsolute("/sys/class/hwmon", .{ .iterate = true });
defer hwmon_dir.close();
var iter = hwmon_dir.iterate();
while (try iter.next()) |entry| {
const name_path = try std.fmt.bufPrint(&bufs.path_buf, "/sys/class/hwmon/{s}/name", .{entry.name});
const name_content = fs.cwd().readFileAlloc(allocator, name_path, 64) catch continue;
defer allocator.free(name_content);
const trimmed_name = mem.trim(u8, name_content, "\n");
if (mem.eql(u8, trimmed_name, "coretemp")) {
self.sensor_type = .coretemp;
const path_slice = try std.fmt.bufPrint(&self.path, "/sys/class/hwmon/{s}/", .{entry.name});
self.path_len = path_slice.len;
break;
} else if (mem.eql(u8, trimmed_name, "k10temp")) {
self.sensor_type = .k10temp;
const path_slice = try std.fmt.bufPrint(&self.path, "/sys/class/hwmon/{s}/", .{entry.name});
self.path_len = path_slice.len;
break;
}
}
self.initialized = true;
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var shared_bufs = SharedBuffers{};
var temp_cache = TempSensorCache{};
// Default sleep duration of 3 seconds
var sleep_duration: u64 = 3 * time.ns_per_s;
// Parse command line args for custom duration
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len > 1) {
const duration_str = args[1];
if (parseDuration(duration_str)) |duration| {
sleep_duration = duration;
} else |_| {
std.debug.print("Invalid duration format. Using default of 3s.\n", .{});
}
}
const stdout = std.io.getStdOut().writer();
while (true) {
const mem_info = try getMemoryUsage(&shared_bufs);
const cpu_usage = try getCpuUsage(&shared_bufs);
const cpu_temp = try getCpuTemp(allocator, &shared_bufs, &temp_cache);
try stdout.print("{{ \"mem\":\"{d:.1}\", \"cpu\": \"{d:.1}\", \"cputemp\": \"{d}\" }}\n", .{ mem_info, cpu_usage, cpu_temp });
time.sleep(sleep_duration);
}
}
fn getMemoryUsage(bufs: *SharedBuffers) !f64 {
const file = try fs.openFileAbsolute("/proc/meminfo", .{});
defer file.close();
var total: u64 = 0;
var available: u64 = 0;
var found_count: u8 = 0;
// Read the file in one go
const bytes_read = try file.read(&bufs.read_buf);
var start: usize = 0;
var i: usize = 0;
// Manual line parsing to avoid buffered reader overhead
while (i < bytes_read and found_count < 2) {
if (bufs.read_buf[i] == '\n' or i == bytes_read - 1) {
const line = bufs.read_buf[start..i];
if (mem.indexOf(u8, line, "MemTotal:") != null) {
var it = mem.tokenize(u8, line, " kB");
_ = it.next();
total = try std.fmt.parseInt(u64, it.next() orelse "0", 10);
found_count += 1;
} else if (mem.indexOf(u8, line, "MemAvailable:") != null) {
var it = mem.tokenize(u8, line, " kB");
_ = it.next();
available = try std.fmt.parseInt(u64, it.next() orelse "0", 10);
found_count += 1;
}
start = i + 1;
}
i += 1;
}
const byte_to_giga = 1000000.0;
return @as(f64, @floatFromInt(total - available)) / byte_to_giga;
}
fn getCpuUsage(bufs: *SharedBuffers) !f64 {
const first = try getCpuTimes(bufs);
time.sleep(50 * time.ns_per_ms);
const second = try getCpuTimes(bufs);
const idle_diff = second.idle - first.idle;
const total_diff = second.total - first.total;
if (total_diff == 0) return 0;
return (1.0 - @as(f64, @floatFromInt(idle_diff)) / @as(f64, @floatFromInt(total_diff))) * 100.0;
}
const CpuTimes = struct {
idle: u64,
total: u64,
};
fn getCpuTimes(bufs: *SharedBuffers) !CpuTimes {
const file = try fs.openFileAbsolute("/proc/stat", .{});
defer file.close();
const bytes_read = try file.read(&bufs.read_buf);
const first_line = mem.sliceTo(bufs.read_buf[0..bytes_read], '\n');
if (mem.startsWith(u8, first_line, "cpu ")) {
var it = mem.tokenize(u8, first_line, " ");
_ = it.next();
var total: u64 = 0;
var idle: u64 = 0;
var i: u32 = 0;
while (it.next()) |val| {
const num = try std.fmt.parseInt(u64, val, 10);
if (i == 3) idle = num;
if (i == 4) idle += num;
total += num;
i += 1;
if (i >= 5) break;
}
return CpuTimes{ .idle = idle, .total = total };
}
return error.NoCpuLine;
}
fn getCpuTemp(allocator: std.mem.Allocator, bufs: *SharedBuffers, cache: *TempSensorCache) !i32 {
try cache.init(allocator, bufs);
if (cache.sensor_type == .none) return 0;
const path = cache.path[0..cache.path_len];
return switch (cache.sensor_type) {
.coretemp => try getCoreTemp(allocator, path, bufs),
.k10temp => try getK10Temp(allocator, path, bufs),
.none => 0,
};
}
// Update getK10Temp and getCoreTemp to use the shared buffers
fn getK10Temp(allocator: std.mem.Allocator, hwmon_path: []const u8, bufs: *SharedBuffers) !i32 {
var dir = try fs.openDirAbsolute(hwmon_path, .{ .iterate = true });
defer dir.close();
var iter = dir.iterate();
while (try iter.next()) |entry| {
if (mem.startsWith(u8, entry.name, "temp") and mem.endsWith(u8, entry.name, "_label")) {
const label_path = try std.fmt.bufPrint(&bufs.path_buf, "{s}{s}", .{ hwmon_path, entry.name });
const label_content = fs.cwd().readFileAlloc(allocator, label_path, 64) catch continue;
defer allocator.free(label_content);
if (mem.eql(u8, mem.trim(u8, label_content, "\n"), "Tctl")) {
const label_index = mem.indexOf(u8, entry.name, "_label").?;
const input_path = try std.fmt.bufPrint(&bufs.path_buf, "{s}{s}_input", .{ hwmon_path, entry.name[0..label_index] });
const temp_content = fs.cwd().readFileAlloc(allocator, input_path, 64) catch continue;
defer allocator.free(temp_content);
const temp = try std.fmt.parseInt(i32, mem.trim(u8, temp_content, "\n"), 10);
return @divTrunc(temp, 1000);
}
}
}
return 0;
}
fn getCoreTemp(allocator: std.mem.Allocator, hwmon_path: []const u8, bufs: *SharedBuffers) !i32 {
var dir = try fs.openDirAbsolute(hwmon_path, .{ .iterate = true });
defer dir.close();
var total_temp: i32 = 0;
var count: f64 = 0;
var iter = dir.iterate();
while (try iter.next()) |entry| {
if (mem.startsWith(u8, entry.name, "temp") and mem.endsWith(u8, entry.name, "_input")) {
const temp_path = try std.fmt.bufPrint(&bufs.path_buf, "{s}{s}", .{ hwmon_path, entry.name });
const temp_content = fs.cwd().readFileAlloc(allocator, temp_path, 64) catch continue;
defer allocator.free(temp_content);
const temp = try std.fmt.parseInt(i32, mem.trim(u8, temp_content, "\n"), 10);
total_temp += temp;
count += 1;
}
}
if (count > 0) {
return @divTrunc(@as(i32, @intFromFloat(@round(@as(f64, @floatFromInt(total_temp)) / count))), 1000);
}
return 0;
}
/// Parses duration strings like "1s", "500ms", "1m", etc.
/// Returns the duration in nanoseconds
fn parseDuration(str: []const u8) !u64 {
if (str.len < 2) return error.InvalidFormat;
const value_str = str[0 .. str.len - 1];
const unit = str[str.len - 1];
const value = try std.fmt.parseInt(u64, value_str, 10);
return switch (unit) {
's' => value * time.ns_per_s,
'm' => value * time.ns_per_s * 60,
'h' => value * time.ns_per_s * 60 * 60,
else => if (mem.eql(u8, str[str.len - 2 ..], "ms"))
value * time.ns_per_ms
else
error.InvalidUnit,
};
}