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
↓ nina-9c82a195.tar.gzatlas at this save
files changed1 file
src/exec.zig+42 −21src/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 {