diff --git a/bazelmod/clang_tidy_targets.bzl b/bazelmod/clang_tidy_targets.bzl index 690ac7fb0bc..64e5e01a52c 100644 --- a/bazelmod/clang_tidy_targets.bzl +++ b/bazelmod/clang_tidy_targets.bzl @@ -12,6 +12,7 @@ CLANG_TIDY_MANUAL_TARGETS = [ "//xff/cli:globals_benchmark", "//xff/engine:read_accounting_test", "//xff/engine:compare_benchmark", + "//xff/engine:executor_benchmark", "//xff/engine:rg_benchmark", "//xff/engine:expression_benchmark", "//xff/engine:expression_run_benchmark", diff --git a/docs/benchmark-correctness-audit.md b/docs/benchmark-correctness-audit.md index aa31c563182..73bac288599 100644 --- a/docs/benchmark-correctness-audit.md +++ b/docs/benchmark-correctness-audit.md @@ -381,6 +381,19 @@ task size are unchanged; no performance or native macOS acceptance is claimed. ### Memory-accounting finding and remaining acceptance work +The separate diagnostic [executor lifecycle/task-grain probe](benchmarks/executor-dispatch-probe.md) +measures first dispatch and warmed leaf/claimable/drain sweeps with independently +checkable counts and unsigned checksums. Cold samples require a fresh process; +probe bookkeeping is included and no VFS/storage cost is measured. It does not +change runtime admission or establish a remaining-work threshold. Its completed +Linux diagnostic retains 4,860 checked fresh invocations across three independently +ordered default/clock-control sessions, all raw stdout/resources and independent +offline verification. Warmed controls show that thread reuse alone does not amortize +fine-grained dispatch; cheap work can still lose even with drains. These synthetic +results are not a VFS or full-engine acceptance measurement. Actual-workload +allocation/phase attribution, remaining-cost admission, metadata/slow-storage +controls and native macOS acceptance remain required. + Direct `/usr/bin/time -v` runs show approximately 5.3-5.4 MiB peak RSS for a small XFF invocation, while the Python comparison worker reports approximately 22-23 MiB for the same command. A trivial native command also reproduces the diff --git a/docs/benchmarks/executor-dispatch-probe.md b/docs/benchmarks/executor-dispatch-probe.md new file mode 100644 index 00000000000..d2bcf737835 --- /dev/null +++ b/docs/benchmarks/executor-dispatch-probe.md @@ -0,0 +1,196 @@ +# Executor lifecycle and task-grain probe + +`//xff/engine:executor_benchmark` is an explicit diagnostic, not a shipping +dependency or a new runtime admission rule. It separates thread-start calls, +first dispatch, warmed work and executor teardown. Use a fresh executable process +for every cold sample: a previous executor or other Abseil contention may already +have calibrated the process's clock. Ordinary in-process repetition cannot +establish fresh-process startup cost. + +Build before measuring, then invoke the resolved executable directly: + +```sh +bazel build --config=clang_release //xff/engine:executor_benchmark +bazel-bin/xff/engine/executor_benchmark leaf 3 8192 16 128 32 +``` + +The six arguments are dispatch style (`leaf`, `claimable`, `drain`), caller-inclusive +participants (1..64), items per warmed sweep, sweep count, chunk grain and unsigned +mixing iterations per item. All dimensions except mixing iterations must be positive; +item and queued-job totals must fit `size_t`. A mixing count of zero keeps only +the seed and checksum accumulation, providing a cheap-work control. + +Each run explicitly starts `N-1` threads; it does not apply the production +directory/stat/content admission heuristics. The first phase queues one trivial +job per background worker and performs one on the caller. The warmed phase uses +the same executor for all sweeps: + +- `leaf` queues at most `N-1` chunks beside a reserved caller chunk, joins that + batch, and repeats. Each chunk has its own producer/result state. +- `claimable` additionally uses the shared exactly-once callback and separate + result/wrapper states from the caller-participation experiment. After its own + chunk, the caller can claim each still-queued required chunk before joining. +- `drain` queues at most one leaf drain per background worker per sweep. An + atomic chunk index assigns disjoint work to those drains and the caller. + Workers never submit or wait for other executor jobs. Some drains can be empty + if another participant claims the work first; observed identities count only + participants that actually process items. + +The work value for item `i` starts at `i+1`, then repeatedly applies unsigned +64-bit xor-shifts by left 7, right 9, and left 8, in that order. Each phase reports +the modulo-64-bit sum of item values, executed item count, queued-job count, +observed participant count and whether the caller processed work. These permit +an independent oracle, not just a successful exit. A single-worker run never +queues work. Correctness tests exercise all modes, partial chunks, budgets +1/2/3/10, independently known sums and rejected configurations. + +Phase times include probe bookkeeping (including caller-side accumulation of +participant identities), queue/producer operations and synthetic work. Vector +preparation and final destruction are outside some phase boundaries, and the +native process's wall time includes executable startup and JSON output as well. +Do not treat the phase sum as the process wall time or infer an isolated cost for +every queue operation. Thread identities are not forced to rendezvous, which +would add synchronization to the costs being investigated. + +This does not measure VFS reads, frontier path/storage accounting, lazy metadata, +expression evaluation, content scanning, output rendering, actual storage latency +or RSS improvements. In particular, `claimable` is not a complete walk benchmark. +Its batch join pattern differs from a sliding nested frontier. Allocation and +native full-workload profiles remain necessary before tuning those mechanisms. + +Retain exact source/tree, compiler flags, binary/helper hashes, raw stdout, all +fresh-process samples and verified affinity. Finish every build, sanitizer and +lint before timing collection. Rotate dispatch styles and grain sizes in repeated +independent sessions; report cold and warm costs separately. Keep optional +clock-control builds separate from the default binary. The probe runs portably +on Linux/macOS, but Linux results do not establish native M5 Pro safety or +P/E-core scheduling equivalence. + +## Retained Linux diagnostic (2026-10-10) + +The terminal collection used source `c8929d16c77e318b15d87c5bfcbf07af0fa7058e` +and tree `58bc0b9b3a4626c63ae859557551c93308e9e0a4`, before the evidence and +release-page mapping were added. It ran from `06:27:51` to `06:33:18` UTC. +Both binaries use the historical workspace `clang_release` configuration and +Clang 23.1.2. System/home RC files were disabled, the workspace RC was announced, +and no workspace user RC existed. The explicit retained ICU 70 build-library +path is recorded; a rejected missing-library attempt is retained, not measured. +No source, configuration, build, sanitizer or lint work overlapped collection. + +The default executable SHA-256 is +`c730092bdc474154024799b1fa9348484853489bb682c6164ee14888113d2051`. +The separate clock-control executable SHA-256 is +`9faa695cda7dba14b2c0226d3b950b8927a18bc4c9e4bdc64f60a1d17fa69812`; +its only additional compilation option is +`--copt=-DABSL_USE_UNSCALED_CYCLECLOCK=0`. This is a diagnostic control, not a +proposed production default. Native resource accounting uses driver +`0e296e40ca6bf523215d80d1dc02deb3503b796c` and helper SHA-256 +`60bcb395bb3d0eccccd0a3d0cb2407538b1f96c45d5e1e156a0bd4522f93328e`. +The original executable/helper bytes remain at the indexed local paths and were +rehash-verified on export; the public bundle excludes those binary bytes. + +Each invocation executes 8 sweeps of 8,192 items. The matrix covers all three +dispatch styles, participant counts 1/3/10, grains 1/16/128 and mixing counts +0/32/512. Three complete independent orderings use declared, reverse-declared +and seeded-shuffled order (`20261010`). Default/control order alternates by +cell and repetition. Each version/cell has one fresh-process warmup and nine +fresh-process samples: 486 cells, 486 warmups, 4,374 samples and **4,860 checked +invocations**. The independent verifier recalculates checksums with a separate +unsigned-mixing implementation and checks every raw result, work/job total, +contributor bound, command/order, native resource field and estimator. + +`taskset` restricts each invocation to the first N of ten distinct allowed +physical `(package, core)` pairs, CPUs 0..9. The manifest retains the allowed +CPU list, topology and start/end load averages. It does not record CPU model, +kernel version or per-sample thermal/load observations. This is repeated, +reordered diagnostic evidence on this host, not a host-independent calibrated +dispatch constant. + +### Cold and warmed results + +Across all retained per-cell nine-sample cold medians, median start-call plus +first-dispatch time is 3,242.70 us for three participants and 3,268.54 us for ten +with the default clock. Corresponding clock-control medians are 86.28 us and +298.70 us. These phase measurements are consistent with the separately profiled +one-time Linux/x86 Abseil clock-calibration cliff; they do not attribute 3 ms to +each `pthread_create` or isolate creation from dispatch/bookkeeping. + +Selected default-clock warmed results follow. Times are the median of three +session estimates, each the arithmetic mean of the seven lowest warmed-phase +times out of nine. Selection uses the warmed phase, **not native process wall +time**; this is not the full-engine grid estimator. Each row uses its own +same-style/grain serial control. + +| Mix iterations | Grain | Style | Serial ms | 3 participants ms | 10 participants ms | +| -------------: | ----: | :-------- | --------: | ----------------: | -----------------: | +| 0 | 16 | leaf | 0.0197 | 33.6261 | 10.9164 | +| 0 | 16 | drain | 0.0195 | 0.1948 | 0.1852 | +| 32 | 16 | leaf | 1.5625 | 34.3747 | 11.3201 | +| 32 | 16 | claimable | 1.5739 | 34.0290 | 11.1849 | +| 32 | 16 | drain | 1.5595 | 0.6466 | 0.5831 | +| 512 | 128 | leaf | 35.6879 | 16.6467 | 9.6011 | +| 512 | 128 | drain | 35.7687 | 12.3751 | 5.0017 | + +For the 32-iteration, grain-16, three-participant case, separate leaf batches +take 21.79..22.08 times their same-session serial controls; drains take +0.410..0.422 times theirs. Both patterns reuse the same threads. Conversely, +zero-work drains still lose to serial work. Enough useful work and dispatch +granularity matter independently of thread reuse and thread caps. + +The 32-iteration drain's warmed saving alone is less than default cold startup. +An admission rule must consider the useful work **still available**, cold versus +warm executor costs and diminishing returns from extra participants. Completed +work is not remaining backlog. None of these synthetic mixing counts establishes +a file/stat/content threshold. Actual VFS latency, metadata and slow-storage +controls are still required. Long drains can also delay another queue class; +this probe does not justify replacing the bounded directory frontier with an +unbounded drain or weakening pruning, cancellation, ordered results or memory +accounting. + +### Lossless review bundle + +The [bundle index](native-executor-dispatch-controls.json) records compressed +and original byte counts/SHA-256 values for all nine archives: + +- Raw stdout, warmups, samples, native resources and exact commands: + [session 1](native-executor-dispatch-raw-1.gz), + [session 2](native-executor-dispatch-raw-2.gz), + [session 3](native-executor-dispatch-raw-3.gz). +- Original per-session analysis arrays: + [session 1](native-executor-dispatch-analysis-1.gz), + [session 2](native-executor-dispatch-analysis-2.gz), + [session 3](native-executor-dispatch-analysis-3.gz). +- [Independently verified analysis](native-executor-dispatch-verified-analysis.gz) + and [same-session ratio summary](native-executor-dispatch-summary.gz). +- [Provenance and reproduction scripts](native-executor-dispatch-provenance.gz): + frozen manifest/configuration/source, compiler identity, build logs, helper and + driver source, complete collection log, original verifier/summary/export + scripts, offline verifier, and local validation logs. + +To independently verify the published archives without building, executing a +measured binary, contacting GitHub or accessing the original `/tmp` paths, extract +the offline verifier from a checked-out bundle and inspect it before running: + +```sh +python3.13 - <<'PY' +import gzip +import json +from pathlib import Path + +bundle = Path("docs/benchmarks") +metadata = json.loads(gzip.decompress( + (bundle / "native-executor-dispatch-provenance.gz").read_bytes())) +script = metadata["scripts"]["offline_verifier"]["text"] +with Path("/tmp/xff-executor-review-verifier.py").open("x") as output: + output.write(script) +PY +python3.13 /tmp/xff-executor-review-verifier.py --bundle=docs/benchmarks +``` + +The offline verifier checks all archive hashes and independently recomputes the +4,860 output oracles, all three orderings, native-resource relationships, all +seven-of-nine estimates and the published ratio summary. It cannot verify the +excluded executable bytes. The collection script is Linux-specific (`taskset` +and sysfs); the C++ diagnostic is portable, but neither native M5 Pro execution +nor equivalent P/E-core scheduling has been established. No shipping threshold, +full-engine performance acceptance or completion of the audit is claimed. diff --git a/docs/benchmarks/native-executor-dispatch-analysis-1.gz b/docs/benchmarks/native-executor-dispatch-analysis-1.gz new file mode 100644 index 00000000000..540e8c6e82a Binary files /dev/null and b/docs/benchmarks/native-executor-dispatch-analysis-1.gz differ diff --git a/docs/benchmarks/native-executor-dispatch-analysis-2.gz b/docs/benchmarks/native-executor-dispatch-analysis-2.gz new file mode 100644 index 00000000000..e43ca21dcaa Binary files /dev/null and b/docs/benchmarks/native-executor-dispatch-analysis-2.gz differ diff --git a/docs/benchmarks/native-executor-dispatch-analysis-3.gz b/docs/benchmarks/native-executor-dispatch-analysis-3.gz new file mode 100644 index 00000000000..4bc4ead5506 Binary files /dev/null and b/docs/benchmarks/native-executor-dispatch-analysis-3.gz differ diff --git a/docs/benchmarks/native-executor-dispatch-controls.json b/docs/benchmarks/native-executor-dispatch-controls.json new file mode 100644 index 00000000000..ca47973fce8 --- /dev/null +++ b/docs/benchmarks/native-executor-dispatch-controls.json @@ -0,0 +1,148 @@ +{ + "purpose": "synthetic diagnostic; not engine or native macOS acceptance", + "source_revision": "c8929d16c77e318b15d87c5bfcbf07af0fa7058e", + "source_tree": "58bc0b9b3a4626c63ae859557551c93308e9e0a4", + "started_utc": "2026-10-10T06:27:51Z", + "completed_utc": "2026-10-10T06:33:18Z", + "invocations": 4860, + "cells": 486, + "warmups": 486, + "raw_samples": 4374, + "physical_cpus": [ + { + "cpu": 0, + "package": 0, + "core": 0 + }, + { + "cpu": 1, + "package": 0, + "core": 1 + }, + { + "cpu": 2, + "package": 0, + "core": 2 + }, + { + "cpu": 3, + "package": 0, + "core": 3 + }, + { + "cpu": 4, + "package": 0, + "core": 4 + }, + { + "cpu": 5, + "package": 0, + "core": 5 + }, + { + "cpu": 6, + "package": 0, + "core": 6 + }, + { + "cpu": 7, + "package": 0, + "core": 7 + }, + { + "cpu": 8, + "package": 0, + "core": 8 + }, + { + "cpu": 9, + "package": 0, + "core": 9 + } + ], + "binaries": { + "default": { + "path": "/tmp/xff-executor-dispatch-native-session.d9IxJs/executor-frozen-default", + "sha256": "c730092bdc474154024799b1fa9348484853489bb682c6164ee14888113d2051" + }, + "clock-control": { + "path": "/tmp/xff-executor-dispatch-native-session.d9IxJs/executor-frozen-clock-control", + "sha256": "9faa695cda7dba14b2c0226d3b950b8927a18bc4c9e4bdc64f60a1d17fa69812" + } + }, + "helper": { + "path": "/tmp/xff-perf-baseline-exact772-native-session1/resource-launcher", + "sha256": "60bcb395bb3d0eccccd0a3d0cb2407538b1f96c45d5e1e156a0bd4522f93328e" + }, + "artifacts": { + "raw-1": { + "name": "native-executor-dispatch-raw-1.gz", + "bytes": 143901, + "sha256": "69a8cc9e6cd1d29964433832902407547ad90c91a56c6098df3d6185c753e78b", + "raw_bytes": 3036184, + "raw_sha256": "ce825b4a4160314f8e69bedc12afdd33bbd57ebadc807e7c55e0f9f81ec3514e" + }, + "analysis-1": { + "name": "native-executor-dispatch-analysis-1.gz", + "bytes": 37920, + "sha256": "3ccf411fe8e0570b6c07dbe81c1622171109ba78bef4630b8b5767cdb28f6fea", + "raw_bytes": 1292653, + "raw_sha256": "b7a20c6e478bfa1a35af9cbdc6435b98d2f32eb65764372485d379c79c1a0e28" + }, + "raw-2": { + "name": "native-executor-dispatch-raw-2.gz", + "bytes": 143265, + "sha256": "3f146087bfd95f2b01e594e885fd82e25e8e706fba9d87bcdb17507c8bd0c450", + "raw_bytes": 3036228, + "raw_sha256": "0287b68d29ea2f0e4aa8d5313a43059ce2e60812c0b939da5524ea3aedbc8952" + }, + "analysis-2": { + "name": "native-executor-dispatch-analysis-2.gz", + "bytes": 37676, + "sha256": "b3a5071e78f9981ead6615c923f9053febd205e0f687b58ff917e55c7605206b", + "raw_bytes": 1292581, + "raw_sha256": "d1a85168f4cb6a402b25cc41f36b5e05f40ae6e729b244d9abb3ac4885d099ef" + }, + "raw-3": { + "name": "native-executor-dispatch-raw-3.gz", + "bytes": 148628, + "sha256": "d95f8ed1ae69a17323d40eb4ad354d6bc368aefc17f799a80bb0f2947ae490b4", + "raw_bytes": 3036175, + "raw_sha256": "941c7ce8501c474572ea41433d1aedf0a89742a6833aee420263a6f2039288a5" + }, + "analysis-3": { + "name": "native-executor-dispatch-analysis-3.gz", + "bytes": 39111, + "sha256": "93f2eac45ef6b922f885f6bbc38dfa1c0b8236db3d37bf7575e27f90f964b779", + "raw_bytes": 1292335, + "raw_sha256": "9c1a0293aaeb312bf946ca88ee70fa52328b037bfa683cff0122ab6e0a5cc17b" + }, + "verified-analysis": { + "name": "native-executor-dispatch-verified-analysis.gz", + "bytes": 114543, + "sha256": "eb7d80ff07505cba6fef8a5f955e1e53572069b061cae5f9db99cd9df85ba8e5", + "raw_bytes": 4154647, + "raw_sha256": "3417ca03736d9c3a45c66177a5d5e18a464a6df3b021ff9edd514ae615c176cd" + }, + "summary": { + "name": "native-executor-dispatch-summary.gz", + "bytes": 2949, + "sha256": "a4d09625c8e5b96da6c18bc72d65548c7773a8af43668a0590ac3a209e45bbbe", + "raw_bytes": 17033, + "raw_sha256": "0651ce65e462b066ed223c3fc8dfd71dd953feea4a966d9b1f81eeedf94fb4d7" + }, + "provenance": { + "name": "native-executor-dispatch-provenance.gz", + "bytes": 61448, + "sha256": "d70c6f63f2607345a1a68147124ba4bdb6285ded1c93ae69ce80f24123be4954", + "raw_bytes": 280625, + "raw_sha256": "cc3da9ef7092d042f19f42a4d3b0f52c3022cd39094dab0c599e14cca996bea4" + } + }, + "exclusions": [ + "Executable/helper bytes remain at recorded local paths; only hashes, source and logs are exported.", + "No VFS, metadata, storage, full engine A/B, native M5 Pro or remaining-cost admission evidence.", + "Phase times include diagnostic bookkeeping; fastest seven selects warmed phase, not process wall time.", + "CPU model, kernel version and per-sample thermal/load observations were not recorded by this harness." + ] +} diff --git a/docs/benchmarks/native-executor-dispatch-provenance.gz b/docs/benchmarks/native-executor-dispatch-provenance.gz new file mode 100644 index 00000000000..9a49a8a6dfd Binary files /dev/null and b/docs/benchmarks/native-executor-dispatch-provenance.gz differ diff --git a/docs/benchmarks/native-executor-dispatch-raw-1.gz b/docs/benchmarks/native-executor-dispatch-raw-1.gz new file mode 100644 index 00000000000..adf0f399180 Binary files /dev/null and b/docs/benchmarks/native-executor-dispatch-raw-1.gz differ diff --git a/docs/benchmarks/native-executor-dispatch-raw-2.gz b/docs/benchmarks/native-executor-dispatch-raw-2.gz new file mode 100644 index 00000000000..ad0edbfbf5a Binary files /dev/null and b/docs/benchmarks/native-executor-dispatch-raw-2.gz differ diff --git a/docs/benchmarks/native-executor-dispatch-raw-3.gz b/docs/benchmarks/native-executor-dispatch-raw-3.gz new file mode 100644 index 00000000000..818608cc525 Binary files /dev/null and b/docs/benchmarks/native-executor-dispatch-raw-3.gz differ diff --git a/docs/benchmarks/native-executor-dispatch-summary.gz b/docs/benchmarks/native-executor-dispatch-summary.gz new file mode 100644 index 00000000000..c47d24f3190 Binary files /dev/null and b/docs/benchmarks/native-executor-dispatch-summary.gz differ diff --git a/docs/benchmarks/native-executor-dispatch-verified-analysis.gz b/docs/benchmarks/native-executor-dispatch-verified-analysis.gz new file mode 100644 index 00000000000..95aebf971ed Binary files /dev/null and b/docs/benchmarks/native-executor-dispatch-verified-analysis.gz differ diff --git a/release-site.json b/release-site.json index bf7a7a0c4c7..9e7b84aac24 100644 --- a/release-site.json +++ b/release-site.json @@ -4,6 +4,7 @@ "CHANGELOG.md": "CHANGELOG.html", "CONTRIBUTING.md": "CONTRIBUTING.html", "docs/benchmarks/execution-resources.md": "docs/benchmarks/execution-resources.html", + "docs/benchmarks/executor-dispatch-probe.md": "docs/benchmarks/executor-dispatch-probe.html", "docs/execution-resources.md": "docs/execution-resources.html", "docs/ci-performance.md": "docs/ci-performance.html", "docs/coverage.md": "docs/coverage.html", diff --git a/xff/engine/BUILD.bazel b/xff/engine/BUILD.bazel index 9e923681db1..158ff7a1b74 100644 --- a/xff/engine/BUILD.bazel +++ b/xff/engine/BUILD.bazel @@ -44,6 +44,45 @@ cc_test( ], ) +cc_library( + name = "executor_measurement_cc", + srcs = ["executor_measurement.cc"], + hdrs = ["executor_measurement.h"], + deps = [ + ":run_executor_cc", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + ], +) + +cc_test( + name = "executor_measurement_test", + size = "small", + srcs = ["executor_measurement_test.cc"], + deps = [ + ":executor_measurement_cc", + "@abseil-cpp//absl/status", + "@googletest//:gtest", + "@googletest//:gtest_main", + "@mboworks_mbo//mbo/testing:status_cc", + ], +) + +# Fresh-process lifecycle / warmed-dispatch diagnostic, not runtime admission. +cc_binary( + name = "executor_benchmark", + srcs = ["executor_benchmark.cc"], + tags = [ + "clang-tidy", + "manual", + ], + deps = [ + ":executor_measurement_cc", + "@abseil-cpp//absl/strings", + "@nlohmann_json//:json", + ], +) + # Directory traversal over a vfs::FileSystem: a worker pool reads (readdir+lstat) # in parallel while a single coordinator thread visits in --sort order. cc_library( diff --git a/xff/engine/executor_benchmark.cc b/xff/engine/executor_benchmark.cc new file mode 100644 index 00000000000..5bc616a00d9 --- /dev/null +++ b/xff/engine/executor_benchmark.cc @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include + +#include "absl/strings/numbers.h" +#include "nlohmann/json.hpp" +#include "xff/engine/executor_measurement.h" + +namespace { + +using ::xff::engine::ExecutorProbePhase; + +nlohmann::json Phase(const ExecutorProbePhase& phase) { + return { + {"seconds", phase.seconds}, + {"items", phase.items}, + {"queued_jobs", phase.queued_jobs}, + {"observed_participants", phase.observed_participants}, + {"caller_participated", phase.caller_participated}, + {"checksum", phase.checksum}, + }; +} + +// XFF_HOST_IO: diagnostic executable's result/error stream adapter; no path I/O. +int Error(std::string_view message) { + std::cerr << message << '\n'; + return 2; +} + +// XFF_HOST_IO: diagnostic executable's result stream adapter; no path I/O. +void Emit(const nlohmann::json& report) { + std::cout << report.dump() << '\n'; +} + +} // namespace + +// XFF_ABI_POINTER: standard process entry point; argv is converted inside this adapter. +int main(int argc, char** argv) { + if (argc != 7) { + return Error("usage: executor_benchmark leaf|claimable|drain PARTICIPANTS ITEMS SWEEPS GRAIN WORK_UNITS"); + } + const std::span argument_view(argv, static_cast(argc)); + const std::vector arguments(argument_view.begin(), argument_view.end()); + const auto dispatch = xff::engine::ParseExecutorProbeDispatch(arguments.at(1)); + if (!dispatch.ok()) { + return Error(dispatch.status().message()); + } + xff::engine::ExecutorProbeConfig config{.dispatch = *dispatch}; + if (!absl::SimpleAtoi(arguments.at(2), &config.participants) || !absl::SimpleAtoi(arguments.at(3), &config.items) + || !absl::SimpleAtoi(arguments.at(4), &config.sweeps) || !absl::SimpleAtoi(arguments.at(5), &config.grain) + || !absl::SimpleAtoi(arguments.at(6), &config.work_units)) { + return Error("probe dimensions must be unsigned integers"); + } + const auto result = xff::engine::MeasureExecutor(config); + if (!result.ok()) { + return Error(result.status().message()); + } + Emit({ + {"dispatch", arguments.at(1)}, + {"participants", config.participants}, + {"items", config.items}, + {"sweeps", config.sweeps}, + {"grain", config.grain}, + {"work_units", config.work_units}, + {"background_workers", result->background_workers}, + {"start_seconds", result->start_seconds}, + {"first_dispatch", Phase(result->first_dispatch)}, + {"warmed", Phase(result->warmed)}, + {"teardown_seconds", result->teardown_seconds}, + {"measurement", "synthetic executor phases including bookkeeping; fresh process required for cold costs"}, + }); + return 0; +} diff --git a/xff/engine/executor_measurement.cc b/xff/engine/executor_measurement.cc new file mode 100644 index 00000000000..bcdd741283d --- /dev/null +++ b/xff/engine/executor_measurement.cc @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +// SPDX-License-Identifier: Apache-2.0 + +#include "xff/engine/executor_measurement.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "xff/engine/run_executor.h" + +namespace xff::engine { +namespace { + +using Clock = std::chrono::steady_clock; + +double Elapsed(Clock::time_point start) { + return std::chrono::duration(Clock::now() - start).count(); +} + +struct Chunk { + std::size_t items = 0; + std::uint64_t checksum = 0; + std::thread::id participant = std::this_thread::get_id(); +}; + +// Runtime-dependent unsigned mixing keeps the work observable in the returned +// checksum. Zero units measures dispatch with only the seed and accumulation. +Chunk Work(std::size_t first, std::size_t end, std::size_t units) { + Chunk result; + for (std::size_t index = first; index < end; ++index) { + std::uint64_t value = index + 1; + for (std::size_t unit = 0; unit < units; ++unit) { + value ^= value << 7U; + value ^= value >> 9U; + value ^= value << 8U; + } + result.checksum += value; + ++result.items; + } + return result; +} + +class Accumulator final { + public: + void Add(const Chunk& chunk) { + phase_.items += chunk.items; + phase_.checksum += chunk.checksum; + if (chunk.items != 0) { + participants_.insert(chunk.participant); + } + } + + void Queued() { ++phase_.queued_jobs; } + + ExecutorProbePhase Finish(double seconds) && { + phase_.seconds = seconds; + phase_.observed_participants = participants_.size(); + phase_.caller_participated = participants_.contains(std::this_thread::get_id()); + return phase_; + } + + private: + ExecutorProbePhase phase_; + std::set participants_; +}; + +struct PendingChunk { + RunTask result; + RunTask wrapper; + std::shared_ptr work; + + Chunk Get() { + if (work) { + work->Run(); + } + auto chunk = result.Get(); + if (wrapper.Valid()) { + wrapper.Get(); + } + return chunk; + } +}; + +PendingChunk Queue( + RunExecutor& executor, + ExecutorProbeDispatch dispatch, + std::size_t first, + std::size_t end, + std::size_t units) { + const auto job = [first, end, units] { return Work(first, end, units); }; + if (dispatch != ExecutorProbeDispatch::kClaimable) { + return {.result = executor.Submit(job)}; + } + RunPromise promise; + auto result = promise.Task(); + const auto work = std::make_shared([job, promise = std::move(promise)] mutable { promise.SetValue(job()); }); + auto wrapper = executor.Submit([work] { work->Run(); }, RunTaskClass::kReadAhead); + return {.result = std::move(result), .wrapper = std::move(wrapper), .work = work}; +} + +ExecutorProbePhase FirstDispatch(RunExecutor& executor, std::size_t participants) { + Accumulator total; + std::vector pending; + pending.reserve(participants - 1); + const auto start = Clock::now(); + for (std::size_t index = 0; index + 1 < participants; ++index) { + pending.push_back(Queue(executor, ExecutorProbeDispatch::kLeaf, index, index + 1, 0)); + total.Queued(); + } + total.Add(Work(participants - 1, participants, 0)); + for (auto& task : pending) { + total.Add(task.Get()); + } + return std::move(total).Finish(Elapsed(start)); +} + +ExecutorProbePhase LeafSweeps(RunExecutor& executor, const ExecutorProbeConfig& config) { + Accumulator total; + std::vector pending; + pending.reserve(config.participants - 1); + const auto start = Clock::now(); + for (std::size_t sweep = 0; sweep < config.sweeps; ++sweep) { + std::size_t next = 0; + while (next < config.items) { + pending.clear(); + const std::size_t caller_first = next; + const std::size_t caller_end = next + std::min(config.grain, config.items - next); + next = caller_end; + for (std::size_t worker = 1; worker < config.participants && next < config.items; ++worker) { + const std::size_t end = next + std::min(config.grain, config.items - next); + pending.push_back(Queue(executor, config.dispatch, next, end, config.work_units)); + total.Queued(); + next = end; + } + total.Add(Work(caller_first, caller_end, config.work_units)); + for (auto& task : pending) { + total.Add(task.Get()); + } + } + } + return std::move(total).Finish(Elapsed(start)); +} + +ExecutorProbePhase DrainSweeps(RunExecutor& executor, const ExecutorProbeConfig& config) { + Accumulator total; + std::vector> pending; + pending.reserve(config.participants - 1); + // Only this atomic assigns chunk indices. Each drain owns its local checksum; + // only the coordinator merges returned chunks and changes total/pending. + std::atomic next = 0; + const std::size_t chunks = ((config.items - 1) / config.grain) + 1; + const auto drain = [&] { + Chunk result; + for (;;) { + const std::size_t index = next.fetch_add(1, std::memory_order_relaxed); + if (index >= chunks) { + return result; + } + const std::size_t first = index * config.grain; + const auto chunk = Work(first, first + std::min(config.grain, config.items - first), config.work_units); + result.items += chunk.items; + result.checksum += chunk.checksum; + } + }; + const auto start = Clock::now(); + for (std::size_t sweep = 0; sweep < config.sweeps; ++sweep) { + next.store(0, std::memory_order_relaxed); + pending.clear(); + for (std::size_t worker = 1; worker < config.participants; ++worker) { + pending.push_back(executor.Submit(drain)); + total.Queued(); + } + total.Add(drain()); + for (auto& task : pending) { + total.Add(task.Get()); + } + } + return std::move(total).Finish(Elapsed(start)); +} + +} // namespace + +absl::StatusOr ParseExecutorProbeDispatch(std::string_view name) { + if (name == "leaf") { + return ExecutorProbeDispatch::kLeaf; + } + if (name == "claimable") { + return ExecutorProbeDispatch::kClaimable; + } + if (name == "drain") { + return ExecutorProbeDispatch::kDrain; + } + return absl::InvalidArgumentError("dispatch must be leaf, claimable or drain"); +} + +absl::StatusOr MeasureExecutor(const ExecutorProbeConfig& config) { + if (config.participants == 0 || config.participants > 64 || config.items == 0 || config.sweeps == 0 + || config.grain == 0 || config.items > std::numeric_limits::max() / config.sweeps + || config.sweeps > std::numeric_limits::max() / config.participants + || config.items > std::numeric_limits::max() - config.participants) { + return absl::InvalidArgumentError("participants must be 1..64; items/sweeps/grain positive and representable"); + } + if (config.dispatch != ExecutorProbeDispatch::kLeaf && config.dispatch != ExecutorProbeDispatch::kClaimable + && config.dispatch != ExecutorProbeDispatch::kDrain) { + return absl::InvalidArgumentError("unknown dispatch"); + } + auto executor = std::make_unique(config.participants - 1); + ExecutorProbeMeasurement result; + auto start = Clock::now(); + executor->Start(config.participants - 1); + result.start_seconds = Elapsed(start); + result.background_workers = executor->worker_count(); + result.first_dispatch = FirstDispatch(*executor, config.participants); + result.warmed = + config.dispatch == ExecutorProbeDispatch::kDrain ? DrainSweeps(*executor, config) : LeafSweeps(*executor, config); + start = Clock::now(); + executor.reset(); + result.teardown_seconds = Elapsed(start); + return result; +} + +} // namespace xff::engine diff --git a/xff/engine/executor_measurement.h b/xff/engine/executor_measurement.h new file mode 100644 index 00000000000..09151dc5c22 --- /dev/null +++ b/xff/engine/executor_measurement.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +// SPDX-License-Identifier: Apache-2.0 + +#ifndef XFF_ENGINE_EXECUTOR_MEASUREMENT_H_ +#define XFF_ENGINE_EXECUTOR_MEASUREMENT_H_ + +#include +#include +#include + +#include "absl/status/statusor.h" + +namespace xff::engine { + +enum class ExecutorProbeDispatch { kLeaf, kClaimable, kDrain }; + +absl::StatusOr ParseExecutorProbeDispatch(std::string_view name); + +struct ExecutorProbeConfig { + ExecutorProbeDispatch dispatch = ExecutorProbeDispatch::kLeaf; + std::size_t participants = 1; + std::size_t items = 1'024; + std::size_t sweeps = 16; + std::size_t grain = 1; + std::size_t work_units = 0; +}; + +struct ExecutorProbePhase { + double seconds = 0; + std::size_t items = 0; + std::size_t queued_jobs = 0; + std::size_t observed_participants = 0; + bool caller_participated = false; + std::uint64_t checksum = 0; +}; + +struct ExecutorProbeMeasurement { + std::size_t background_workers = 0; + double start_seconds = 0; + ExecutorProbePhase first_dispatch; + ExecutorProbePhase warmed; + double teardown_seconds = 0; +}; + +// Diagnostic only: explicitly start N-1 workers, dispatch one cold batch, then +// sweep synthetic checksum work with a warm pool. Cold results require a fresh +// process: earlier Abseil contention may already have calibrated its clock. +// Phase times include probe bookkeeping, not just isolated queue operations. +// No VFS, traversal/frontier, storage or runtime admission policy is measured. +absl::StatusOr MeasureExecutor(const ExecutorProbeConfig& config); + +} // namespace xff::engine + +#endif // XFF_ENGINE_EXECUTOR_MEASUREMENT_H_ diff --git a/xff/engine/executor_measurement_test.cc b/xff/engine/executor_measurement_test.cc new file mode 100644 index 00000000000..af9a68a1842 --- /dev/null +++ b/xff/engine/executor_measurement_test.cc @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors +// SPDX-License-Identifier: Apache-2.0 + +#include "xff/engine/executor_measurement.h" + +#include +#include +#include + +#include "absl/status/status.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "mbo/testing/status.h" + +namespace xff::engine { +namespace { +using ::mbo::testing::IsOkAndHolds; +using ::mbo::testing::StatusIs; +using ::testing::Eq; +using ::testing::Ge; +using ::testing::IsTrue; +using ::testing::Le; + +struct ExecutorMeasurementTest : ::testing::Test { + static constexpr std::array kDispatches = { + ExecutorProbeDispatch::kLeaf, + ExecutorProbeDispatch::kClaimable, + ExecutorProbeDispatch::kDrain, + }; +}; + +TEST_F(ExecutorMeasurementTest, ParsesOnlyDiagnosticDispatches) { + EXPECT_THAT(ParseExecutorProbeDispatch("leaf"), IsOkAndHolds(ExecutorProbeDispatch::kLeaf)); + EXPECT_THAT(ParseExecutorProbeDispatch("claimable"), IsOkAndHolds(ExecutorProbeDispatch::kClaimable)); + EXPECT_THAT(ParseExecutorProbeDispatch("drain"), IsOkAndHolds(ExecutorProbeDispatch::kDrain)); + EXPECT_THAT(ParseExecutorProbeDispatch("unknown"), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST_F(ExecutorMeasurementTest, CountsEveryPartialChunkAndKeepsTheCallerInTheBudget) { + for (const auto dispatch : kDispatches) { + SCOPED_TRACE(static_cast(dispatch)); + for (const std::size_t participants : {1, 2, 3, 10}) { + SCOPED_TRACE(participants); + for (const std::size_t grain : {1, 16, 128}) { + SCOPED_TRACE(grain); + ASSERT_OK_AND_ASSIGN( + const auto result, + MeasureExecutor( + {.dispatch = dispatch, .participants = participants, .items = 513, .sweeps = 2, .grain = grain})); + EXPECT_THAT(result.background_workers, Eq(participants - 1)); + EXPECT_THAT(result.first_dispatch.items, Eq(participants)); + EXPECT_THAT(result.first_dispatch.queued_jobs, Eq(participants - 1)); + EXPECT_THAT(result.first_dispatch.checksum, Eq(participants * (participants + 1) / 2)); + EXPECT_THAT(result.first_dispatch.observed_participants, Le(participants)); + EXPECT_THAT(result.first_dispatch.caller_participated, IsTrue()); + EXPECT_THAT(result.warmed.items, Eq(1'026)); + EXPECT_THAT(result.warmed.checksum, Eq(263'682)); + EXPECT_THAT(result.warmed.observed_participants, Le(participants)); + // Drain workers may claim every chunk before their coordinator does. + // Caller participation is forced by leaf sweeps, not assumed for drains. + if (dispatch != ExecutorProbeDispatch::kDrain) { + EXPECT_THAT(result.warmed.caller_participated, IsTrue()); + } + if (participants == 1) { + EXPECT_THAT(result.warmed.queued_jobs, Eq(0)); + } + EXPECT_THAT(result.start_seconds, Ge(0.0)); + EXPECT_THAT(result.first_dispatch.seconds, Ge(0.0)); + EXPECT_THAT(result.warmed.seconds, Ge(0.0)); + EXPECT_THAT(result.teardown_seconds, Ge(0.0)); + } + } + } +} + +TEST_F(ExecutorMeasurementTest, RuntimeWorkHasTheIndependentlyKnownUnsignedChecksum) { + for (const auto dispatch : kDispatches) { + ASSERT_OK_AND_ASSIGN( + const auto result, + MeasureExecutor( + {.dispatch = dispatch, .participants = 3, .items = 2, .sweeps = 3, .grain = 1, .work_units = 1})); + EXPECT_THAT(result.warmed.items, Eq(6)); + EXPECT_THAT(result.warmed.checksum, Eq(3 * (33'153 + 66'306))); + } +} + +TEST_F(ExecutorMeasurementTest, RejectsInvalidOrUnrepresentableWork) { + const auto invalid = [](const ExecutorProbeConfig& config) { + EXPECT_THAT(MeasureExecutor(config), StatusIs(absl::StatusCode::kInvalidArgument)); + }; + invalid({.participants = 0}); + invalid({.participants = 65}); + invalid({.items = 0}); + invalid({.sweeps = 0}); + invalid({.grain = 0}); + invalid({.items = std::numeric_limits::max(), .sweeps = 2}); + invalid({.participants = 3, .items = 1, .sweeps = std::numeric_limits::max()}); + invalid({.dispatch = static_cast(99)}); +} + +} // namespace +} // namespace xff::engine