Skip to content

shard the full benchmark per fixture (generate-once + matrix + aggregate) #162

Description

@BryanFRD

Problem

The full benchmark is a single job that runs every fixture × every tool sequentially (plus the competitor npm installs). Wall-clock is the sum of all fixtures, dominated by mono-large (200 packages × 10k commits). The micro benchmarks are already matrix-sharded with a build-once + aggregate job — the full benchmark never got the same treatment.

Sharding per fixture would make wall-clock ≈ the slowest single fixture, which in turn buys the budget to raise runs (currently 10) for tighter confidence intervals.

Why it isn't just strategy: matrix

The action is a monolith: build → generate fixtures → run → download baseline → compare → upload baseline → comment. Naively matrixing the caller would mean:

  • every shard regenerates all fixtures (the expensive part — that alone cancels the win);
  • every shard runs compare and uploads a competing hyperfine-baseline artifact.

So the action has to be decomposable first.

Design

Action inputs (same shape as the binary-dir input added in #160/#161 — default ''/false keeps current behaviour):

  • fixtures-dir — use pre-generated fixtures and skip the internal Generate benchmark fixtures step.
  • shard (bool) — run + write latest.json only; skip baseline download, compare, upload and PR comment.

No fixture-filter is needed: run.sh already discovers fixtures from --fixtures-dir, so a shard just gets a directory containing its one fixture (exactly the filtered-fixtures/ trick FerrFlow's Fixtures jobs already use).

Merge scriptscripts/merge-results.sh. latest.json is metadata + a flat map:

{ "timestamp": …, "ferrflow_version": …, "runner_cores": …,
  "benchmarks": { "<fixture>-<tool>-<variant>-<cmd>": { "median_ms": …, "stddev_ms": …, "memory_mb": … } },
  "ferrflow_parallel": { }, "install_sizes": { } }

Keys are fixture-scoped, so merging shards is a map union:

jq -s '.[0] + { benchmarks: (map(.benchmarks) | add),
                ferrflow_parallel: (map(.ferrflow_parallel) | add),
                install_sizes: (map(.install_sizes) | add) }'

Covered by a bats test like the other scripts.

Caller (FerrFlow ci.yml):

  • bench-fixtures — generate all bench fixtures once → artifact.
  • benchmarkstrategy.matrix.fixture: [single, mono-small, mono-medium, mono-100-1k, mono-50-5k, mono-large, complex]; each shard downloads ferrflow-binary + the fixtures artifact, copies its own fixture into a filtered dir, calls the action with binary-dir + fixtures-dir + shard: true, uploads bench-partial-<fixture>.
  • benchmark-aggregate — downloads all partials, merges, runs compare, uploads hyperfine-baseline + step summary. Mirrors the existing Micro Benchmark (aggregate) job.

Shard per fixture, never per tool

Each fixture's tools must stay on one runner: the perf page compares ferrflow against competitors within a fixture, so splitting tools across machines would compare timings from different hardware. Per-fixture sharding keeps every comparison intra-runner and valid. The trade-off is that absolute numbers between fixtures come from different machines — acceptable, since nothing presents cross-fixture absolutes as comparable. runner_cores / ferrflow_parallel are per-runner stats and should be recorded per shard rather than merged blindly.

Follow-up

Once wall-clock drops, raise runs (10 → 20-30). Note that more runs tightens variance within a run; it doesn't remove systematic runner-to-runner noise, so the relative full-regression-threshold stays the right gate for regressions over time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions