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
UnitInfoshould 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.