diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffbda54d90..4993552d84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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|derived-content-handoff|immutable-default-catalogs|prepared-diff-configuration|prepared-hash-operands|aggregate-comparison-totals|byte-shingle-representations|archive-input-buffers)-20261102|(digest-and-record-storage|caller-owned-streams|direct-source-materialization|hash-field-binding|hash-field-binding-tag|forward-cursor-skipping|raw-record-reuse|similarity-prefix-view|similarity-reference-reuse|owned-word-moves|smaller-shingle-probes|native-zip-seek-evaluation|encoded-record-reuse|threshold-aware-similarity)-20261103|(column-row-reuse|word-set-insertion|field-context-owned-results|selected-summary-output|fixed-summary-metrics|direct-summary-cells|compact-comparison-entries|comparison-category-sequence|native-name-paths|uniform-native-case|compact-traversal-metadata|eager-native-paths|native-case-experiments)-20261104|(folded-ascii-globs|bulk-directory-metadata|comparison-retention-followups|comparison-storage-followups|archive-prefix-reuse|archive-directory-range|archive-directory-allocation|archive-directory-observations|native-symlink-observations|native-metadata-followups|symlink-target-demand|native-pgo|readlink-short-target|cctz-empty-format|readlink-input-path|readlink-owned-transfer|archive-lookup-trials|archive-member-keys)-20261105|archive-directory-child-capacity-20261106|shared-stage-workers-20261107|archive-index-insertion-20261107|transactional-rg-records-20261107|immutable-zip-locator-evaluation-20261107|queued-read-cancellation-20261107|archive-btree-historical-decision-20261107|compiled-field-stat-demand-20261107|unchanged-site-payloads-20261107|archive-read-buffer-clear-20261107|archive-read-buffer-requalification-20261108|language-list-reserve-20261107|language-list-reserve-requalification-20261108|ordered-output-credit-20261107|adaptive-ordered-output-20261109)\.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|immutable-default-catalogs|prepared-diff-configuration|prepared-hash-operands|aggregate-comparison-totals|byte-shingle-representations|archive-input-buffers)-20261102|(digest-and-record-storage|caller-owned-streams|direct-source-materialization|hash-field-binding|hash-field-binding-tag|forward-cursor-skipping|raw-record-reuse|similarity-prefix-view|similarity-reference-reuse|owned-word-moves|smaller-shingle-probes|native-zip-seek-evaluation|encoded-record-reuse|threshold-aware-similarity)-20261103|(column-row-reuse|word-set-insertion|field-context-owned-results|selected-summary-output|fixed-summary-metrics|direct-summary-cells|compact-comparison-entries|comparison-category-sequence|native-name-paths|uniform-native-case|compact-traversal-metadata|eager-native-paths|native-case-experiments)-20261104|(folded-ascii-globs|bulk-directory-metadata|comparison-retention-followups|comparison-storage-followups|archive-prefix-reuse|archive-directory-range|archive-directory-allocation|archive-directory-observations|native-symlink-observations|native-metadata-followups|symlink-target-demand|native-pgo|readlink-short-target|cctz-empty-format|readlink-input-path|readlink-owned-transfer|archive-lookup-trials|archive-member-keys)-20261105|archive-directory-child-capacity-20261106|shared-stage-workers-20261107|archive-index-insertion-20261107|transactional-rg-records-20261107|immutable-zip-locator-evaluation-20261107|queued-read-cancellation-20261107|archive-btree-historical-decision-20261107|compiled-field-stat-demand-20261107|unchanged-site-payloads-20261107|archive-read-buffer-clear-20261107|archive-read-buffer-requalification-20261108|language-list-reserve-20261107|language-list-reserve-requalification-20261108|ordered-output-credit-20261107|adaptive-ordered-output-20261109|direct-line-scan-spans-20261109-(linux|macos))\.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 diff --git a/TODO.md b/TODO.md index 2430d94832..f4be59b404 100644 --- a/TODO.md +++ b/TODO.md @@ -269,6 +269,12 @@ regresses 3-11% across the two sessions. Keep this tradeoff visible when compari ## Toolchain follow-up +- [ ] Fix explicit Linux `--config=clang_opt --config=lto`: the configured action + combines Bazel split ThinLTO indexing with `-ffat-lto-objects`, and the compiler + does not emit `word.indexing.o`. The scanner qualification retains the failed + action and succeeds with shipping `xff_production_lto`. Keep shipping object + sharing intact and verify feature/configuration order before a separate fix. + - [ ] Complete the Clang/LLVM 23.1.2 upgrade: validate native macOS linking, Linux LTO, the LLVM 22 MSan compiler/runtime override, matching formatting, full CI, benchmark results, and stripped binary sizes before merging the separate follow-up to #936. @@ -511,12 +517,15 @@ EP/JC/P work; candidates are not speedup claims until measured on both platforms keep the specialized pools and retain [all variants and counterexamples](docs/benchmarks/shared-stage-workers-20261107.md). Future reuse designs must preserve independent root/mount allowances, leaf jobs, owner draining and JC gates, and remove the repeatable Linux comparison regression before adoption. -- [ ] Qualify direct span reads in `content::ScanLines` separately from O18. Native - profiling attributes 43.94% of sampled baseline CPU to zeroing beneath its owning - 64 KiB read loop. Reuse `ReadInto` only with initialized-range, binary/EOF/error - tests and native/memory/archive controls; retain O07's rejected variants rather - than assuming removal of zeroing is always a win. See the current O18 qualification - notes in [the performance analysis](docs/performance-analysis.md). +- [x] Qualify direct span reads in `content::ScanLines` separately from O18. Retain + the distinct 43.94% and current 40.81% sampled CPU zeroing profiles; neither + predicts elapsed gains. Compare stack and lazy overwrite heap storage with + native/memory/archive and incapable-stream controls, preserving initialized + prefixes, changing capabilities, EOF, binary detection and late errors. + Both-host complete native/engine/kernel/image-control qualification selects stack + storage, with measured legacy and high-worker costs retained. Keep O07's + rejected variants and every adverse result in + [the scanner report](docs/benchmarks/direct-line-scan-spans-20261109.md). - [ ] O18: Qualify byte-bounded overlap with backpressure, ordered publication and cancellation; avoid per-batch throwaway threads and nested waits on the same saturated pool. Retain the [ordered-output credit experiment](docs/benchmarks/ordered-output-credit-20261107.md). @@ -526,15 +535,19 @@ EP/JC/P work; candidates are not speedup claims until measured on both platforms Retain ordinary-path costs and anomalous Linux populations alongside large-output gains. Keep full source/decoder/derived-cache/renderer-temporary admission and broader JC gates open; output credit is not a whole-process cap. No public crossover setting is exposed. +- [ ] Benchmark qualification: independent CLI copies must preserve `xff`/`xff_full` + basenames, because invocation names select configuration. Retain earlier controls + with different names, disclose that limitation and revisit conclusions depending + only on those controls. The scanner qualification adds a matching-basename cohort. - [ ] Benchmark qualification: distinguish repeated execution of one image from independent file-backed baseline copies. Retain both controls when investigating unexplained native costs, and compare loaded code/data identities before repeating full populations for a build-ID-only change. Preserve all observations rather than choosing a favorable cohort. -- [ ] Qualify delayed, bounded stdout buffering independently of broader O18 admission. - PR #1156 retains full/minimal native production measurements on both hosts and all - adverse controls in [the report](docs/benchmarks/bounded-stdout-buffer-20261109.md). - Complete the stream-contract coverage follow-up and current-head verification before - merging; a 4 KiB stdout buffer does not complete source/decoder/derived-cache budgets. +- [x] Qualify delayed, bounded stdout buffering independently of broader O18 admission. + Merged PR #1156 retains full/minimal native production measurements on both hosts + and adverse controls in [the report](docs/benchmarks/bounded-stdout-buffer-20261109.md). + The 16-case stream-contract test, current-head checks and post-merge Test/Benchmarks + pass; a 4 KiB stdout buffer does not complete source/decoder/derived-cache budgets. - [ ] Keep the performance-analysis index below the Markdown service request limit before appending further findings. Retain full results and sources in linked qualification reports; consider local rendering or independently renderable sections for further growth. @@ -598,6 +611,13 @@ EP/JC/P work; candidates are not speedup claims until measured on both platforms separate hardware diagnostics are retained in [the allocation decisions](docs/benchmarks/archive-directory-allocation-20261105.md); microbenchmark gains alone do not qualify either runtime implementation. +- [ ] O19g3: Prepare the requested member's normalized name once per streamed open, + preserving null-header, normalization, error and duplicate-selection behavior. The + current `MemberSource::Open` repeats `NormalizeMemberName(member_)` while walking + preceding headers. Retain cold/first-member/missing-member controls and measure on + both hosts before adoption. The scanner qualification also exposes roughly eight + seconds for the broad 10,000-member, single-worker TAR invocation; reuse observed plain-TAR payload locators + as a separate, format-validated O19b experiment rather than bypass decoder checks. - [x] O19i: Bound initial archive-directory result storage to reduce small-listing vector reallocations, retaining tiny/root/chain costs and complete native controls. Full both-host qualification and the held O19g variants are in diff --git a/bazelmod/clang_tidy_targets.bzl b/bazelmod/clang_tidy_targets.bzl index 252e117746..1a38cfb700 100644 --- a/bazelmod/clang_tidy_targets.bzl +++ b/bazelmod/clang_tidy_targets.bzl @@ -17,6 +17,7 @@ CLANG_TIDY_MANUAL_TARGETS = [ "//tools:read_source_benchmark", "//tools:time_format_benchmark", "//xff/cli:globals_benchmark", + "//xff/content:line_scan_benchmark", "//xff/engine:read_accounting_test", "//xff/engine:compare_benchmark", "//xff/engine:executor_benchmark", diff --git a/docs/benchmarks/direct-line-scan-spans-20261109-linux.json.gz b/docs/benchmarks/direct-line-scan-spans-20261109-linux.json.gz new file mode 100644 index 0000000000..bce4e1ff4b Binary files /dev/null and b/docs/benchmarks/direct-line-scan-spans-20261109-linux.json.gz differ diff --git a/docs/benchmarks/direct-line-scan-spans-20261109-macos.json.gz b/docs/benchmarks/direct-line-scan-spans-20261109-macos.json.gz new file mode 100644 index 0000000000..87739ffb3c Binary files /dev/null and b/docs/benchmarks/direct-line-scan-spans-20261109-macos.json.gz differ diff --git a/docs/benchmarks/direct-line-scan-spans-20261109.md b/docs/benchmarks/direct-line-scan-spans-20261109.md new file mode 100644 index 0000000000..55b12f6374 --- /dev/null +++ b/docs/benchmarks/direct-line-scan-spans-20261109.md @@ -0,0 +1,371 @@ +# Direct span reads for streaming line scans + +## Scope and implementation + +The candidate changes `content::ScanLines` to use `ReadStream::ReadInto` when the +stream currently supports it. A single caller-owned, uninitialized 64 KiB buffer +lives for the scan. Only the returned prefix is observed. This removes the owning +`Read` result's allocation, initialization and copy from capable streams without +changing the public VFS or output interface. + +Capabilities are checked on each read. An incapable stream retains the existing +owning `Read` path, whose string remains alive through line processing. A failed +capable read is propagated; it is not retried through another interface after +input might have been consumed. Oversized returned counts fail before creating a +view. Binary detection, final-line handling and draining after visitor stop retain +their original semantics. Buffer ownership is local: no locks, shared scratch, +thread-local storage or pool is introduced. + +A separate implementation lazily allocates the same uninitialized capacity with +`make_unique_for_overwrite`. It remains a distinct measured source. Measurements +must decide between the stack and heap forms; the existence of a zeroing hotspot +is not an elapsed-speedup forecast. O07's rejected uninitialized cursor and shared +fallback-loop implementations remain retained in their existing reports. + +## Source identities + +- Baseline: `cc30ffd2f85cd32fb902750af5023b6596889faf`, actual merged main after #1156. +- Stack: `4cd48ae6d7a2f83a824b85ec7f87c8baedb29eba`, containing that baseline. +- Lazy overwrite heap: `01689b4d83cac401edba8258a75dbee8eb475d09`, based on the stack source. + +Every source is a clean actual Git revision on both hosts. Frozen images retain +source/tree identities, configuration hashes, the configured executable query, +binary SHA-256 and actual ThinLTO link actions. Later report commits must not +relabel these source IDs. All stack/heap outcomes, controls and adverse cells +remain in the raw evidence. + +The identical added serial diagnostic is built at benchmark-only baseline +`7e4ef7909d27a78fa782e0d098196dd5c4fde7b5`, stack +`f4290d429a1c58dc3b8e8941a8fdf2540954965f`, and heap +`74650ad5697b2f19e15dd268e6ee6722cdfcf0c5`. Their runtime parents are the three +original sources above. Both hosts use the same signed commits, verified after +transfer; headless Linux signing is not bypassed. Native confirmations use the +actual frozen `f429` shipping images, with the byte-equality proof described below. + +## Behavioral verification + +The affected suites cover line scanning, line matching, read-source contracts and +engine rg execution. Eleven added scanner cases exercise short reads, CRLF and +blank/final lines, empty and exact-boundary input, poisoned/uninitialized unused +tails, changing capabilities in either direction, long lines, binary-scan limits, +oversized counts, late errors after stop/binary detection and reentrant scanning. +Existing incapable-stream cases remain. Both variants pass the four focused +suites and two-translation-unit clang-tidy on both hosts, ASan on both hosts and +MSan on Linux. These development results retain the source IDs above. + +Implementation head `f4290d429a1c58dc3b8e8941a8fdf2540954965f` adds the diagnostic +benchmark and documentation. It passes all 344 configured release test targets +on both hosts, including required Linux FUSE tests, four freshly executed affected +release suites, and eight fresh affected suites under ASan/TSan on both hosts and +MSan on Linux. Source hashes are unchanged throughout each qualification. Its +full/minimal shipping images are byte-identical to the original stack images on +both hosts; independently retained build/link proofs establish that fact. + +The later `f0f2963124e853801c1fbd347b51fa159a68ca1f` changes only the release-site +page mapping. Its complete PR Test workflow `42634678865`, including coverage, +clang-tidy, all platforms, sanitizers and benchmark aggregation, succeeds. Final +report/data publication must receive its own current-head checks; these results +must not be relabeled as testing an unpublished later head. + +## Measurement contract + +The unchanged in-memory engine benchmark covers traversal, preloaded native line +output, streaming rg line output, reduced rg file/count output and large output. +Its existing worker counts are 1 and 4; these are not renamed to the native +1/3/10 CPU-budget cells. Each participant has one discarded warmup and nine +measured rounds, retaining the fastest seven per case. A copied baseline image +is an independent ordinary file with the same bytes, not proof of physical +backing-page isolation. Mac uses ordinary Bazel ThinLTO; Linux uses shipping +ThinLTO because ordinary split-indexing LTO plus fat objects fails to emit an +indexing output. The failed build and its actual actions are retained. No +sanitizer or memory-engine result is described as shipping CLI elapsed time. + +Native qualification uses unsanitized full and minimal shipping ThinLTO CLIs, +broad/deep RAM-filesystem trees, 100 and 100,000 files and budgets 1/3/10. Linux +pins each process to the recorded allowed logical CPUs, preferring distinct +reported cores. Drogon is a KVM guest exposing ten logical CPUs on a Ryzen 9 +9950X host; guest affinity is not proof of host physical-core pinning. The Mac +is an 18-CPU Apple M5 Pro with 64 GiB memory, and records worker requests without +claiming CPU affinity. These are separate host populations, not direct OS rankings. +Separate full-CLI tar-member cases cover 100 and 10,000 members. Archive filters +exclude the raw container, so the oracle checks actual member lines. + +Each native case interleaves baseline, stack, heap, repeated baseline and a +separate byte-identical baseline image, plus GNU find or rg where applicable. +The native fork/exec/wait interval excludes Python output validation. Every +invocation has an independent filename/line/count/metadata oracle generated +from the fixture recipe; member names and line numbers are not inferred from +the baseline output. All raw rounds, including warmups and excluded rounds, +are retained. No post-hoc division by copied-baseline controls is applied. + +Reduced rg file/count output still uses `ScanLines` for unloaded content; those +are other consumers of the changed scanner, not untouched negative controls. +Preloaded native `-M` output uses `VisitLines`. Listing, name and metadata paths +provide unrelated-work controls. General source/decoder/derived-cache admission, +prepared expression work and the broader O18/JC scope remain open. + +## Decision and complete results + +Select the caller-owned stack implementation. Corrected native confirmation shows +about 18% less single-CPU large streaming elapsed time on Linux and 5-6.4% less on +Mac. Higher worker requests have smaller gains or remain near parity, including +the adverse observations below. Lazy heap storage adds an allocation and does not +improve the complete kernel/engine populations consistently enough to select it. +Its source, correctness results and every measurement remain retained. + +The original 180-case native matrices complete on both hosts, with 10,680 checked +invocations each. Corrected matching-invocation confirmations and fixed independent +small-image studies complete separately on both hosts. This is a scoped streaming +optimization, not a claim of a universal speedup or a zero-cost legacy fallback. +Final report/data publication requires its own current-head validation. + +### Mac confirmation + +The same predeclared selection rule used on Linux selects 121 cases on Mac, with +4,780 checked invocations. Every output and image identity passes. All original +and confirmation samples remain separate. The complete large streaming group's +broad/deep geometric means are: + +| Shipping configuration | Requested workers | Stack / baseline | Independent baseline copy / baseline | +| :--------------------- | ----------------: | ---------------: | -----------------------------------: | +| Full | 1 | 0.9506 | 0.9960 | +| Full | 3 | 0.9978 | 1.0273 | +| Full | 10 | 1.0059 | 1.0025 | +| Minimal | 1 | 0.9355 | 0.9945 | +| Minimal | 3 | 0.9947 | 1.0191 | +| Minimal | 10 | 0.9957 | 0.9993 | + +The single-worker group improves by about 5-6.4%; higher worker requests are +closer to parity, with a 0.6% slower full/10-worker aggregate. Two individual +all-nine paired diagnostics have intervals wholly above one: minimal/broad/ +100,000/3-worker rg counts (fastest-seven ratio 1.0196, copied baseline 1.0390) +and full/broad/100,000/10-worker rg lines (1.0163, copied baseline 1.0080). +Their all-nine geometric ratios are 1.0262 and 1.0315, with within-case bootstrap +intervals [1.0034, 1.0547] and [1.0059, 1.0705]. These are the same distinct +diagnostic estimator described below, not fastest-seven confidence bounds. +The observations remain adverse tradeoffs, without declaring either code or +host variation a proven cause. A favorable small-image study must not erase +these large-case observations. + +The original 10,000-member archive cases are near parity: stack point ratios +range from 0.9821 to 1.0016. Replaying preceding headers remains the dominant +separate archive opportunity; this scanner change does not remove that work. + +The separate 90-case small-image study applies exactly the Linux protocol below: +three independent copies per variant, equal-length paired paths, matching program +basenames, rotated image order, and nine measured rounds retaining seven per image. +All 5,400 invocations pass their independent output oracles. The group geometric +means and all individual observations are retained separately from confirmation. + +| Small-case task | Cases | Stack / baseline geometric mean | +| :------------------------------ | ----: | ------------------------------: | +| Streaming rg lines | 12 | 0.9917 | +| Streaming rg matching filenames | 12 | 0.9877 | +| Streaming rg counts | 12 | 0.9914 | +| Preloaded native lines | 12 | 0.9999 | +| Exact file listing | 12 | 0.9982 | +| Native name filter | 12 | 0.9971 | +| Metadata filter | 12 | 1.0041 | +| TAR member lines | 6 | 0.9880 | + +The worst aggregate cell is 1.0204 for minimal/broad/100/10-worker name filtering; +individual image-pair costs can be larger and are retained. These same-machine +copies characterize image variation, not independent hardware or population +confidence. The study does not replace the two slower large cases above. + +### Linux confirmation + +The matching-invocation confirmation preserves the executable basename and gives +each image one invocation per round. The selection rule was recorded before this +confirmation: all 100-file/native and 100-member/archive cases, every 100,000-file +`rg-lines` case, and any original case with a stack ratio above 1.05 or either +baseline control outside 0.95-1.05. This produces 109 cases and 4,300 checked +invocations. Original, first balanced, and corrected confirmation cohorts remain +separate, with every adverse observation preserved. + +For the complete primary large streaming group, the table shows the geometric +mean of the broad/deep ratios. Each underlying time is the arithmetic mean of +its fastest seven of nine measured rounds, following one discarded warmup. +Ratios below one mean less elapsed time. These are per-machine observations, +not confidence bounds or forecasts for arbitrary storage and workloads. + +| Shipping configuration | Logical CPU allowance | Stack / baseline | Independent baseline copy / baseline | +| :--------------------- | --------------------: | ---------------: | -----------------------------------: | +| Full | 1 | 0.8208 | 0.9969 | +| Full | 3 | 0.8740 | 1.0006 | +| Full | 10 | 0.9339 | 0.9939 | +| Minimal | 1 | 0.8199 | 0.9993 | +| Minimal | 3 | 0.8733 | 0.9909 | +| Minimal | 10 | 0.9390 | 1.0018 | + +Small unrelated-work cases in the confirmation include slower individual points: +full/deep/100/10-CPU metadata is 1.0928 and minimal/deep/100/3-CPU name filtering +is 1.0364. Their copied-baseline ratios are 1.1134 and 0.9510 respectively. +Separate all-nine paired log-ratio bootstrap diagnostics have intervals barely +above one for those two cases. Those intervals are not confidence intervals for +the fastest-seven estimator and do not correct for multiple comparisons. The +entire original full/deep/100,000/1-CPU native-line disturbance is retained too: +stack 1.302, heap 1.504 and the exact repeated baseline image 1.512. It does not +establish a runtime-code cause. + +To examine ordinary small-case image variation, a separately recorded study uses +three independent ordinary copies per variant, matching basenames and path +lengths, alternating copy creation order, and rotating all six images once per +round. All 90 small cases complete, with 5,400 checked invocations. It averages +the three per-image fastest-seven means before computing a variant ratio; it is +not pooled with the original or confirmation cohorts. + +| Small-case task | Cases | Stack / baseline geometric mean | +| :------------------------------ | ----: | ------------------------------: | +| Streaming rg lines | 12 | 0.9623 | +| Streaming rg matching filenames | 12 | 0.9617 | +| Streaming rg counts | 12 | 0.9586 | +| Preloaded native lines | 12 | 0.9993 | +| Exact file listing | 12 | 1.0046 | +| Native name filter | 12 | 0.9984 | +| Metadata filter | 12 | 0.9968 | +| TAR member lines | 6 | 0.9874 | + +No aggregate cell in that study is more than 5% slower; the worst is 1.0253 for +minimal/deep/100/10-CPU exact listing. Individual paired-image ratios can exceed +that, and remain in the data. Copies on one host are not independent machines, +do not establish population confidence, and do not prove physical backing-page +isolation. The purpose is to retain and characterize the earlier small-case +variation rather than silently exclude it or divide by a favorable control. + +### Serial scanner and in-memory engine + +The scanner diagnostic completes 32 cases per image, four images and ten rounds +on each host: 1,280 checked benchmark rows per host. Forced legacy and direct +streams share the same owning-read implementation and byte fixture; their only +capability difference is whether `SupportsReadInto` returns true. Input lengths +are 0, 256, 4,096, 65,536 and 131,072 bytes, with 8,192/65,536-byte chunk limits; +the stop group omits empty and 4,096-byte inputs. The callback checks line/byte +counts before timing. No filesystem, source opening or multi-worker execution +is inside this serial kernel. + +| Host | Kernel group | Stack / baseline | Lazy heap / baseline | Baseline copy / baseline | +| :---- | :----------- | ---------------: | -------------------: | -----------------------: | +| Mac | Legacy/all | 1.0101 | 0.9920 | 1.0333 | +| Mac | Legacy/stop | 0.9984 | 1.0197 | 1.0324 | +| Mac | Direct/all | 0.8105 | 1.1628 | 1.0294 | +| Mac | Direct/stop | 0.6575 | 0.8244 | 1.0337 | +| Linux | Legacy/all | 1.0581 | 1.0613 | 1.0081 | +| Linux | Legacy/stop | 1.0473 | 1.0656 | 0.9983 | +| Linux | Direct/all | 0.8318 | 1.0321 | 1.0055 | +| Linux | Direct/stop | 0.7315 | 0.8953 | 1.0025 | + +These are geometric means of per-case elapsed ratios, with no input-size or +production-frequency weighting. In particular, tiny and empty direct inputs +benefit more than long inputs: Linux direct/all at 128 KiB has ratios 1.0143 +(64 KiB chunks) and 0.9891 (8 KiB chunks). This is not a universal per-read gain. +Both new policies incur measurable Linux legacy costs, detailed below. + +The unchanged engine diagnostic completes 48 cases per image and ten rounds, +or 1,920 rows per host. Its streaming `rg-lines` geometric mean is 0.9895 on Mac +and 0.9892 on Linux for stack storage; lazy heap is 0.9992 and 1.0540. The large +output group is 0.8610/0.9898 for stack and 0.8472/1.0188 for heap (Mac/Linux). +These are in-memory engine observations and retain their original 1/4-worker +contract; they must not be described as shipping process startup or native CLI +elapsed time. + +To run the committed serial diagnostic with the shipping configuration: + +```sh +bazel run //xff/content:line_scan_benchmark --config=clang_release --config=xff_full -- \ + --benchmark_min_time=0.05s --benchmark_out=scanner.json --benchmark_out_format=json +``` + +The retained drivers freeze each configuration immediately after its configured +build, verify image/source identities, shut down Bazel before timing, interleave +images, preserve warmups, and check every native result against the fixture +recipe. Reproducing the qualified comparison requires the recorded baseline, +stack and heap source revisions and the same contracts, rather than comparing +two uncontrolled invocations of that command. + +### Shipping image sizes + +These are actual configured ThinLTO build outputs before release stripping or +compression, not tagged release assets. The implementation head produces the +same bytes as the original stack runtime images. Full/minimal configurations +are built and queried separately; no stale `bazel-bin` alias supplies the sizes. + +| Host | Configuration | Baseline bytes | Stack bytes | Change bytes | +| :---- | :------------ | -------------: | ----------: | -----------: | +| Mac | Full | 8,322,616 | 8,322,040 | -576 | +| Mac | Minimal | 4,242,080 | 4,241,904 | -176 | +| Linux | Full | 18,575,776 | 18,578,064 | 2,288 | +| Linux | Minimal | 12,831,704 | 12,834,000 | 2,296 | + +### Lossless evidence + +The [Mac dataset](direct-line-scan-spans-20261109-macos.json.gz) and +[Linux dataset](direct-line-scan-spans-20261109-linux.json.gz) contain all +separate native populations, every raw benchmark JSON, measured and excluded +rounds, contracts, exact drivers, source/build identities and qualification +receipts. It retains all slower observations and the invocation-name limitation +of the earlier controls. There is no post-hoc control division or cohort pooling. +JSON documents are stored as parsed values with original file hashes and lengths; +the capsules retain the byte-exact original files as well. +Linux compressed SHA-256 is +`407dec1f4bc2e8d7d0a97384b978e0a7924a171d2d73b079dbe900a579483330` +(1,885,769 bytes). + +Mac compressed SHA-256 is +`c9190c76258f53fbe21c88ad22b07b7231946bcc101fc0b3bfb1a582fc85a8c6` +(2,135,444 bytes). The original, confirmation and small-image campaigns complete +with 180/121/90 cases and 10,680/4,780/5,400 checked invocations respectively. + +The durable Linux capsule is +`/home/marcus/Documents/direct-line-scan-spans-20261109-linux-evidence.tar.gz` +on drogon: 59,565,583 bytes, SHA-256 +`d76fc9a95beed63cb9f3d05707234dbe7d5e1d2a5a64068e3da46034c96a679a`. +All 394 manifest entries are verified after copying to durable storage. It also +retains actual executables, complete logs, signatures, failed experiments and +link actions. Byte-identical archived contents may share a tar hard-link entry; +the distinct original measurement images and their identities are recorded in +their contracts. Bazel caches, toolchain downloads, Python bytecode, generated +full release sites and fixture mounts are excluded. These exclusions do not +remove timing samples or their independent validation recipes. + +The corresponding Mac capsule is +`/home/marcus/Documents/direct-line-scan-spans-20261109-macos-evidence.tar.gz` +on drogon: 31,917,717 bytes, SHA-256 +`5725b6217259db7c1ef2b5e22741fd2717416ea17cde26fa026d44df16572f6d`. +All 409 manifest entries pass verification again after the durable transfer. + +## Additional observations retained for separate work + +The legacy scanner kernel uses an incapable stream whose capability method returns +false. On Linux, the stack implementation's 256-byte scan costs about 8 ns more +than baseline (+25%); its 128 KiB scan costs about 0.14-0.18 us more (+1.9-2.4%). +Do not describe fallback performance as free or infer a native CLI regression +from this isolated kernel. Mac legacy measurements are near parity. Both raw +populations and all copied-image controls must remain in the final data. + +The call-local 64 KiB buffer also exists when the stream uses owning reads; that +path retains its owning chunk as well. Logical buffer capacity is not a measured +RSS value or a whole-process bound. The candidate makes no general source-memory +admission claim. + +The TAR content cases expose a separate opportunity. In +`extra_modules/archive/archive_reader.cc`, `MemberSource::Open` creates a session +and walks headers to find each requested member. The requested name is normalized +again in each comparison. Preparing that invariant once per open is a separate +O19g3 candidate, with first/missing-member controls. Reusing observed payload +locations for validated uncompressed regular TAR members is a distinct O19b +proposal; compressed, sparse and other format/decoder semantics must retain their +existing path. These ideas are not implemented or counted as gains by this PR. + +### Invocation-name control audit + +`InvocationConfigForProgram` treats an executable basename as an implicit config +selector, mapping `xff_full` to `xff` and preserving other names verbatim. The +original native and first confirmation copy controls used different basenames. +Their output oracles pass, but their startup/config-selection path is not an +identical invocation. Do not attribute their timing differences solely to image +backing. Candidate and baseline invocations already preserve the same basename. +Retain those earlier cohorts, and use a separately recorded confirmation whose +independent copies preserve `xff`/`xff_full`. The serial/engine kernel timings +exclude executable initialization and do not dispatch XFF by their benchmark +binary's name. diff --git a/docs/performance-analysis.md b/docs/performance-analysis.md index c0fa5b8386..c57ecc26e3 100644 --- a/docs/performance-analysis.md +++ b/docs/performance-analysis.md @@ -5213,27 +5213,23 @@ JC/source/temporary admission is inferred. Those parts of O18 remain open. ### Prepared immutable predicate guards -The [predicate-guard qualification](benchmarks/prepared-predicate-guards-20261109.md) -prepares five immutable decisions next to each expression callback: content invalidation, -metadata demand, security previews, top selection and shard-status selection. Reached -predicates borrow these guards; conditional metadata reads, error propagation, short-circuiting -and action invalidation remain in source order. The production record size for the measured -listing expression remains 328 bytes, excluding the AST, backends and worker storage. - -Large Linux listings execute about 3% fewer userspace instructions. The macOS process -instruction diagnostic falls about 0.7-1.0%; the two operating systems' counters have different -scopes and are not compared directly. Elapsed gains are smaller: the large three-worker -full/minimal Linux repeats give candidate/baseline ratios 0.99338/0.99213, while Linux still -takes about 7-10% longer than GNU find. Preserve the initial slower cases, independent repeats -and identical-baseline controls rather than infer a universal speedup from instruction counts. - -The report identifies measured candidate `374c2588f876` and baseline `ed0d34be2b41` explicitly, -with full/minimal production ThinLTO build identities and durable raw archives for both hosts. -Later synchronization does not relabel those binaries. This removes one invariant predicate -setup cost; the remaining EP03/O15 operand families, expression-program decisions and wider -pipeline/admission work remain open. +The [predicate-guard report](benchmarks/prepared-predicate-guards-20261109.md) retains +all source identities, full/minimal production measurements, instruction diagnostics, +slower observations and repeat controls. Prepare five immutable decisions once and +borrow them in reached predicates; metadata and effects retain their original order. +The remaining EP03/O15 operand families and wider pipeline work remain open. ### Delayed listing output buffer See the [qualification report](benchmarks/bounded-stdout-buffer-20261109.md) for the 4 KiB buffer, production comparisons on both hosts, adverse results and retained evidence. + +### Direct line-scanner span reads + +The [scanner qualification](benchmarks/direct-line-scan-spans-20261109.md) compares +caller-owned stack and lazy heap storage with owning reads. Preserve short-read, +initialized-prefix, changing-capability, EOF, binary, late-error and reentrant +contracts. Both-host native, engine, legacy/direct kernel and independent-image +qualification select stack storage, retaining the heap variant and every slower +observation. Full implementation-head release and affected sanitizer suites pass; +the report/data commit receives its own current-head checks. Broader O18 remains open. diff --git a/release-site.json b/release-site.json index 871363e5b5..4e3bebecad 100644 --- a/release-site.json +++ b/release-site.json @@ -160,7 +160,8 @@ "docs/benchmark-merged-pr-coverage.md": "docs/benchmark-merged-pr-coverage.html", "docs/benchmarks/adaptive-ordered-output-20261109.md": "docs/benchmarks/adaptive-ordered-output-20261109.html", "docs/benchmarks/prepared-predicate-guards-20261109.md": "docs/benchmarks/prepared-predicate-guards-20261109.html", - "docs/benchmarks/bounded-stdout-buffer-20261109.md": "docs/benchmarks/bounded-stdout-buffer-20261109.html" + "docs/benchmarks/bounded-stdout-buffer-20261109.md": "docs/benchmarks/bounded-stdout-buffer-20261109.html", + "docs/benchmarks/direct-line-scan-spans-20261109.md": "docs/benchmarks/direct-line-scan-spans-20261109.html" }, "generated_html": { "XFF.md": { @@ -210,6 +211,8 @@ "docs/benchmarks/linux-inline-raw-records-20261109.json": "docs/benchmarks/linux-inline-raw-records-20261109.json", "docs/benchmarks/adaptive-ordered-output-20261109.json.gz": "docs/benchmarks/adaptive-ordered-output-20261109.json.gz", "docs/benchmarks/prepared-predicate-guards-20261109.json.gz": "docs/benchmarks/prepared-predicate-guards-20261109.json.gz", - "docs/benchmarks/bounded-stdout-buffer-20261109.json.gz": "docs/benchmarks/bounded-stdout-buffer-20261109.json.gz" + "docs/benchmarks/bounded-stdout-buffer-20261109.json.gz": "docs/benchmarks/bounded-stdout-buffer-20261109.json.gz", + "docs/benchmarks/direct-line-scan-spans-20261109-linux.json.gz": "docs/benchmarks/direct-line-scan-spans-20261109-linux.json.gz", + "docs/benchmarks/direct-line-scan-spans-20261109-macos.json.gz": "docs/benchmarks/direct-line-scan-spans-20261109-macos.json.gz" } } diff --git a/xff/content/BUILD.bazel b/xff/content/BUILD.bazel index 5f3149f01a..879d0b98d6 100644 --- a/xff/content/BUILD.bazel +++ b/xff/content/BUILD.bazel @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("//xff:cc.bzl", "cc_library", "cc_test") +load("//xff:cc.bzl", "cc_binary", "cc_library", "cc_test") package(default_visibility = ["//visibility:private"]) @@ -28,6 +28,7 @@ cc_library( "//xff/vfs:local_fs_cc", "//xff/vfs:vfs_cc", "@abseil-cpp//absl/functional:function_ref", + "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", "@mboworks_mbo//mbo/status:status_macros_cc", ], @@ -53,8 +54,11 @@ cc_test( ":line_match_cc", "//xff/vfs:vfs_cc", "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", "@googletest//:gtest", "@googletest//:gtest_main", + "@mboworks_mbo//mbo/status:status_macros_cc", + "@mboworks_mbo//mbo/testing:matchers_cc", "@mboworks_mbo//mbo/testing:status_cc", ], ) @@ -104,3 +108,19 @@ cc_test( "@mboworks_mbo//mbo/testing:status_cc", ], ) + +# Isolated direct/legacy stream cost experiment; validates line and byte totals before timing. +cc_binary( + name = "line_scan_benchmark", + srcs = ["line_scan_benchmark.cc"], + tags = [ + "clang-tidy", + "manual", + ], + deps = [ + ":line_match_cc", + "//xff/vfs:vfs_cc", + "@abseil-cpp//absl/status:statusor", + "@com_github_google_benchmark//:benchmark", + ], +) diff --git a/xff/content/line_match.cc b/xff/content/line_match.cc index 16b58f2a8e..edaa3460c3 100644 --- a/xff/content/line_match.cc +++ b/xff/content/line_match.cc @@ -16,6 +16,7 @@ #include "xff/content/line_match.h" #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include "absl/functional/function_ref.h" +#include "absl/status/status.h" #include "absl/status/statusor.h" #include "mbo/status/status_macros.h" #include "xff/vfs/local_fs.h" @@ -116,8 +118,21 @@ absl::StatusOr ScanLines( absl::FunctionRef visit) { LineScanResult result; StreamLines lines(visit); + // Direct reads initialize only their returned prefix. No other byte is observed. + std::array read_buffer; for (;;) { - MBO_ASSIGN_OR_RETURN(const auto block, stream.Read(65'536)); + std::string owned; + std::string_view block; + if (stream.SupportsReadInto()) { + MBO_ASSIGN_OR_RETURN(const auto count, stream.ReadInto(read_buffer)); + if (count > read_buffer.size()) { + return absl::DataLossError("ReadInto returned more bytes than its buffer"); + } + block = std::string_view(read_buffer.data(), count); + } else { + MBO_ASSIGN_OR_RETURN(owned, stream.Read(read_buffer.size())); + block = owned; + } if (block.empty()) { if (!result.binary) { lines.Finish(); @@ -125,7 +140,7 @@ absl::StatusOr ScanLines( return result; } const auto sniff = static_cast(std::min(block.size(), binary_scan_bytes)); - result.binary = result.binary || std::string_view(block).substr(0, sniff).contains('\0'); + result.binary = result.binary || block.substr(0, sniff).contains('\0'); binary_scan_bytes -= sniff; if (result.binary) { lines.Discard(); diff --git a/xff/content/line_scan_benchmark.cc b/xff/content/line_scan_benchmark.cc new file mode 100644 index 0000000000..14a93de8d7 --- /dev/null +++ b/xff/content/line_scan_benchmark.cc @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include + +#include "absl/status/statusor.h" +#include "benchmark/benchmark.h" +#include "xff/content/line_match.h" +#include "xff/vfs/read_source.h" + +namespace { +// The same bytes and owning Read implementation back both capabilities. Unlike the +// engine's memory source, this fixture can deliberately force the legacy path. +class FixtureStream final : public xff::vfs::ReadStream { + public: + FixtureStream(std::string_view data, std::size_t chunk, bool direct) : data_(data), chunk_(chunk), direct_(direct) {} + + absl::StatusOr Read(std::size_t maximum) override { + const auto size = std::min({maximum, chunk_, data_.size() - position_}); + const auto begin = position_; + position_ += size; + return std::string(data_.substr(begin, size)); + } + + bool SupportsReadInto() const noexcept override { return direct_; } + + absl::StatusOr ReadInto(std::span buffer) override { + const auto size = std::min({buffer.size(), chunk_, data_.size() - position_}); + data_.copy(buffer.data(), size, position_); + position_ += size; + return size; + } + + private: + std::string_view data_; + std::size_t chunk_; + bool direct_; + std::size_t position_ = 0; +}; + +struct Observed { + std::size_t lines = 0; + std::size_t bytes = 0; +}; + +void Scan(benchmark::State& state, bool direct, bool stop) { + const auto size = static_cast(state.range(0)); + const auto chunk = static_cast(state.range(1)); + std::string text(size, 'x'); + for (std::size_t index = 79; index < size; index += 80) { + text[index] = '\n'; + } + const auto expected_lines = (size / 80) + (size % 80 == 0 ? 0UZ : 1UZ); + const auto expected_bytes = size - (size / 80); + const auto check = [&] { + FixtureStream stream(text, chunk, direct); + Observed observed; + const auto result = xff::content::ScanLines(stream, 8'000, [&](std::size_t, std::string_view line) { + ++observed.lines; + observed.bytes += line.size(); + return !stop; + }); + return result.ok() && !result->binary && observed.lines == (stop ? std::min(expected_lines, 1UZ) : expected_lines) + && observed.bytes == (stop ? std::min(expected_bytes, 79UZ) : expected_bytes); + }; + if (!check()) { + state.SkipWithError("scanner output differs from fixture line/byte oracle"); + return; + } + for (auto iteration : state) { + benchmark::DoNotOptimize(iteration); + FixtureStream stream(text, chunk, direct); + Observed observed; + const auto result = xff::content::ScanLines(stream, 8'000, [&](std::size_t, std::string_view line) { + ++observed.lines; + observed.bytes += line.size(); + return !stop; + }); + if (!result.ok() || result->binary) { + state.SkipWithError("scanner failed"); + break; + } + benchmark::DoNotOptimize(observed); + } + state.SetBytesProcessed(state.iterations() * static_cast(size)); +} +} // namespace + +// XFF_ABI_POINTER: Google Benchmark's process argument interface. +int main(int argc, char** argv) { + benchmark::Initialize(&argc, argv); + benchmark::RegisterBenchmark("legacy/all", Scan, false, false) + ->ArgsProduct({{0, 256, 4'096, 65'536, 131'072}, {8'192, 65'536}}) + ->UseRealTime(); + benchmark::RegisterBenchmark("direct/all", Scan, true, false) + ->ArgsProduct({{0, 256, 4'096, 65'536, 131'072}, {8'192, 65'536}}) + ->UseRealTime(); + benchmark::RegisterBenchmark("legacy/stop", Scan, false, true) + ->ArgsProduct({{256, 65'536, 131'072}, {8'192, 65'536}}) + ->UseRealTime(); + benchmark::RegisterBenchmark("direct/stop", Scan, true, true) + ->ArgsProduct({{256, 65'536, 131'072}, {8'192, 65'536}}) + ->UseRealTime(); + benchmark::RunSpecifiedBenchmarks(); + benchmark::Shutdown(); +} diff --git a/xff/content/line_scan_test.cc b/xff/content/line_scan_test.cc index 55425b3d81..478bede455 100644 --- a/xff/content/line_scan_test.cc +++ b/xff/content/line_scan_test.cc @@ -5,26 +5,35 @@ #include #include #include +#include #include #include #include #include #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "mbo/status/status_macros.h" +#include "mbo/testing/matchers.h" #include "mbo/testing/status.h" #include "xff/content/line_match.h" #include "xff/vfs/read_source.h" namespace xff::content { namespace { +using ::mbo::testing::EqualsText; using ::mbo::testing::IsOkAndHolds; using ::mbo::testing::StatusIs; using ::testing::ElementsAre; using ::testing::Eq; using ::testing::Field; +using ::testing::Ge; +using ::testing::Gt; using ::testing::IsEmpty; +using ::testing::IsFalse; +using ::testing::IsTrue; using ::testing::Lt; class ChunkStream final : public vfs::ReadStream { @@ -191,5 +200,246 @@ TEST_F(LineScanTest, StreamingContextMatchesBufferedSelectionAcrossChunkBoundari } } } + +// A single byte cursor can change its advertised read capability after each read. +class CapabilityStream final : public vfs::ReadStream { + public: + std::string bytes; + std::vector direct_per_read{true}; + std::size_t chunk = 1; + std::size_t position = 0; + std::size_t read_calls = 0; + std::size_t owned_reads = 0; + std::size_t direct_reads = 0; + bool poison_unused = false; + bool invalid_count = false; + absl::Status tail; + + bool SupportsReadInto() const noexcept override { + return !direct_per_read.empty() && direct_per_read.at(std::min(read_calls, direct_per_read.size() - 1)); + } + + absl::StatusOr Read(std::size_t max_bytes) override { + ++owned_reads; + MBO_ASSIGN_OR_RETURN(const auto part, NextBytes(max_bytes)); + return std::string(part); + } + + absl::StatusOr ReadInto(std::span destination) override { + ++direct_reads; + if (!SupportsReadInto()) { + return absl::UnimplementedError("direct reads are currently unavailable"); + } + MBO_ASSIGN_OR_RETURN(const auto part, NextBytes(destination.size())); + if (poison_unused) { + std::ranges::fill(destination, '\0'); + } + std::ranges::copy(part, destination.begin()); + return invalid_count ? destination.size() + 1 : part.size(); + } + + private: + absl::StatusOr NextBytes(std::size_t max_bytes) { + ++read_calls; + if (position == bytes.size()) { + MBO_RETURN_IF_ERROR(tail); + return std::string_view{}; + } + const auto size = std::min({chunk, max_bytes, bytes.size() - position}); + const auto result = std::string_view(bytes).substr(position, size); + position += size; + return result; + } +}; + +struct DirectLineScanTest : ::testing::Test { + CapabilityStream stream; + std::vector lines; + + bool Visit(std::size_t number, std::string_view text) { + EXPECT_THAT(number, Eq(lines.size() + 1)); + lines.emplace_back(text); + return true; + } + + void Rewind() { + stream.position = 0; + stream.read_calls = 0; + stream.owned_reads = 0; + stream.direct_reads = 0; + lines.clear(); + } +}; + +TEST_F(DirectLineScanTest, ShortReadsPreserveCrLfBlankAndFinalLines) { + static constexpr std::array kChunks = std::to_array({ + 1, + 2, + 3, + 8, + 65'536, + }); + stream.bytes = "alpha\r\n\nbeta\r\nlast\r"; + for (const auto chunk : kChunks) { + SCOPED_TRACE(chunk); + Rewind(); + stream.chunk = chunk; + EXPECT_THAT( + ScanLines(stream, 8'000, [this](auto number, auto text) { return Visit(number, text); }), + IsOkAndHolds(Field("binary", &LineScanResult::binary, IsFalse()))); + EXPECT_THAT(lines, ElementsAre(EqualsText("alpha"), EqualsText(""), EqualsText("beta"), EqualsText("last"))); + EXPECT_THAT(stream.owned_reads, Eq(0)); + EXPECT_THAT(stream.direct_reads, Gt(0)); + } +} + +TEST_F(DirectLineScanTest, EmptyAndExactBlockInputsFinishWithoutPhantomLines) { + EXPECT_THAT( + ScanLines(stream, 0, [this](auto number, auto text) { return Visit(number, text); }), + IsOkAndHolds(Field("binary", &LineScanResult::binary, IsFalse()))); + EXPECT_THAT(lines, IsEmpty()); + Rewind(); + stream.chunk = 65'536; + stream.bytes.assign(65'536, 'x'); + EXPECT_THAT( + ScanLines(stream, 0, [this](auto number, auto text) { return Visit(number, text); }), + IsOkAndHolds(Field("binary", &LineScanResult::binary, IsFalse()))); + EXPECT_THAT(lines, ElementsAre(EqualsText(stream.bytes))); + EXPECT_THAT(stream.owned_reads, Eq(0)); + EXPECT_THAT(stream.direct_reads, Ge(2)); +} + +TEST_F(DirectLineScanTest, UnusedBufferTailCannotBecomeContentOrBinaryEvidence) { + stream.bytes = "ok\nx"; + stream.chunk = 2; + stream.poison_unused = true; + EXPECT_THAT( + ScanLines(stream, 8'000, [this](auto number, auto text) { return Visit(number, text); }), + IsOkAndHolds(Field("binary", &LineScanResult::binary, IsFalse()))); + EXPECT_THAT(lines, ElementsAre(EqualsText("ok"), EqualsText("x"))); + EXPECT_THAT(stream.owned_reads, Eq(0)); +} + +TEST_F(DirectLineScanTest, CapabilityChangesPreserveOneCursorPositionInBothDirections) { + static constexpr std::array kPatterns = std::to_array>({ + {false, true, false, true, false}, + {true, false, true, false, true}, + }); + stream.bytes = "a\nb\n"; + for (const auto& pattern : kPatterns) { + Rewind(); + stream.direct_per_read.assign(pattern.begin(), pattern.end()); + EXPECT_THAT( + ScanLines(stream, 8'000, [this](auto number, auto text) { return Visit(number, text); }), + IsOkAndHolds(Field("binary", &LineScanResult::binary, IsFalse()))); + EXPECT_THAT(lines, ElementsAre(EqualsText("a"), EqualsText("b"))); + EXPECT_THAT(stream.position, Eq(stream.bytes.size())); + EXPECT_THAT(stream.direct_reads, Gt(0)); + EXPECT_THAT(stream.owned_reads, Gt(0)); + } +} + +TEST_F(DirectLineScanTest, GiantLineSurvivesSwitchesBetweenOwnedAndDirectBlocks) { + const std::string giant(200'000, 'x'); + stream.bytes = giant + "\nnext"; + stream.chunk = 65'536; + stream.direct_per_read = {true, false, true, false, true}; + EXPECT_THAT( + ScanLines(stream, 0, [this](auto number, auto text) { return Visit(number, text); }), + IsOkAndHolds(Field("binary", &LineScanResult::binary, IsFalse()))); + EXPECT_THAT(lines, ElementsAre(EqualsText(giant), EqualsText("next"))); +} + +TEST_F(DirectLineScanTest, CallbackStopStillDrainsAndPropagatesLateReadFailure) { + stream.bytes = "hit\nlater\n"; + stream.direct_per_read = {true, false, true, false}; + stream.tail = absl::DataLossError("late read failure"); + EXPECT_THAT( + ScanLines( + stream, 0, + [this](auto number, auto text) { + Visit(number, text); + return false; + }), + StatusIs(absl::StatusCode::kDataLoss)); + EXPECT_THAT(lines, ElementsAre(EqualsText("hit"))); + EXPECT_THAT(stream.position, Eq(stream.bytes.size())); +} + +TEST_F(DirectLineScanTest, CallbackStopStillDetectsLateBinaryData) { + stream.bytes = "hit\nlater"; + stream.bytes.push_back('\0'); + stream.chunk = 2; + EXPECT_THAT( + ScanLines( + stream, std::numeric_limits::max(), + [this](auto number, auto text) { + Visit(number, text); + return false; + }), + IsOkAndHolds(Field("binary", &LineScanResult::binary, IsTrue()))); + EXPECT_THAT(lines, ElementsAre(EqualsText("hit"))); + EXPECT_THAT(stream.position, Eq(stream.bytes.size())); +} + +TEST_F(DirectLineScanTest, BinaryDataStillPropagatesLaterReadFailures) { + stream.bytes = std::string("\0later", 6); + stream.tail = absl::DataLossError("after binary detection"); + EXPECT_THAT( + ScanLines(stream, 8'000, [this](auto number, auto text) { return Visit(number, text); }), + StatusIs(absl::StatusCode::kDataLoss)); + EXPECT_THAT(lines, IsEmpty()); +} + +TEST_F(DirectLineScanTest, InvalidReturnedCountFailsBeforeVisitingAnyBytes) { + stream.bytes = "x"; + stream.invalid_count = true; + EXPECT_THAT( + ScanLines(stream, 8'000, [this](auto number, auto text) { return Visit(number, text); }), + StatusIs(absl::StatusCode::kDataLoss)); + EXPECT_THAT(lines, IsEmpty()); +} + +TEST_F(DirectLineScanTest, BinaryPrefixBoundaryIsIndependentOfShortReadBoundaries) { + stream.bytes = std::string(7'999, 'a') + std::string("\0\n", 2); + stream.chunk = 3; + for (const std::uint64_t limit : {0, 7'999, 8'000}) { + SCOPED_TRACE(limit); + Rewind(); + EXPECT_THAT( + ScanLines(stream, limit, [this](auto number, auto text) { return Visit(number, text); }), + IsOkAndHolds(Field("binary", &LineScanResult::binary, Eq(limit == 8'000)))); + if (limit == 8'000) { + EXPECT_THAT(lines, IsEmpty()); + } else { + EXPECT_THAT(lines, ElementsAre(EqualsText(stream.bytes.substr(0, stream.bytes.size() - 1)))); + } + } +} + +TEST_F(DirectLineScanTest, ReentrantScansPreserveBorrowedOuterLineUntilCallbackReturns) { + stream.bytes = "outer one\nouter two\n"; + stream.chunk = 65'536; + std::vector inner_lines; + EXPECT_THAT( + ScanLines( + stream, 0, + [&](auto number, auto text) { + CapabilityStream inner; + inner.bytes = "inner\n"; + EXPECT_THAT( + ScanLines( + inner, 0, + [&](auto, auto line) { + inner_lines.emplace_back(line); + return true; + }), + IsOkAndHolds(Field("binary", &LineScanResult::binary, IsFalse()))); + return Visit(number, text); + }), + IsOkAndHolds(Field("binary", &LineScanResult::binary, IsFalse()))); + EXPECT_THAT(lines, ElementsAre(EqualsText("outer one"), EqualsText("outer two"))); + EXPECT_THAT(inner_lines, ElementsAre(EqualsText("inner"), EqualsText("inner"))); +} } // namespace } // namespace xff::content