Skip to content

chore(xtest): refactor to validate benchmark options before session setup - #640

Merged
dmihalcik-virtru merged 3 commits into
mainfrom
DSPX-4372-fix-bench-option-validation
Oct 13, 2026
Merged

dmihalcik-virtru merged 3 commits into
mainfrom
DSPX-4372-fix-bench-option-validation

Conversation

@dmihalcik-virtru

@dmihalcik-virtru dmihalcik-virtru commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Split out of #626.

--bench* options were validated late: a bad --bench-threshold surfaced from the bench_config fixture after payloads were built and CLIs installed, and an unknown SDK in an arm spec was skipped silently by select_arms.

  • xtest/perf/config.py — pytest-free module owning BenchConfig (moved from perf/runner.py, re-imported there so from perf.runner import BenchConfig still resolves) and the request rules: SDK-name validation, arm ceiling, duplicate rejection, finite positive budgets/timeouts, max_rounds >= min_rounds.
  • Eager validation — conftest.pytest_configure checks every --bench* option before session setup. A full baseline/candidate pair gets the paired rules (same SDK, no duplicates); either flag alone is still allowed — select_arms defaults the other side, as the option help says — and is checked only for a valid SDK name.
--bench-candidate go@main                          -> collects (7 items)
--bench-baseline gp@main                           -> ERROR: unknown SDK 'gp'
--bench-baseline go@main --bench-candidate java@main -> ERROR: must use the same SDK
--bench-threshold 0.9                              -> ERROR: threshold is a ratio above 1.0

Tests: xtest/test_bench_config.py; all test_bench*.py pass (167).

Summary by CodeRabbit

  • Improvements
    • Benchmark runs now validate their configuration before setup, including round counts, timing limits, confidence settings, and selected SDKs.
    • Baseline and candidate options are checked for completeness and compatibility. Invalid or duplicate benchmark arms, unsupported SDKs, and malformed version specifications are rejected with a clear options error.
    • Benchmark budgets now scale with the number of arms, and SDK selections preserve their supplied order.

@dmihalcik-virtru
dmihalcik-virtru requested review from a team as code owners October 8, 2026 19:31
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 49 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 20280397-7c8b-4aec-9930-35d806c2077e
📥 Commits

Reviewing files that changed from the base of the PR and between 81b7300 and 7bed50d.

📒 Files selected for processing (5)
  • .github/workflows/check.yml
  • xtest/conftest.py
  • xtest/perf/config.py
  • xtest/perf/runner.py
  • xtest/test_bench_config.py

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 78660c8c-dd75-4885-b197-d5030bbcf908
📥 Commits

Reviewing files that changed from the base of the PR and between bc4ead4 and 81b7300.

📒 Files selected for processing (5)
  • .github/workflows/check.yml
  • xtest/conftest.py
  • xtest/perf/config.py
  • xtest/perf/runner.py
  • xtest/test_bench_config.py

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

Benchmark configuration and request validation now live in a shared module. The pytest plugin validates supplied benchmark options and selected SDKs. Tests cover the shared configuration and pytest validation, and the offline harness runs the new test module.

Changes: Benchmark configuration

Layer / File(s) Summary
Shared benchmark configuration
xtest/perf/config.py, xtest/perf/runner.py, xtest/test_bench_config.py
Adds shared benchmark settings, request models, parsing, SDK validation, and arm-count budget calculation. The runner imports BenchConfig from the shared module. Tests cover configuration and request validation.
Pytest benchmark option validation
xtest/conftest.py, xtest/test_bench_config.py, .github/workflows/check.yml
The pytest plugin validates benchmark options and SDK selections, and extracts unique SDK names in input order. Tests cover accepted and rejected options. The offline harness includes the new test module.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: b-long

Merge Risk: ⚪ Minimal · up to 81b73

No merge-blocking benchmark configuration issue was established. The change is ready to merge after normal checks.

Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 18.03% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 61 functions across 4 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: refactoring benchmark option validation to occur before session setup.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 18.03% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 61 functions across 4 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks the bench at dawn
Each SDK name is neatly drawn
The rounds are set, the tests take flight
The harness hops through code tonight
Then carrots crown the config right

Comment @coderabbitai help to get the list of available commands.

ci: run test_bench_config.py in the offline harness check
@dmihalcik-virtru dmihalcik-virtru changed the title fix(xtest): validate benchmark options before session setup chore(xtest): refactor to validate benchmark options before session setup Oct 9, 2026

@b-long b-long left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: #640

Validating options early is a clear improvement. I checked these against main: gp@main, --bench-threshold 0.9, mismatched SDKs and go@ all used to collect without error. Now they fail at configure time.

1. The pytest_configure wiring has no test

test_bench_config.py tests perf/config.py on its own. Nothing tests that pytest_configure raises UsageError, or that a lone --bench-baseline/--bench-candidate is accepted, as the description says. A fake config, like the one in test_sizes_units.py, covers this:

class _FakeConfig:
    def __init__(self, **opts):
        self._options = {
            "--bench": True, "--sdks": "go",
            "--bench-baseline": None, "--bench-candidate": None,
            "--bench-threshold": stats.DEFAULT_THRESHOLD,
            "--bench-min-rounds": 20, "--bench-max-rounds": 60,
            "--bench-warmup": 5, "--bench-budget-seconds": 1500.0,
            "--bench-seed": 0, "numprocesses": None,
        } | opts

    def getoption(self, name, default=None):
        return self._options.get(name, default)


def configure(**opts):
    conftest.pytest_configure(cast(pytest.Config, _FakeConfig(**opts)))


@pytest.mark.parametrize("opts", [
    {}, {"--bench-candidate": "go@main"}, {"--bench-baseline": "go@v0.29.0"},
    {"--bench-baseline": "go@v0.29.0", "--bench-candidate": "go@main"},
])
def test_accepted(opts):
    configure(**opts)


@pytest.mark.parametrize(("opts", "match"), [
    ({"--bench-baseline": "gp@main"}, "unknown SDK 'gp'"),
    ({"--bench-candidate": "go@"}, "empty version"),
    ({"--bench-threshold": 0.9}, "threshold"),
    ({"--bench-baseline": "go@main", "--bench-candidate": "java@main"}, "same SDK"),
    ({"--bench-baseline": "go@main", "--bench-candidate": "go@main"}, "duplicate"),
])
def test_rejected(opts, match):
    with pytest.raises(pytest.UsageError, match=match):
        configure(**opts)

All 9 cases pass against this branch. Another option is to move the block into a validate_bench_options(config) helper and test that directly.

2. A spec for an SDK outside --sdks is still silently dropped

--sdks java --bench-candidate go@main   -> collects 7 java cells; go@main ignored

go is a valid name, so it passes the new check. _spec_for then drops the spec, because it matches no SDK being benchmarked. This is the same silent green run the PR targets. It already happens on main, so it can be a follow-up, but it fits here. Suggested test, which fails today:

def test_spec_for_unbenchmarked_sdk_rejected():
    with pytest.raises(pytest.UsageError):
        configure(**{"--sdks": "java", "--bench-candidate": "go@main"})

3. Pin the new --bench-baseline "" behavior

An empty spec used to mean "use the default". Now it raises unknown SDK '', which fits the PR's aim of rejecting specs that would otherwise be silently ignored. Since this changes behavior from main, please pin it with a test:

def test_empty_spec_rejected():
    with pytest.raises(pytest.UsageError, match="unknown SDK ''"):
        configure(**{"--bench-baseline": ""})

Reject a --bench-baseline/--bench-candidate naming an SDK outside --sdks,
which was otherwise dropped per-SDK and silently measured the defaults.
Pin the empty-spec rejection and the lone-spec acceptance.
CPython 3.15.0 has no markupsafe wheel, so uv sync built it from source
and the --frozen --no-build test step refused to install it.
@dmihalcik-virtru
dmihalcik-virtru force-pushed the DSPX-4372-fix-bench-option-validation branch from 81b7300 to 7bed50d Compare October 13, 2026 13:36
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

X-Test Failure Report

@dmihalcik-virtru

Copy link
Copy Markdown
Member Author

@b-long Addressed issues in 27c2d10

I've also had to pin the python version to 3.14 to avoid some warnings under 3.15, which is out now.

@b-long b-long left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming CI jobs pass, this PR is approved 👍

@dmihalcik-virtru
dmihalcik-virtru merged commit 5694eb7 into main Oct 13, 2026
21 checks passed
@dmihalcik-virtru
dmihalcik-virtru deleted the DSPX-4372-fix-bench-option-validation branch October 13, 2026 13:49
dmihalcik-virtru added a commit that referenced this pull request Oct 13, 2026
Follow-up to #640, which pinned \`scriptcheck\` to 3.14 after the
CPython 3.15.0 release broke it.

### Why it broke

The CI Python version wasn't pinned (\`requires-python\` is just
\`>=3.14\`), so uv picked up 3.15 as soon as it shipped. markupsafe
3.0.3 had no cp315 wheel, so \`uv sync\` built it from source. The \`uv
run --frozen --no-build\` step then refused to install it.

### Changes

- **Pin 3.15 everywhere CI sets up Python:** \`check.yml\`'s
\`setup-uv\`, plus \`xtest.yml\`'s \`setup-python\` (3.14 → 3.15) and
its three \`setup-uv\` steps that had no pin. Those three were also
floating to 3.15 and building pydantic-core from source with Rust.
- **Relock** so every compiled dependency has a cp315 wheel:
- pydantic 2.14.1 / pydantic-core 2.50.1 in \`xtest\`, \`otdf-local\`
and \`otdf-sdk-mgr\`
  - markupsafe 3.0.4 in \`xtest\`

\`requires-python\` stays at \`>=3.14\`. This changes what CI runs, not
the minimum for local development.

### Verification

Locally on CPython 3.15.0, I ran the same steps as \`scriptcheck\`
against fresh environments:
- xtest: ruff, ruff format, pyright, and the \`--frozen --no-build\`
offline harness tests (299 passed)
- otdf-local: lint, pyright, and \`-m "not integration"\` tests (29
passed)
- otdf-sdk-mgr: lint, pyright, and \`-m "not integration"\` tests (138
passed)

A scan of all three lock files found no compiled package without a cp315
or abi3 wheel. On this PR, the X-Test `xct` matrix (go, java, js;
platform `main` and v0.28.0) passes on 3.15. The `bench` and `zip64`
jobs are opt-in and were skipped, so their `setup-uv` pins have not run
yet.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
dmihalcik-virtru added a commit that referenced this pull request Oct 14, 2026
## Problem

The benchmark report's `p (BH)` column always showed the **slower**-tail
p-value (`p_adjusted` / `p_value`). For a real speedup that tail is ~1,
so an `IMPROVED` row read e.g. `ratio 0.50 … p 1.000 … IMPROVED` — the
p-value contradicted the verdict on the same row. The verdict itself was
correct (it already used `p_adjusted_faster`); only the rendering was
wrong.

## Fix

`_p_cell` now shows the one-sided tail in the direction of the observed
ratio: the faster tail when `ratio < 1`, the slower tail otherwise, each
preferring the BH-adjusted value and falling back to unadjusted
(controls stay outside the BH families). The table footer notes that p
is one-sided.

Ported from the closed #621 branch. Stage-1 follow-up to #625
(DSPX-4372).

## Tests

New offline `xtest/test_bench_report.py` (added to the `check.yml`
harness list): improvement → faster tail, slowdown/no-change → slower
tail, unadjusted fallback, `<0.001` floor, NaN → `-`.

Note: the `check.yml` list edit touches the same lines as #640 (which
adds `test_bench_config.py`); whichever lands second needs a trivial
rebase.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Performance reports now show the p-value for the observed direction of
change: faster for improvements and slower for regressions or no change.
The footer clarifies that p-values are one-sided.
* **Tests**
* Added offline coverage for report p-values, including adjusted and
unadjusted values, very small values, and empty comparisons.
* Expanded the offline test run to include benchmark measurement,
runner, and report tests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
dmihalcik-virtru added a commit that referenced this pull request Oct 21, 2026
Stages are milestones, not pull requests. Records the stack each stage lands
as, with branch names; runs stages 2 and 3 as parallel tracks from main, since
they share no imports; and adds a size budget and refactor-first rule to the
merge discipline.

Also brings the plan up to date: the work that landed as separate fixes
(#638, #640, #641, #647), #622's payload sizes, item 7's move into stage 3,
the neutral-nightly gap (DSPX-5260), and scratch-disk scheduling (DSPX-5253).

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
dmihalcik-virtru added a commit that referenced this pull request Oct 30, 2026
… record benchmark arms (#626)

## TL;DR

Adds **`otdf-sdk-mgr install benchmark <sdk> <ref>...`**: one command
that resolves any number of SDK refs (releases, branches, SHAs, `pr:N`)
to commits, installs them side by side under `xtest/sdk/<sdk>/dist/`,
and writes a manifest recording exactly which commit is in each
directory. It makes comparing SDK variants a local, one-command job
instead of a CI dispatch or a hand-built set of `dist/` directories. It
is part 2c of the [DSPX-4372-t1
plan](#657), based on `main` now
that #667 and #668 (the manifest contract) have merged, and it is the
preparation step CI will share (#660).

## Why: quicker local testing of multiple SDK variants

Today, there are two ways to compare SDK builds:

- **Dispatch the xtest workflow with `run-benchmarks`.** That is about
45 minutes per SDK per attempt, and a mis-set input (`main` rather than
`main latest`) spends the whole run before it says so.
- **Reproduce locally**, per `xtest/perf/README.md`: "whatever two
builds you want, side by side under `sdk/<name>/dist/`". You install
each build yourself and need to know:
- whether each ref is a release or a source build, and the right `--ref`
for it;
- which directory it will land in: `pr:123` builds into
`refs--pull--123--head`, and `otdfctl/v0.24.0` and `v0.24.0` share
`dist/v0.24.0`;
  - how to provision `otdfctl` and the platform separately.

Nothing records which commit a directory holds. A `dist/main` left from
last week is measured, and labelled, as today's `main`.

With this PR:

```bash
otdf-sdk-mgr install benchmark go v0.38.0 main pr:123 --platform main --otdfctl v0.38.1
```

- **Any number of variants in one command.** Refs are kept in order, and
the first one is the reference. Stage 3 (#658) measures up to four.
- **Fails fast.** Every ref, and the `--otdfctl` pin, is resolved to a
full commit before anything builds, so a mistyped branch or tag costs a
lookup rather than minutes of compilation. Two refs that would share one
`dist/` directory at different commits are rejected before either
builds. (A full SHA is not checked to exist; see Follow-ups.)
- **Doesn't measure a commit against itself.** If every ref resolves to
one commit, preparation stops with `neutral` instead of benchmarking A
against A.
- **Cheap re-runs.** A source build records its commit in
`dist/<slug>/.version` and is reused only when that record matches.
Re-running after pushing to a branch rebuilds the branch, not
everything. The record is all it checks, so after a hand `make` of local
edits, delete `dist/<slug>` (the README says so).
- **A manifest instead of hand-built env vars.**
`xtest/sdk/benchmark.installed.json` records, for each arm, the
requested alias, repository, commit, `dist/` directory and install
method, plus the platform and `otdfctl` pins. `otdf-local env` exports
its path and the `otdfctl` pin (#642), and from #670 `pytest --bench`
reads the arms from it. Until then, name the arms with
`--bench-baseline`/`--bench-candidate`, as the README now says. CI runs
this same command (#660), so a local result and a CI result come from
the same preparation.

## What's in this PR

- **`install benchmark`** (`cli_install.py`, `bench_prepare.py`)
resolves, checks for collisions, installs releases via `install_release`
and source refs via `cmd_tip`, then installs the platform and the
`otdfctl` pin.
- **Separate provisioning pin.** `--otdfctl` is recorded apart from the
measured arms. It joins the collision check only when it shares the
arms' `dist/` tree, which is only the case for Go.
- **Collision and neutral checks** run before anything builds. They,
`dist_slug`, and the manifest's data model are in #668; this PR is the
command that uses them.
- **Manifest** (schema `v1alpha1`, defined in #668). The status is
`success`, `neutral`, `partial` or `failed`, with a `status_message`:
- `failed` means nothing was installed; `partial` means some installs
completed but not all.
- The manifest is written on every exit path, an exception or Ctrl-C
included.
  - The exit code is 0 for `success` or `neutral`, 1 otherwise.

**Size.** About 640 non-test lines, over the plan's ~400-line budget.
About 315 of them are code; the rest are docstrings, comments and the
README section. Resolution, installation and the manifest's exit paths
are one command: a PR that resolved without installing would ship a
command that does nothing. Two thirds of the diff is tests.

**Since the first review:**
- Java release arms resolved nothing, because the wrong tag infix was
used. Fixed.
- The manifest now has one write site: `status` is required, reads check
`schema_version`, and writes create the parent directory and replace the
file atomically.
- JS release arms land in `dist/vX.Y.Z`.
- `platform` is rejected as an SDK.
- The platform pin records the directory it was installed to.
- A first-arm failure is now `failed`, not `partial`.

**Since review of #668:**
- **Abbreviated commits are rejected.** A ref that resolves to a commit
that isn't a full SHA (in practice, an abbreviated SHA that no branch,
tag or PR head points at) now fails before anything installs: "give the
full commit SHA for …", with a `failed` manifest and exit code 1.
Before, `abc1234` and its own full SHA weren't recognised as one build,
so the commit was built twice and reported as `success`. Reusing a
`dist/` directory and detecting a moved ref now compare full SHAs
exactly.
- An empty ref list fails instead of writing a `success` manifest with
no arms. Every manifest written sets `metadata.sdk` and passes #668's
strict reader.
- Platform and `otdfctl` install failures are caught narrowly
(`PlatformInstallError`, `BenchPrepareError`, `OSError`). Anything else
aborts with its traceback, and the manifest records `partial`
"Preparation aborted", as arm installs already did. A platform whose
`.version` has no full commit is `partial`.

**Since the 2026-10-29 review:**
- An arm whose stale `dist/` cannot be removed (`OSError`) is now
`failed`/`partial` with the error, like the `otdfctl` pin, rather than
"Preparation aborted".
- If the *first* manifest write fails, the CLI no longer prints
"Manifest: …" pointing at an earlier run's file. `prepare_benchmark`
raises `BenchPrepareError` for that case. `RuntimeError` is no longer
caught around the whole run: `xtest/sdk` is located up front,
`--manifest` or not.
- The CLI tests now go through Typer (`CliRunner().invoke(install_app,
…)`), so a lost registration or renamed option fails them. They also
assert a clean `typer.Exit`, since `CliRunner` reports an uncaught
exception as exit 1 too.
- New tests:
- npm-only js releases: one version is `neutral` ("whose commit is
unknown"), two versions are not;
  - a dist of unknown provenance is emptied before rebuilding;
  - an arm `OSError`;
  - a failed final write.
- Comments: two were wrong and are now corrected:
- the repeated-builds example `v0.38.0 main` is in fact neutral, so it
is now `v0.38.0 main pr:123`;
- the `otdfctl` pin "never provisions the fixtures it is measured
against" contradicted `check_collision`.

Stale ones fixed: js tags are `sdk-vX.Y.Z`; the go fallback affects
`v0.30.x`, not "older tags"; the java Makefile sources
`java/<slug>.env`. The `# type: ignore` is gone, and the CLI help
example now uses `v0.38.0`/`v0.38.1`.
- README:
- says reuse trusts `.version`, and that a release `dist/` is reused as
is;
  - says a full SHA is not checked;
  - says `pytest --bench` doesn't read the manifest yet.

**Validation** (on this commit):
- `uv run pytest` 404 passed; ruff check, ruff format and pyright are
clean, with no `noqa` or `type: ignore`.
- Each new test was checked against a deliberate break of the code it
covers: the unregistered command, the missing `rmtree`, the uncaught arm
`OSError`, the unwrapped first write and the neutral message all fail at
least one test.
- Live, from an empty `xtest/sdk/go`:
- `install benchmark go v0.38.0 main --otdfctl v0.38.1` gave `success`,
exit 0. The arms were `v0.38.0` at `308002c2` (release) and `main` at
`ef874839` (source). `dist/main/.version` matches `git -C src/main
rev-parse HEAD`. The pin was `v0.38.1` at `99b51145`.
- The same command re-run gave `success` in 1.4 s, with nothing rebuilt.
- `install benchmark go 0e4b0d1 main` gave `failed`, "give the full
commit SHA for 0e4b0d1", with exit 1 and no arms.
- `--manifest` under a regular file gave "Error: cannot write the
manifest …", exit 1, and no "Manifest:" line.
- The platform pin (`--platform`) was not exercised live on this commit;
its last live run predates the review fixes.

## Stack ([DSPX-4372-t1
plan](#657))

Two parallel tracks from `main`; they share no imports and meet in stage
5.

| Part | PR | Base |
|---|---|---|
| 2a: bare-repo and js install fixes | #667 (merged) | `main` |
| 2b: manifest contract | #668 (merged) | `main` |
| 2c: `install benchmark` | **#626 (this PR)** | `main` |
| 2d: `otdf-local env` exports | #642 | 2c |
| 3a: arms as a tuple | #669 | `main` |
| 3b: two to four arms | #658 | 3a |
| 3c: pytest reads the manifest | #670 | 3b |
| 4: candidate-to-candidate conclusions | #659 | 3c |
| 5, 6: CI and report | #660, #661 | not yet moved onto these stacks |

#667 and #668 were split out of this PR so the fixes and the contract
can be reviewed on their own. Split out of an earlier version so they
could land first: #638, #639, #640 and #641, all merged. #647, the
golden-key configuration CI needs once `otdf-local` starts the platform,
is still open.

**Related, independent of this stack:** #684 makes go release installs
put a real `otdfctl` binary in `dist/`. Before it, a go release arm ran
`go run` on every call (about 370 ms wall and 1 s CPU, against 54 ms for
the binary), while source-built candidates ran a binary. That made
release baselines look slower than they are.

## Follow-ups

None of these blocks merge. All but the "Re-run" message are now noted
in the code or the README.

- **Go repository URL.** Go arms are all recorded under the monorepo
URL. The monorepo carries `otdfctl/v0.10.0`–`v0.29.0` at the archived
repo's commits, but not `v0.30.x`, which `resolve` answers from the
archived repo. That makes `v0.30.x` arms the ones recorded with a commit
the monorepo lacks. The fix needs `resolve` to report the repo it
matched.
- **Fallback on any error.** Go resolution falls back to the archived
otdfctl repo whenever the monorepo lookup fails, so a transient error
can resolve `main` or `pr:N` against the wrong repo. A source arm then
fails the read-back, but the error says "the ref moved".
- **Unhelpful "Re-run" advice.** "The ref moved… Re-run" is wrong for a
worktree that `is_mutable_ref` treats as fixed (full SHAs; web-sdk
`release/x.y.z` branches). Checkout never resets those, so a re-run
fails the same way. The message should name `src/<slug>` and say to
delete it.
- **Reuse trusts `.version`.** The SDK Makefiles rebuild into
`dist/<slug>` without touching `.version`, and `_built_commit` doesn't
check for a dirty worktree. The README now tells users to delete the
directory; a code check (`git status --porcelain`, a binary hash) would
be better.
- **Release dists reused as is.** A release arm adopts an existing
`dist/<tag>` (`install_release`'s skip), including a half-finished
install.
- **Full SHAs unverified.** A typo fails at checkout after the arms
before it have built, giving `partial`.
- **`--platform` not resolved up front.** It is not resolved before the
arms build: `--platform latest` becomes `service/vlatest` and fails
after every build. The release/source label for the pin is also a
`startswith("v")` guess; both installers build from source.

**Downstream:** #642 is rebased onto this commit with no conflicts (it
touches only `otdf-local` and `xtest/perf/README.md`). Its local-run
example uses `--otdfctl v0.30.0`, one of the `v0.30.x` tags above;
`v0.38.1` would avoid that.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Added an `install benchmark` command to prepare SDK benchmarks from
ordered references, with optional platform and CLI tool artifacts.
* Benchmark preparation validates references and installation conflicts,
reuses matching builds, and records progress and results in a manifest.
Results distinguish successful, neutral, partial, and failed
preparations.
* **Documentation**
* Added guidance for preparing benchmarks and troubleshooting common
test failures.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants