Share `UnitInfo`s across `UnitEnv` using `UnitIndex` and `ExternalUnitDatabaseCache`

Introduces a global cache for already read databases instead of reparsing the unit databases per home unit. This saves quite a lot of memory at the cost of a little bit more global state.

How much memory? The GHC CI says it reduces the allocated bytes by 13% for mhu-perf test and peak megabytes from 20 to 18 MB which is quite nice, considering the only thing that is actually shared in this test is the global package db.

This MR is part of a larger effort to restructure UnitState to be more performant with multiple home units and easier to reason about. Related to #27308 (closed), which is part of the same effort.

Introduce global unit database cache

As a first step for better sharing of UnitInfo across UnitEnv, we introduce a new datatype called ExternalUnitDatabases.

It primarily serves as an in-memory representation of all UnitDatabases across UnitEnv. This means, if multiple HomeUnitEnvs depend on the same database, one way or another, we make sure that we don't parse from disk every time. Instead, we store the in-memory representation in ExternalUnitDatabases.

ExternalUnitDatabaseCache is the equivalent of ExternalUnitState in the UnitEnv. It is a mutable variable wrapping ExternalUnitDatabases.

The mutable ExternalUnitDatabaseCache is used in initUnits to make sure we don't parse the same unit database multiple times.

Almost by accident, we change the semantics of initUnits to honour modifications to packageDBFlags. The inability to change packageDBFlags while also reusing the already parsed UnitDatabases was reported in #26423 (closed) as a bug. Hence, we think this behaviour change is warranted and acceptable, especially since it comes with a breaking change to the initUnits API.

Add regression test for #26423 (closed)

Closes #26423 (closed)

Introduce UnitIndex for global external unit caching

UnitInfos have been observed to cause a lot of memory usage in #27500 (closed). Especially with multiple home units, as the same (external) units are processed from scratch, even though most of the time we end up with exactly the same UnitInfo.

We introduce a UnitEnv global cache that allows us to store external unit information that is used across all HomeUnitEnvs.

The most important change in this commit is the introduction of the UnitIndex. It stores a global mapping of UnitId -> UnitInfo, and initUnits always uses the cached UnitInfo entry to populate each HomeUnitEnv's UnitState. This allows us to ensure the following property:

Each UnitInfo should be alive exactly once in GHC.

All UnitStates should reference 'UnitInfo's stored in the 'UnitIndex'. This ensured by calling 'initUnits' with the 'UnitIndex'.

In addition, the ExternalUnitDatabases may also hold a reference to each on-disk representation of UnitInfo.

This means, we impose an hard upper bound on the number of UnitInfos alive in the GHC session:

The number of alive UnitInfos closure objects must be the sum of all loaded unit database times two.

We add performance regression tests that make sure the number of live UnitInfo cannot exceed this threshold.

Closes #27500 (closed)

-------------------------
Metric Decrease:
    MultiComponentModules
    MultiComponentModulesRecomp
    MultiComponentModulesRecomp100
    mhu-perf
    LinkableUsage02
-------------------------

These metrics increases are especially notable, as we are not even sharing anything big but merely the global package database with 50 entries. It shows how careful sharing of UnitInfo can improve memory usage. We expect this to be much more notable when the whole cabal package database is shared across multiple home units.

LinkableUsage02 metric decreases on unreg and i386 platform, only.


Technical details

To share the UnitInfos correctly, it is important that we extract the WireMap into the UnitIndex. At the moment of writing, WireMap must be globally the same for all HomeUnitEnvs. This is important, as we could otherwise not cache the "fully-resolved" UnitInfo, as we don't change the UnitId or unitAbiHash when resolving wired-in units. Thus, there could be ambiguities, when the WireMap is not the same for all UnitStates across the UnitEnv.

We consider a UnitInfo fully-resolved, if wired-in units have been updated, the UnitInfo has been validated and variables in the unit config, such as ${pkgroot} have been resolved.

Updating the wired-in units requires the WireMap to be globally the same.

Edited by Hannes Siebenhandl

Merge request reports

Loading