Repository navigation
Conversation
There was a problem hiding this comment.
4 issues found across 74 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests/bench-oci/jvm.sh">
<violation number="1" location="tests/bench-oci/jvm.sh:13">
P1: Create the compiler output directory before invoking `javac`; otherwise every JVM benchmark sample fails with a missing destination-directory error.</violation>
</file>
<file name="tests/bench-corpus/Makefile">
<violation number="1" location="tests/bench-corpus/Makefile:5">
P3: The default `OUT` violates the Makefile’s own requirement and writes generated artifacts into the fixed corpus for ordinary `make` invocations. Use an external temporary default or require callers to provide `OUT`.</violation>
</file>
<file name="scripts/bench-promote.py">
<violation number="1" location="scripts/bench-promote.py:211">
P2: On a partial capture, the new medians are merged but `captured.<env>` keeps the previous run's date and host. The values no longer match the provenance recorded for them. Either update `captured` whenever any values change, or record the partial capture's provenance separately.</violation>
</file>
<file name=".github/workflows/bench-report.yml">
<violation number="1" location=".github/workflows/bench-report.yml:78">
P3: When a later run produces no report, this step is skipped and an earlier regression comment stays up. The file header says a later commit must rewrite that comment, so a missing-report run should also clear or mark the old comment as stale.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| src="$1" | ||
| work="$(mktemp -d /tmp/bench-jvm.XXXXXX)" | ||
| cp "$src"/*.java "$work/" | ||
| javac -d "$work/classes" "$work"/*.java |
There was a problem hiding this comment.
P1: Create the compiler output directory before invoking javac; otherwise every JVM benchmark sample fails with a missing destination-directory error.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/bench-oci/jvm.sh, line 13:
<comment>Create the compiler output directory before invoking `javac`; otherwise every JVM benchmark sample fails with a missing destination-directory error.</comment>
<file context>
@@ -0,0 +1,15 @@
+src="$1"
+work="$(mktemp -d /tmp/bench-jvm.XXXXXX)"
+cp "$src"/*.java "$work/"
+javac -d "$work/classes" "$work"/*.java
+java -cp "$work/classes" Bench > /dev/null
+rm -rf "$work"
</file context>
| javac -d "$work/classes" "$work"/*.java | |
| mkdir -p "$work/classes" | |
| javac -d "$work/classes" "$work"/*.java |
There was a problem hiding this comment.
javac creates the -d directory itself when it is missing. The jvm workload ran to exit 0 under elfuse with this script in eclipse-temurin:21 (all 12 samples in the local make bench-ci run, and one direct run after this review), so there is no failure to fix.
| set -euo pipefail | ||
| python3 scripts/bench-report.py \ | ||
| --report "$REPORT_PATH" --output "$COMMENT_PATH" \ | ||
| --environment elfuse-aarch64 --threshold 0.20 |
There was a problem hiding this comment.
The renderer is given a hardcoded --threshold 0.20, and the comment text above says ±20%, while bench-compare.py honors BENCH_REGRESSION_THRESHOLD and records the value it used as threshold in the artifact. Change the env var and the console log and the PR comment disagree about what counts as a regression. Have scripts/bench-report.py read threshold from the validated report (bounded to a sane range) instead of taking it from the workflow, and drop the flag here.
There was a problem hiding this comment.
Done in 0073af6. bench-report.py reads threshold from the validated report, limited to 0 through 1 because the comment shows it as ±N%, and the --threshold flag is gone. It also writes the all-inside text itself, so the workflow no longer holds a percentage: the script prints clean or changes, and the comment step only updates an existing comment on clean.
| try: | ||
| write_report(args.report_json, environment=env, | ||
| threshold=args.threshold, | ||
| metrics=comparison_metrics) |
There was a problem hiding this comment.
comparison_metrics only holds metrics present in both the baseline and the results, so a metric whose measurement failed never reaches the artifact. With --fail-on-missing the step fails, but the artifact is still uploaded and bench-report.yml still runs: if the remaining metrics are inside the threshold, the render is empty and the existing regression comment is overwritten with "No benchmark metric is outside ±20%". Put the missing metrics in the report (a status per entry) and have scripts/bench-report.py render them as failures, or skip the comment update when the comparison was incomplete.
There was a problem hiding this comment.
Done in 0073af6. bench-compare.py now writes each missing metric to the report with status: missing (compared ones carry status: compared), and bench-report.py lists them under "Missing measurements" and reports changes. A run with a failed measurement therefore never renders the all-inside text. Checked by dropping lat_fs.create from a results file: the comment lists it as missing, and the status is changes. b68579c extends this to a missing metric whose baseline is zero.
There was a problem hiding this comment.
All reported issues were addressed across 15 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| test_timeout: '' | ||
| job_timeout: 100 | ||
| shared_lock_wait: 15 | ||
| exclusive_lock_wait: 60 |
There was a problem hiding this comment.
Once this leg publishes its writer marker, host-lock.sh makes every new shared acquirer wait until the exclusive lock is granted, and that wait can run the full 60 minutes behind a TSAN leg. Legs from another run (and oci-smoke) that start in that window budget only 15 minutes, so they fail with a lock timeout that reads like a CI failure. Either make the shared wait cover exclusive_lock_wait plus the measurement window (raising job_timeout to match), or cap this wait at what the readers can absorb.
There was a problem hiding this comment.
Done in 45a63f0, the first option. The bench step now has timeout-minutes: 25 (it took 15 on the last run), exclusive_lock_wait is 45, the runtime budget of the longest sibling (TSAN), and every shared_lock_wait, oci-smoke included, is 75 = 45 + 25 + 5. Each job_timeout grows by the same 60 minutes, so Benchmark goes to 160. The comment above timeout-minutes states how the numbers relate.
| ) | ||
| if document.get("direction") != "lower-is-better": | ||
| raise ReportError("unsupported metric direction") | ||
| threshold = finite_number(document.get("threshold"), field="threshold") |
There was a problem hiding this comment.
The threshold and every baseline value come from the artifact, which the PR run built from its own tests/bench-baseline.json. A PR that refreshes the baseline with bench-promote.py is compared against itself, yet the comment still says "checked-in baseline". This job already checks out the default branch: load tests/bench-baseline.json and the threshold from that checkout, take only current from the artifact, and treat a trusted baseline metric absent from the artifact as missing.
There was a problem hiding this comment.
Done in ba4892d. The artifact (schema 2) now carries only the environment and the measured current values. bench-report.py loads bench-compare.py from its own checkout, the default branch, for flatten and DEFAULT_THRESHOLD, and compares against that checkout's tests/bench-baseline.json. A trusted baseline metric that the artifact lacks is listed under Missing measurements. The comment text now says "the default branch's baseline". The threshold therefore no longer comes from the artifact either, which replaces what 0073af6 did.
| lock_fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o666) | ||
| if mode == "exclusive": | ||
| marker_fd, marker_path = publish_writer() | ||
| fcntl.flock(lock_fd, fcntl.LOCK_EX) |
There was a problem hiding this comment.
If the job is cancelled while this call blocks (a new push does that through cancel-in-progress), the runner kills the acquire parent but not this setsid holder. It stays in flock(LOCK_EX) with no deadline and keeps its writer marker locked, so every new shared acquirer stalls behind it, and the parent's wait_seconds cleanup never runs. Record the parent pid before the fork and poll here with LOCK_EX | LOCK_NB, giving up once os.getppid() no longer matches. The shared loop below needs the same check.
There was a problem hiding this comment.
Done in 922bd60. The holder records the parent pid before the fork and polls with LOCK_EX | LOCK_NB. The shared loop polls with LOCK_SH | LOCK_NB. Once os.getppid() differs, both raise SystemExit, so the finally block removes the writer marker and the socket. Tested locally with a shared holder, an exclusive waiter and a shared waiter queued behind its marker: after kill -9 of both acquire parents, both holders were gone and no marker was left. The previous script kept the exclusive holder and its marker.
| -N "$BENCH_LMBENCH_REPS" "$@")" || rc=$? | ||
| value="$(printf '%s\n' "$out" | awk -F': ' -v l="$label" \ | ||
| '$1 == l && $2 ~ /microseconds/ { split($2, a, " "); print a[1]; exit }')" | ||
| if [ -n "$value" ]; then |
There was a problem hiding this comment.
value is accepted whatever rc is, so an attempt that printed its result line and then exited non-zero (a guest crash on exit, or timeout firing after the print) counts as a good sample in the median. lmbench_lat_fs has the same gap.
| if [ -n "$value" ]; then | |
| if [ "$rc" -eq 0 ] && [ -n "$value" ]; then |
There was a problem hiding this comment.
Done in 041aa46, in lmbench_metric and lmbench_lat_fs: the parsed value is cleared when rc is non-zero, so that attempt is retried and then recorded as ERR.
| fsdir="${fs_host}/fs" | ||
| echo probe > "$probe" | ||
| mkdir -p "$fsdir" | ||
| cp "${LMBENCH_DIR}/hello-s" /tmp/hello-s |
There was a problem hiding this comment.
This overwrites any existing /tmp/hello-s and removes it only on the success path. A bench_die later in Tier 1, or an interrupt, leaves both it and $fs_host behind because cleanup knows about neither. Keep both in globals that cleanup removes, and fail when /tmp/hello-s already exists instead of clobbering it.
There was a problem hiding this comment.
Done in a4bf814. T1_FS_HOST and T1_HELLO are globals that cleanup removes through tier1_unstage, which the success path calls too. /tmp/hello-s is created with set -C, so the suite fails when the file already exists, in the OrbStack machine as well. Tested: with an existing /tmp/hello-s, the suite stops with that message, the file is left as it was, and no elfuse-bench-fs directory stays behind.
| echo "bench-suite: real-workload sample '$metric' failed (rc=$rc, out='$out'):" >&2 | ||
| echo " $ref: $cmd" >&2 | ||
| tail -n 20 "${SAMPLES}.err" | sed 's/^/ | /' >&2 | ||
| exit 1 |
There was a problem hiding this comment.
One failed sample in Tier 3 (or in Tier 2 via timeit_sample_us) exits the suite before emit_json, so a single JVM flake discards every Tier 1 and Tier 2 result. Compare against baseline then never runs, no bench-pr-report artifact is uploaded, and an earlier regression comment on the PR stays as it was. Record the metric as ERR the way lmbench_metric does and continue, so bench-compare.py --fail-on-missing reports it as missing and the leg still fails.
There was a problem hiding this comment.
Done in 135d888. timeit_sample_us returns 1 instead of exiting. run_tier2_tool, run_startup_metric and Tier 3 then record the metric as ERR and move on, and emit_json writes {"status": "ERR"} for it. Tested by making rg and the node workload fail: the results file was written with ripgrep_ms and node_ms as ERR, and bench-compare.py --fail-on-missing exited 1 listing ripgrep_ms (node_ms has no baseline yet).
| # Tier-2 application benchmark tools for tests/bench-suite.sh plus their | ||
| # runtime dependency closure as resolved against the v3.21 APKINDEX (so:* | ||
| # dependencies mapped back to their providing packages). | ||
| "main:git" |
There was a problem hiding this comment.
These packages land in the shared fixture rootfs, and initramfs.cpio.gz is built from that tree. So python3, git, ripgrep and their libraries now ship in every test-matrix qemu boot, not only in the Benchmark leg, and the description does not mention that cost. Please state the initramfs size and boot-time change, or stage the bench tools as an overlay that only the bench path adds.
There was a problem hiding this comment.
Measured, and now stated in the description. The initramfs grows from 35.4 MB to 51.8 MB gzip, and the unpacked rootfs from 61 MB to 107 MB of the 2048 MB guest. qemu boot to ssh stays at 1.1 to 1.2 s with and without the tools, over 3 boots each (the runner polls once a second). The qemu bench column runs these tools inside the VM, so an overlay would save only that memory. I can split it out if you want the boot image kept minimal.
jserv
left a comment
There was a problem hiding this comment.
Rebase the current branch onto the upstream default branch and rework the series into functionally minimal commits, folding similar ones and enforcing the project's commit message rules.
| - name: Benchmark suite (make bench-ci) | ||
| if: ${{ matrix.run_bench }} | ||
| # Bounds the exclusive hold that every shared wait budgets for. | ||
| timeout-minutes: 25 |
There was a problem hiding this comment.
timeout-minutes: 25 bounds the whole step, including acquire exclusive, which is allowed to wait 45 minutes (exclusive_lock_wait). When a TSAN leg keeps its shared lock for longer than 25 minutes minus the bench runtime, the step is killed while still queueing, so the exclusive wait the job budget is built around is never available. Move release and acquire exclusive into their own step with timeout-minutes: 50, keep this one bounding only make bench-ci, and add a release step with if: always() right after it so Compare and the uploads do not run under the exclusive lock.
There was a problem hiding this comment.
Split in 6544180. Acquire exclusive host lock is now its own step with timeout-minutes: 50, the bench step bounds only make bench-ci at 25, and Release exclusive host lock runs with always() right after it, so Compare and the uploads run without the exclusive lock.
| f"{source}: metric '{prefix}' has a negative median") | ||
| flat[prefix] = number | ||
| return | ||
| if "status" in node: |
There was a problem hiding this comment.
flatten drops every {"status": "ERR"} node, so a failed metric surfaces only through missing, which is computed from the baseline. The four Tier 3 metrics have no baseline yet, so an OCI row that errors or times out leaves --fail-on-missing passing and the PR comment clean, which is not what the "Compare against baseline" step comment promises. Collect the ERR paths during the walk and count them as missing in main, independent of the baseline.
There was a problem hiding this comment.
Fixed. flatten now collects the path of each {"status": "ERR"} leaf, and bench-compare.py counts each one as missing whether or not it has a baseline (1b32aba). The report artifact carries them as failed, and bench-report.py lists them under Missing measurements with - as the baseline (b255cdd). With oci.python_ms ERR and no baseline, --fail-on-missing now exits 1 where it exited 0 before, and the self-test covers the comment.
| tmp_path = f"{pid_path}.{os.getpid()}.tmp" | ||
| with open(tmp_path, "w", encoding="ascii") as state: | ||
| state.write(f"{child}\n{control_path}\n") | ||
| os.replace(tmp_path, pid_path) |
There was a problem hiding this comment.
The holder already owns the lock when the state file is written. If the job is cancelled in that window or the write fails (full $RUNNER_TEMP), release finds no state, prints "nothing to release" and exits 0, and the orphaned holder keeps the lock for CI_HOST_LOCK_MAX_HOLD_MIN (120 minutes), timing out every Benchmark leg's exclusive wait meanwhile. child and control_path are both known right after fork, so write the state file before the select and remove it on the not-acquired path: the holder already listens before it locks, and it exits by itself when the parent dies while waiting.
There was a problem hiding this comment.
Fixed in 6544180. The state file is written right after fork, before the select, and removed on the timeout and holder-failure paths. Checked locally: the file exists during the wait, a kill -9 of the acquire makes the holder exit and release clean up, and a timed-out acquire leaves no state file.
| GUEST_WORK="$ORB_WORK" | ||
| local prep | ||
| prep="rm -rf $ORB_WORK && mkdir -p $ORB_WORK" | ||
| prep="$prep && cp -R $CORPUS $ORB_WORK/corpus" |
There was a problem hiding this comment.
$CORPUS goes into the orb run sh -c line unquoted, so a checkout path containing a space or a shell metacharacter splits or expands inside the machine and prep dies with "orbstack corpus prep failed". sh_quote already exists for this.
| prep="$prep && cp -R $CORPUS $ORB_WORK/corpus" | |
| prep="$prep && cp -R $(sh_quote "$CORPUS") $ORB_WORK/corpus" |
There was a problem hiding this comment.
Applied in 1b32aba. Not run under OrbStack here.
| with open(samples_path, encoding="utf-8") as fh: | ||
| for line in fh: | ||
| section, bench, metric, value = line.split() | ||
| rows = data.setdefault(section, {}).setdefault(bench, {}) |
There was a problem hiding this comment.
Every writer of $SAMPLES passes the metric name twice (startup_ms startup_ms, and record_err writes "$2" "$2"), so bench always equals metric, and the duplicate-metric check further down can never fire because each metric sits under its own bench key. Drop the bench column from the row format and key data by section and metric; repeated rows for one metric are then just its samples, and the dead check goes away.
There was a problem hiding this comment.
Fixed in 1b32aba. Rows are now <section> <metric> <value>, the emitter keys the samples by section and metric, and the duplicate check is gone.
| (void) waitpid(pid, NULL, 0); | ||
| return 1; | ||
| } | ||
| if (install_forwarder(SIGTERM) != 0 || install_forwarder(SIGINT) != 0 || |
There was a problem hiding this comment.
The workload is forked before these handlers exist. A cancellation or timeout signal landing between fork and here kills the wrapper with the default action, while the child has already moved to its own process group and survives into the next sample, which is what the forwarder is meant to prevent. Block SIGTERM/SIGINT/SIGHUP/SIGQUIT with sigprocmask before fork, restore the old mask in the child before execvp, and unblock in the parent once the forwarders are installed.
There was a problem hiding this comment.
Fixed in 1b32aba. The four signals are blocked before fork, the child restores the old mask before execvp, and the parent unblocks once the forwarders are installed; a signal held across fork makes the wrapper return 128 + signo. With SIGTERM sent 0 to 4 ms after start, 400 runs each, the old binary left 41 workloads running and the new one none.
|
Rebased onto main (170b5df) and folded the 30 commits into four: bceac52 (the fixture cache swap, a race that exists without the benchmark too), 1b32aba (the suite), 6544180 (the CI leg and the host lock), b255cdd (the PR comment reporter). Apart from one reflowed docstring, the tree is the same as before the fold. |
There was a problem hiding this comment.
All reported issues were addressed across 6 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
|
After the fold, the commit ids in my earlier replies no longer exist on the branch. Where each change lives now:
|
Release legs of two runs can save the fixture cache at the same time, and another runner can be restoring it. The save removed the cache and copied the new tree in place, so a restore could copy a half-written tree and two saves could interleave. Each save now copies to a directory of its own and swaps it in under a lockf lock that restores also take.
make bench runs tests/bench-suite.sh and writes build/bench-results.json. Tier 1 runs lmbench lat_syscall, lat_proc and lat_fs, built by tests/fetch-fixtures.sh from a pinned source snapshot. Tier 2 times application workloads over the Alpine fixture tools: startup, python startup, git status, ripgrep, zstd, and make over the fixed synthetic C tree in tests/bench-corpus. Tier 3 runs python, node, C compile and JVM workloads in OCI images pinned by digest, with the image pulls done before the timed samples. A sample that fails or times out records its metric as ERR; the other metrics still reach the results file. scripts/bench-compare.py compares a result file with tests/bench-baseline.json, fails on each metric more than 20 percent slower than its baseline unless --report-only is given, and counts an ERR metric as missing, with or without a baseline; --fail-on-missing keeps that fatal in report-only mode. scripts/bench-promote.py turns a result file into a baseline. make bench skips a tier whose prerequisites are missing, and make bench-ci fails instead. A failed lmbench download or build makes tests/fetch-fixtures.sh warn and drop lmbench rather than exit, so the other fixtures stay usable. The format, commentflow and pre-commit checks skip tests/bench-corpus, whose content is fixed by the benchmark. Closes sysprog21#195
The Build workflow gains a Benchmark leg on the self-hosted runner. It pulls the prerequisites, runs make bench-ci, compares the result with the baseline in report-only mode with --fail-on-missing, so a slow run does not fail the build but a failed metric does, and uploads the results. All self-hosted runners share one Mac, so a timing taken while another leg builds measures that leg too. scripts/ci/host-lock.sh is a machine-wide reader-writer lock: the other legs and the OCI smoke job hold it shared, and the Benchmark leg holds it exclusive only around make bench-ci, in a step of its own. A detached holder keeps the flock across steps and drops it when a later step asks over a Unix socket, or when the acquire that started it exits before the grant. A writer-intent marker stops new shared holders from starving the Benchmark leg, so every shared wait and job budget covers the exclusive wait plus the 25 minute measurement step.
The Benchmark leg uploads a data-only report: the measured value of each metric and the metrics that failed. bench-report.yml runs on the workflow_run event from the default branch, compares that report with the default branch's baseline and threshold through scripts/bench-report.py, and posts or updates one pull request comment. A pull request that edits the baseline or the threshold is therefore still compared with the merged ones, and its code never runs with the token that posts the comment. scripts/bench-report.py rejects a malformed report and a baseline of zero, and lists failed metrics as missing so a clean comment cannot replace a regression one. make check runs its self-test.
| core.setOutput('found', false); | ||
| return; | ||
| } | ||
| core.setOutput('found', report !== undefined); |
There was a problem hiding this comment.
When make bench-ci fails or the exclusive acquire times out, Compare against baseline is skipped, no bench-pr-report artifact is uploaded, and this job returns early. That leaves the previous commit's comment up, possibly listing regressions or a clean result the current head never produced. When no artifact is found and workflow_run.conclusion is not success, rewrite the existing marked comment to say the benchmark did not complete at head_sha, with a link to the run.
| CI_HOST_LOCK_WAIT_MIN=${{ matrix.exclusive_lock_wait }} \ | ||
| scripts/ci/host-lock.sh acquire exclusive |
There was a problem hiding this comment.
The exclusive holder outlives a crashed runner for the default CI_HOST_LOCK_MAX_HOLD_MIN of 120 minutes, while every shared acquirer gives up after 75. One runner dying during the measurement therefore fails every leg on the host for two hours. The measurement step is capped at 25 minutes, so cap the hold near that.
| CI_HOST_LOCK_WAIT_MIN=${{ matrix.exclusive_lock_wait }} \ | |
| scripts/ci/host-lock.sh acquire exclusive | |
| CI_HOST_LOCK_WAIT_MIN=${{ matrix.exclusive_lock_wait }} \ | |
| CI_HOST_LOCK_MAX_HOLD_MIN=30 \ | |
| scripts/ci/host-lock.sh acquire exclusive |
| if not math.isfinite(float(value)) or value < 0: | ||
| raise BenchPromoteError( | ||
| f"results {tag}.median must be finite and non-negative") |
There was a problem hiding this comment.
A median of 0 is promoted into the baseline, but bench-compare.py and bench-report.py both reject a baseline <= 0, so the next comparison reports that metric as unusable instead of comparing it. Apply the same rule here.
| if not math.isfinite(float(value)) or value < 0: | |
| raise BenchPromoteError( | |
| f"results {tag}.median must be finite and non-negative") | |
| if not math.isfinite(float(value)) or value <= 0: | |
| raise BenchPromoteError( | |
| f"results {tag}.median must be finite and positive") |
Supersedes #223, based on the benchmark suite @Max042004 wrote there.
make benchrunstests/bench-suite.shin three tiers and writesbuild/bench-results.json:lat_syscall,lat_procandlat_fs, built from a pinned source snapshot bytests/fetch-fixtures.sh.git status, ripgrep, zstd, make overtests/bench-corpus).scripts/elfuse-oci.py. Image pulls happen before the timed samples.BENCH_OCI=0skips the tier. The go row is left out: gofmt there can need a 65th thread, and elfuse gives a guest process 64 (Guest processes are limited to 64 threads #494).The Tier 2 tools land in the shared fixture rootfs, so every qemu boot carries them: the initramfs grows from 35.4 MB to 51.8 MB gzip (61 MB to 107 MB unpacked, out of the 2048 MB guest), and qemu boot to ssh stays at 1.1 to 1.2 s over 3 boots each.
The Build workflow gains a Benchmark leg that runs
make bench-ciand compares againsttests/bench-baseline.jsonin report-only mode;bench-report.ymlposts the result as a PR comment.scripts/ci/host-lock.shkeeps the other legs off the host while the Benchmark leg measures: they hold it shared for their own run, and the Benchmark leg holds it exclusive only aroundmake bench-ci.Changes from the last revision of #223, for the open review threads there:
BENCH_PREP_TIMEOUT(default 900 s) instead of the per-sample bound.bench-timeitrejects an elapsed time under 1 us instead of printing 0.tests/bench-corpus, and the corpus files have descriptive names.docs/testing.mdonly adds a benchmark section.BENCH_LMBENCH_ENOUGHrestores lmbench auto-calibration.fetch-fixtures.shskip lmbench instead of failing the whole fetch.Publishing results to GitHub Pages is left for a follow-up PR.
Tested on current main (170b5df):
make checkpasses, andmake bench-cipasses with all three tiers producing every metric, in about 12.5 minutes. The four Tier 3 metrics have no baseline yet; a baseline from the CI host should be promoted withscripts/bench-promote.pyafter the first runs there.Closes #195