From db51987446de358a1b83e41266d815561131847c Mon Sep 17 00:00:00 2001 From: Yorhel Date: Sun, 14 Jul 2024 19:57:32 +0200 Subject: [PATCH] Re-add hard link counting + parent suberror & stats propagation Ended up turning the Links into a doubly-linked list, because the current approach of refreshing a subdirectory makes it more likely to run into problems with the O(n) removal behavior of singly-linked lists. Also found a bug that was present in the old scanning code as well; fixed here and in c41467f2400c9416db49dc699be368e33e68ffa7. --- src/model.zig | 60 ++++++++++++++++++++++++++++++++++++++++++++++++--- src/sink.zig | 29 +++++++++++++++++++++---- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/model.zig b/src/model.zig index 2e70db7..957e462 100644 --- a/src/model.zig +++ b/src/model.zig @@ -102,6 +102,14 @@ pub const Entry = extern struct { else false; } + fn removeLinks(self: *Entry) void { + if (self.dir()) |d| { + var it = d.sub; + while (it) |e| : (it = e.next) e.removeLinks(); + } + if (self.link()) |l| l.removeLink(); + } + fn zeroStatsRec(self: *Entry) void { self.pack.blocks = 0; self.size = 0; @@ -111,7 +119,7 @@ pub const Entry = extern struct { d.pack.err = false; d.pack.suberr = false; var it = d.sub; - while (it) |e| : (it = e.next) zeroStatsRec(e); + while (it) |e| : (it = e.next) e.zeroStatsRec(); } } @@ -120,7 +128,7 @@ pub const Entry = extern struct { // XXX: Does not update the 'suberr' flag of parent directories, make sure // to call updateSubErr() afterwards. pub fn zeroStats(self: *Entry, parent: ?*Dir) void { - // TODO: Uncount nested links. + self.removeLinks(); var it = parent; while (it) |p| : (it = p.parent) { @@ -198,7 +206,8 @@ pub const Dir = extern struct { pub const Link = extern struct { entry: Entry, parent: *Dir align(1) = undefined, - next: *Link align(1) = undefined, // Singly circular linked list of all *Link nodes with the same dev,ino. + next: *Link align(1) = undefined, // circular linked list of all *Link nodes with the same dev,ino. + prev: *Link align(1) = undefined, // dev is inherited from the parent Dir ino: u64 align(1) = undefined, name: [0]u8 = undefined, @@ -211,6 +220,49 @@ pub const Link = extern struct { out.appendSlice(self.entry.name()) catch unreachable; return out.toOwnedSliceSentinel(0) catch unreachable; } + + // Add this link to the inodes map and mark it as 'uncounted'. + pub fn addLink(l: *@This(), nlink: u31) void { + var d = inodes.map.getOrPut(l) catch unreachable; + if (!d.found_existing) { + d.value_ptr.* = .{ .counted = false, .nlink = nlink }; + l.next = l; + l.prev = l; + } else { + inodes.setStats(.{ .key_ptr = d.key_ptr, .value_ptr = d.value_ptr }, false); + // If the nlink counts are not consistent, reset to 0 so we calculate with what we have instead. + if (d.value_ptr.nlink != nlink) + d.value_ptr.nlink = 0; + l.next = d.key_ptr.*; + l.prev = d.key_ptr.*.prev; + l.next.prev = l; + l.prev.next = l; + } + inodes.addUncounted(l); + } + + // Remove this link from the inodes map and remove its stats from parent directories. + fn removeLink(l: *@This()) void { + const entry = inodes.map.getEntry(l) orelse return; + inodes.setStats(entry, false); + if (l.next == l) { + _ = inodes.map.remove(l); + _ = inodes.uncounted.remove(l); + } else { + // XXX: If this link is actually removed from the filesystem, then + // the nlink count of the existing links should be updated to + // reflect that. But we can't do that here, because this function + // is also called before doing a filesystem refresh - in which case + // the nlink count likely won't change. Best we can hope for is + // that a refresh will encounter another link to the same inode and + // trigger an nlink change. + if (entry.key_ptr.* == l) + entry.key_ptr.* = l.next; + inodes.addUncounted(l.next); + l.next.prev = l.prev; + l.prev.next = l.next; + } + } }; // Anything that's not an (indexed) directory or hardlink. Excluded directories are also "Files". @@ -275,6 +327,8 @@ pub const inodes = struct { var uncounted = std.HashMap(*Link, void, HashContext, 80).init(main.allocator); var uncounted_full = true; // start with true for the initial scan + pub var lock = std.Thread.Mutex{}; + const Inode = packed struct { // Whether this Inode is counted towards the parent directories. counted: bool, diff --git a/src/sink.zig b/src/sink.zig index cfa5775..3ef70ef 100644 --- a/src/sink.zig +++ b/src/sink.zig @@ -131,6 +131,7 @@ const MemDir = struct { fn addSpecial(self: *MemDir, alloc: std.mem.Allocator, name: []const u8, t: Special) void { self.dir.items += 1; + if (t == .err) self.dir.pack.suberr = true; const e = self.getEntry(alloc, .file, false, name); e.file().?.pack = switch (t) { @@ -159,7 +160,13 @@ const MemDir = struct { d.pack.dev = model.devices.getId(stat.dev); } if (e.file()) |f| f.pack = .{ .notreg = !stat.dir and !stat.reg }; - if (e.link()) |l| l.ino = stat.ino; // TODO: Add to inodes table + if (e.link()) |l| { + l.parent = self.dir; + l.ino = stat.ino; + model.inodes.lock.lock(); + defer model.inodes.lock.unlock(); + l.addLink(stat.nlink); + } if (e.ext()) |ext| ext.* = stat.ext; return e; } @@ -173,7 +180,7 @@ const MemDir = struct { if (self.entries.count() > 0) { var it = &self.dir.sub; while (it.*) |e| { - if (self.entries.contains(e)) it.* = e.next + if (self.entries.getKey(e) == e) it.* = e.next else it = &e.next; } } @@ -197,7 +204,7 @@ const MemDir = struct { if (self.dir.entry.ext()) |e| { if (e.mtime > p.mtime) e.mtime = p.mtime; } - if (self.suberr or self.dir.pack.err) p.suberr = true; + if (self.suberr or self.dir.pack.suberr or self.dir.pack.err) p.suberr = true; } self.entries.deinit(); } @@ -343,7 +350,21 @@ pub fn createThreads(num: usize) []Thread { // Must be the last thing to call from a source. pub fn done() void { - // TODO: Do hardlink stuff. + if (state.out == .mem) { + state.status = .hlcnt; + main.handleEvent(false, true); + model.inodes.addAllStats(); + const dir = state.out.mem orelse model.root; + var it: ?*model.Dir = dir; + while (it) |p| : (it = p.parent) { + p.updateSubErr(); + if (p != dir) { + p.entry.pack.blocks +|= dir.entry.pack.blocks; + p.entry.size +|= dir.entry.size; + p.items +|= dir.items + 1; + } + } + } state.status = .done; main.allocator.free(state.threads); // Clear the screen when done.