Skip to content

Commit 4f7ac10

Browse files
committed
Solver: Combine dependencies on the same package in the same 'build-depends'.
This commit is unlikely to have an effect on real packages, but it reduces the chance of performance problems caused by duplicate dependencies in the solver quickcheck tests. It also adds a regression test to memory-usage-tests.
1 parent 5270950 commit 4f7ac10

File tree

2 files changed

+93
-12
lines changed

2 files changed

+93
-12
lines changed

cabal-install/Distribution/Solver/Modular/IndexConversion.hs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ module Distribution.Solver.Modular.IndexConversion
33
) where
44

55
import Data.List as L
6-
import Data.Map as M
6+
import Data.Map (Map)
7+
import qualified Data.Map as M
78
import Data.Maybe
89
import Data.Monoid as Mon
910
import Data.Set as S
@@ -245,12 +246,12 @@ flagInfo (StrongFlags strfl) =
245246
-- dependencies.
246247
type IPNs = Set PN
247248

248-
-- | Convenience function to delete a 'FlaggedDep' if it's
249+
-- | Convenience function to delete a 'Dependency' if it's
249250
-- for a 'PN' that isn't actually real.
250-
filterIPNs :: IPNs -> Dependency -> FlaggedDep PN -> FlaggedDeps PN
251-
filterIPNs ipns (Dependency pn _) fd
252-
| S.notMember pn ipns = [fd]
253-
| otherwise = []
251+
filterIPNs :: IPNs -> Dependency -> Maybe Dependency
252+
filterIPNs ipns d@(Dependency pn _)
253+
| S.notMember pn ipns = Just d
254+
| otherwise = Nothing
254255

255256
-- | Convert condition trees to flagged dependencies. Mutually
256257
-- recursive with 'convBranch'. See 'convBranch' for an explanation
@@ -262,8 +263,8 @@ convCondTree :: Map FlagName Bool -> DependencyReason PN -> PackageDescription -
262263
SolveExecutables ->
263264
CondTree ConfVar [Dependency] a -> FlaggedDeps PN
264265
convCondTree flags dr pkg os arch cinfo pn fds comp getInfo ipns solveExes@(SolveExecutables solveExes') (CondNode info ds branches) =
265-
concatMap
266-
(\d -> filterIPNs ipns d (D.Simple (convLibDep dr d) comp)) ds -- unconditional package dependencies
266+
L.map (\d -> D.Simple (convLibDep dr d) comp)
267+
(mergeDeps $ mapMaybe (filterIPNs ipns) ds) -- unconditional package dependencies
267268
++ L.map (\e -> D.Simple (LDep dr (Ext e)) comp) (PD.allExtensions bi) -- unconditional extension dependencies
268269
++ L.map (\l -> D.Simple (LDep dr (Lang l)) comp) (PD.allLanguages bi) -- unconditional language dependencies
269270
++ L.map (\(PkgconfigDependency pkn vr) -> D.Simple (LDep dr (Pkg pkn vr)) comp) (PD.pkgconfigDepends bi) -- unconditional pkg-config dependencies
@@ -273,14 +274,28 @@ convCondTree flags dr pkg os arch cinfo pn fds comp getInfo ipns solveExes@(Solv
273274
-- is True. It might be false in the legacy solver
274275
-- codepath, in which case there won't be any record of
275276
-- an executable we need.
276-
++ [ D.Simple (convExeDep dr exeDep) comp
277+
++ L.map (\d -> D.Simple (convExeDep dr d) comp)
278+
(mergeExeDeps
279+
[ exeDep
277280
| solveExes'
278281
, exeDep <- getAllToolDependencies pkg bi
279282
, not $ isInternal pkg exeDep
280-
]
283+
])
281284
where
282285
bi = getInfo info
283286

287+
-- Combine dependencies on the same package.
288+
mergeDeps :: [Dependency] -> [Dependency]
289+
mergeDeps deps =
290+
L.map (uncurry Dependency) $ M.toList $
291+
M.fromListWith (.&&.) [(p, vr) | Dependency p vr <- deps]
292+
293+
-- Combine dependencies on the same package and executable.
294+
mergeExeDeps :: [ExeDependency] -> [ExeDependency]
295+
mergeExeDeps deps =
296+
L.map (\((p, exe), vr) -> ExeDependency p exe vr) $ M.toList $
297+
M.fromListWith (.&&.) [((p, exe), vr) | ExeDependency p exe vr <- deps]
298+
284299
-- | Branch interpreter. Mutually recursive with 'convCondTree'.
285300
--
286301
-- Here, we try to simplify one of Cabal's condition tree branches into the
@@ -365,8 +380,8 @@ convBranch flags dr pkg os arch cinfo pn fds comp getInfo ipns solveExes (CondBr
365380
addFlag v = M.insert fn v flags'
366381
in extractCommon (t (addFlag True) (addFlagValue FlagBoth))
367382
(f (addFlag False) (addFlagValue FlagBoth))
368-
++ [ Flagged (FN pn fn) (fds ! fn) (t (addFlag True) (addFlagValue FlagTrue))
369-
(f (addFlag False) (addFlagValue FlagFalse)) ]
383+
++ [ Flagged (FN pn fn) (fds M.! fn) (t (addFlag True) (addFlagValue FlagTrue))
384+
(f (addFlag False) (addFlagValue FlagFalse)) ]
370385
go (Var (OS os')) t f
371386
| os == os' = t
372387
| otherwise = f

cabal-install/tests/UnitTests/Distribution/Solver/Modular/MemoryUsage.hs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ tests = [
1111
runTest $ basicTest "basic space leak test"
1212
, runTest $ flagsTest "package with many flags"
1313
, runTest $ issue2899 "issue #2899"
14+
, runTest $ duplicateDependencies "duplicate dependencies"
1415
]
1516

1617
-- | This test solves for n packages that each have two versions. There is no
@@ -95,3 +96,68 @@ issue2899 name =
9596

9697
goals :: [ExampleVar]
9798
goals = [P QualNone "setup-dep", P (QualSetup "target") "setup-dep"]
99+
100+
-- | Test for an issue related to lifting dependencies out of conditionals when
101+
-- converting a PackageDescription to the solver's internal representation.
102+
--
103+
-- Issue:
104+
-- For each conditional and each package B, the solver combined each dependency
105+
-- on B in the true branch with each dependency on B in the false branch. It
106+
-- added the combined dependencies to the build-depends outside of the
107+
-- conditional. Since dependencies could be lifted out of multiple levels of
108+
-- conditionals, the number of new dependencies could grow exponentially in the
109+
-- number of levels. For example, the following package generated 4 copies of B
110+
-- under flag-2=False, 8 copies under flag-1=False, and 16 copies at the top
111+
-- level:
112+
--
113+
-- if flag(flag-1)
114+
-- build-depends: B, B
115+
-- else
116+
-- if flag(flag-2)
117+
-- build-depends: B, B
118+
-- else
119+
-- if flag(flag-3)
120+
-- build-depends: B, B
121+
-- else
122+
-- build-depends: B, B
123+
--
124+
-- This issue caused the quickcheck tests to start frequently running out of
125+
-- memory after an optimization that pruned unreachable branches (See PR #4929).
126+
-- Each problematic test case contained at least one build-depends field with
127+
-- duplicate dependencies, which was then duplicated under multiple levels of
128+
-- conditionals by the solver's "buildable: False" transformation, when
129+
-- "buildable: False" was under multiple flags. Finally, the branch pruning
130+
-- feature put all build-depends fields in consecutive levels of the condition
131+
-- tree, causing the solver's representation of the package to follow the
132+
-- pattern in the example above.
133+
--
134+
-- Now the solver avoids this issue by combining all dependencies on the same
135+
-- package within a build-depends field before lifting them out of conditionals.
136+
--
137+
-- This test case is an expanded version of the example above, with library and
138+
-- build-tool dependencies.
139+
duplicateDependencies :: String -> SolverTest
140+
duplicateDependencies name =
141+
mkTest pkgs name ["A"] $ solverSuccess [("A", 1), ("B", 1)]
142+
where
143+
copies, depth :: Int
144+
copies = 50
145+
depth = 50
146+
147+
pkgs :: ExampleDb
148+
pkgs = [
149+
Right $ exAv "A" 1 (flaggedDependencies 1)
150+
, Right $ exAv "B" 1 [] `withExe` ExExe "exe" []
151+
]
152+
153+
flaggedDependencies :: Int -> [ExampleDependency]
154+
flaggedDependencies n
155+
| n > depth = buildDepends
156+
| otherwise = [exFlagged (flagName n) buildDepends
157+
(flaggedDependencies (n + 1))]
158+
where
159+
buildDepends = replicate copies (ExFix "B" 1)
160+
++ replicate copies (ExBuildToolFix "B" "exe" 1)
161+
162+
flagName :: Int -> ExampleFlagName
163+
flagName x = "flag-" ++ show x

0 commit comments

Comments
 (0)