sonar

Scan files at memory bandwidth speed.
Log | Files | Refs

commit b612a0a8d351cbd4631354ede35479e696a8d266
parent f58720b55051a177b5cdfd15d835a37e170faeb0
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Sun, 20 Sep 2026 09:45:31 -0300

walk: read a directory behind a seam

Enumerating one directory is the only part of walking that differs by platform,
and it was welded to the part that does not: claiming a slot, interning the name,
setting the flags. Nothing could be specialised without duplicating the meaning
of a node alongside it.

`read_dir` is now the whole of the platform's job and `record` the whole of the
tree's, so a second reader cannot quietly disagree about what a node is. No
behaviour changes here: over /usr, /usr/share/doc and /home/jfm the output is
identical line for line apart from the time it reports.

Diffstat:
Awalk/read.odin | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwalk/walk.odin | 102++++++++++++++++++++++++++++++++++++-------------------------------------------
2 files changed, 116 insertions(+), 56 deletions(-)

diff --git a/walk/read.odin b/walk/read.odin @@ -0,0 +1,70 @@ +package walk + +import "core:os" +import "core:strings" + +/* +Read one directory through core:os. + +The portable reader: whatever the platform, core:os describes every entry and this +records what the tree keeps. It asks for a great deal more than that per entry, which +is the cost of not knowing which platform it is on. +*/ +@(private) +read_dir :: proc(d: Dir, w: ^Worker) -> (nodes, bytes: u64, ok: bool) { + f, open_err := os.open(d.path) + if open_err != nil { + return 0, 0, false + } + defer os.close(f) + + it := os.read_directory_iterator_create(f) + defer os.read_directory_iterator_destroy(&it) + + for info in os.read_directory_iterator(&it) { + if _, err := os.read_directory_iterator_error(&it); err != nil { + continue + } + e := Entry { + name = info.name, + size = u64(max(info.size, 0)), + directory = info.type == .Directory, + symlink = info.type == .Symlink, + } + if !e.directory && !e.symlink { + // core:os reports the logical length and nothing about allocation, so the + // on-disk figure is the best estimate available here: the size rounded up + // to a block. + e.disk = round_up(e.size, BLOCK_ESTIMATE) + } + index, descend, recorded := record(w, d.index, e) + if !recorded { + return nodes, bytes, false + } + if descend { + path, clone_err := strings.clone(info.fullpath, w.allocator) + if clone_err != nil { + w.err = .Out_Of_Memory + return nodes, bytes, false + } + append(&w.found, Dir{index = index, path = path}) + } + nodes += 1 + bytes += e.disk + } + return nodes, bytes, true +} + +// Without filesystem-specific knowledge the allocation unit is a guess, and four +// kilobytes is the common one. It makes small files cost something rather than +// nothing, which matters more than being exactly right. +@(private) +BLOCK_ESTIMATE :: 4096 + +@(private) +round_up :: proc(v, unit: u64) -> u64 { + if v == 0 { + return 0 + } + return (v + unit - 1) / unit * unit +} diff --git a/walk/walk.odin b/walk/walk.odin @@ -8,11 +8,13 @@ fallback rather than the default, and the yardstick a specialised reader is meas against. A directory listing is already the shape of the tree, so it writes straight into it. +Reading one directory is the only part that differs by platform, and it is the part +worth specialising: `read_dir` is portable through core:os everywhere but Linux, +which asks the kernel directly. */ package walk import "core:mem" -import "core:os" import "core:strings" import "../flow" @@ -137,66 +139,54 @@ walk_dir :: proc(d: Dir, w: ^Worker) -> bool { if scan.cancelled(w.writer.tree) { return false } - f, open_err := os.open(d.path) - if open_err != nil { - return false - } - defer os.close(f) - - it := os.read_directory_iterator_create(f) - defer os.read_directory_iterator_destroy(&it) - - nodes: u64 - bytes: u64 - for info in os.read_directory_iterator(&it) { - if _, err := os.read_directory_iterator_error(&it); err != nil { - continue - } - index, claim_err := scan.claim(&w.writer, 1) - if claim_err != nil { - w.err = .Out_Of_Memory - return false - } - n := scan.node(w.writer.tree, index) - n.parent = d.index - n.name = scan.intern(&w.writer, info.name) - n.size = u64(max(info.size, 0)) - // A walker reaches a child through its parent, so the link is never in doubt. - n.flags = {.Used, .Settled} - - switch info.type { - case .Directory: - n.flags |= {.Directory} - append(&w.found, Dir{index = index, path = strings.clone(info.fullpath, w.allocator)}) - case .Symlink: - n.flags |= {.Reparse} - if w.follow { - append(&w.found, Dir{index = index, path = strings.clone(info.fullpath, w.allocator)}) - } - case .Undetermined, .Regular, .Named_Pipe, .Socket, .Character_Device, .Block_Device: - // core:os reports the logical length and nothing about allocation, so - // the on-disk figure is the best estimate available here: the size - // rounded up to a block. A reader that talks to the filesystem directly - // knows the real answer; this one cannot. - n.disk = round_up(n.size, BLOCK_ESTIMATE) - } - nodes += 1 - bytes += n.disk - } + nodes, bytes, ok := read_dir(d, w) scan.progress(w.writer.tree, nodes, bytes) - return true + return ok } -// Without filesystem-specific knowledge the allocation unit is a guess, and four -// kilobytes is the common one. It makes small files cost something rather than -// nothing, which matters more than being exactly right. +// One entry as a directory read describes it, before it becomes a node. `disk` is +// whatever the platform could learn about allocation; every reader fills it, since +// only the reader knows how good its own answer is. @(private) -BLOCK_ESTIMATE :: 4096 +Entry :: struct { + name: string, + size: u64, + disk: u64, + directory: bool, + symlink: bool, +} + +/* +Record one entry against the directory holding it. +`descend` marks an entry to walk in turn: a directory, or a symlink when the caller +asked for those to be followed. Shared so that what a node means cannot drift between +one platform's reader and another's. +*/ @(private) -round_up :: proc(v, unit: u64) -> u64 { - if v == 0 { - return 0 +record :: proc(w: ^Worker, parent: u32, e: Entry) -> (index: u32, descend: bool, ok: bool) { + slot, err := scan.claim(&w.writer, 1) + if err != nil { + w.err = .Out_Of_Memory + return 0, false, false + } + index = slot + n := scan.node(w.writer.tree, index) + n.parent = parent + n.name = scan.intern(&w.writer, e.name) + n.size = e.size + // A walker reaches a child through its parent, so the link is never in doubt. + n.flags = {.Used, .Settled} + + switch { + case e.directory: + n.flags |= {.Directory} + descend = true + case e.symlink: + n.flags |= {.Reparse} + descend = w.follow + case: + n.disk = e.disk } - return (v + unit - 1) / unit * unit + return index, descend, true }