xff v0.8.0

xff - Parallel Traversal Design

Detailed spec for parallel directory traversal and --sort (issue #43).
Refines design.md section"Cross-cutting concerns" (Determinism). Where the two
differ, this document is authoritative for traversal/ordering specifically.
Status: Implemented / 2026-06-27 / Org: MBO Works

Purpose

The directory walk is the hot path and it is IO-bound: most time is spent
blocked in readdir/lstat, not in CPU. Running it across several workers
hides that latency and is the single biggest performance lever for xff,
especially on deep trees and high-latency (network) filesystems.

Parallelism must not cost correctness. Three things are trivially correct in the
current sequential walk and must stay correct under concurrency: deterministic
output (when asked for), the -prune/-quit/-depth control semantics, and the
exit-code model. The VFS layer is already contractually safe to call from many
threads (vfs::FileSystem), so the foundation is ready.

v1 scope (what shipped first)

The first implementation (engine PR for #43) takes three deliberate simplifications
of the design below; each has a noted follow-up:

Architecture

A bounded directory-read worker pool with a single coordinator:

--sort modes

Six modes, separating root order from order below each root. name stays an alias for dir
(back-compat with the current SortOrder::kName).

--sort Root order Order below each root Result buffer
none command line filesystem order none
dir command line sorted child blocks, then their descendants none
subtree command line sorted files, then contiguous sorted subtrees none
tree command line sorted depth-first none
roots sorted filesystem order none
global sorted sorted depth-first none

All modes materialize the current directory's stat'd listing. With -j > 1, the
walker may additionally hold listings read ahead for its child directories. That
is traversal read-ahead, not matched-result buffering, and it does not change the
coordinator's visit order. Result formats have a separate buffering policy:
plain/nul/jsonl/csv/tsv stream, aligned/Markdown use --buffer, and tree-format
output builds its complete display tree. --sort=score is also separate: it
buffers and ranks the implicit listing after evaluation, while side-effecting
actions still occur in traversal order.

Parallelism control

A single knob, -j N (long form --jobs), configures two independent limits:
N directory-read workers and at most N outstanding semicolon-form
-exec/-execdir children. Directory reads and child processes can overlap, so
this is not one shared N-operation budget. -j 1 makes both parts synchronous.
-j all (--jobs=all) uses every detected core (hardware_concurrency()) for
each limit, regardless of the active mode's default.

Under -j > 1 the serial -exec ... ; / -execdir ... ; form launches its child
on a bounded runner (at most N outstanding) instead of running it synchronously.
Because the child's exit status is not yet known when the action returns, the action
reports success on launch, so its truth value cannot gate a predicate to its right
(e.g. -exec a \; -exec b \; or -exec test \; -print). This matches find's exit
model: find's ; form is a predicate whose nonzero exit makes only the action false
and never raises find's own exit status (verified against BSD and GNU find), so
nothing observable is lost at the exit-code level - the children are still reaped at
the end-of-walk drain. Use -j 1 for find's strict synchronous, status-gating
semantics. The + batch forms are unaffected: they always accumulate and flush once
at end-of-walk, and a nonzero exit there does raise the exit status, as in find.

When -j is omitted the default is style-scoped. Invocation name and the
last explicit --config=find|xff|rg selector choose the style (see
design-config.md):

Style Workers (no -j) Default --sort
find all cores none
xff max(1, min(cores-1, 15)) dir
rg all cores none

Concurrency correctness

ThreadSanitizer

As soon as the walk is multithreaded we add a TSan run. TSan is mutually
exclusive with ASan, so it is a separate --config=tsan in .bazelrc
(-fsanitize=thread copt/linkopt, TSAN_OPTIONS=halt_on_error=1, the symbolizer,
mirroring the asan block) and its own tsan CI job wired into
the done gate. It lands in the same PR that introduces threads (it is a no-op on
single-threaded code). The asan config also runs UBSan as of #138.

Implementation history

The traversal was delivered incrementally:

  1. Worker pool + --sort=none - parallel walk behind the existing sink, plus
    the tsan config and CI job (threads arrive here, so TSan does too).
    The sequential walk stays available via -j 1.
  2. Ordering layer + --sort=dir - generalize SortOrder::kName to kDir
    over the parallel walk (per-directory sorted listing blocks).
  3. --sort=subtree - files-first contiguous-subtree traversal (kSubtree).
  4. --sort=tree - sorted depth-first traversal within each root (kTree).
  5. -j / --jobs + style-scoped defaults - the flag and the per-style
    worker-count and default---sort wiring.
  6. --sort=roots|global - stable root ordering, either alone or combined
    with tree ordering below each root.