Visitar URL original
Drop elabGPkgDescription from the elaborated plan by PaleRoses · Pull Request #12464 · haskell/cabal · GitHub
Skip to content

Drop elabGPkgDescription from the elaborated plan - #12464

Merged
mergify[bot] merged 3 commits into
haskell:masterfrom
PaleRoses:plan-cache-shared-descriptions
Nov 11, 2026
Merged

mergify[bot] merged 3 commits into
haskell:masterfrom
PaleRoses:plan-cache-shared-descriptions

Conversation

@PaleRoses

@PaleRoses PaleRoses commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

This removes elabGPkgDescription from each elaborated component and gives the solved generic description one owner per package: ElaboratedPackageSource, in a PackageId-keyed map in ElaboratedSharedConfig. The in-library configure reads that description for internal libraries and package checks, preserving rewrites made before solving.

The description hash is computed once per package and retained by each component. An edit to an inactive branch therefore still invalidates configure even when the finalized PackageDescription is unchanged. The shared map is serialized once per plan and stays out of the per-component configure cache.

Addresses #12463. Both comparisons below improve on the PR base, with a remaining gap to 3.16.1.0. All three Cabal binaries were built with GHC 9.12.4, -O1, and assertions enabled. Sizes are decimal MB.

On the synthetic project with 80 public sublibraries, the previously reported comparison measured twelve warm no-op cabal build all runs:

Version improved-plan Median no-op time
3.16.1.0 8.30 MB 0.263 s
PR base 538d465eb 118.42 MB 1.915 s
Shared-map revision 9083dfc5 10.90 MB 0.430 s

I also ran the HLS comparison Andrea suggested, at bc2bbb8e. I kept its upstream project configuration unchanged, with GHC 9.12.4 and index-state 2026-10-07T00:00:00Z. Each version ran cabal build all --dry-run --offline -j1 +RTS -N1 -RTS twice in a fresh build directory, repeated six times with version order balanced. The table measures the second run; setup, index-cache warmup and first-run solving are excluded.

Version improved-plan Median second dry-run Min–max
3.16.1.0 22.56 MB 0.582 s 0.569–0.589 s
PR base 538d465eb 59.92 MB 1.273 s 1.257–1.313 s
PR 00d86feb5 32.28 MB 0.959 s 0.940–0.963 s

On HLS, the plan is 46% smaller and the median second run takes 25% less time than the base. It is still 43% larger and takes 65% longer than 3.16. These are measurements on a shared development machine.

The semantic dependency graphs matched: 382 units from 285 package versions, with 29 of HLS's 31 declared plugin sublibraries enabled. The shared map contains 248 package sources for 345 configured units, and all 345 description fingerprints matched.

All runs used the same unchanged store and reused only 37 compiler-provided units. I haven't measured a populated HLS dependency store. The map still retains every configured solver package, including packages later satisfied by the store; every source remained needed in these plans, so this comparison leaves that retention cost unmeasured.

Sharing the finalized PackageDescription and pruning the source map remain follow-ups. Program fingerprints are separate in #12500. Squash merge would remove the superseded reparse approach from the history.


QA Notes

Build a package with several public sublibraries twice; the second build stays up to date. Change an inactive flag branch without changing the finalized package description, then build twice again: the edit reconfigures the components, and the following build stays up to date. T12463 covers this sequence. The internal-library and warning regressions check that configure still uses the shared description correctly.

@PaleRoses
PaleRoses force-pushed the plan-cache-shared-descriptions branch from 6d57dc6 to 219ee89 Compare October 15, 2026 06:58
@sheaf
sheaf self-requested a review October 15, 2026 07:44
@sheaf

sheaf commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

This MR attempts to regain sharing after-the-fact during serialisation. I think that's papering over the issue in an incomplete and non-robust way.

As far as I understand, the regression was due to the addition of elabGPkgDescription to ElaboratedConfiguredPackage. So the fix should be to remove that field and find another way to thread that information through which avoids the duplication.

A similar win in this area is to stop elabPkgDescription being duplicated in every single unit of a package, which could be similarly achieved by removing it from the unit-specific data and shared, but that change is not strictly needed to address the regression.

@PaleRoses
PaleRoses force-pushed the plan-cache-shared-descriptions branch from 219ee89 to ab7be0d Compare October 15, 2026 10:07
Every unit carried its package's GenericPackageDescription, so the plan
caches grew with the square of a package's component count. The
in-library configure now reads it from the package's .cabal file, as a
Setup script does.

Fixes haskell#12463.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PaleRoses
PaleRoses force-pushed the plan-cache-shared-descriptions branch from ab7be0d to a42643c Compare October 15, 2026 10:37
@PaleRoses PaleRoses changed the title Write each package's descriptions once in the plan caches Drop elabGPkgDescription from the elaborated plan Oct 15, 2026
@PaleRoses

Copy link
Copy Markdown
Contributor Author

done: dropped elabGPkgDescription, the in-library configure reads the .cabal file like Setup does. n=80 repro: improved-plan 27.3 → 14.8 MB. happy to do the same for elabPkgDescription in a follow-up.

@sheaf

sheaf commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Yes, much better approach, thanks. We will need to think through what it means for error messages for users, e.g. ensure that it doesn't cause duplicate error messages.

@ulysses4ever

ulysses4ever commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Thank you for working on this: the regression does look nasty.

Re-parsing cabal file feels like a stopgap solution though, and since we’re not releasing cabal any time soon, we still have time to think about a proper solution.

My advice would be to factor out PD-related structures into a new data type (say, ElaboratedPackageSource) and add a map PackageId -> (this new type) to ElaboratedSharedConfig. At first, the new type could only hold GPD, and that alone would allow to solve the regression. Then we can move more stuff there (PD and friends) to kill more duplication. ElaboratedSharedConfig seems to travel to all the right places for the idea to be viable.

@sheaf

sheaf commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Thank you for working on this: the regression does look nasty.

Re-parsing cabal file feels like a stopgap solution though, and since we’re not releasing cabal any time soon, we still have time to think about a proper solution.

My advice would be to factor out PD-related structures into a new data type (say, ElaboratedPackageSource) and add a map PackageId -> (this new type) to ElaboratedSharedConfig. At first, the new type could only hold GPD, that would allow to solve the regression. Then we can move more stuff there (PD and friends) to kill more duplication. ElaboratedSharedConfig seems to travel to all the right places for the plan to be viable.

Yes, I completely agree with this kind of approach.

@PaleRoses

Copy link
Copy Markdown
Contributor Author

Thank you for working on this: the regression does look nasty.

Re-parsing cabal file feels like a stopgap solution though, and since we’re not releasing cabal any time soon, we still have time to think about a proper solution.

My advice would be to factor out PD-related structures into a new data type (say, ElaboratedPackageSource) and add a map PackageId -> (this new type) to ElaboratedSharedConfig. At first, the new type could only hold GPD, and that alone would allow to solve the regression. Then we can move more stuff there (PD and friends) to kill more duplication. ElaboratedSharedConfig seems to travel to all the right places for the idea to be viable.

Agreed, this sounds like a better direction. Would it be useful if I tried the GPD-only version and reported back with results from my larger Haskell projects?

Keep the original GPD once per PackageId in ElaboratedSharedConfig and use it for in-library configure instead of reparsing the cabal file.

Retain a GPD fingerprint in each component configuration key so inactive conditional changes still trigger reconfiguration. Add T12463 coverage and update the changelog.
@ulysses4ever

Copy link
Copy Markdown
Collaborator

@PaleRoses absolutely. Thank you.

@PaleRoses

Copy link
Copy Markdown
Contributor Author

@sheaf @ulysses4ever
I pushed a GPD-only version in 9083dfc. On a synthetic project with 80 sublibraries, the plan went from 118 MB to 11 MB and the median no-op time from 2.0s to 0.5s. I haven’t tried it on my larger projects yet.

@ulysses4ever

Copy link
Copy Markdown
Collaborator

sounds promising. In the 118 MB → 11 MB case, what's the number for cabal 3.16?

@PaleRoses

Copy link
Copy Markdown
Contributor Author

@ulysses4ever
On the same synthetic project with 80 sublibraries, 3.16.1.0 gives an 8.30 MB plan and a 0.263s median no-op time. The PR base gives 118.42 MB / 1.915s, and 9083dfc gives 10.90 MB / 0.430s. So this recovers most of the regression, though there’s still a gap to 3.16. All three were built with GHC 9.12.4 and -O1, with assertions enabled.

@ulysses4ever ulysses4ever 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, the shared map works well. A few comments inline, and some on the description.

What I checked:

  • ProjectPlanning.hs is the only place that builds an ElaboratedSharedConfig, so no site misses the new field. The keys of the map, the packageId of each configured solver package, are the values that the lookup by elabPkgSourceId uses, and Backpack instantiations share the id of their package.
  • The key of the per-component cache/config is the elaborated package alone, not the shared config, so the map is written once per plan and never per component. That also removes the per-component GPD that @andreabedini measured.
  • The hash is needed, not a leftover: the in-library configure really reads the GPD (computePackageInfoFromIndex for internal libraries, and checkPackageProblems), so an edit that leaves the finalized PackageDescription unchanged can still change what configure does.
  • #11392 (MicroHs) rewrites packages before solving and relies on the in-library configure seeing the rewritten descriptions. I built it with this PR merged in: its MHS tests and T12463 pass, and the first 16 packages of its replay of MicroHs's package list build and run. Re-reading the .cabal file, as the first commit did, would have bypassed that rewrite; the map doesn't.

On the description and the history:

  • The description still says "the in-library configure reads the .cabal file instead, as Setup does. unpacking already writes revisions there", and its table has the numbers of that first approach. Could you describe the shared map and the hash, and give the final numbers, with 3.16 for reference? Your comment above has them: 8.30 MB / 0.26 s with 3.16, 118 MB / 1.9 s at the base, 10.9 MB / 0.43 s with the PR.
  • The QA notes are about Hackage revisions, which only mattered for re-reading the file, and "Tests have been added" is unticked, although T12463 is there now.
  • The first commit still has the re-reading approach, which the second one replaces, so I'd merge with squash (squash+merge me), so that the history doesn't keep the abandoned approach.

Not blocking, for later: elabPkgDescription is still copied into every component, and @sheaf and I both suggested moving it into the same per-package record; the PATH copies in elabConfiguredPrograms are separate, as @andreabedini says.

@andreabedini

Copy link
Copy Markdown
Collaborator

Looks right to me: the config cache key stays the normalised elab, the hash is computed once per package (components are record updates of elab0), and the hash is needed since configure consumes the GPD.

One thing before calling #12463 fixed: the map holds a GPD for every Configured solver package, i.e. every Hackage dependency, including the ones already in the store. 3.16 had one of those. the 80-sublib benchmark is a single package, so it doesn't exercise this.

haskell-language-server would be a good real-world check: 31 plugin sublibraries in one package plus a large hackage closure. You won't even need to build it, running cabal build all --dry-run twice is enough. Then compare the size of dist-newstyle/cache/improved-plan and the time of the second run across 3.16.1.0, the PR base and this PR.

The first commit is the superseded re-parse approach, so this wants squash+merge (IMHO).

@PaleRoses

Copy link
Copy Markdown
Contributor Author

@ulysses4ever thanks. Both inline points are done in 00d86fe, and the description now describes the shared map and the hash, with the final numbers and 3.16 for reference.

@andreabedini I ran the HLS comparison; the table is in the description. Against the base, improved-plan goes 59.92 → 32.28 MB and the second dry-run 1.273 → 0.959 s, with 3.16 at 22.56 MB / 0.582 s. On the store question: every source in the map was still needed in these plans, so the retention cost is unmeasured here. Squash+merge is fine by me.

@sheaf the duplicate-messages question was about re-reading the .cabal file, which is gone. The shared map hands configure the same description the field did, so nothing is reported more often than before. Could you take another look?

@sheaf

sheaf commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

Yes, this looks great. To me the new approach is nice, principled and elegant. Something we can build on for future improvements. Thank you for persevering!

@ulysses4ever ulysses4ever added the squash+merge me Tell Mergify Bot to squash-merge label Nov 8, 2026
@mergify mergify Bot added the ready and waiting Mergify is waiting out the cooldown period label Nov 9, 2026
@ulysses4ever

Copy link
Copy Markdown
Collaborator

Was this by any chance prepared with some help from AI? We need to put a label for that in that case. Otherwise it’s good to go. Link the follow-ups from here if any

@PaleRoses

Copy link
Copy Markdown
Contributor Author

Yes, AI helped prepare this. The follow-up is #12500.

@mergify mergify Bot added merge delay passed Applied (usually by Mergify) when PR approved and received no updates for 2 days queued labels Nov 11, 2026
@mergify

mergify Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Merged into master as 3c15ba7 at 02:10 UTC

2 h 9 min in the queue, 1 h 58 min of it CI.

So far

12 merge conditions met
  • #review-threads-unresolved = 0 · required by GitHub branch protection
  • github-review-approved · required by GitHub branch protection
  • any of · required by GitHub branch protection:
    • check-success = Doctest Cabal
    • check-neutral = Doctest Cabal
    • check-skipped = Doctest Cabal
  • any of · required by GitHub branch protection:
    • check-success = Meta checks
    • check-neutral = Meta checks
    • check-skipped = Meta checks
  • any of · required by GitHub branch protection:
    • check-success = docs/readthedocs.org:cabal
    • check-neutral = docs/readthedocs.org:cabal
    • check-skipped = docs/readthedocs.org:cabal
  • any of · required by GitHub branch protection:
    • check-success = Validate post job
    • check-neutral = Validate post job
    • check-skipped = Validate post job
  • any of · required by GitHub branch protection:
    • check-success = fourmolu
    • check-neutral = fourmolu
    • check-skipped = fourmolu
  • any of · required by GitHub branch protection:
    • check-success = hlint
    • check-neutral = hlint
    • check-skipped = hlint
  • any of · required by GitHub branch protection:
    • check-success = Bootstrap post job
    • check-neutral = Bootstrap post job
    • check-skipped = Bootstrap post job
  • any of · required by GitHub branch protection:
    • check-success = whitespace
    • check-neutral = whitespace
    • check-skipped = whitespace
  • any of · required by GitHub branch protection:
    • check-success = Check sdist post job
    • check-neutral = Check sdist post job
    • check-skipped = Check sdist post job
  • any of · required by GitHub branch protection:
    • check-success = Changelogs
    • check-neutral = Changelogs
    • check-skipped = Changelogs

Times in UTC · dashboard · how the merge queue works

@PaleRoses PaleRoses mentioned this pull request Nov 11, 2026
5 of 6 tasks
@mergify
mergify Bot merged commit 3c15ba7 into haskell:master Nov 11, 2026
73 checks passed
@mergify mergify Bot removed the queued label Nov 11, 2026
fendor pushed a commit to fendor/cabal that referenced this pull request Nov 11, 2026
* Drop elabGPkgDescription from the elaborated plan

Every unit carried its package's GenericPackageDescription, so the plan
caches grew with the square of a package's component count. The
in-library configure now reads it from the package's .cabal file, as a
Setup script does.

Fixes haskell#12463.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Share generic package descriptions across elaborated components

Keep the original GPD once per PackageId in ElaboratedSharedConfig and use it for in-library configure instead of reparsing the cabal file.

Retain a GPD fingerprint in each component configuration key so inactive conditional changes still trigger reconfiguration. Add T12463 coverage and update the changelog.

* Clarify shared package source lookup and documentation

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
fendor pushed a commit to fendor/cabal that referenced this pull request Nov 11, 2026
* Drop elabGPkgDescription from the elaborated plan

Every unit carried its package's GenericPackageDescription, so the plan
caches grew with the square of a package's component count. The
in-library configure now reads it from the package's .cabal file, as a
Setup script does.

Fixes haskell#12463.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Share generic package descriptions across elaborated components

Keep the original GPD once per PackageId in ElaboratedSharedConfig and use it for in-library configure instead of reparsing the cabal file.

Retain a GPD fingerprint in each component configuration key so inactive conditional changes still trigger reconfiguration. Add T12463 coverage and update the changelog.

* Clarify shared package source lookup and documentation

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-assisted merge delay passed Applied (usually by Mergify) when PR approved and received no updates for 2 days ready and waiting Mergify is waiting out the cooldown period squash+merge me Tell Mergify Bot to squash-merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants