Apr 22, 6-7 AM (34)
Apr 22, 7-8 AM (21)
Apr 22, 8-9 AM (37)
Apr 22, 9-10 AM (18)
Apr 22, 10-11 AM (47)
Apr 22, 11-12 PM (45)
Apr 22, 12-1 PM (56)
Apr 22, 1-2 PM (64)
Apr 22, 2-3 PM (44)
Apr 22, 3-4 PM (86)
Apr 22, 4-5 PM (46)
Apr 22, 5-6 PM (17)
Apr 22, 6-7 PM (10)
Apr 22, 7-8 PM (18)
Apr 22, 8-9 PM (15)
Apr 22, 9-10 PM (23)
Apr 22, 10-11 PM (31)
Apr 22, 11-12 AM (17)
Apr 23, 12-1 AM (7)
Apr 23, 1-2 AM (4)
Apr 23, 2-3 AM (4)
Apr 23, 3-4 AM (6)
Apr 23, 4-5 AM (3)
Apr 23, 5-6 AM (8)
Apr 23, 6-7 AM (17)
Apr 23, 7-8 AM (26)
Apr 23, 8-9 AM (33)
Apr 23, 9-10 AM (33)
Apr 23, 10-11 AM (29)
Apr 23, 11-12 PM (30)
Apr 23, 12-1 PM (51)
Apr 23, 1-2 PM (69)
Apr 23, 2-3 PM (74)
Apr 23, 3-4 PM (26)
Apr 23, 4-5 PM (22)
Apr 23, 5-6 PM (7)
Apr 23, 6-7 PM (7)
Apr 23, 7-8 PM (11)
Apr 23, 8-9 PM (14)
Apr 23, 9-10 PM (6)
Apr 23, 10-11 PM (28)
Apr 23, 11-12 AM (18)
Apr 24, 12-1 AM (7)
Apr 24, 1-2 AM (4)
Apr 24, 2-3 AM (7)
Apr 24, 3-4 AM (5)
Apr 24, 4-5 AM (8)
Apr 24, 5-6 AM (13)
Apr 24, 6-7 AM (12)
Apr 24, 7-8 AM (33)
Apr 24, 8-9 AM (40)
Apr 24, 9-10 AM (41)
Apr 24, 10-11 AM (72)
Apr 24, 11-12 PM (57)
Apr 24, 12-1 PM (100)
Apr 24, 1-2 PM (57)
Apr 24, 2-3 PM (35)
Apr 24, 3-4 PM (19)
Apr 24, 4-5 PM (16)
Apr 24, 5-6 PM (38)
Apr 24, 6-7 PM (27)
Apr 24, 7-8 PM (12)
Apr 24, 8-9 PM (42)
Apr 24, 9-10 PM (17)
Apr 24, 10-11 PM (30)
Apr 24, 11-12 AM (16)
Apr 25, 12-1 AM (8)
Apr 25, 1-2 AM (1)
Apr 25, 2-3 AM (10)
Apr 25, 3-4 AM (5)
Apr 25, 4-5 AM (3)
Apr 25, 5-6 AM (13)
Apr 25, 6-7 AM (1)
Apr 25, 7-8 AM (4)
Apr 25, 8-9 AM (24)
Apr 25, 9-10 AM (17)
Apr 25, 10-11 AM (4)
Apr 25, 11-12 PM (4)
Apr 25, 12-1 PM (13)
Apr 25, 1-2 PM (3)
Apr 25, 2-3 PM (10)
Apr 25, 3-4 PM (6)
Apr 25, 4-5 PM (10)
Apr 25, 5-6 PM (16)
Apr 25, 6-7 PM (13)
Apr 25, 7-8 PM (30)
Apr 25, 8-9 PM (55)
Apr 25, 9-10 PM (13)
Apr 25, 10-11 PM (21)
Apr 25, 11-12 AM (22)
Apr 26, 12-1 AM (5)
Apr 26, 1-2 AM (0)
Apr 26, 2-3 AM (2)
Apr 26, 3-4 AM (5)
Apr 26, 4-5 AM (2)
Apr 26, 5-6 AM (2)
Apr 26, 6-7 AM (3)
Apr 26, 7-8 AM (8)
Apr 26, 8-9 AM (3)
Apr 26, 9-10 AM (0)
Apr 26, 10-11 AM (2)
Apr 26, 11-12 PM (1)
Apr 26, 12-1 PM (6)
Apr 26, 1-2 PM (4)
Apr 26, 2-3 PM (14)
Apr 26, 3-4 PM (14)
Apr 26, 4-5 PM (0)
Apr 26, 5-6 PM (13)
Apr 26, 6-7 PM (13)
Apr 26, 7-8 PM (7)
Apr 26, 8-9 PM (7)
Apr 26, 9-10 PM (5)
Apr 26, 10-11 PM (27)
Apr 26, 11-12 AM (21)
Apr 27, 12-1 AM (7)
Apr 27, 1-2 AM (7)
Apr 27, 2-3 AM (9)
Apr 27, 3-4 AM (9)
Apr 27, 4-5 AM (5)
Apr 27, 5-6 AM (13)
Apr 27, 6-7 AM (7)
Apr 27, 7-8 AM (82)
Apr 27, 8-9 AM (47)
Apr 27, 9-10 AM (33)
Apr 27, 10-11 AM (62)
Apr 27, 11-12 PM (80)
Apr 27, 12-1 PM (66)
Apr 27, 1-2 PM (44)
Apr 27, 2-3 PM (51)
Apr 27, 3-4 PM (39)
Apr 27, 4-5 PM (36)
Apr 27, 5-6 PM (26)
Apr 27, 6-7 PM (13)
Apr 27, 7-8 PM (26)
Apr 27, 8-9 PM (13)
Apr 27, 9-10 PM (15)
Apr 27, 10-11 PM (42)
Apr 27, 11-12 AM (28)
Apr 28, 12-1 AM (17)
Apr 28, 1-2 AM (8)
Apr 28, 2-3 AM (4)
Apr 28, 3-4 AM (5)
Apr 28, 4-5 AM (5)
Apr 28, 5-6 AM (8)
Apr 28, 6-7 AM (8)
Apr 28, 7-8 AM (36)
Apr 28, 8-9 AM (54)
Apr 28, 9-10 AM (59)
Apr 28, 10-11 AM (53)
Apr 28, 11-12 PM (56)
Apr 28, 12-1 PM (48)
Apr 28, 1-2 PM (53)
Apr 28, 2-3 PM (68)
Apr 28, 3-4 PM (31)
Apr 28, 4-5 PM (13)
Apr 28, 5-6 PM (47)
Apr 28, 6-7 PM (9)
Apr 28, 7-8 PM (8)
Apr 28, 8-9 PM (14)
Apr 28, 9-10 PM (20)
Apr 28, 10-11 PM (34)
Apr 28, 11-12 AM (29)
Apr 29, 12-1 AM (11)
Apr 29, 1-2 AM (1)
Apr 29, 2-3 AM (1)
Apr 29, 3-4 AM (6)
Apr 29, 4-5 AM (1)
Apr 29, 5-6 AM (3)
Apr 29, 6-7 AM (0)
3,800 commits this week Apr 22, 2026 - Apr 29, 2026
feat(tx-evaluation): squashed work for stacked rebase
- chore: add devshell tools and CI tweaks for tx evaluation
- chore: update workspace deps for tx evaluation
- feat(common): add lazy chain config loader and helpers
- fix(node): improve CBOR validation error messages
- feat(node): add chain config, ledger, and utxo helpers
- feat: add bf-tx-evaluator crate with native and external evaluators
- feat(platform): add /utils/txs/evaluate endpoints
- test: add coverage for tx evaluate endpoints
Optimize evaluation performance: uniqueWithName, checkUnique, planned.nix, load-cabal-plan (#2495)
* Optimize uniqueWithName and checkUnique for eval performance

Profiling cardano-node evaluation with per-function allocation attribution
revealed that uniqueWithName/checkUnique consume 36% of inclusive eval time
(2.4s out of 6.7s) and ~1 GiB of memory.

Three changes:

1. Replace uniqueWithName implementation: the old approach used
   `groupBy` + `lib.unique` per group. `lib.unique` is O(n²) via
   linear `elem` scan with structural equality.  The new implementation
   uses `builtins.listToAttrs` which runs entirely in C++ at O(n log n).
   First occurrence wins (same semantics for derivation dedup by name).

2. Fix checkUnique: the old implementation called `uniqueWithName` just
   to compare list lengths, then discarded the result.  If no duplicates
   existed (the common case), it returned the original list and all the
   dedup work was wasted.  The error path then called `groupBy` a third
   time.  The new implementation uses a single `groupBy` to find groups
   with length > 1, avoiding redundant work.

3. Fuse four nested `map` passes in make-config-files.nix:
   The Nix evaluator does not perform list fusion -- each `map` allocates
   a full intermediate list.  The old code chained dependToLib, profiled
   variant, DWARF variant, and chooseDrv as four separate `map` calls.
   Fused into a single pass to eliminate three intermediate allocations
   per component across hundreds of packages.

* Enable use-package-keys by default for faster module evaluation

When use-package-keys is false (the old default), the `packages` option
is declared as `attrsOf package` -- an open-ended attribute set where the
NixOS module system must handle arbitrary keys via recursive submodule
merging.

When true, it uses `genAttrs config.package-keys` to declare a closed
set of known package names.  The module system knows exactly which
packages exist upfront and can skip the open-ended merge machinery.

The package-keys are computed from the cabal plan (via addPackageKeys),
so the set is always correct for the resolved plan.  Users who inject
unplanned packages via modules should add names to package-keys
explicitly.

* Optimize load-cabal-plan.nix: single-pass plan partitioning

The install-plan was being filtered 3+ times with separate concatMap
passes (pre-existing, configured/global, configured/local), each
iterating over all ~600 packages.  Replace with a single builtins.groupBy
call that categorizes entries in one C++ pass at O(n log k).

The consumers (pkgs, extras) now iterate only over their relevant
partition instead of the entire install-plan with inline filters.
This eliminates redundant type comparisons and optional allocations.

Also restructured pre-existing-depends for clarity while keeping
the lazy memoization that avoids recomputing transitive lookups.

* Simplify planned.nix: remove mkOverride and avoid getComponents

Two optimizations to planned.nix:

1. Replace `lib.mkOverride 900 true` with plain `true` for the
   `planned` flag.  The plan is definitive -- no other module in
   the loadCabalPlan code path sets `planned`, so the override
   priority machinery is wasted work.  Avoids allocating ~3000
   override wrapper attrsets through the module system.

2. Extract component keys directly from plan.json's `p.components`
   structure using the prefix mapping (exe:→exes, test:→tests, etc.)
   instead of calling the full `getComponents` function.  The old
   code invoked dependency resolution (lookupDependencies,
   lookupPreExisting) against an empty hsPkgs -- all that work was
   thrown away since planned.nix only needs component keys to set
   `planned = true` on them.

* Fix uniqueWithName to preserve structurally distinct items

The listToAttrs rewrite in bd93f01d3 keyed every item by `x.name or
"noname"` and let listToAttrs first-wins handle collisions.  That
collapses any two items sharing a key (or both lacking `.name`) to
one survivor, even when they are structurally different -- which
contradicts the function's contract.  uniqueWithName must behave like
`lib.unique`; the name partition is a *speedup* (so lib.unique only
has to scan within a bucket), not a truncation.

This broke `shellFor`: builder/shell-for.nix:92-93 runs uniqueWithName
on `cfg.depends`, which lookupDependency populates with haskell.nix
package values (`hsPkgs.<unit-id>`).  Those expose `.identifier.{name,
version}` but not `.name`, so every dependency landed in the noname
bucket and was reduced to one entry.  The resulting GHC package DB
held only one library, surfacing as `Could not find module 'Conduit'`
in the shell-for and shell-for-setup-deps tests across all GHC
versions in eval 1223.

Restore the contract: groupBy by a synthesized key, then `lib.unique`
per bucket.  Single-item buckets stay linear (lib.unique on a
1-element list is O(1)); multi-item buckets are correctly structural-
deduped instead of silently truncated.  Keying:
- derivations: their `.name` already encodes name+version
- haskell.nix packages: `"${identifier.name} ${identifier.version}"`
  with a space separator so package keys cannot collide with
  derivation keys derived from the same words
- everything else: shared `_noname`/`_notset` bucket; correctness still
  holds via the inner lib.unique, just at the slower path.

Note: shellFor has fed packages-without-`.name` into uniqueWithName
since at least 2019 (hspkg-builder.nix has never set `.name`), so this
is a long-standing reliance on the slow path's tolerance for noname
collisions, not a new condition.

* Bump stale head.hackage pin in test/cabal.project.local

* planned.nix: drop unused getComponents parameter

After d3819208f, planned.nix extracts component keys directly from
plan.json and no longer calls getComponents, but it still took it
as a parameter and the call site in lib/load-cabal-plan.nix still
passed it.  Drop both.

getComponents (plural) is internal to load-cabal-plan.nix and is not
exposed publicly; the externally-visible name is the singular
project.getComponent / package.getComponent, which is unrelated.

* planned.nix: derive prefixMap from componentPrefix

The hard-coded prefix table duplicated haskellLib.componentPrefix.
If anyone adds a new component type to that single source of truth,
planned.nix would silently miss it.

Pass componentPrefix in and invert it (collection -> prefix becomes
prefix -> collection).  Verified the inverted map equals the previous
literal { exe = "exes"; test = "tests"; bench = "benchmarks";
flib = "foreignlibs"; lib = "sublibs"; }.

* load-cabal-plan: reject unknown install-plan entries

The single-pass groupBy in b3f7c7dfa replaced an explicit
`type == "configured" && (style == "global" || style == "inplace")`
filter with an else-branch, so any plan entry that wasn't pre-existing
or configured/local fell into `nonLocal` -- which the consumer treats
as a global hackage package.  For current cabal output that's the same
set, but the change silently widens the bucket if cabal ever emits a
new type or style we haven't taught us to handle.

Restore the explicit guard.  Anything outside the four recognized
shapes (pre-existing; configured/local; configured/global;
configured/inplace) throws with the offending id/type/style so a real
plan shape we need to handle surfaces immediately, instead of being
silently misclassified as a hackage import.

* Bump head.hackage

* checkUnique: warn-not-error on name-only collisions

bd93f01d3 rewrote checkUnique to error on any two items sharing a name,
which is stricter than master's behavior.  Master's checkUnique
(`length x == length (uniqueWithName x)`) only errored when
`uniqueWithName` would actually drop items -- i.e., when two items were
structurally identical.  Two structurally-distinct items with the same
name (e.g. an HsOpenSSL.libs override that appended a non-static openssl
on top of the default openssl from cabal2nix) used to pass silently.

The bd93f01d3 strictness surfaced as `Duplicate items found in
HsOpenSSL ... buildInputs ["openssl-..."]` for th-dlls.build-ei.  That
duplication is real and worth flagging, but escalating to a hard error
is too aggressive for a long-standing pattern.

Restore master's "structurally-identical = error" rule, and add a
warning for the name-only case so the underlying list-merge issue is
visible without breaking builds.  Both functions now share a
`uniqueWithNameKey` helper so the name+version logic stays consistent.

---------

Co-authored-by: Hamish Mackenzie <[email protected]>