9c82a195ab39

fix store info hang: drop pathological 'nix path-info -S --all' and slow 'du -sh /nix/store'; sum narSize from path-info --json (timeout-wrapped) for count + total

time
2026-06-04 23:05 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
fast store info
parent
15cc71755519
1 file src/exec.zig+42 −21
src/exec.zig+42 −21
@@ -361,40 +361,61 @@361361 }362362 363363 pub fn storeInfo(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) !types.StoreInfo {364-    const pi = try capture(gpa, io, machine, &.{ "nix", "path-info", "-S", "--all", "--json" });364+    // `nix path-info -S --all` computes the CLOSURE size of every path — O(paths^2)365+    // graph work that takes minutes on a real store (59k+ paths) and hangs the366+    // command. We don't need closures: `--all --json` (no -S) carries each path's367+    // own narSize, so one fast pass gives the path count and the summed store size.368+    // `du -sh /nix/store` is just as slow on a big store, so it's gone too.369+    // timeout-wrapped so a pathological store degrades to "?" instead of hanging.370+    const pi = try capture(gpa, io, machine, &.{ "sh", "-c", "timeout 90 nix path-info --all --json 2>/dev/null" });365371     defer gpa.free(pi.stdout);366372     defer gpa.free(pi.stderr);367373 368-    var live: u64 = 0;369-    if (pi.exit_code == 0) {370-        live = countJsonPaths(pi.stdout);371-    }374+    const scan = scanStoreJson(pi.stdout);375+    const total_owned = try humanSize(gpa, scan.total_bytes);372376 373-    const du = try capture(gpa, io, machine, &.{ "du", "-sh", "/nix/store" });374-    defer gpa.free(du.stderr);375-    const total = blk: {376-        const line = std.mem.trim(u8, du.stdout, " \t\n\r");377-        const tab = std.mem.indexOfScalar(u8, line, '\t') orelse break :blk "?";378-        break :blk line[0..tab];379-    };380-    const total_owned = try gpa.dupe(u8, total);381-    gpa.free(du.stdout);382-383377     return .{384378         .total_size = total_owned,385-        .live_paths = live,379+        .live_paths = scan.count,386380         .reclaimable_paths = 0,387381     };388382 }389383 390-fn countJsonPaths(json_str: []const u8) u64 {391-    // quick count: number of top-level keys384+const StoreScan = struct { count: u64, total_bytes: u64 };385+386+// One linear pass over `nix path-info --json`: each path object has exactly one387+// "narSize": field, so counting them gives the true path count and summing the388+// values gives the total store size — no per-path closure walk.389+fn scanStoreJson(json_str: []const u8) StoreScan {390+    const needle = "\"narSize\":";392391     var count: u64 = 0;392+    var total: u64 = 0;393393     var i: usize = 0;394-    while (i < json_str.len) : (i += 1) {395-        if (json_str[i] == '"') count += 1;394+    while (std.mem.indexOfPos(u8, json_str, i, needle)) |pos| {395+        var j = pos + needle.len;396+        while (j < json_str.len and json_str[j] == ' ') : (j += 1) {}397+        var val: u64 = 0;398+        var any = false;399+        while (j < json_str.len and json_str[j] >= '0' and json_str[j] <= '9') : (j += 1) {400+            val = val * 10 + (json_str[j] - '0');401+            any = true;402+        }403+        if (any) {404+            count += 1;405+            total += val;406+        }407+        i = j;396408     }397-    return count / 2; // each path has 2 quotes409+    return .{ .count = count, .total_bytes = total };410+}411+412+fn humanSize(gpa: std.mem.Allocator, bytes: u64) ![]u8 {413+    const units = [_][]const u8{ "B", "KB", "MB", "GB", "TB" };414+    var val: f64 = @floatFromInt(bytes);415+    var unit: usize = 0;416+    while (val >= 1024.0 and unit + 1 < units.len) : (unit += 1) val /= 1024.0;417+    if (unit == 0) return std.fmt.allocPrint(gpa, "{d} {s}", .{ bytes, units[0] });418+    return std.fmt.allocPrint(gpa, "{d:.1} {s}", .{ val, units[unit] });398419 }399420 400421 pub fn storePath(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine, attr: []const u8) ![]u8 {

view file