Skip to content

Fix cabal issue 7557: don’t run benchmarks in parallel or with the build - #12407

Open
ulysses4ever wants to merge 5 commits into
haskell:masterfrom
ulysses4ever:claude/cabal-issue-7557-m1znrj
Open

ulysses4ever wants to merge 5 commits into
haskell:masterfrom
ulysses4ever:claude/cabal-issue-7557-m1znrj

Conversation

@ulysses4ever

@ulysses4ever ulysses4ever commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Disclaimer: this was created with Claude Code (Opus 5.5, ultra code first and then medium). It went through a couple of design iterations with me.


Fixes #7557: cabal bench should not run benchmarks in parallel.

With -j, every node of the install plan runs in parallel, and with per-component builds each benchmark suite is its own node. So cabal bench could run several benchmarks at the same time, from the same package or across a project, and also while other components were still being compiled. Both skew benchmark results.

Change. cabal bench now builds everything first and only then runs the benchmarks, one at a time:

  • The bench phase of a package (buildAndRegisterUnpackedPackage) no longer runs its benchmarks, but records them in a DeferredBenchmarks queue threaded from rebuildTargets.
  • Once InstallPlan.execute has finished (so all builds, and downloads, are done), rebuildTargets runs the recorded benchmarks one at a time, in plan order (InstallPlan.executionOrder), and records their failures in the BuildOutcomes, exactly as before.
  • Building stays parallel and -j defaults are unchanged. No new concurrency primitive is needed. See Note [Running benchmarks].

Behaviour changes to be aware of

  • Benchmarks start only once everything is built, so the first benchmark result comes later than before.
  • Output order changes even with -j1: builds now come before benchmark runs. The golden outputs of NewBuild/CmdBench/MultipleBenchmarks and Regression/T5309 were re-accepted; both diffs are pure reorderings (same lines).
  • Without --keep-going, no benchmark is run if any package fails to build (previously, benchmarks that happened to be built before the failure could run), and no further benchmark is run after one fails. With --keep-going, the benchmarks of all successfully built packages run, one at a time.
  • For a legacy whole-package node (e.g. build-type: Custom) with both a library and a benchmark, its dependents may now be built before its benchmark runs, and a failing benchmark no longer marks them as DependentFailed.
  • Separate cabal processes are not serialised.
  • prs: 0000 in changelog.d/issue-7557.md is a placeholder until there is an upstream PR number.

History. The first commit serialised benchmark runs with a lock; review pointed out that builds could still overlap a running benchmark, so the second commit replaces the lock with deferring benchmarks until everything is built. Squashing on merge is fine.

Testing (Linux, GHC 9.4.7)

  • New test cabal-testsuite/PackageTests/NewBuild/CmdBench/Sequential: v2-bench -j3 all over three benchmarks in two packages (one benchmark depends on a library). Each benchmark fails if another one runs at the same time, and the test checks that no Configuring/Preprocessing/Building/Compiling/Linking line appears after the first Benchmark …: RUNNING.... It fails 3/3 on master (benchmarks overlap) and 3/3 with only the first, lock-based commit (a benchmark is still being linked while another runs), and passes 3/3 with this PR.
  • Related cabal-testsuite tests pass (22): NewBuild/CmdBench/*, NewBuild/CmdTest/*, NewBuild/T3460, NewBuild/CmdBuild/OnlyConfigure, TestChangeDir, Target, Regression/T5309, AutogenModules/MainIsBench, AllowNewer, AllowOlder, Freeze/*, ShowBuildInfo/Complex/single, GHCJS/BuildRunner. No other golden file in cabal-testsuite has a build step after a benchmark run.
  • cabal-install:test:integration-tests2: 29/30 pass; the one failure is a haddock test that needs boot-library .haddock files missing from Ubuntu's GHC package, unrelated to this change.
  • Manually checked: a failing benchmark with and without --keep-going, a build failure with and without --keep-going, and --build-timings.
  • fourmolu 0.12 and typos are clean; hlint 3.8 reports no hints (CI uses 3.10).

QA Notes

In a project with several benchmark suites (e.g. in two packages, one of whose benchmarks depends on a library), run cabal bench all -j4 from a clean state:

  • All build output (Configuring, Building, [n of m] Compiling, Linking) should appear before the first Benchmark <name>: RUNNING....
  • Each benchmark should print Benchmark <name>: FINISH (or ERROR) before the next one starts, in the order of the build plan.

Make one benchmark exit with failure: without --keep-going the following benchmarks are not run; with --keep-going they are, and the failure is reported at the end. Make one component fail to compile: without --keep-going no benchmark is run; with it, the benchmarks of the components that built are run.


Template Α: This PR modifies behaviour or interface

🤖 Generated with Claude Code

https://claude.ai/code/session_016nzdeKCYFDCkWsyM6etgkL

claude added 2 commits October 2, 2026 20:40
With `-j`, every plan node runs in parallel, and with per-component
builds each benchmark suite is its own node, so `cabal bench` could run
several benchmarks at the same time, from the same package or across a
project. Concurrent benchmarks compete for resources and skew each
other's results.

Add a `benchLock`, created in `rebuildTargets` next to `registerLock`
and `cacheLock`, and take it around the bench phase in
`buildAndRegisterUnpackedPackage`. Benchmarks now run one at a time,
while configuring and building other components stays parallel. The
lock is taken outside `timedDelegate`, so `--build-timings` does not
count time spent waiting for other benchmarks.

Add a cabal-testsuite test with three benchmarks in two packages, run
with `-j3`, each of which fails if another benchmark runs at the same
time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nzdeKCYFDCkWsyM6etgkL
Holding a lock while running a benchmark keeps benchmarks from running
at the same time, but other components could still be built while a
benchmark runs, which skews its results just as much.

Instead, the bench phase of a package no longer runs its benchmarks,
but records them. Once all the packages of the plan are built (still in
parallel), `rebuildTargets` runs the recorded benchmarks one at a time,
in plan order, and records their failures in the build outcomes. Unless
we keep going after failures, no benchmark is run if a package failed to
build, and no more benchmarks are run once one of them failed. See
Note [Running benchmarks].

This replaces the `benchLock` introduced by the previous commit.

The Sequential test now also checks that nothing is built once the
first benchmark started: pkg-b's benchmark depends on a library, so that
it is still being built when pkg-a's benchmarks are ready to run. The
golden outputs of MultipleBenchmarks and T5309 change accordingly:
builds now come before benchmark runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nzdeKCYFDCkWsyM6etgkL
@ulysses4ever ulysses4ever changed the title Claude/cabal issue 7557 m1znrj Fix cabal issue 7557: don’t run benchmarks in parallel or with the build Oct 3, 2026
Comment thread cabal-install/src/Distribution/Client/ProjectBuilding/UnpackedPackage.hs Outdated
claude added 2 commits October 3, 2026 00:36
A benchmark suite is not a package of its own: the units of the install
plan are single components, or whole packages when they cannot be built
per component. Say so in Note [Running benchmarks], and refer to units
rather than packages in the related comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVJmRffSh16Wx8JR7Kd1dC
Each update of the deferred benchmarks is a transaction of its own, and
they are only read once all the units are built, so a TVar buys nothing
over an IORef updated with atomicModifyIORef'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVJmRffSh16Wx8JR7Kd1dC
@ulysses4ever

Copy link
Copy Markdown
Collaborator Author

@Bodigrim are you interested in approving? I thought I'd ask since you left a comment...

@Bodigrim Bodigrim left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for working on this!

Comment thread changelog.d/issue-7557.md Outdated
Comment thread cabal-install/src/Distribution/Client/ProjectBuilding.hs
Comment thread cabal-install/src/Distribution/Client/ProjectBuilding.hs Outdated
Comment thread cabal-install/src/Distribution/Client/ProjectBuilding.hs Outdated
@Mikolaj

Mikolaj commented Oct 7, 2026

Copy link
Copy Markdown
Member

Would the "significant" changelog flag make sense? Maybe a release note snippet? This is sure to break peoples workflows, often for good.

…icant

- Walk the plan's execution order directly instead of building an
  intermediate list of benchmarks; give `go` a signature and drop the
  now-redundant `BuildFailure` annotation.
- changelog.d/issue-7557.md: set the PR number, add
  `significance: significant`, and spell out the user-visible consequences.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0176vAefrw8jPKXdh9h1wKuj
@ulysses4ever

Copy link
Copy Markdown
Collaborator Author

@Mikolaj, agreed on significance, added. Also expanded the changelog entry with the user-visible consequences, which should be helpful for release-notes readers. Let me know if you have specific suggestions for it though.

Thanks for the comments, I think I addressed all of them by basically accepting all of your suggestions and resolved the comments threads (I hope I wasn't too eager to do it, let me know!).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cabal bench should not run benchmarks in parallel

6 participants