Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion XFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ See also: [Configuration](#topic-config), [Archives](#topic-archive), [Output](#
<a id="flag-jobs"></a>

- `-j N, -j=N, --jobs=N|all` - directory-read, eligible content-match, comparison and -exec workers _(global, find, xff, rg)_
`N` is a positive integer; use `-j 4`, `-j=4`, or `--jobs=4`. The conventional attached short form `-j4` is also accepted. Every form accepts `all` in place of `N`. `-j 1` makes directory reads and `-exec ... ;` synchronous. At larger values, xff reads directories using one lazily started pool of up to `N` worker threads, reused for directory reads, required eager metadata and eligible content matching. Workers drain batches of independent work; small workloads stay on the coordinator. Lazy and metadata-free listings do not start stat workers. Independent content predicates and rg searches match in bounded parallel batches, with output kept in traversal order. Small initial batches, archive members, stateful expressions stay on the coordinator. Metadata-consuming output does not itself prevent eligible content filters from running in workers; reductions and rendering remain serial. Independent regular-file comparison pairs can also use bounded workers, preserving relative-path output order. Batches of only small or metadata-only comparisons stay inline; archive and owned filesystem sources stay serial. Xff may independently keep up to `N` semicolon-form `-exec` / `-execdir` children outstanding. Reads and children can overlap; `N` is not one shared operation budget. The children's truth value is therefore success on launch. The `... +` batch forms still run once after the walk and propagate a failing exit status. With no flag, xff uses one fewer than the detected cores, capped at 15 and floored at 1; find and rg modes use every detected core. `all` always means every detected core.
`N` is a positive integer; use `-j 4`, `-j=4`, or `--jobs=4`. The conventional attached short form `-j4` is also accepted. Every form accepts `all` in place of `N`. `-j 1` makes directory reads and `-exec ... ;` synchronous. At larger values, xff reads directories using one command-owned, lazily started pool of up to `N-1` background workers plus the calling coordinator, reused for directory reads, required eager metadata, eligible content matching and file comparison. Comparison trees are coordinated sequentially within that same budget. The caller participates in eligible content and comparison work. Workers drain batches of independent work; small workloads stay on the coordinator. Lazy and metadata-free listings do not start stat workers. Independent content predicates and rg searches match in bounded parallel batches, with output kept in traversal order. Small initial batches, archive members, stateful expressions stay on the coordinator. Metadata-consuming output does not itself prevent eligible content filters from running in workers; reductions and rendering remain serial. Independent regular-file comparison pairs can also use bounded workers, preserving relative-path output order. Batches of only small or metadata-only comparisons stay inline; archive and owned filesystem sources stay serial. Xff may independently keep up to `N` semicolon-form `-exec` / `-execdir` children outstanding. Reads and children can overlap; `N` is not one shared operation budget. The children's truth value is therefore success on launch. The `... +` batch forms still run once after the walk and propagate a failing exit status. With no flag, xff uses one fewer than the detected cores, capped at 15 and floored at 1; find and rg modes use every detected core. `all` always means every detected core.
See also: [Output](#topic-output), [Statistics](#topic-stats)

<a id="flag-sort"></a>
Expand Down
25 changes: 21 additions & 4 deletions docs/benchmark-correctness-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,20 +275,37 @@ available local Linux artifacts; they do not verify expired hosted artifacts.
### Worker scheduling experiment

Linux `kernel.perf_event_paranoid` is now `0`, and native `perf` hardware counters
are available. Startup-boundary profiles identify worker creation and teardown,
Abseil mutex slow paths and scheduling overhead. Keeping directory reads serial
are available. Startup-boundary profiles include worker creation and teardown,
Abseil mutex slow paths and scheduling overhead. The later symbolized controls
retained in [PR #995](https://github.com/mboworks/xff/pull/995) identify the
Linux/x86 one-time Abseil TSC frequency calibration (1 ms and 2 ms sleeps) on
the first queue mutex path as the dominant fixed cliff. They do not attribute
those sleeps to each thread's creation. Keeping directory reads serial
removes the small-work cliff but loses useful concurrency on large broad trees.
[PR #991](https://github.com/mboworks/xff/pull/991) admits directory reads only
when at least 64 independent sibling directories are available. Its hosted Linux
and macOS tests, sanitizers, all three benchmark shards on each platform and
combined benchmark comparison pass.

The next experiment shares one run-owned executor across directory read-ahead,
The initial shared-executor experiment shares one traversal-owned executor across directory read-ahead,
eager stat work and eligible content matching. Each admitted batch submits at
most one drain job per useful worker. Directory jobs repeatedly claim independent
listing reads; stat jobs claim 128-entry chunks; matcher jobs claim entry chunks.
Only the coordinator recurses, evaluates traversal controls and consumes ordered
results. Threads remain bounded and are reused until command completion.
results. Its pools are traversal-owned, so two comparison-side coordinators can
still create separate executors before a later comparison-only pool starts.

A separate command-ownership follow-up removes that gap: `RunFind` owns one
executor, each comparison side walks on the calling coordinator, and eligible
file pairs reuse the same executor after both inventories are collected. The
`--jobs=N` participant budget includes the caller, leaving at most `N-1`
background workers. The caller also claims independent stat, content and
comparison chunks. This is a concurrency experiment, not a performance
acceptance claim. Directory drain fairness, bounded read-ahead, cancellation,
remaining-work admission, slow-storage controls and native M5 Pro validation
remain outstanding. The fresh native accounting and exact-source baseline
evidence live separately in PRs #994 and #995; this branch does not mix their
measurement artifacts into a new engine result.

Correctness checks include nested read-ahead above the admission threshold,
partial stat chunks with missing entries, repeated matcher transitions and
Expand Down
56 changes: 38 additions & 18 deletions docs/design-parallel.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@

## 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.
Directory enumeration and metadata lookup are hot paths. Parallel reads can
hide I/O latency, especially on high-latency filesystems. Small or memory-backed
workloads can instead be dominated by startup, dispatch and per-entry CPU work;
admitting workers must be justified by useful remaining work, not file count
alone. Worker reuse removes repeated creation, but does not make dispatch free.

Parallelism must not cost correctness. Three things are trivially correct in the
current sequential walk and must stay correct under concurrency: deterministic
Expand Down Expand Up @@ -40,22 +41,24 @@ of the design below; each has a noted follow-up:

## Architecture

A bounded directory-read worker pool with a single coordinator:
A command-owned executor with one calling coordinator and at most `N-1`
background workers under `--jobs=N`:

- Workers perform only `ReadDir` plus metadata lookup and return complete listings.
The coordinator submits sibling-directory reads ahead, then consumes their
futures in the order required by `--sort`. Directory workers start only when at least
two sibling directories provide independent work. For a coordinator-read directory with at least
512 children and eagerly required metadata, 128-entry stat chunks use the same pool. At most one
chunk per worker is pending; worker jobs never wait on nested jobs. Metadata-free, lazy and
result handles in the order required by `--sort`. Directory workers start only when at least
64 sibling directories provide independent work. For a coordinator-read directory with at least
512 children and eagerly required metadata, 128-entry stat chunks use the same executor. The
coordinator also claims stat chunks. At most one drain job per background worker is queued for
each batch; worker jobs never wait on nested jobs. Metadata-free, lazy and
owned/archive-source listings keep their serial per-directory path.
- The coordinator applies traversal controls and writes the output sink in traversal order.
Independent content predicates and rg content searches can evaluate in a separate bounded worker
pool; all stateful expressions, mutations and execution actions retain the coordinator evaluator.
Independent content predicates and rg content searches reuse that executor, with the caller also
claiming chunks; all stateful expressions, mutations and execution actions retain the coordinator evaluator.
- Content matching schedules chunks of four owned entries, so clustered expensive files can use
several workers. Decision-only batches hold at most 1,024 entries; matching-line output retains
the smaller 256-entry bound. Fewer than 64 entries avoid initial thread startup, and tails below
16 run inline even after a pool exists. Later larger batches can grow the pool up to the worker
several workers. Decision-only batches hold at most 8,192 entries; matching-line output retains
the smaller 256-entry bound. Cold admission is described under Parallelism control; tails below
16 run inline even after workers exist. Later larger batches can grow the executor up to the background-worker
allowance. Completed results return in input order, including a trailing path-only print action.
Cheap name/type matching stays inline to avoid scheduling overhead.
- Eligibility comes from audited descriptor capabilities, not names: only independent tests
Expand All @@ -77,8 +80,10 @@ A bounded directory-read worker pool with a single coordinator:
- A listing future may retain a completed directory read until the coordinator
reaches it. No worker emits a match and no traversal sort collects all matches.

Comparison evaluates eligible large regular-file pairs in batches of at most 64 after collecting
both trees. Each pair owns persistent read cursors; small patch inputs can be retained up to 64 KiB
Comparison collects both trees sequentially on the same calling coordinator, reusing the
command's executor across the two walks. It then evaluates eligible large regular-file pairs
in batches of at most 64 on that executor and the caller, without creating a comparison-only
pool or asynchronous side coordinator. Each pair owns persistent read cursors; small patch inputs can be retained up to 64 KiB
per side. The coordinator publishes sorted results, summaries and patches. Archive/owned-source
pairs remain serial. This is not yet a completed-directory comparison pipeline.

Expand Down Expand Up @@ -132,8 +137,11 @@ actions still occur in traversal order.

## Parallelism control

A single knob, `-j N` (long form `--jobs`), caps one run-owned executor at `N`
workers for directory reads, eager stat batches and eligible content matching.
A single knob, `-j N` (long form `--jobs`), caps discovery, eager stat, eligible
content and file comparison at `N` participants for the entire command, including
its calling coordinator. The command owns one executor with at most `N-1`
background threads; standalone `Walk` calls likewise reserve one participant for
their caller.
The coordinator retains traversal and output ownership. The executor starts
lazily, grows only as useful work becomes available, and reuses its threads
across phases and mounted-filesystem walks until the command finishes.
Expand All @@ -143,6 +151,18 @@ expression. `-j 1` makes each part synchronous. `-j all` (`--jobs=all`) uses eve
detected core (`hardware_concurrency()`) for these limits, regardless of the
active mode's default.

This ownership change remains an experiment, not an accepted performance result.
The caller claims independent stat, content and comparison chunks rather than
waiting idle while `N` additional workers run. Sequential comparison inventories
remove the second coordinator, but may reduce overlap when each tree has a
narrow frontier on slow storage; native A/B controls must quantify that tradeoff.
The executor still uses FIFO batch-drain jobs. A directory drain can occupy a
worker until its entire batch is exhausted, and completed read-ahead listings are
not bounded by a command-wide byte budget. Fair scheduling, bounded read-ahead,
cancellation, and useful-work admission based only on remaining work are still
required; a thread-count limit alone does not solve those problems. The cold
matcher policy below is inherited for isolation, not endorsed by this change.

The coordinator admits directory read-ahead when at least 64 independent sibling
reads are available. It submits at most one drain job per worker; each job claims
successive directories from that batch and publishes indexed listing results.
Expand Down
6 changes: 4 additions & 2 deletions xff/cli/explain_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -630,11 +630,13 @@ test::explain_resources_use_selected_config_without_traversal() {
--buffer=2KiB
INI
out="$(XFF_TEST_USER_CONFIG="${cfg}" "$(_xff_bin)" "${dir}/absent" --config=resource-view --explain)"
expect_output_contains $'directory-workers-per-walk\t3' "${out}" || return
expect_output_contains $'execution-participants-per-command\t3' "${out}" || return
expect_output_contains $'background-workers-per-command\t2' "${out}" || return
expect_output_contains $'content-field-occurrences\t2' "${out}" || return
expect_output_contains $'listing-column-buffer\t2048 cell bytes' "${out}" || return
out="$(XFF_TEST_USER_CONFIG="${cfg}" "$(_xff_bin)" "${dir}/absent" --config=resource-view --columns=name --jobs=1 --explain)"
expect_output_contains $'directory-workers-per-walk\t1' "${out}" || return
expect_output_contains $'execution-participants-per-command\t1' "${out}" || return
expect_output_contains $'background-workers-per-command\t0' "${out}" || return
expect_output_contains $'content-field-occurrences\t0' "${out}" || return
}

Expand Down
41 changes: 22 additions & 19 deletions xff/cli/globals.cc
Original file line number Diff line number Diff line change
Expand Up @@ -900,25 +900,28 @@ constexpr std::array kGlobals = std::to_array<GlobalFlag>({
.group = "scheduling",
.header = "Concurrency and ordering",
.summary = "directory-read, eligible content-match, comparison and -exec workers",
.details = "`N` is a positive integer; use `-j 4`, `-j=4`, or `--jobs=4`. The conventional attached "
"short form `-j4` is also accepted. Every form accepts `all` in place of `N`. `-j 1` makes "
"directory reads and `-exec ... ;` synchronous. At larger values, xff reads directories "
"using one lazily started pool of up to `N` worker threads, reused for directory reads, "
"required eager metadata and eligible content matching. Workers drain batches of independent "
"work; small workloads stay on the coordinator. Lazy and metadata-free listings do not start "
"stat workers. Independent content predicates and rg searches match in bounded parallel batches, "
"with output kept in traversal order. Small initial batches, archive members, stateful expressions "
"stay on the coordinator. Metadata-consuming output does not itself prevent eligible content "
"filters from running in workers; reductions and rendering remain serial. Independent "
"regular-file comparison pairs can also use bounded workers, preserving relative-path output "
"order. Batches of only small or metadata-only comparisons stay inline; archive and owned "
"filesystem sources stay serial. "
"Xff may independently keep up to `N` semicolon-form `-exec` / "
"`-execdir` children outstanding. Reads and children can overlap; `N` is not one shared "
"operation budget. The children's truth value is therefore success on launch. The "
"`... +` batch forms still run once after the walk and propagate a failing exit status. With "
"no flag, xff uses one fewer than the detected cores, capped at 15 and floored at 1; find and "
"rg modes use every detected core. `all` always means every detected core.",
.details =
"`N` is a positive integer; use `-j 4`, `-j=4`, or `--jobs=4`. The conventional attached "
"short form `-j4` is also accepted. Every form accepts `all` in place of `N`. `-j 1` makes "
"directory reads and `-exec ... ;` synchronous. At larger values, xff reads directories "
"using one command-owned, lazily started pool of up to `N-1` background workers plus the calling "
"coordinator, reused for directory reads, required eager metadata, eligible content matching "
"and file comparison. Comparison trees are coordinated sequentially within that same budget. "
"The caller participates in eligible content and comparison work. Workers drain batches of independent "
"work; small workloads stay on the coordinator. Lazy and metadata-free listings do not start "
"stat workers. Independent content predicates and rg searches match in bounded parallel batches, "
"with output kept in traversal order. Small initial batches, archive members, stateful expressions "
"stay on the coordinator. Metadata-consuming output does not itself prevent eligible content "
"filters from running in workers; reductions and rendering remain serial. Independent "
"regular-file comparison pairs can also use bounded workers, preserving relative-path output "
"order. Batches of only small or metadata-only comparisons stay inline; archive and owned "
"filesystem sources stay serial. "
"Xff may independently keep up to `N` semicolon-form `-exec` / "
"`-execdir` children outstanding. Reads and children can overlap; `N` is not one shared "
"operation budget. The children's truth value is therefore success on launch. The "
"`... +` batch forms still run once after the walk and propagate a failing exit status. With "
"no flag, xff uses one fewer than the detected cores, capped at 15 and floored at 1; find and "
"rg modes use every detected core. `all` always means every detected core.",
.see_also = "output,stats",
.alias_modes = registry::Modes::kNative,
.alias_argument = "N",
Expand Down
5 changes: 4 additions & 1 deletion xff/cli/help_topic_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,11 @@ test::bad_flags_are_hard_errors_even_with_help() {
test::meta_spelling_inside_exec_stays_a_child_argument() {
# Meta recognition follows parser boundaries. A child command is an opaque
# argument run, so its `--help` must not turn xff itself into the usage page.
# Use the POSIX shell builtin: external printf implementations can interpret
# --help specially even after a format argument (including local uutils 0.10.0).
local out
out="$("$(_xff_bin)" --allow-exec . -maxdepth 0 -exec printf 'child:%s\n' --help ';' 2>&1)"
# shellcheck disable=SC2016 # The child shell expands its argument, not this test's shell.
out="$("$(_xff_bin)" --allow-exec . -maxdepth 0 -exec sh -c 'printf "child:%s\n" "$1"' sh --help ';' 2>&1)"
expect_output_contains 'child:--help' "${out}"
expect_output_not_contains 'eXtended File Find' "${out}"
}
Expand Down
Loading
Loading