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 c41467f240.
This commit is contained in:
Yorhel 2024-07-14 19:57:32 +02:00
parent cc12c90dbc
commit db51987446
2 changed files with 82 additions and 7 deletions

View file

@ -102,6 +102,14 @@ pub const Entry = extern struct {
else false; 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 { fn zeroStatsRec(self: *Entry) void {
self.pack.blocks = 0; self.pack.blocks = 0;
self.size = 0; self.size = 0;
@ -111,7 +119,7 @@ pub const Entry = extern struct {
d.pack.err = false; d.pack.err = false;
d.pack.suberr = false; d.pack.suberr = false;
var it = d.sub; 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 // XXX: Does not update the 'suberr' flag of parent directories, make sure
// to call updateSubErr() afterwards. // to call updateSubErr() afterwards.
pub fn zeroStats(self: *Entry, parent: ?*Dir) void { pub fn zeroStats(self: *Entry, parent: ?*Dir) void {
// TODO: Uncount nested links. self.removeLinks();
var it = parent; var it = parent;
while (it) |p| : (it = p.parent) { while (it) |p| : (it = p.parent) {
@ -198,7 +206,8 @@ pub const Dir = extern struct {
pub const Link = extern struct { pub const Link = extern struct {
entry: Entry, entry: Entry,
parent: *Dir align(1) = undefined, 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 // dev is inherited from the parent Dir
ino: u64 align(1) = undefined, ino: u64 align(1) = undefined,
name: [0]u8 = undefined, name: [0]u8 = undefined,
@ -211,6 +220,49 @@ pub const Link = extern struct {
out.appendSlice(self.entry.name()) catch unreachable; out.appendSlice(self.entry.name()) catch unreachable;
return out.toOwnedSliceSentinel(0) 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". // 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 = std.HashMap(*Link, void, HashContext, 80).init(main.allocator);
var uncounted_full = true; // start with true for the initial scan var uncounted_full = true; // start with true for the initial scan
pub var lock = std.Thread.Mutex{};
const Inode = packed struct { const Inode = packed struct {
// Whether this Inode is counted towards the parent directories. // Whether this Inode is counted towards the parent directories.
counted: bool, counted: bool,

View file

@ -131,6 +131,7 @@ const MemDir = struct {
fn addSpecial(self: *MemDir, alloc: std.mem.Allocator, name: []const u8, t: Special) void { fn addSpecial(self: *MemDir, alloc: std.mem.Allocator, name: []const u8, t: Special) void {
self.dir.items += 1; self.dir.items += 1;
if (t == .err) self.dir.pack.suberr = true;
const e = self.getEntry(alloc, .file, false, name); const e = self.getEntry(alloc, .file, false, name);
e.file().?.pack = switch (t) { e.file().?.pack = switch (t) {
@ -159,7 +160,13 @@ const MemDir = struct {
d.pack.dev = model.devices.getId(stat.dev); d.pack.dev = model.devices.getId(stat.dev);
} }
if (e.file()) |f| f.pack = .{ .notreg = !stat.dir and !stat.reg }; 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; if (e.ext()) |ext| ext.* = stat.ext;
return e; return e;
} }
@ -173,7 +180,7 @@ const MemDir = struct {
if (self.entries.count() > 0) { if (self.entries.count() > 0) {
var it = &self.dir.sub; var it = &self.dir.sub;
while (it.*) |e| { while (it.*) |e| {
if (self.entries.contains(e)) it.* = e.next if (self.entries.getKey(e) == e) it.* = e.next
else it = &e.next; else it = &e.next;
} }
} }
@ -197,7 +204,7 @@ const MemDir = struct {
if (self.dir.entry.ext()) |e| { if (self.dir.entry.ext()) |e| {
if (e.mtime > p.mtime) e.mtime = p.mtime; 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(); self.entries.deinit();
} }
@ -343,7 +350,21 @@ pub fn createThreads(num: usize) []Thread {
// Must be the last thing to call from a source. // Must be the last thing to call from a source.
pub fn done() void { 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; state.status = .done;
main.allocator.free(state.threads); main.allocator.free(state.threads);
// Clear the screen when done. // Clear the screen when done.