diff --git a/docs/benchmark-comparisons.md b/docs/benchmark-comparisons.md index aceab18c721..aa4abf17628 100644 --- a/docs/benchmark-comparisons.md +++ b/docs/benchmark-comparisons.md @@ -455,6 +455,8 @@ contract; their scheduling is separate from these ordinary PR and main workflows `tools/benchmark_landscape.py` renders existing comparison JSON as a standalone interactive HTML landscape. It requires no remeasurement. The 3D chart is open by default; its show/hide disclosure collapses the chart, controls and explanation without hiding the tables. +On the history page, a keyboard-accessible Show/Hide chart button sits on the right of the +Benchmark history title instead of separate chart headings; the history tables remain visible. Reopening resizes the chart to the available width. Absolute measurements are collapsed by default; the comparison table stays visible below the plot. Benchmark publication inserts the landscape above all tables on retained reports with the one-worker/one-larger-allocation matrix, including both hosted platforms' 1/3 groups and historical 1/4 groups. diff --git a/docs/benchmark-correctness-audit.md b/docs/benchmark-correctness-audit.md index 45fe5937afe..b03f336aca8 100644 --- a/docs/benchmark-correctness-audit.md +++ b/docs/benchmark-correctness-audit.md @@ -552,6 +552,20 @@ coordinators: its startup and worker-count interface assumes one coordinator. Thread reuse and count-based admission are not acceptance evidence for these requirements; native Linux and M5 Pro controls remain necessary. +### Executor dispatch evidence independent of engine adoption + +The retained [executor lifecycle/task-grain probe](benchmarks/executor-dispatch-probe.md) +separates fresh-process cold startup from warmed leaf/claimable/drain sweeps, +with independently checkable counts and unsigned checksums. Its nine lossless +archives preserve 4,860 checked invocations in three independent orders, raw +samples/resources, source identities and an offline verification script. +This evidence is extracted from [PR #1000](https://github.com/mboworks/xff/pull/1000) +without its C++ probe or unmerged executor dependency. Rebuilding the probe +requires its exact measured source, not main. Guest-visible topology is not +independent verification of physical-host affinity. The results do not establish +VFS/storage performance, a production admission threshold or native M5 Pro +acceptance; the engine experiments and the complete audit remain open. + ### Memory-accounting finding and remaining acceptance work Direct `/usr/bin/time -v` runs show approximately 5.3-5.4 MiB peak RSS for a small diff --git a/docs/benchmarks/executor-dispatch-probe.md b/docs/benchmarks/executor-dispatch-probe.md new file mode 100644 index 00000000000..0c88c911378 --- /dev/null +++ b/docs/benchmarks/executor-dispatch-probe.md @@ -0,0 +1,211 @@ +# Executor lifecycle and task-grain probe + +This page retains the measurement evidence from [PR #1000](https://github.com/mboworks/xff/pull/1000) +without merging its executor or diagnostic C++ targets. The probe target is not +available on main: reproducing the experiment requires its exact measured source, +`c8929d16c77e318b15d87c5bfcbf07af0fa7058e`. The lossless bundle can instead be +verified offline from main without building or executing any measured binary. + +At that source, `//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. + +In a separate checkout of the measured source, build before measuring, then +invoke the resolved executable directly. Do not build this target from main or +relabel a new build as verification of the historical executable bytes: + +```sh +git fetch origin audit/executor-dispatch-costs +git worktree add --detach /tmp/xff-executor-probe-repro c8929d16c77e318b15d87c5bfcbf07af0fa7058e +cd /tmp/xff-executor-probe-repro +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 uses portable +C++/Abseil, 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 +observer-reported `(package, core)` pairs, CPUs 0..9. These are guest-visible +topology observations, not independent verification of physical host cores or +QEMU thread affinity. The original manifest/index field `physical_cpus` is +preserved as recorded; its name does not establish physical-host pinning. +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 uses portable APIs, 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..312d25b75b2 --- /dev/null +++ b/docs/benchmarks/native-executor-dispatch-controls.json @@ -0,0 +1,152 @@ +{ + "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, + "cpu_allocation_scope": "guest-visible observer topology; physical-host pinning was not independently verified", + "historical_topology_field": "physical_cpus is preserved as recorded, not evidence of physical-host affinity", + "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.", + "Guest-visible distinct package/core pairs do not independently verify physical-host cores or QEMU thread affinity.", + "Diagnostic C++ targets and their executor dependency are not part of this evidence-only extraction onto main." + ] +} 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 17b980f5f26..e2fa3c0213d 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/tools/benchmark_landscape.py b/tools/benchmark_landscape.py index f27b026644f..62733d5ce2f 100644 --- a/tools/benchmark_landscape.py +++ b/tools/benchmark_landscape.py @@ -309,8 +309,8 @@ def view_controls(attribute): def history_panel(catalog): """A bounded catalog; only the selected report's six views are downloaded.""" data = json.dumps(catalog, allow_nan=False).replace('<', '\\u003c') - return ('

Comparison landscape history

' - '
3D comparison chart (show/hide)' + return ('
' + '
' '
' 'Help
' '

Select a measured dataset and version. Oldest is left; newest is right. ' @@ -383,6 +383,13 @@ def publish_history(root, catalog): if not separator: raise ValueError('incomplete benchmark explorer section') text = before + after + toggle_start, toggle_end = '', '' + if toggle_start in text: + before, _, rest = text.partition(toggle_start) + _, separator, after = rest.partition(toggle_end) + if not separator: + raise ValueError('incomplete benchmark chart toggle') + text = before + after selected = {} for item in catalog: record_path = root / item['report'] / 'report.json' @@ -399,6 +406,11 @@ def publish_history(root, catalog): report_path = root / item['report'] / 'report.json' if benchmark_records.exists(report_path): entries[index] = dict(item, **dataset_metadata(benchmark_records.read(report_path))) + heading_end = text.index('') + toggle = (toggle_start + '' + toggle_end) + text = text[:heading_end] + toggle + text[heading_end:] insertion = text.index('') + len('') page.write_text(text[:insertion] + start + history_panel(entries) + end + text[insertion:], encoding='utf-8') diff --git a/tools/benchmark_landscape_test.py b/tools/benchmark_landscape_test.py index 375f73df254..f1cbfb63ca0 100644 --- a/tools/benchmark_landscape_test.py +++ b/tools/benchmark_landscape_test.py @@ -407,6 +407,32 @@ def test_local_history_keeps_layout_revisions_as_separate_platforms(self): 'Local / zen5 / x86_64 / broad/v2 + deep/v2', }) + def test_history_has_no_duplicate_chart_heading(self): + for catalog in ([], [dict(label='Measured dataset')]): + with self.subTest(catalog=catalog): + text = landscape.history_panel(catalog) + self.assertIn('

', text) + self.assertIn('', text) + self.assertNotIn('3D comparison chart (show/hide)', text) + self.assertNotIn('Comparison landscape history', text) + + def test_history_fold_control_is_in_page_title_and_publication_is_idempotent(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + page = root / 'index.html' + page.write_text('

Benchmark history

Retained history
') + landscape.publish_history(root, []) + first = page.read_text() + title = first.split('

', 1)[1].split('

', 1)[0] + self.assertIn('Benchmark history', title) + self.assertIn('data-history-chart-toggle', title) + self.assertIn('aria-controls="benchmark-history-chart"', title) + self.assertIn('aria-expanded="true"', title) + self.assertEqual(first.count('data-history-chart-toggle'), 1) + self.assertTrue(first.endswith('Retained history
')) + landscape.publish_history(root, []) + self.assertEqual(page.read_text(), first) + def test_history_catalog_escapes_script_content_and_handles_empty_data(self): text = landscape.history_panel([dict(label='')]) self.assertNotIn('