sonar

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

walk.odin (5904B)


      1 /*
      2 Package walk enumerates directories through core:os.
      3 
      4 It is the reader that works anywhere: any filesystem, any platform, no privilege. It
      5 is also much slower than reading a filesystem's own metadata, because it pays a
      6 syscall per directory and learns nothing the OS does not hand it. That makes it the
      7 fallback rather than the default, and the yardstick a specialised reader is measured
      8 against.
      9 
     10 A directory listing is already the shape of the tree, so it writes straight into it.
     11 Reading one directory is the only part that differs by platform, and it is the part
     12 worth specialising: `read_dir` is portable through core:os everywhere but Linux,
     13 which asks the kernel directly.
     14 */
     15 package walk
     16 
     17 import "core:mem"
     18 import "core:strings"
     19 
     20 import "jm:flow"
     21 import "../scan"
     22 
     23 Config :: struct {
     24 	workers: int, // 0 selects DEFAULT_WORKERS
     25 	follow:  bool, // descend through symlinks and junctions; loops are not detected
     26 }
     27 
     28 // Directory reads are latency bound and hold no core while they wait, so more
     29 // workers than cores is right here in a way it was not for the MFT reader.
     30 DEFAULT_WORKERS :: 8
     31 
     32 Error :: enum {
     33 	None,
     34 	Out_Of_Memory,
     35 	Cancelled,
     36 }
     37 
     38 /*
     39 Read everything under `root` into the tree.
     40 
     41 Every directory below it is opened, each entry it holds becomes a node, and each
     42 directory among them is read in turn. A worker takes the next directory the moment
     43 one is known rather than waiting for a level to finish, so a tree that is wide near
     44 the root and one that is deep in a single branch both keep them busy.
     45 
     46 Returns once the tree holds everything under `root`; Cancelled if the scan was called
     47 off part way, and Out_Of_Memory if the tree ran out of room, in which case what is
     48 there is short rather than wrong.
     49 */
     50 read :: proc(root: string, t: ^scan.Tree, cfg := Config{}) -> Error {
     51 	allocator := context.allocator
     52 
     53 	w := scan.writer(t, 0)
     54 	first, claim_err := scan.claim(&w, 1)
     55 	if claim_err != nil {
     56 		return .Out_Of_Memory
     57 	}
     58 	n := scan.node(t, first)
     59 	n.parent = first // a root holds itself
     60 	n.name = scan.intern(&w, root)
     61 	n.flags = {.Used, .Settled, .Directory}
     62 	t.root = first
     63 
     64 	workers := cfg.workers
     65 	if workers <= 0 {
     66 		workers = DEFAULT_WORKERS
     67 	}
     68 	// One writer per worker, and a writer needs an arena of its own, so the tree
     69 	// caps how wide this can run.
     70 	workers = min(workers, len(t.arenas))
     71 
     72 	states := make([]Worker, workers, allocator)
     73 	defer delete(states, allocator)
     74 	for i in 0 ..< workers {
     75 		states[i] = Worker {
     76 			writer    = scan.writer(t, i),
     77 			found     = make([dynamic]Dir, allocator),
     78 			follow    = cfg.follow,
     79 			allocator = allocator,
     80 		}
     81 	}
     82 	defer for &s in states {
     83 		delete(s.found)
     84 	}
     85 
     86 	seed := []Dir{{index = first, path = strings.clone(root, allocator)}}
     87 	flow.manage(seed, states, walk_dir, hand_over, drop)
     88 
     89 	for &s in states {
     90 		if s.err != nil {
     91 			return s.err
     92 		}
     93 	}
     94 	if scan.cancelled(t) {
     95 		return .Cancelled
     96 	}
     97 	return .None
     98 }
     99 
    100 // Move what a worker found into the queue, and judge whatever it could not read.
    101 // Serialised by `manage`, so this is the one place the set of directories still to
    102 // walk is touched.
    103 @(private)
    104 hand_over :: proc(d: Dir, ok: bool, w: ^Worker, queue: ^[dynamic]Dir) -> bool {
    105 	for found in w.found {
    106 		append(queue, found)
    107 	}
    108 	clear(&w.found)
    109 	// A directory that would not open is worth nothing and is not worth retrying,
    110 	// but it is no reason to abandon the rest of the volume. Running out of room
    111 	// for the tree is, since everything after it would be missing silently.
    112 	return w.err == nil && !scan.cancelled(w.writer.tree)
    113 }
    114 
    115 // A directory a stopped scan never reached. `walk_dir` frees what it is handed; this
    116 // is the other half, since cancelling mid-volume otherwise strands a path per
    117 // directory found but not yet walked. Runs on the thread that called `scan`, and
    118 // paths are cloned from that context, so the two allocators are the same one.
    119 @(private)
    120 drop :: proc(d: Dir) {
    121 	delete(d.path, context.allocator)
    122 }
    123 
    124 // A directory waiting to be read, and the node already standing for it.
    125 @(private)
    126 Dir :: struct {
    127 	index: u32,
    128 	path:  string, // owned; freed once walked
    129 }
    130 
    131 @(private)
    132 Worker :: struct {
    133 	writer:    scan.Writer,
    134 	found:     [dynamic]Dir,
    135 	follow:    bool,
    136 	err:       Error, // why this worker gave up, which only the manager acts on
    137 	allocator: mem.Allocator,
    138 }
    139 
    140 @(private)
    141 walk_dir :: proc(d: Dir, w: ^Worker) -> bool {
    142 	defer delete(d.path, w.allocator)
    143 	if scan.cancelled(w.writer.tree) {
    144 		return false
    145 	}
    146 	nodes, bytes, ok := read_dir(d, w)
    147 	scan.progress(w.writer.tree, nodes, bytes)
    148 	return ok
    149 }
    150 
    151 // One entry as a directory read describes it, before it becomes a node. `disk` is
    152 // whatever the platform could learn about allocation; every reader fills it, since
    153 // only the reader knows how good its own answer is.
    154 @(private)
    155 Entry :: struct {
    156 	name:      string,
    157 	size:      u64,
    158 	disk:      u64,
    159 	directory: bool,
    160 	symlink:   bool,
    161 }
    162 
    163 /*
    164 Record one entry against the directory holding it.
    165 
    166 `descend` marks an entry to walk in turn: a directory, or a symlink when the caller
    167 asked for those to be followed. Shared so that what a node means cannot drift between
    168 one platform's reader and another's.
    169 */
    170 @(private)
    171 record :: proc(w: ^Worker, parent: u32, e: Entry) -> (index: u32, descend: bool, ok: bool) {
    172 	slot, err := scan.claim(&w.writer, 1)
    173 	if err != nil {
    174 		w.err = .Out_Of_Memory
    175 		return 0, false, false
    176 	}
    177 	index = slot
    178 	n := scan.node(w.writer.tree, index)
    179 	n.parent = parent
    180 	n.name = scan.intern(&w.writer, e.name)
    181 	n.size = e.size
    182 
    183 	// A walker reaches a child through its parent, so the link is never in doubt.
    184 	flags := scan.Node_Flags{.Used, .Settled}
    185 	switch {
    186 	case e.directory:
    187 		flags |= {.Directory}
    188 		descend = true
    189 	case e.symlink:
    190 		flags |= {.Reparse}
    191 		descend = w.follow
    192 	case:
    193 		n.disk = e.disk
    194 	}
    195 	// Last, so a builder watching this tree never sees a node before its size.
    196 	scan.node_publish(n, flags)
    197 	return index, descend, true
    198 }