~alva/scratchy

edb328ae7526e7073005857b5e068f12491629b1 — ugla 7 months ago
Initial commit
5 files changed, 191 insertions(+), 0 deletions(-)

A README.md
A build.zig
A src/c.zig
A src/linux.zig
A src/main.zig
A  => README.md +8 -0
@@ 1,8 @@
# scratchy
A program that makes noise on disk activity.
Currently Linux-only, but organized so that other platforms can be added.

build/run:
```bash
zig build run
```
\ No newline at end of file

A  => build.zig +40 -0
@@ 1,40 @@
const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    // Standard target options allows the person running `zig build` to choose
    // what target to build for. Here we do not override the defaults, which
    // means any target is allowed, and the default is native. Other options
    // for restricting supported target set are available.
    const target = b.standardTargetOptions(.{});

    // Standard release options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

    const exe = b.addExecutable("scratchy", "src/main.zig");
    exe.setTarget(target);
    exe.setBuildMode(mode);
    exe.linkLibC();

    if (target.getOsTag() == .linux) {
        exe.linkSystemLibrary("pulse-simple");
    }

    exe.install();

    const run_cmd = exe.run();
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    const exe_tests = b.addTest("src/main.zig");
    exe_tests.setTarget(target);
    exe_tests.setBuildMode(mode);

    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&exe_tests.step);
}

A  => src/c.zig +5 -0
@@ 1,5 @@
const pulse = @cImport({
    @cInclude("pulse/simple.h");
});

pub usingnamespace pulse;

A  => src/linux.zig +127 -0
@@ 1,127 @@
const std = @import("std");
const sep_str = std.fs.path.sep_str;
const disk_stats_path = sep_str ++ "proc" ++ sep_str ++ "diskstats";
const c = @import("c.zig");

const DiskStats = struct {
    reads_completed: u64,
    reads_merged: u64,
    sectors_read: u64,
    ms_spent_reading: u64,
    writes_completed: u64,
    sectors_written: u64,
    ms_spent_writing: u64,
    ios_in_progress: u64,
    ms_spent_doing_io: u64,
    weighted_ms_spent_doing_io: u64,
};

const N_DISKS_SHOULD_BE_ENOUGH_FOR_ANYONE = 64;
const DiskStatsArray = std.BoundedArray(DiskStats, N_DISKS_SHOULD_BE_ENOUGH_FOR_ANYONE);

const UPDATE_FREQ_NS = std.time.ns_per_s / 10;

const Total = struct {
    reads: u64 = 0,
    writes: u64 = 0,
};

pub fn crackle() !void {
    var buf: [4096]u8 = undefined;
    var total = Total{};
    var file = try std.fs.cwd().openFile(disk_stats_path, .{});
    defer file.close();

    var default_rando = std.rand.DefaultPrng.init(666.0);
    var prng = default_rando.random();

    while (true) {
        std.os.nanosleep(0, UPDATE_FREQ_NS);
        const nr = try file.readAll(&buf);
        var read = buf[0..nr];
        var all_stats = try DiskStatsArray.init(0);
        var lines = std.mem.split(u8, read, "\n");

        while (lines.next()) |line| {
            if (line.len == 0)
                break;

            try all_stats.append(try crackleLine(line));
        }

        try processStats(&all_stats, &total, &prng);
        try all_stats.resize(0);
        try file.seekTo(0);
    }
}

fn crackleLine(line: []const u8) !DiskStats {
    var stats: DiskStats = std.mem.zeroes(DiskStats);

    var it = std.mem.tokenize(u8, line, " ");

    _ = it.next(); // major number
    _ = it.next(); // minor number
    _ = it.next(); // device name

    inline for (comptime std.meta.fieldNames(DiskStats)) |fname| {
        @field(stats, fname) = try std.fmt.parseInt(u64, it.next() orelse unreachable, 10);
    }

    return stats;
}

fn processStats(stats: *DiskStatsArray, total: *Total, prng: anytype) !void {
    var writes_total: u64 = 0;
    var reads_total: u64 = 0;

    for (stats.slice()) |s| {
        writes_total += s.writes_completed;
        reads_total += s.reads_completed;
    }

    const wd = writes_total -| total.writes -| 8;
    const rd = reads_total -| total.reads;

    if (0 < wd + rd) {
        var crap: [2048]f32 = undefined;
        var ss = c.pa_sample_spec{
            .format = c.PA_SAMPLE_FLOAT32NE,
            .rate = 44100,
            .channels = 1,
        };
        // this can only play once, very efficient!
        var ps = c.pa_simple_new(
            null,
            "bongos",
            c.PA_STREAM_PLAYBACK,
            null,
            "playback",
            &ss,
            null,
            null,
            null,
        ) orelse return error.Nope;
        defer c.pa_simple_free(ps);

        generateNoise(wd + rd, &crap, prng);
        var ret: c_int = undefined;
        ret = c.pa_simple_write(ps, &crap, crap.len, null);
        if (ret < 0)
            std.debug.print("pulse write error: {d}\n", .{ret});
        ret = c.pa_simple_drain(ps, null);
        if (ret < 0)
            std.debug.print("pulse drain error: {d}\n", .{ret});
        std.debug.print("r/w: {d} {d}\n", .{ rd, wd });
    }

    total.reads = reads_total;
    total.writes = writes_total;
}

fn generateNoise(amount: u64, buf: *[2048]f32, prng: anytype) void {
    var i: usize = 0;
    while (i < buf.len and i < amount) : (i += 1) {
        buf[i] = prng.float(f32);
    }
}
\ No newline at end of file

A  => src/main.zig +11 -0
@@ 1,11 @@
const std = @import("std");
const builtin = @import("builtin");

pub fn main() !void {
    try switch (builtin.os.tag) {
        .linux => @import("linux.zig").crackle(),
        // here you can add other implementations
        //.windows => @import("windows.zig").crackle(),
        else => @compileError("unsupported OS: " ++ @tagName(builtin.os.tag)),
    };
}