Skip to content
Merged
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 .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ repos:
hooks:
- id: check-added-large-files
# Retain the exact lossless cross-platform performance evidence archives.
exclude: ^docs/benchmarks/((executor-copy-audit|bounded-read-ahead)-20261101|(directory-task-slots|packed-visit-snapshots|bounded-match-results|useful-match-workers|direct-evaluator-workers)-20261102)\.json\.gz$
exclude: ^docs/benchmarks/((executor-copy-audit|bounded-read-ahead)-20261101|(directory-task-slots|packed-visit-snapshots|bounded-match-results|useful-match-workers|direct-evaluator-workers|derived-content-handoff)-20261102)\.json\.gz$
- id: check-merge-conflict
- id: check-yaml
# BINARY_TYPES below: archive containers are binary but can START with text (a phar begins with
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

# 0.11.0

- Reuse requested hashes and line counts from content already loaded by workers, without retaining file bytes between stages.

- Limit warmed matcher participation to available work chunks while retaining useful read concurrency and persistent workers.

- Reuse bounded matcher output buffers between batches while clearing stale selection/error state and releasing oversized old output.
Expand Down
6 changes: 5 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,11 @@ EP/JC/P work; candidates are not speedup claims until measured on both platforms
retain real allocation savings alongside unacceptable Linux comparison regressions.
A deeper typed-plan representation remains unqualified; EP07 boundaries still apply.
Preserve existing stage policy while isolating task storage and lifecycle costs.
- [ ] O13-O22: Reconcile remaining derived-value handoff, static catalogs, invariant operands,
- [x] O13: Qualify requested derived-value handoff from matching workers to ordered consumers.
[Cross-platform evidence](docs/benchmarks/derived-content-handoff-20261102.md) retains
read-count, lifetime/error/limit tests, large field-workload gains and slower controls.
Transfer requested hashes/line values only; keep raw bytes confined to each evaluator.
- [ ] O14-O22: Reconcile remaining static catalogs, invariant operands,
paired streaming compare, reusable stage pools, byte-bounded overlap, archive indexing/admission,
optional bulk metadata, similarity representations and output churn with EP/JC/P acceptance.
Keep macrobenchmark, kernel and diagnostic contracts separate.
Expand Down
Binary file not shown.
72 changes: 72 additions & 0 deletions docs/benchmarks/derived-content-handoff-20261102.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Reuse requested content values between stages (2026-11-02)

## Decision

Retain the request-driven derived-cache handoff. Compiled field renderers identify required hashes and line counts once per run, including defaults and transformed fields. Histogram enums add line demand. Matched/selected workers derive only those values from content already loaded by their evaluator. Move requested digest map nodes and line values into an optional result-indexed snapshot vector; keep raw content in the worker. Ordinary filters retain their result layout and allocate no derived-cache vector.

The existing per-entry snapshot already reuses reads within one evaluator. The gap was the worker-to-coordinator boundary: the coordinator could reread content for fields and reductions after filtering. Gains combine reuse of those bytes and moving requested digest/line computation onto existing workers. They must not be attributed entirely to fewer copies. No thread, clock, stat, shared mutable cache, generic wrapper or user flag is added.

Keep streaming sources without loaded snapshots on the lazy coordinator path. Preserve result-limit laziness, selected/error states, empty/unmatched/regrowing batches, binary line handling, transforms/defaults, archive serial admission and effect invalidation. Cached missing values remain distinct from values not requested. A returned derived snapshot has no raw content; later unrequested consumers can still read lazily.

## Measurement contract

Measured parent: `bcfef58c06979254b2081b6f02b1040821464b5b` (#1052, documentation only). Baseline and `derived_values` use clean `clang_release`/ThinLTO executables copied before timing. Runtime source identities, binary hashes, fixture data, scripts and all raw samples are retained. Later support-test refinements do not alter the timed runtime. Compilation, lint and sanitizers do not overlap timing on a host.

Each host has 50 native cases in three sessions: warmup discarded per case/variant/session, nine rotating rounds and fastest-seven means. Independent output oracles validate every invocation; hash/line expectations are computed from actual fixture bytes before timing. Workloads use 10/1,000/10,000 files plus 272/288/320-entry fixtures with 256 short entries followed by expensive 128 KiB entries. Requested worker counts are one/three/ten. Linux uses tmpfs and CPU affinity; macOS uses warm APFS without affinity.

Twenty-six whole-engine in-memory cells run three sessions and nine repetitions/fastest seven, with separate processes and rotating participant order. Content fields, output/filter/traversal and comparison controls use one/four workers. Four native cases receive five-session confirmation and five kernels receive three-session confirmation, each adding a byte-identical baseline participant. Confirmation never replaces initial slower samples.

## Native results

Median paired session changes; negative means faster.

| Fixture / task | Workers | Linux | macOS |
| :----------------------- | ------: | ------: | ------: |
| flat10000 / derived-hash | 1 | +0.60% | -0.10% |
| flat10000 / derived-hash | 3 | -36.22% | -48.53% |
| flat10000 / derived-hash | 10 | -43.18% | -49.17% |
| heavy320 / derived-lines | 1 | -0.35% | +0.78% |
| heavy320 / derived-lines | 3 | -26.57% | -35.04% |
| heavy320 / derived-lines | 10 | -19.76% | -34.55% |
| heavy320 / derived-hash | 3 | -57.77% | -56.91% |
| heavy320 / derived-hash | 10 | -72.61% | -73.62% |
| flat10000 / files | 1 | +1.55% | +0.12% |
| compare-large / compare | 3 | -2.55% | +0.69% |

One-worker cases stay near baseline; large parallel hash cases improve substantially on both hosts. These are selected workloads, not a claim that every search becomes faster. The largest initial native median costs are +1.55% Linux and +0.78% macOS, with session reversals retained.

## Kernel results and confirmation

| Kernel | Initial Linux | Confirm Linux | Baseline repeat Linux | Initial macOS | Confirm macOS | Baseline repeat macOS |
| :------------------------- | ------------: | ------------: | --------------------: | ------------: | ------------: | --------------------: |
| RE2/content-fields/1000/4 | -67.84% | -66.75% | -0.07% | -71.82% | -72.19% | -0.14% |
| RE2/content-fields/10000/1 | +0.09% | +0.08% | +0.05% | +0.94% | -1.67% | +0.04% |
| RE2/content-fields/10000/4 | -70.14% | -70.00% | +0.14% | -73.01% | -72.80% | +0.19% |
| RE2/large-output/10000/1 | -0.54% | +0.26% | +0.14% | +6.63% | -0.52% | +2.66% |
| RE2/rg-files/10000/4 | +0.25% | +2.03% | +1.58% | +1.47% | -2.31% | -3.37% |

Retain the initial macOS 10,000-file/one-worker large-output control (+6.63%) and its targeted confirmation above. That output path requests no derived values, so the exact cause must not be assigned to saved hashing work. The byte-identical baseline participant helps expose noise but does not eliminate it. All slower samples and unrelated shifts remain in the archive.

## Native confirmation

| Fixture / task | Workers | Linux candidate | Linux baseline repeat | macOS candidate | macOS baseline repeat |
| :----------------------- | ------: | --------------: | --------------------: | --------------: | --------------------: |
| heavy320 / derived-hash | 10 | -72.38% | +0.48% | -73.52% | +0.24% |
| flat10000 / files | 1 | +2.07% | +0.35% | +0.28% | +0.02% |
| flat10000 / derived-hash | 1 | +0.70% | +0.55% | +0.51% | -0.00% |
| heavy320 / derived-lines | 1 | +0.32% | +0.03% | +0.82% | +0.13% |

## Correctness, ownership and footprint

The native field test processes 128 files, including unselected entries, and observes exactly 128 VFS reads with serial and parallel execution while repeated digest/line fields produce identical output. A result limit preserves the lazy path and its single consumed output read. Snapshot tests prove that returned caches retain no bytes, do not initiate reads for unloaded sources, move only requested values and retain binary/read-failure null results. Pool tests cover unmatched, empty and inline regrowth without stale values. Dedicated parallel column, summary, line-bucket histogram, line-sum histogram and count-only histogram cases match serial output and observe one read per file. Rg summary tests exercise rejected content matches and read-error equivalence.

Derived storage is bounded by the current batch population and requested cache values, rather than total file bytes. It is not a new total-process memory cap. Hash strings/map nodes and snapshot metadata still use memory; ordinary searches requesting no such values allocate no side vector. No first-output or total-RSS improvement is inferred from elapsed time alone.

- linux: unstripped CLI output 12,292,800 -> 12,334,504 bytes (+41,704).
- macos: unstripped CLI output 4,051,424 -> 4,069,744 bytes (+18,320).

Both complete core/extension release suites pass 306/306, with required Linux FUSE mounts. Focused ASan passes five suites per host, and Linux TSan passes three worker/run suites. Header-aware local clang-tidy covers 42 affected translation units; initial test const-qualification findings are fixed and rechecked separately. Final normal hooks and current pushed-head CI remain required before merge.

## Lossless evidence

[derived-content-handoff-20261102.json.gz](derived-content-handoff-20261102.json.gz): 1,176,635 bytes; SHA-256 `ad5f377bacbac56bf45aa504038db70f40893324d648e55c67580a1e1a478550`. Resolve fixtures through `fixtures[report.fixtures_ref]`; timed and final support sources are separate, and initial failure/confirmation logs remain distinct.
6 changes: 5 additions & 1 deletion docs/design-parallel.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ A bounded directory-read worker pool with a single coordinator:
does not disable host-file parallelism: mounted/archive-member entries flush pending host results
and use the serial evaluator. Both paths keep the same VFS and safety preflight.
- Workers own each entry's content snapshot and rendered output. Predicates and match output reuse
that snapshot; source bytes are released when evaluation finishes. The batch retains rendered
that snapshot; source bytes are released when evaluation finishes. When templates or histograms
request hashes or line counts, selected workers derive those values from bytes already loaded and
move only requested caches to the ordered coordinator. A separate result-indexed vector exists
only when values are requested; raw content never crosses this boundary. Streaming sources without
loaded snapshots and result-limited listings keep lazy coordinator reads. The batch retains rendered
records for ordered emission. Full line/context output buffers each input; filename/count/quiet
selection streams where the VFS supports it, retaining the longest line and draining the source
for binary/error checks. Neither the entry-count batch bound nor streaming limits record size.
Expand Down
18 changes: 18 additions & 0 deletions docs/performance-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -3323,3 +3323,21 @@ virtual calls alone do not establish an acceptable elapsed improvement. Preserve
the rejected source rather than adding this representation to the shipping runtime. A deeper typed
plan needs a separate lifetime-safe design and fresh profiles; O13 and the remaining candidates
continue independently.

## Requested content values across the stage boundary

[The O13 report and raw evidence](benchmarks/derived-content-handoff-20261102.md) qualify requested
derived caches from content already loaded by matching workers. Existing snapshots already cache
within one evaluator; this removes the later coordinator reread and computes requested digests/line
counts using existing workers. Compiled fields and histogram enums define demand once per run.
Only requested derived values move to ordered consumers, not raw file bytes or a shared cache.
Ordinary filters allocate no side vector and keep their result layout. Result limits and streaming
sources without loaded snapshots retain the lazy path.

Four-worker content-field kernels confirm roughly 67-70% lower elapsed time on Linux and 72-73%
on macOS for the larger populations. Heavy native ten-worker hash cases improve about 73-74%.
One-worker cases stay near baseline. These gains combine avoided reads/copies with parallel derived
work, rather than proving that copies alone accounted for that time. The report keeps the initial
macOS output control (+6.63%) and its confirmation (-0.52%), identical-baseline participants, all
slower samples, source identities and validation. This does not replace O14/O15 invariant/catalog
work, O17/O18 scheduling or O22 output construction.
3 changes: 2 additions & 1 deletion release-site.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
"docs/benchmarks/packed-visit-snapshots-20261102.md": "docs/benchmarks/packed-visit-snapshots-20261102.html",
"docs/benchmarks/bounded-match-results-20261102.md": "docs/benchmarks/bounded-match-results-20261102.html",
"docs/benchmarks/useful-match-workers-20261102.md": "docs/benchmarks/useful-match-workers-20261102.html",
"docs/benchmarks/direct-evaluator-workers-20261102.md": "docs/benchmarks/direct-evaluator-workers-20261102.html"
"docs/benchmarks/direct-evaluator-workers-20261102.md": "docs/benchmarks/direct-evaluator-workers-20261102.html",
"docs/benchmarks/derived-content-handoff-20261102.md": "docs/benchmarks/derived-content-handoff-20261102.html"
},
"generated_html": {
"XFF.md": {
Expand Down
36 changes: 36 additions & 0 deletions xff/content/snapshot.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,22 @@

#include "xff/content/snapshot.h"

#include <algorithm>
#include <utility>

#include "xff/content/line_match.h"

namespace xff::content {

void Derivation::Include(Derivation other) {
lines |= other.lines;
for (auto& digest : other.digests) {
if (!std::ranges::contains(digests, digest)) {
digests.push_back(std::move(digest));
}
}
}

absl::StatusOr<std::string_view> Snapshot::Read(const vfs::FileSystem& fs, std::string_view path) const {
if (!bytes_) {
bytes_ = fs.ReadContent(path);
Expand All @@ -21,6 +33,13 @@ std::optional<std::string> Snapshot::Digest(
const vfs::FileSystem& fs,
std::string_view path,
const hash::AlgoEncoding& spec) const {
return DigestValue(fs, path, spec);
}

const std::optional<std::string>& Snapshot::DigestValue(
const vfs::FileSystem& fs,
std::string_view path,
const hash::AlgoEncoding& spec) const {
auto [entry, inserted] = digests_.try_emplace(std::pair{std::string(spec.algo), spec.encoding});
if (inserted) {
const auto bytes = Read(fs, path);
Expand All @@ -39,6 +58,23 @@ std::optional<std::size_t> Snapshot::Lines(const vfs::FileSystem& fs, std::strin
return *lines_;
}

Snapshot Snapshot::TakeDerived(const Derivation& request, const vfs::FileSystem& fs, std::string_view path) && {
Snapshot derived;
if (!Loaded()) {
return derived;
}
if (request.lines) {
static_cast<void>(Lines(fs, path));
derived.lines_ = std::exchange(lines_, std::nullopt);
}
for (const auto& digest : request.digests) {
static_cast<void>(DigestValue(fs, path, {.algo = digest.algorithm, .encoding = digest.encoding}));
auto node = digests_.extract(std::pair{digest.algorithm, digest.encoding});
derived.digests_.insert(std::move(node));
}
return derived;
}

void Snapshot::Invalidate() {
bytes_.reset();
digests_.clear();
Expand Down
26 changes: 26 additions & 0 deletions xff/content/snapshot.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,30 @@
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "absl/status/statusor.h"
#include "xff/hash/hash.h"
#include "xff/vfs/filesystem.h"

namespace xff::content {

// Immutable after run preparation. Algorithm names own their bytes independently of templates.
struct DigestRequest {
std::string algorithm;
hash::Encoding encoding = hash::Encoding::kHex;
bool operator==(const DigestRequest&) const = default;
};

struct Derivation {
bool lines = false;
std::vector<DigestRequest> digests;

bool Empty() const { return !lines && digests.empty(); }

void Include(Derivation other);
};

// Lazily owned bytes and requested derived values for exactly one entry. A snapshot belongs
// to one evaluator; it has no locks and is never shared between workers. Read errors are cached.
// Invalidate at effect boundaries and before using a different entry. Views expire on invalidation.
Expand All @@ -28,9 +45,18 @@ class Snapshot final {
std::optional<std::size_t> Lines(const vfs::FileSystem& fs, std::string_view path) const;
void Invalidate();

// Extract only requested derived caches from already loaded entry bytes. This never initiates
// a VFS read and the returned snapshot owns no raw content, including on a cached read error.
Snapshot TakeDerived(const Derivation& request, const vfs::FileSystem& fs, std::string_view path) &&;

bool Loaded() const { return bytes_.has_value(); }

private:
const std::optional<std::string>& DigestValue(
const vfs::FileSystem& fs,
std::string_view path,
const hash::AlgoEncoding& spec) const;

mutable std::optional<absl::StatusOr<std::string>> bytes_;
mutable std::map<std::pair<std::string, hash::Encoding>, std::optional<std::string>> digests_;
mutable std::optional<std::optional<std::size_t>> lines_;
Expand Down
Loading
Loading