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;
}
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,

View file

@ -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.