File: MemoryUsage.hs

package info (click to toggle)
haskell-cabal-install 3.10.3.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, trixie
  • size: 3,400 kB
  • sloc: haskell: 52,202; sh: 80; makefile: 9
file content (196 lines) | stat: -rw-r--r-- 7,915 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
-- | Tests for detecting space leaks in the dependency solver.
module UnitTests.Distribution.Solver.Modular.MemoryUsage (tests) where

import Test.Tasty (TestTree)

import UnitTests.Distribution.Solver.Modular.DSL
import UnitTests.Distribution.Solver.Modular.DSL.TestCaseUtils

tests :: [TestTree]
tests = [
      runTest $ basicTest "basic space leak test"
    , runTest $ flagsTest "package with many flags"
    , runTest $ issue2899 "issue #2899"
    , runTest $ duplicateDependencies "duplicate dependencies"
    , runTest $ duplicateFlaggedDependencies "duplicate flagged dependencies"
    ]

-- | This test solves for n packages that each have two versions. There is no
-- solution, because the nth package depends on another package that doesn't fit
-- its version constraint. Backjumping and fine grained conflicts are disabled,
-- so the solver must explore a search tree of size 2^n. It should fail if
-- memory usage is proportional to the size of the tree.
basicTest :: String -> SolverTest
basicTest name =
    disableBackjumping $
    disableFineGrainedConflicts $
    mkTest pkgs name ["target"] anySolverFailure
  where
    n :: Int
    n = 18

    pkgs :: ExampleDb
    pkgs = map Right $
           [ exAv "target" 1 [ExAny $ pkgName 1]]
        ++ [ exAv (pkgName i) v [ExRange (pkgName $ i + 1) 2 4]
           | i <- [1..n], v <- [2, 3]]
        ++ [exAv (pkgName $ n + 1) 1 []]

    pkgName :: Int -> ExamplePkgName
    pkgName x = "pkg-" ++ show x

-- | This test is similar to 'basicTest', except that it has one package with n
-- flags, flag-1 through flag-n. The solver assigns flags in order, so it
-- doesn't discover the unknown dependencies under flag-n until it has assigned
-- all of the flags. It has to explore the whole search tree.
flagsTest :: String -> SolverTest
flagsTest name =
    disableBackjumping $
    disableFineGrainedConflicts $
    goalOrder orderedFlags $ mkTest pkgs name ["pkg"] anySolverFailure
  where
    n :: Int
    n = 16

    pkgs :: ExampleDb
    pkgs = [Right $ exAv "pkg" 1 $
                [exFlagged (numberedFlag n) [ExAny "unknown1"] [ExAny "unknown2"]]

                -- The remaining flags have no effect:
             ++ [exFlagged (numberedFlag i) [] [] | i <- [1..n - 1]]
           ]

    orderedFlags :: [ExampleVar]
    orderedFlags = [F QualNone "pkg" (numberedFlag i) | i <- [1..n]]

-- | Test for a space leak caused by sharing of search trees under packages with
-- link choices (issue #2899).
--
-- The goal order is fixed so that the solver chooses setup-dep and then
-- target-setup.setup-dep at the top of the search tree. target-setup.setup-dep
-- has two choices: link to setup-dep, and don't link to setup-dep. setup-dep
-- has a long chain of dependencies (pkg-1 through pkg-n). However, pkg-n
-- depends on pkg-n+1, which doesn't exist, so there is no solution. Since each
-- dependency has two versions, the solver must try 2^n combinations when
-- backjumping and fine grained conflicts are disabled. These combinations
-- create large search trees under each of the two choices for
-- target-setup.setup-dep. Although the choice to not link is disallowed by the
-- Single Instance Restriction, the solver doesn't know that until it has
-- explored (and evaluated) the whole tree under the choice to link. If the two
-- trees are shared, memory usage spikes.
issue2899 :: String -> SolverTest
issue2899 name =
    disableBackjumping $
    disableFineGrainedConflicts $
    goalOrder goals $ mkTest pkgs name ["target"] anySolverFailure
  where
    n :: Int
    n = 16

    pkgs :: ExampleDb
    pkgs = map Right $
           [ exAv "target" 1 [ExAny "setup-dep"] `withSetupDeps` [ExAny "setup-dep"]
           , exAv "setup-dep" 1 [ExAny $ pkgName 1]]
        ++ [ exAv (pkgName i) v [ExAny $ pkgName (i + 1)]
           | i <- [1..n], v <- [1, 2]]

    pkgName :: Int -> ExamplePkgName
    pkgName x = "pkg-" ++ show x

    goals :: [ExampleVar]
    goals = [P QualNone "setup-dep", P (QualSetup "target") "setup-dep"]

-- | Test for an issue related to lifting dependencies out of conditionals when
-- converting a PackageDescription to the solver's internal representation.
--
-- Issue:
-- For each conditional and each package B, the solver combined each dependency
-- on B in the true branch with each dependency on B in the false branch. It
-- added the combined dependencies to the build-depends outside of the
-- conditional. Since dependencies could be lifted out of multiple levels of
-- conditionals, the number of new dependencies could grow exponentially in the
-- number of levels. For example, the following package generated 4 copies of B
-- under flag-2=False, 8 copies under flag-1=False, and 16 copies at the top
-- level:
--
-- if flag(flag-1)
--   build-depends: B, B
-- else
--   if flag(flag-2)
--     build-depends: B, B
--   else
--     if flag(flag-3)
--       build-depends: B, B
--     else
--       build-depends: B, B
--
-- This issue caused the quickcheck tests to start frequently running out of
-- memory after an optimization that pruned unreachable branches (See PR #4929).
-- Each problematic test case contained at least one build-depends field with
-- duplicate dependencies, which was then duplicated under multiple levels of
-- conditionals by the solver's "buildable: False" transformation, when
-- "buildable: False" was under multiple flags. Finally, the branch pruning
-- feature put all build-depends fields in consecutive levels of the condition
-- tree, causing the solver's representation of the package to follow the
-- pattern in the example above.
--
-- Now the solver avoids this issue by combining all dependencies on the same
-- package before lifting them out of conditionals.
--
-- This test case is an expanded version of the example above, with library and
-- build-tool dependencies.
duplicateDependencies :: String -> SolverTest
duplicateDependencies name =
    mkTest pkgs name ["A"] $ solverSuccess [("A", 1), ("B", 1)]
  where
    copies, depth :: Int
    copies = 50
    depth = 50

    pkgs :: ExampleDb
    pkgs = [
        Right $ exAv "A" 1 (dependencyTree 1)
      , Right $ exAv "B" 1 [] `withExe` exExe "exe" []
      ]

    dependencyTree :: Int -> [ExampleDependency]
    dependencyTree n
        | n > depth = buildDepends
        | otherwise = [exFlagged (numberedFlag n) buildDepends
                                                  (dependencyTree (n + 1))]
      where
        buildDepends = replicate copies (ExFix "B" 1)
                    ++ replicate copies (ExBuildToolFix "B" "exe" 1)

-- | This test is similar to duplicateDependencies, except that every dependency
-- on B is replaced by a conditional that contains B in both branches. It tests
-- that the solver doesn't just combine dependencies within one build-depends or
-- build-tool-depends field; it also needs to combine dependencies after they
-- are lifted out of conditionals.
duplicateFlaggedDependencies :: String -> SolverTest
duplicateFlaggedDependencies name =
    mkTest pkgs name ["A"] $ solverSuccess [("A", 1), ("B", 1)]
  where
    copies, depth :: Int
    copies = 15
    depth = 15

    pkgs :: ExampleDb
    pkgs = [
        Right $ exAv "A" 1 (dependencyTree 1)
      , Right $ exAv "B" 1 [] `withExe` exExe "exe" []
      ]

    dependencyTree :: Int -> [ExampleDependency]
    dependencyTree n
        | n > depth = flaggedDeps
        | otherwise = [exFlagged (numberedFlag n) flaggedDeps
                                                  (dependencyTree (n + 1))]
      where
        flaggedDeps = zipWith ($) (replicate copies flaggedDep) [0 :: Int ..]
        flaggedDep m = exFlagged (numberedFlag n ++ "-" ++ show m) buildDepends
                                                                   buildDepends
        buildDepends = [ExFix "B" 1, ExBuildToolFix "B" "exe" 1]

numberedFlag :: Int -> ExampleFlagName
numberedFlag n = "flag-" ++ show n