Skip to content

Add CodSpeed continuous performance benchmarks - #623

Closed
codspeed[bot] wants to merge 6 commits into
mainfrom
codspeed/setup-benchmarks
Closed

codspeed[bot] wants to merge 6 commits into
mainfrom
codspeed/setup-benchmarks

Conversation

@codspeed

@codspeed codspeed Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

This sets up continuous performance measurement with CodSpeed for both halves of MMUX Vite: the Flask backend and the React frontend. Benchmarks run in CPU simulation mode, so results are hardware-independent and every pull request gets a performance report.

What was added

Backend benchmarks (flaskapi/benchmarks/, pytest-codspeed)

29 benchmarks covering the code that runs on the request path of every SuMo / UQ / MOGA interaction:

  • test_bench_data_processing.py — variable-name sanitization, load_data for .dat and .csv job collections, process_input_file with log transforms, Pareto-front extraction (get_non_dominated_indices), correlation indices, manual UQ sample generation (1k and 10k samples), grid and along-axes sample generation.
  • test_bench_lhs.py — the Latin Hypercube sampler used by POST /flask/sampling/lhs, across all supported methods (classic, center, maximin, centermaximin, correlation, lhsmu) plus a larger design.
  • test_bench_preprocessor.py — DataPreprocessor setup/fit, transform (DataFrame and record payloads) and inverse transform.
  • test_bench_serialization.py — the camelCase/snake_case conversion helpers on a realistic plot payload, pydantic validation of the SuMo along-axes and cross-validation requests with 300 jobs, response serialization, and a full Flask request/response round trip through register_json_transformers.

The benchmarks live outside tests/ on purpose: the test suite's autouse fixtures (DEBUG logging to disk, environment patching) would otherwise be measured together with the code under test. All inputs are deterministic (fixed seeds and shapes).

Frontend benchmarks (node/bench/, tinybench)

  • jobCollectionCsv.bench.ts — parseJobCollectionCsv on 50, 1k and 5k job collections; the upload path parses the whole file on the main thread.
  • dakotaRequestKey.bench.ts — buildDakotaRequestKey, rebuilt on every render of the 1D/2D/3D surrogate plots (slider drags).
  • sampling.bench.ts — getSamplingStartValue / getSamplingEndValue across 100 input variables with mixed distributions.
  • index.ts — entry point that registers every group on one withCodSpeed(new Bench()) suite, run with npm run bench.

Only framework-independent utilities are benchmarked, so no DOM or generated oSPARC client is needed.

CI and tooling

  • .github/workflows/codspeed.yml — two jobs (backend and frontend) using CodSpeedHQ/action@v5 in simulation mode on ubuntu-latest, with OIDC authentication. It mirrors the existing CI setup (actions/checkout@v5, astral-sh/setup-uv@v3, Node 24 with npm cache, make install-flaskapi-deps).
  • make bench-flaskapi, make bench-node and make bench for local runs.
  • npm run bench script in node/package.json.
  • CodSpeed badge and a short "Performance benchmarks" section in the README.

Notes

  • tinybench instead of vitest bench on the frontend. main recently upgraded Vitest to v5, and @codspeed/vitest-plugin currently supports Vitest ^3.2 || ^4 only (including its 6.0 beta), so the vitest integration cannot be used here. The benchmarks therefore run on tinybench with @codspeed/tinybench-plugin. tinybench is pinned to v5 (^5.1.0): v6 is not supported by the CodSpeed plugin yet — tracking issue: Support tinybench 6.0.0 CodSpeedHQ/codspeed-node#69. When Vitest 5 support lands, these benchmarks can be moved to vitest bench with very little churn (each file already exports a register*Benchmarks function). tsx was added as a dev dependency to execute the TypeScript entry point (node --import tsx bench/index.ts); the CodSpeed runner needs node as the top-level process, which is why the script does not shell out through a separate TS runner binary.
  • pytest-codspeed is installed as an overlay (uv run --with 'pytest-codspeed>=5,<6') rather than being added to flaskapi/pyproject.toml. The pinned itis-dakota==1.5.9 only publishes cp311/cp312 wheels, so the uv lock file cannot currently be re-resolved at all (uv lock fails on a clean checkout, independently of this change). Adding a dependency would therefore require touching requires-python or the Dakota pin, which is out of scope here. Once the lock can be resolved again, moving pytest-codspeed into the test dependency group and simplifying the CI command is a one-line follow-up.
  • BLAS thread pools are pinned to 1 in flaskapi/benchmarks/conftest.py (before numpy is imported). Under CPU simulation the threads are serialized and OpenBLAS' spin-waiting made the correlation-based LHS benchmark hang for tens of minutes; with the pin the whole backend suite completes in about 75 seconds.

Verification

Both suites were run locally with the CodSpeed CLI in simulation mode (29 backend + 7 frontend benchmarks, all reporting results), ruff check / ruff format and ty check pass on the backend, Prettier passes on the new frontend files, and the existing vitest tests still pass. node/package-lock.json was regenerated with npm (not hand-merged) after merging main.

Next steps

  • Merge to main so CodSpeed records a baseline; subsequent pull requests will then show relative changes.
  • Extend coverage as needed — the Dakota-driven surrogate workflows (cross-validation, Sobol indices) are not benchmarked because they spawn a real Dakota subprocess.

@codecov

codecov Bot commented Sep 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 57.97%. Comparing base (4725a8b) to head (cc1c81c).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #623   +/-   ##
=======================================
  Coverage   57.97%   57.97%           
=======================================
  Files         106      106           
  Lines        6436     6436           
  Branches      497      497           
=======================================
  Hits         3731     3731           
  Misses       2643     2643           
  Partials       62       62           
Flag Coverage Δ
flaskapi 88.56% <ø> (ø)
node 20.70% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed

codspeed Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Author

Congrats! CodSpeed is installed 🎉

🆕 29 new benchmarks were detected.

You will start to see performance impacts in the reports once the benchmarks are run from your default branch.

Detected benchmarks


ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.


Open in CodSpeed

JavierGOrdonnez and others added 6 commits September 24, 2026 13:27
* feat(node): add sensitivity analysis views

* test(e2e): cover real material sweep workflow

* test(e2e): use synthetic sensitivity fixtures

* test(e2e): remove obsolete realdata screenshots

* [FIX] toggles in titles + updated screenshots

* fix(node): preserve raw sensitivity values on log plots

* [DOCS] Sobol higher-index discussion, to-be-implemented

* [FIX] updated package-lock

---------

Co-authored-by: Javier Garcia Ordonez <ordonez@zmt.swiss>
* fix(node): surface backend errors in UQ plots

* fix(node): display plot backend errors

* fix(node): propagate sensitivity backend errors

* fix(node): remove incomplete CV accuracy fetch

* fix(node): capture and propagate fetchWithRetry network errors

Address Copilot review on #546:
- Record the last thrown error in fetchWithRetry so genuine network
  failures surface their message instead of a generic fallback.
- Skip backoff after the final retry attempt.
- Wrap response.clone() in httpError helper try/catch so a consumed
  body cannot make getResponseErrorMessage throw.
- Update the fetchWithRetry failure test to expect the propagated error.

---------

Co-authored-by: Javier Garcia Ordonez <ordonez@zmt.swiss>
#613)

* backprop §B.22zn + §V.36zn: configure writable uv cache.

* backprop §B.23-30: harden dev startup

* fix: initialize persistence mount in backend startup

* fix: parameterize dev node_modules ownership + tag dev image in build-no-cache

Addresses outstanding Copilot review findings on PR #613:

- node/Dockerfile hardcoded `chown node:node` (UID/GID 1000) on the builder
  stage's node_modules, but docker-compose-development.yml starts
  mmux-vite-web as the host's actual `${UID:-1000}:${GID:-1000}`. On hosts
  whose UID/GID differ from 1000, the named volume was seeded unwritable and
  Vite's .vite-temp write failed with EACCES. Now parameterized via
  APP_UID/APP_GID build-args (default 1000), populated from `id -u`/`id -g`
  in the Makefile (B32ui).
- `make build-no-cache` (used by build-publish-local and make ci) never
  tagged the builder-stage dev-web image, unlike `make build`, so a no-cache
  rebuild left docker-compose-development.yml's mmux-vite-web-dev tag stale
  or missing (B33bn).
- SPEC.md V44tf described a root-only mmux-vite-text-files-init service that
  was never implemented; the shipped design (and its regression test) has
  mmux-vite-backend's own entrypoint.sh do the chown before dropping
  privileges via gosu. Updated the invariant text to match (B34sd).

Updated flaskapi/tests/test_compose_depends_on.py to assert the new
build-arg-based ownership and the build-no-cache tagging step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore: revert over-engineered changes

* chore: remove SPEC interface changes

* docs: invariants about file ownership + avoid dev-scope creep

---------

Co-authored-by: Javier Garcia Ordonez <ordonez@zmt.swiss>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix: standardize SuMo validation response model

* fix: address SuMo validation review comments

---------

Co-authored-by: Javier Garcia Ordonez <ordonez@zmt.swiss>
Co-authored-by: Alejandro Parcet Gonzalez <parcet@speag.com>
Add benchmark suites for both halves of the app and run them on CodSpeed
in CPU simulation mode on every pull request:

- flaskapi/benchmarks/: pytest-codspeed benchmarks for the Dakota data
  processing helpers, the LHS sampler, the DataPreprocessor and the
  request/response case-conversion layer (incl. a Flask round trip).
- node/src/utils/*.bench.ts: vitest benchmarks for the job-collection CSV
  import, the Dakota plot request key and the sampling bound helpers.

Also add make bench-flaskapi / bench-node targets, the CodSpeed badge and
a short README section.
@JavierGOrdonnez
JavierGOrdonnez force-pushed the codspeed/setup-benchmarks branch from cc1c81c to 60d8247 Compare September 24, 2026 15:57
@JavierGOrdonnez

Copy link
Copy Markdown
Collaborator

Issue: CodSpeed is incompatible with Vitest 5 atm

  • @codspeed/vitest-plugin 5.7.1 was released on June 24, 2026
  • Vitest 5 was released on September 3, 2026

CodSpeed’s Vitest 5 work is tracked in PR #86, opened June 29, 2026. It remains open and draft, last updated September 22, 2026. No public ETA. Monitor.

JavierGOrdonnez added a commit that referenced this pull request Oct 14, 2026
…/ plugin v6)

Supersedes #623, which had to be closed because `@codspeed/vitest-plugin` 5.x did not support Vitest 5. [codspeed-node v6.0.0](https://github.com/CodSpeedHQ/codspeed-node/releases/tag/v6.0.0) (2026-10-13) added Vitest 5 / Vite 8 support, so this rebase of that work onto current `develop` is unblocked.

## What is benchmarked

Benchmarks run on CodSpeed in CPU simulation mode (`CodSpeedHQ/action@v5`) on every pull request, via `.github/workflows/codspeed.yml`:

- **Frontend** (`node/src/**/*.bench.ts`, `@codspeed/vitest-plugin@6`, Vitest 5 bench-fixture API):
  - job-collection CSV import (50 / 1k / 5k jobs) — the upload path,
  - sampling bound helpers (100 inputs) — form re-renders,
  - the V46sc request-cache key (`buildRequestCacheKey`, 50 / 300 / 2000-job bodies) — canonicalization of the full plot request body on every slider-drag fetch decision, the successor of the deleted hand-curated request key.
- **Backend** (`flaskapi/benchmarks/`, pytest-codspeed): the **Flask API boundary only**, on production-shaped payloads:
  - camelCase↔snake_case conversion exactly as the middleware performs it under the variable-name **preservation contract** (`flaskapi/SPEC.md V13/V37`): variable-name containers (`predictions`, `sobol`, `sobol_second_order`, `slider_values`, job `inputs`/`outputs`) preserved verbatim, API field keys converted — the benches assert this,
  - pydantic request validation (`SumoAlongAxesRequest`, `SumoCrossValidationRequest`, 300 jobs),
  - response serialization and a full Flask JSON round trip through the real transformers.

Scale is fixture-asserted, not claimed: the largest production tree is the 2000-job request (~30k dict keys); the Sobol response over 40 variables carries ~2k keys via O(n²) second-order pairs; along-axes curves ride the engine default of 21 points per axis (flaskapi does not override `points_per_variable`).

## Changes vs. the closed PR #623

Rebase onto `develop` (46 commits later) required reworking both suites:

1. **Vitest 5 bench API.** The top-level `describe`/`bench` imports are gone; benchmarks are declared with the `bench` test fixture. Each `bench()` is still reported as its own benchmark on CodSpeed.
2. **Dakota internals are no longer measured here.** `funs_data_processing`, `lhs` and `DataPreprocessor` moved into the [itis-sumo](https://github.com/ITISFoundation/itis-sumo) package. mmux_vite is the frontend/application repo, so only application-level code is measured; the relocation of those 29 Python benchmarks is tracked in [ITISFoundation/itis-sumo#56](ITISFoundation/itis-sumo#56) (sources preserved there).
3. **Production payload shapes (Copilot review).** The original fixtures predated the V13/V37 preservation work and measured full camel-casing of variable names — behavior the production middleware explicitly avoids. The conversion and round-trip benches now use what `blueprints/dakota.py` and `Curves1DPlot` actually send/receive.
4. **`dakotaRequestKey.bench.ts` replaced, not just dropped.** Its subject was deleted (V46sc session cache); the new `sessionResponseCache.bench.ts` measures its successor (`buildRequestCacheKey`).
5. **Sampling bench data follows the current `VarSelection` model** (3 distribution types, `scale: "linear" | "log"`), and imports are bound to module-level consts per the vitest module-runner guidance (removes getter overhead: ~9× cleaner local hz).
6. **CSV bench contract guard**: the fixture is parsed once at module load and asserted against the documented `ParsedJobCollectionCsv` shape, so format drift fails loudly instead of silently benching a reject-everything path.
7. Minor: `setup-uv@v10.2.0` + `setup-node@v7` (repo pins), coverage excludes `**/*.bench.ts` (the 60% ratchet can never see bench files), refreshed `flaskapi/uv.lock` (develop left the stale `1.6.4.dev6` self-version; `uv run` rewrites it on any invocation).

## Benchmark tier map (no overlap)

| tier | where |
| --- | --- |
| frontend CPU (parse, canonicalize, form helpers) | this repo, `node/src/**/*.bench.ts` |
| HTTP/serialization boundary | this repo, `flaskapi/benchmarks/` |
| Dakota helper internals | itis-sumo ([#56](ITISFoundation/itis-sumo#56)) |
| engine/façade compute (`itis_sumo.api` fit/evaluate/Sobol/optimize) | itis-sumo ([#57](ITISFoundation/itis-sumo#57)) |

## Verified locally

- `npm run bench --reporter=verbose`: 8 benchmarks, 3 files, all pass; no vitest warnings; `tsc -b` clean; `lint` 0 errors; `knip` clean; `test:coverage`: 448 tests pass, thresholds unaffected.
- `uv run --with 'pytest-codspeed>=5,<6' pytest benchmarks/ --no-cov`: 8 benchmarks pass (Python 3.11, pytest 9.1.1); `ruff check`/`format` clean; prek hooks pass on both commits.

The CodSpeed report is the acceptance test: it should detect 8 vitest + 8 pytest benchmarks. The `main` baseline lands after merge (or via a `workflow_dispatch` backtest).
@JavierGOrdonnez
JavierGOrdonnez deleted the codspeed/setup-benchmarks branch October 14, 2026 11:35
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