Skip to content

Solver: Add individual flags to conflict sets. #4562

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 4, 2017

Conversation

grayjay
Copy link
Collaborator

@grayjay grayjay commented Jun 12, 2017

This PR implements #4391 and fixes #3930 and #4390. It isn't ready to be merged yet, because I need to do some performance testing. The change could have a big impact on solver run time for some packages, since it changes conflict counts. Here are the main commits:

Solver: Add individual flags to conflict sets.

This commit changes the way that the solver tracks the variables that introduce
dependencies, in order to fix some bugs that prevented the solver from tracking
individual flags. I refactored Dep, the type that represents a build-depends or
build-tool-depends dependency, so that it stores the package, flag, and stanza
choices that introduced the dependency. That information is stored in a field
of type DependencyReason. The DependencyReason is available to any solver phase
that needs to create conflict sets, such as "build" or "validation". Whenever
there is a conflict involving a dependency, the solver creates a conflict set
and a log message using the dependency's DependencyReason. This means that the
solver only needs to calculate the dependency reasons once, in
IndexConversion.hs, rather than every time they are used, i.e., in Builder.hs
(for goal reasons), Validate.hs, and Linking.hs.

Another difference between this commit and master is the dependencies between
solver variables. On master, each dependency or variable is introduced by
exactly one other variable. For example, if package x has a flag y, which
controls a dependency on package z, then the three variables depend on each
other in a chain, x -> y -> z. If z can't be satisfied, the solver backjumps
to its cause, y, and if flipping y doesn't help, it continues backjumping to y's
cause, which is x. In contrast, this commit allows each dependency to be
introduced by a package and any number of flags and stanzas. So z's
DependencyReason would contain both x and y.

Storing all flags that introduced a dependency allows the solver to correctly
calculate conflict sets when there are nested conditionals. We no longer need
to combine each package's flags into a single conflict set variable. This
commit removes the simplifyVar function and adds flag variables directly to
conflict sets. See issue #4391.

This commit makes another minor change. In this commit and master, if a
dependency appears in the if and else blocks of a conditional, the solver lifts
it out of the conditional. Previously, the solver behaved as if the flag did
not introduce the dependency. This commit adds the flag variable to the
conflict set or error message if the dependency is involved in a conflict. This
change doesn't affect correctness, but I think it improves the error messages,
since it gives the whole reason that each dependency was introduced.

Randomize the goal order in one of the dependency solver QuickCheck tests.

Completely randomizing the goal order exposes more bugs in backjumping than
using --reorder-goals. I only applied the change to one test in this commit,
because the randomization function slowed down some of the other tests
significantly.

Add a test case for issue #4390.

Add a test case for a stanza preference bug (#3930).

Enable stanza preferences in the solver QuickCheck tests.


Here are some examples of changes in solver error messages that also reflect the changes in behavior.

cabal install hackage-server:

 Resolving dependencies...
 cabal: Could not resolve dependencies:
-trying: hackage-server-0.5.0:-build-hackage-import
+trying: hackage-server-0.5.0 (user goal)
 trying: hackage-server-0.5.0:+build-hackage-build
-trying: unix-2.7.2.0/installed-2.7... (dependency of
-hackage-server-0.5.0:+build-hackage-build)
-next goal: aeson (dependency of hackage-server-0.5.0:+build-hackage-build)
+trying: unix-2.7.2.0/installed-2.7... (dependency of hackage-server-0.5.0
++build-hackage-build)
+next goal: aeson (dependency of hackage-server-0.5.0 +build-hackage-build)
 rejecting: aeson-1.2.0.0, aeson-1.1.2.0, aeson-1.1.1.0, aeson-1.1.0.0,
 aeson-1.0.2.1, aeson-1.0.2.0, aeson-1.0.1.0, aeson-1.0.0.0, aeson-0.11.3.0,
 aeson-0.11.2.1, aeson-0.11.2.0, aeson-0.11.1.4, aeson-0.11.1.3,
 aeson-0.11.1.2, aeson-0.11.1.1, aeson-0.11.1.0, aeson-0.11.0.0, aeson-0.9.0.1,
 aeson-0.9.0.0, aeson-0.8.1.1, aeson-0.8.1.0, aeson-0.8.0.2, aeson-0.7.0.6,
-aeson-0.7.0.4, aeson-0.6.2.1, aeson-0.6.2.0 (conflict:
-hackage-server-0.5.0:build-hackage-build => aeson==0.6.1.*)
+aeson-0.7.0.4, aeson-0.6.2.1, aeson-0.6.2.0 (conflict: hackage-server
++build-hackage-build => aeson==0.6.1.*)
 rejecting: aeson-0.6.1.0 (conflict: unix => time==1.6.0.1/installed-1.6...,
 aeson => time<1.5)
 rejecting: aeson-0.6.0.2, aeson-0.6.0.1, aeson-0.6.0.0, aeson-0.5.0.0,
 aeson-0.4.0.1, aeson-0.4.0.0, aeson-0.3.2.14, aeson-0.3.2.13, aeson-0.3.2.12,
 aeson-0.3.2.11, aeson-0.3.2.10, aeson-0.3.2.9, aeson-0.3.2.8, aeson-0.3.2.7,
 aeson-0.3.2.6, aeson-0.3.2.5, aeson-0.3.2.4, aeson-0.3.2.3, aeson-0.3.2.2,
 aeson-0.3.2.1, aeson-0.3.2.0, aeson-0.3.1.1, aeson-0.3.1.0, aeson-0.3.0.0,
 aeson-0.2.0.0, aeson-0.1.0.0, aeson-0.10.0.0, aeson-0.8.0.1, aeson-0.8.0.0,
 aeson-0.7.0.5, aeson-0.7.0.3, aeson-0.7.0.2, aeson-0.7.0.1, aeson-0.7.0.0
-(conflict: hackage-server-0.5.0:build-hackage-build => aeson==0.6.1.*)
+(conflict: hackage-server +build-hackage-build => aeson==0.6.1.*)
 After searching the rest of the dependency tree exhaustively, these were the
-goals I've had most trouble fulfilling: aeson, hackage-server-0.5.0:flag,
-hackage-server-0.4:flag, template-haskell, hackage-server
+goals I've had most trouble fulfilling: hackage-server, aeson,
+hackage-server-0.5.0:build-hackage-build,
+hackage-server-0.4:build-hackage-mirror, template-haskell

There are several differences:

  • The conflict set at the bottom contains the flag names, for example, hackage-server-0.5.0:build-hackage-build instead of hackage-server-0.5.0:flag.
  • The summary of the steps that the solver took only lists the relevant flag choice, i.e., hackage-server-0.5.0:+build-hackage-build, instead of the two hackage-server-0.5.0 flags that had been chosen at that point.
  • The "conflict" message gives the value of the flag causing the conflict, for example, (conflict: hackage-server +build-hackage-build => aeson==0.6.1.*) instead of (conflict: hackage-server-0.5.0:build-hackage-build => aeson==0.6.1.*).

cabal install --dry-run git-annex -v3:

 [__5] trying: ghc-prim-0.5.0.0/installed-0.5... (dependency of base-4.9.0.0/installed-4.9...)
 [__6] trying: unix-2.7.2.0/installed-2.7... (dependency of git-annex-6.20170519)
-[__7] trying: network-2.6.3.2 (dependency of git-annex-6.20170519)
+[__7] trying: network-2.6.3.2 (dependency of git-annex-6.20170519 +/-network-uri)
 [__8] trying: network-2.6.3.2:!test
 [__9] trying: split-0.2.3.2 (dependency of git-annex-6.20170519)
  • This message shows that network is introduced by the network-uri flag, whether it is true or false.

/cc @kosmikus

@mention-bot
Copy link

@grayjay, thanks for your PR! By analyzing the history of the files in this pull request, we identified @kosmikus, @edsko and @ezyang to be potential reviewers.

@ezyang
Copy link
Contributor

ezyang commented Jun 16, 2017

Let me know if you want an in depth code review, but the logic description in the commit sounds good to me.

@kosmikus
Copy link
Contributor

Sounds fantastic. I'll try to take a look. But in any case, it's a good approach, and it should hopefully fix a few pathological cases we currently have. Looking forward to your performance test results.

This commit changes the way that the solver tracks the variables that introduce
dependencies, in order to fix some bugs that prevented the solver from tracking
individual flags.  I refactored Dep, the type that represents a build-depends or
build-tool-depends dependency, so that it stores the package, flag, and stanza
choices that introduced the dependency.  That information is stored in a field
of type DependencyReason.  The DependencyReason is available to any solver phase
that needs to create conflict sets, such as "build" or "validation".  Whenever
there is a conflict involving a dependency, the solver creates a conflict set
and a log message using the dependency's DependencyReason.  This means that the
solver only needs to calculate the dependency reasons once, in
IndexConversion.hs, rather than every time they are used, i.e., in Builder.hs
(for goal reasons), Validate.hs, and Linking.hs.

Another difference between this commit and master is the dependencies between
solver variables.  On master, each dependency or variable is introduced by
exactly one other variable.  For example, if package x has a flag y, which
controls a dependency on package z, then the three variables depend on each
other in a chain, x -> y -> z.  If z can't be satisfied, the solver backjumps
to its cause, y, and if flipping y doesn't help, it continues backjumping to y's
cause, which is x.  In contrast, this commit allows each dependency to be
introduced by a package and any number of flags and stanzas.  So z's
DependencyReason would contain both x and y.

Storing all flags that introduced a dependency allows the solver to correctly
calculate conflict sets when there are nested conditionals.  We no longer need
to combine each package's flags into a single conflict set variable.  This
commit removes the 'simplifyVar' function and adds flag variables directly to
conflict sets.  See issue haskell#4391.

This commit makes another minor change.  In this commit and master, if a
dependency appears in the if and else blocks of a conditional, the solver lifts
it out of the conditional.  Previously, the solver behaved as if the flag did
not introduce the dependency.  This commit adds the flag variable to the
conflict set or error message if the dependency is involved in a conflict.  This
change doesn't affect correctness, but I think it improves the error messages,
since it gives the whole reason that each dependency was introduced.
@grayjay
Copy link
Collaborator Author

grayjay commented Jun 18, 2017

Thanks! I did some performance testing, so I think it's ready for review now.

I ran the solver on all packages on Hackage and found that the change made the solver significantly slower for three packages, Hayoo, hxournal, and nerf. When I looked at the -v3 logs, I saw that the change had caused a bad goal order for Hayoo and hxournal, with this sequence of steps:

  • A dependency was lifted out of a conditional because it appeared under both sides of the conditional.
  • The dependency caused a conflict, before the solver chose a value for the flag in the conditional.
  • The solver added the flag to the conflict set.
  • The flag variable in the conflict set contributed to the conflict count for that variable and caused the solver to choose the flag in the very next step.

Beginning of the log from Hayoo:

[__0] trying: Hayoo-1.2.3 (user goal)
[...]
[__5] next goal: snap-server (dependency of Hayoo-1.2.3 +/-hayoosnap4)
[__5] rejecting: snap-server-1.0.2.2, snap-server-1.0.2.1, snap-server-1.0.2.0, snap-server-1.0.1.1,
snap-server-1.0.1.0, snap-server-1.0.0.0, snap-server-0.9.5.1, snap-server-0.9.5.0,
snap-server-0.9.4.6, snap-server-0.9.4.5, snap-server-0.9.4.4, snap-server-0.9.4.3,
snap-server-0.9.4.2, snap-server-0.9.4.1, snap-server-0.9.4.0, snap-server-0.9.3.4,
snap-server-0.9.3.3, snap-server-0.9.3.1, snap-server-0.9.3, snap-server-0.9.2.4,
snap-server-0.9.2.3, snap-server-0.9.2.2, snap-server-0.9.2.1, snap-server-0.9.2,
snap-server-0.9.0, snap-server-0.8.1.1, snap-server-0.8.1, snap-server-0.8.0.1, snap-server-0.8.0
(conflict: Hayoo +/-hayoosnap4 => snap-server>=0.4 && <0.6 || >=0.7 && <0.8)
[__5] trying: snap-server-0.7.0.1
[__6] rejecting: Hayoo-1.2.3:+hayoosnap4 (conflict: snap-server==0.7.0.1, Hayoo +hayoosnap4 => snap-server>=0.4 && <0.6)

I made another commit after the main commit, which addresses this problem:

Don't add flag to conflict set when true and false introduce the same conflict.

A DependencyReason contains flags paired with values FlagTrue, FlagFalse, or
FlagBoth, where FlagBoth means that the dependency was introduced by either
value of the flag and was lifted out of a conditional.  In the previous commit,
when a dependency led to a conflict, all flags in the DependencyReason were
added to the error message and conflict set.  This commit avoids adding flags
with value FlagBoth to the conflict set, because they can have a negative effect
on goal order: After the solver encounters a conflict involving such a flag, it
starts to prefer the flag when choosing goals, due to the solver's conflict
counting feature.  The solver shouldn't prefer the flag goal because it didn't
actually need to choose the flag to see the dependency.  Flags with value
FlagBoth are still useful for error messages, though.

Then I ran the solver (cabal install --dry-run) on all Hackage packages again and saw that the solver only had worse performance on nerf. It chose some flags earlier in that run, but I didn't see any other reason for the slowdown. It found a solution when I used --max-backjumps -1. I did all testing with GHC 8.0.1 and index-state(hackage.haskell.org) = 2017-06-11T02:16:39Z. I ran each version of cabal once on each package and looked for packages with a difference in run time of at least 10%. Then I reran cabal on those packages to narrow down the list further. Here are the average run times from five runs with the remaining packages:

package master result master time (seconds) branch result branch time (seconds) speedup
concraft backjump limit 5.67 backjump limit 4.89 1.16
forml backjump limit 10.26 backjump limit 8.85 1.16
hack-handler-simpleserver backjump limit 6.28 no solution 1.96 3.21
hack-middleware-clientsession backjump limit 17.86 no solution 1.95 9.14
haskore backjump limit 5.21 no solution 1.67 3.12
haskore-synthesizer no solution 23.14 no solution 2.21 10.49
language-gcl no solution 3.06 no solution 2.31 1.33
ms backjump limit 61.68 no solution 7.95 7.76
nerf solution 4.1 backjump limit 5.87 0.7
puzzle-draw solution 56.28 solution 5.31 10.6
react-haskell backjump limit 44.23 backjump limit 14.46 3.06
reflex-transformers no solution 2.18 no solution 1.9 1.15
shpider backjump limit 7.44 solution 1.88 3.95
thorn backjump limit 11.11 backjump limit 4.44 2.5

Both versions of cabal hit the backjump limit in four of the cases, so they don't really tell us which version performed better. All but one of the others are improvements. I looked at a few of the logs, and, as far as I can tell, most of the improvements are due to flags being used in conflict counting. This behavior is similar to #4147, which was reverted.

These differences in run time are caused by differences in goal order and/or backjumping, but I also wanted to compare performance when the solver went through similar steps on master and the branch. I found two commands that led to very similar -v3 output between the cabal versions. Presumably, package flags didn't cause many conflicts in these runs.

cabal install --dry-run --max-backjumps -1 gi-glib happstack
cabal install --dry-run yesod

I took the average of three runs. There wasn't a big difference between master and the branch.

packages result master time (seconds) branch time (seconds) master memory (MB) branch memory (MB)
gi-glib, happstack no solution 30.02 30.70 245 245
yesod solution 3.53 3.55 239 239.3

@grayjay grayjay changed the title [WIP] Solver: Add individual flags to conflict sets. Solver: Add individual flags to conflict sets. Jun 18, 2017
grayjay added 5 commits June 18, 2017 16:21
… conflict.

A DependencyReason contains flags paired with values FlagTrue, FlagFalse, or
FlagBoth, where FlagBoth means that the dependency was introduced by either
value of the flag and was lifted out of a conditional.  In the previous commit,
when a dependency led to a conflict, all flags in the DependencyReason were
added to the error message and conflict set.  This commit avoids adding flags
with value FlagBoth to the conflict set, because they can have a negative effect
on goal order: After the solver encounters a conflict involving such a flag, it
starts to prefer the flag when choosing goals, due to the solver's conflict
counting feature.  The solver shouldn't prefer the flag goal because it didn't
actually need to choose the flag to see the dependency.  Flags with value
FlagBoth are still useful for error messages, though.

I measured the performance of the last two commits by comparing commit
fc3ef2a (master) with this commit.  I did all
testing with GHC 8.0.1 and
'index-state(hackage.haskell.org) = 2017-06-11T02:16:39Z'.  I ran each version
of cabal (cabal install --dry-run) on each package on Hackage and looked for
packages with a difference in run time of at least 10%.  Then I reran cabal on
those packages to narrow down the list further (The results were noisy).  Here
are the average run times from five runs with the remaining packages:

package                        master result   master time (seconds)   branch result   branch time (seconds)   speedup
concraft                       backjump limit   5.67                   backjump limit   4.89                    1.16
forml                          backjump limit  10.26                   backjump limit   8.85                    1.16
hack-handler-simpleserver      backjump limit   6.28                   no solution      1.96                    3.21
hack-middleware-clientsession  backjump limit  17.86                   no solution      1.95                    9.14
haskore                        backjump limit   5.21                   no solution      1.67                    3.12
haskore-synthesizer            no solution     23.14                   no solution      2.21                   10.49
language-gcl                   no solution      3.06                   no solution      2.31                    1.33
ms                             backjump limit  61.68                   no solution      7.95                    7.76
nerf                           solution         4.1                    backjump limit   5.87                    0.7
puzzle-draw                    solution        56.28                   solution         5.31                   10.6
react-haskell                  backjump limit  44.23                   backjump limit  14.46                    3.06
reflex-transformers            no solution      2.18                   no solution      1.9                     1.15
shpider                        backjump limit   7.44                   solution         1.88                    3.95
thorn                          backjump limit  11.11                   backjump limit   4.44                    2.5

Both versions of cabal hit the backjump limit in four of the cases, so they
don't really tell us which version performed better.  All but one of the others
are improvements.  I looked at a few of the logs, and, as far as I can tell,
most of the improvements are due to flags being used in conflict counting.  This
behavior is similar to haskell#4147, which was reverted.  The log for 'nerf', the one
package that had worse performance, showed that the solver chose some flags
earlier, but I didn't see any other reason for the slowdown.  The solver found a
solution when I used '--max-backjumps -1'.

These differences in run time are caused by differences in goal order and/or
backjumping, but I also wanted to compare performance when the solver went
through similar steps on master and the branch.  I found two commands that led
to very similar -v3 output between the cabal versions.  Presumably, package
flags didn't cause many conflicts in these runs.

cabal install --dry-run --max-backjumps -1 gi-glib happstack
cabal install --dry-run yesod

I took the average of three runs.  There wasn't a big difference between master
and the branch.

packages             result        master time (seconds)   branch time (seconds)   master memory (MB)   branch memory (MB)
gi-glib, happstack   no solution   30.02                   30.70                   245                  245
yesod                solution       3.53                    3.55                   239                  239.3
…ests.

Completely randomizing the goal order exposes more bugs in backjumping than
using --reorder-goals.  I only applied the change to one test in this commit,
because the randomization function slowed down some of the other tests
significantly.
@kosmikus
Copy link
Contributor

@grayjay This all sounds rather good. For the cases where both master and branch hit the backjump limit, would it make sense to re-run with increased / infinite backjump limit?

Also, do you have any intuitive idea what's still difficult about these remaining cases?

@kosmikus
Copy link
Contributor

@grayjay Also, for nerf with unlimited backjumps, how long does it take to find a solution?

@grayjay
Copy link
Collaborator Author

grayjay commented Jun 24, 2017

For the cases where both master and branch hit the backjump limit, would it make sense to re-run with increased / infinite backjump limit?

I ran cabal again on those packages and nerf, with a time limit of an hour:

package result master time (seconds) branch time (seconds)
concraft no solution 112.07 42.80
forml timeout (solution in ~5 seconds with --reorder-goals) 3600.03 3600.00
nerf solution 3.99 22.58
react-haskell no solution 1350.40 1425.86
thorn no solution 22.54 7.49

I don't know why these cases ran for so long. Interestingly, each one took less than 10 seconds with --reorder-goals. I did notice that cabal seemed to spend significant amounts of time exploring only a small number of levels, like it was thoroughly exploring subtrees. I've noticed that behavior before during long runs. I think that it can be explained by --count-conflicts moving all of the conflicting packages earlier in the goal order, so that the solver only looks at packages involved in conflicts. Here's part of the log from forml, where the solver spent a long time between levels 56 and 62:

[_57] fail (backjumping, conflict set: base, bytestring, directory, forml, haskell98, hxt, hxt-regex-xmlschema, network, pandoc, parsec, skylighting, tagsoup, text, skylighting-0.1.1.5:bootstrap)
[_56] trying: network-2.6.3.0
[_57] trying: parsec-3.1.11 (dependency of forml-0.1.1)
[_58] trying: tagsoup-0.14.1 (dependency of pandoc-1.19.2.1)
[_59] trying: skylighting-0.1.1.5:-bootstrap
[_60] trying: hxt-9.3.1.16 (dependency of skylighting-0.1.1.5 -bootstrap)
[_61] next goal: hxt-regex-xmlschema (dependency of hxt-9.3.1.16)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.3 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.2, hxt-regex-xmlschema-9.2.0.1 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10 && <0.11)
[_61] rejecting: hxt-regex-xmlschema-9.2.0 (conflict: base==4.9.0.0/installed-4.9..., hxt-regex-xmlschema => base>=4.5 && <4.8)
[_61] rejecting: hxt-regex-xmlschema-9.1.0, hxt-regex-xmlschema-9.0.4, hxt-regex-xmlschema-9.0.1, hxt-regex-xmlschema-9.0.0 (conflict: hxt => hxt-regex-xmlschema>=9.2)
[_61] fail (backjumping, conflict set: base, bytestring, hxt, hxt-regex-xmlschema)
[_60] trying: hxt-9.3.1.15
[_61] next goal: hxt-regex-xmlschema (dependency of hxt-9.3.1.15)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.3 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.2, hxt-regex-xmlschema-9.2.0.1 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10 && <0.11)
[_61] rejecting: hxt-regex-xmlschema-9.2.0 (conflict: base==4.9.0.0/installed-4.9..., hxt-regex-xmlschema => base>=4.5 && <4.8)
[_61] rejecting: hxt-regex-xmlschema-9.1.0, hxt-regex-xmlschema-9.0.4, hxt-regex-xmlschema-9.0.1, hxt-regex-xmlschema-9.0.0 (conflict: hxt => hxt-regex-xmlschema>=9.2 && <10)
[_61] fail (backjumping, conflict set: base, bytestring, hxt, hxt-regex-xmlschema)
[_60] trying: hxt-9.3.1.14
[_61] next goal: hxt-regex-xmlschema (dependency of hxt-9.3.1.14)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.3 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.2, hxt-regex-xmlschema-9.2.0.1 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10 && <0.11)
[_61] rejecting: hxt-regex-xmlschema-9.2.0 (conflict: base==4.9.0.0/installed-4.9..., hxt-regex-xmlschema => base>=4.5 && <4.8)
[_61] rejecting: hxt-regex-xmlschema-9.1.0, hxt-regex-xmlschema-9.0.4, hxt-regex-xmlschema-9.0.1, hxt-regex-xmlschema-9.0.0 (conflict: hxt => hxt-regex-xmlschema>=9.2 && <10)
[_61] fail (backjumping, conflict set: base, bytestring, hxt, hxt-regex-xmlschema)
[_60] trying: hxt-9.3.1.13
[_61] next goal: hxt-regex-xmlschema (dependency of hxt-9.3.1.13)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.3 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.2, hxt-regex-xmlschema-9.2.0.1 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10 && <0.11)
[_61] rejecting: hxt-regex-xmlschema-9.2.0 (conflict: base==4.9.0.0/installed-4.9..., hxt-regex-xmlschema => base>=4.5 && <4.8)
[_61] rejecting: hxt-regex-xmlschema-9.1.0, hxt-regex-xmlschema-9.0.4, hxt-regex-xmlschema-9.0.1, hxt-regex-xmlschema-9.0.0 (conflict: hxt => hxt-regex-xmlschema>=9.2 && <10)
[_61] fail (backjumping, conflict set: base, bytestring, hxt, hxt-regex-xmlschema)
[_60] trying: hxt-9.3.1.12
[_61] next goal: hxt-regex-xmlschema (dependency of hxt-9.3.1.12)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.3 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.2, hxt-regex-xmlschema-9.2.0.1 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10 && <0.11)
[_61] rejecting: hxt-regex-xmlschema-9.2.0 (conflict: base==4.9.0.0/installed-4.9..., hxt-regex-xmlschema => base>=4.5 && <4.8)
[_61] rejecting: hxt-regex-xmlschema-9.1.0, hxt-regex-xmlschema-9.0.4, hxt-regex-xmlschema-9.0.1, hxt-regex-xmlschema-9.0.0 (conflict: hxt => hxt-regex-xmlschema>=9.2 && <10)
[_61] fail (backjumping, conflict set: base, bytestring, hxt, hxt-regex-xmlschema)
[_60] trying: hxt-9.3.1.11
[_61] next goal: hxt-regex-xmlschema (dependency of hxt-9.3.1.11)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.3 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10)
[_61] rejecting: hxt-regex-xmlschema-9.2.0.2, hxt-regex-xmlschema-9.2.0.1 (conflict: bytestring==0.9.2.1, hxt-regex-xmlschema => bytestring>=0.10 && <0.11)
[_61] rejecting: hxt-regex-xmlschema-9.2.0 (conflict: base==4.9.0.0/installed-4.9..., hxt-regex-xmlschema => base>=4.5 && <4.8)
[_61] rejecting: hxt-regex-xmlschema-9.1.0, hxt-regex-xmlschema-9.0.4 (conflict: base==4.9.0.0/installed-4.9..., hxt-regex-xmlschema => base>=4 && <4.8)
[_61] trying: hxt-regex-xmlschema-9.0.1
[_62] next goal: haskell98 (dependency of hxt-regex-xmlschema-9.0.1)
[_62] rejecting: haskell98-2.0.0.3, haskell98-2.0.0.2, haskell98-2.0.0.1, haskell98-2.0.0.0 (conflict: hxt-regex-xmlschema => haskell98>=1 && <2)
[_62] rejecting: haskell98-1.1.0.1, haskell98-1.1.0.0, haskell98-1.0.1.1 (conflict: base==4.9.0.0/installed-4.9..., haskell98 => base>=3 && <4.6)
[_62] rejecting: haskell98-1.0.1.0 (conflict: base==4.9.0.0/installed-4.9..., haskell98 => base<4.6)
[_62] rejecting: haskell98-1.0 (conflict: base==4.9.0.0/installed-4.9..., haskell98 => base<4.3)
[_62] fail (backjumping, conflict set: base, haskell98, hxt-regex-xmlschema)
[_61] trying: hxt-regex-xmlschema-9.0.0

This log also shows that each of the last three goals is introduced by the level above, i.e, skylighting-0.1.1.5:-bootstrap introduces hxt, hxt introduces hxt-regex-xmlschema, and hxt-regex-xmlschema introduces haskell98. I know that the solver doesn't handle chains of dependencies well when only the last package causes a conflict. It needs to add all of the packages in the chain to the conflict set and retry all of them.

In other cases, the packages in the subtree did not seem to introduce each other in order.

I think we might be able to handle the chains of dependencies by either lifting dependencies out of packages (similar to lifting flags dependencies out of conditionals) or pairing conflict set variables with information about what could resolve the conflict.

@hvr
Copy link
Member

hvr commented Jun 24, 2017

@grayjay btw, one of the packages which I noticed on the matrix builder to use up a lot of solver-time is wolf. While that package has a rather useless dependency specification, I did notice that your PR speeds up cabal finding a "solution" significantly:

I tried solving locally with wolf-0.2.3 (at --index-state='2017-06-24T03:30:40Z' with ghc-8.2.0.20170623) where the solver terminated succesfully in under a minute:

$ time cabal-pr4562 new-build --dry  -w ghc-8.2.1
Resolving dependencies...
In order, the following would be built (use -v for more details):
 - ansi-terminal-0.6.3.1 (lib:ansi-terminal) (requires build)
 - auto-update-0.1.4 (lib) (requires build)
 - base-compat-0.9.3 (lib) (requires build)
 - base-orphans-0.6 (lib) (requires build)
 - base64-bytestring-1.0.0.1 (lib) (requires build)
...
 - amazonka-1.4.5 (lib) (requires build)
 - wolf-0.2.3 (lib) (first run)
 - wolf-0.2.3 (exe:wolf-register) (first run)
 - wolf-0.2.3 (exe:wolf-execute) (first run)
 - wolf-0.2.3 (exe:wolf-decide) (first run)
 - wolf-0.2.3 (exe:wolf-act) (first run)

real	0m56.887s
user	0m56.580s
sys	0m0.220s

whereas without this PR, cabal gave up after 16 minutes:

$ time cabal new-build -w ghc-8.2.1 --dry
Resolving dependencies...
cabal: Could not resolve dependencies:
trying: base-4.10.0.0/installed-4.1... (dependency of wolf-0.2.3)
trying: shelly-1.6.8.3 (dependency of wolf-0.2.3)
trying: system-fileio-0.3.16.3 (dependency of shelly-1.6.8.3)
next goal: unix (dependency of system-fileio-0.3.16.3)
rejecting: unix-2.7.2.2/installed-2.7... (conflict: unix => time==1.8.0.2/installed-1.8..., shelly => time>=1.3 && <1.7)
rejecting: unix-2.7.2.1, unix-2.7.2.0 (conflict: base==4.10.0.0/installed-4.1..., unix => base>=4.5 && <4.10)
rejecting: unix-2.7.1.0 (conflict: base==4.10.0.0/installed-4.1..., unix => base>=4.5 && <4.9)
rejecting: unix-2.7.0.1, unix-2.7.0.0 (conflict: base==4.10.0.0/installed-4.1..., unix => base>=4.5 && <4.8)
rejecting: unix-2.6.0.1, unix-2.6.0.0 (conflict: base==4.10.0.0/installed-4.1..., unix => base>=4.5 && <4.7)
rejecting: unix-2.5.1.1, unix-2.5.1.0 (conflict: base==4.10.0.0/installed-4.1..., unix => base>=4.5 && <4.6)
rejecting: unix-2.5.0.0 (conflict: base==4.10.0.0/installed-4.1..., unix => base>=4.2 && <4.5)
rejecting: unix-2.4.2.0, unix-2.4.1.0 (conflict: base==4.10.0.0/installed-4.1..., unix => base>=4.2 && <4.4)
rejecting: unix-2.4.0.2, unix-2.4.0.1 (conflict: base==4.10.0.0/installed-4.1..., unix => base>=4.2 && <4.3)
rejecting: unix-2.4.0.0 (conflict: base==4.10.0.0/installed-4.1..., unix => base>=4.1 && <4.3)
rejecting: unix-2.3.2.0, unix-2.3.1.0, unix-2.3.0.0 (conflict: base==4.10.0.0/installed-4.1..., unix => base<4.3)
rejecting: unix-2.2.0.0, unix-2.0 (conflict: system-fileio => unix>=2.3)
Backjump limit reached (currently 2000, change with --max-backjumps or try to run with --reorder-goals).

real	16m26.466s
user	16m25.076s
sys	0m1.356s

Here's a list of the worst solve-times from matrix.hho (w/o this PR) resulting in solver failures:

compiler pname pver --index-state solve-time in secs
ghc-8.0.2 wolf 0.3.0 2017-02-17 01:57:27+00 3406.8
ghc-8.0.2 wolf 0.3.1 2017-02-17 01:57:27+00 3363.74
ghc-7.6.3 MFlow 0.2.0.1 2017-06-12 16:47:05+00 1884.79
ghc-7.6.3 MFlow 0.2.0.0 2017-06-12 16:47:05+00 1864.94
ghc-8.2.0.20170522 wolf 0.2.7 2017-06-06 06:41:16+00 1800.68
ghc-8.2.0.20170522 wolf 0.2.8 2017-06-06 06:41:16+00 1793.33
ghc-8.2.0.20170622 wolf 0.2.13 2017-06-23 07:55:35+00 1788.75
ghc-8.2.0.20170622 wolf 0.2.12 2017-06-23 07:55:35+00 1762.51
ghc-8.2.0.20170522 wolf 0.2.13 2017-06-02 23:53:48+00 1759.85
ghc-8.2.0.20170622 wolf 0.2.11 2017-06-23 07:55:35+00 1759.01
ghc-8.2.0.20170522 wolf 0.2.9 2017-06-06 06:41:16+00 1756.72
ghc-8.2.0.20170522 wolf 0.2.11 2017-06-06 06:41:16+00 1746.45
ghc-8.2.0.20170622 wolf 0.2.14 2017-06-23 07:55:35+00 1743.7
ghc-8.2.0.20170622 wolf 0.2.8.1 2017-06-23 07:55:35+00 1742.64
ghc-8.2.0.20170522 wolf 0.2.8.1 2017-06-02 23:53:48+00 1740.58
ghc-8.2.0.20170522 wolf 0.2.8.2 2017-06-02 23:53:48+00 1736.35
ghc-8.2.0.20170623 wolf 0.2.8.1 2017-06-23 07:55:35+00 1731.38
ghc-8.2.0.20170522 wolf 0.2.8.1 2017-06-02 06:55:48+00 1730.65
ghc-8.2.0.20170623 wolf 0.2.13 2017-06-23 07:55:35+00 1729.51
ghc-8.2.0.20170622 wolf 0.2.8.2 2017-06-23 07:55:35+00 1729.19
ghc-8.2.0.20170623 wolf 0.2.14 2017-06-23 07:55:35+00 1728.6
ghc-8.2.0.20170622 wolf 0.2.10 2017-06-23 07:55:35+00 1727.27
ghc-8.2.0.20170522 wolf 0.2.12 2017-06-06 06:41:16+00 1725.75
ghc-8.2.0.20170623 wolf 0.2.12 2017-06-23 07:55:35+00 1723.68
ghc-8.2.0.20170522 wolf 0.2.13 2017-06-06 06:41:16+00 1722.13
ghc-8.2.0.20170522 wolf 0.2.8.2 2017-06-02 06:55:48+00 1716.8
ghc-8.2.0.20170522 wolf 0.2.14 2017-06-02 06:55:48+00 1716.02
ghc-8.2.0.20170522 wolf 0.2.14 2017-06-02 23:53:48+00 1715.54
ghc-8.2.0.20170522 wolf 0.2.10 2017-06-06 06:41:16+00 1714.13
ghc-8.2.0.20170522 wolf 0.2.8.2 2017-06-06 06:41:16+00 1711.71
ghc-8.2.0.20170522 wolf 0.2.14 2017-06-06 06:41:16+00 1708.33
ghc-8.2.0.20170522 wolf 0.2.8.1 2017-06-06 06:41:16+00 1707.83

@BardurArantsson
Copy link
Collaborator

+1, merge at will.

... but I get the feeling that we should be looking towards SAT solvers/"Theorem Provers". Even assuming awful error messages, I get the feeling that Cabal would be up a couple of points in terms of "just working". That's just my (maybe-uninformed) opinion. Thoughts, anyone?

@hvr
Copy link
Member

hvr commented Jun 24, 2017

@BardurArantsson There were several attempts at using a stock SAT/SMT solver (including Z3). Unfortunately, while e.g. Z3 is very fast to find some arbitrary solution (after having fed the facts into the Z3 process which takes a bit of time too), the problem is rather having the SMT solver optimize for a reasonable cost-function, as then its performance takes a substantial dive (at least for the attempts I'm aware of) to the point where the domain-specific modular cabal solver looks way better in comparison.

@BardurArantsson
Copy link
Collaborator

@hvr Ah, thanks for reminding me of the intricacies. (Still +1 to this PR, btw.) Hopefully a (Future (Arbitrary Student)) student will figure the details out :).

@grayjay
Copy link
Collaborator Author

grayjay commented Jun 25, 2017

@hvr Thanks for testing the PR on wolf. I tried running cabal new-build on wolf-0.3.0 with GHC 8.0.2, and I was surprised to see that the solver often backjumps for more than 100 levels. I wonder why it doesn't choose the conflicting packages sooner. I want to look into it more when I have time.

I tried out the idea I mentioned in the last comment, and it seems promising. I added information about version conflicts to the variables in conflict sets, so that the solver can determine whether each version of a package will cause the same conflicts as the previous version that it tried. If a version causes the same conflicts, then the solver can skip it.

Then I reran cabal install on the five packages from my last comment with this PR and the new branch (https://github.com/grayjay/cabal/tree/version-and-goal-conflicts). The branch builds off of this PR. I used a 90 second timeout and --max-backjumps -1.

package #4562 (seconds) version-and-goal-conflicts branch (seconds)
concraft 42.41 3.98
forml 90.00 3.04
nerf 22.57 3.36
react-haskell 90.00 16.29
thorn 7.53 1.87

The new branch was also about 25% faster when solving for wolf-0.3.0.

@hvr
Copy link
Member

hvr commented Jun 25, 2017

@grayjay btw, while you're working on the solver... how hard would it be to emit simple stats about the "solver" costs, like e.g. how much backjump-budget was required for a decision (i.e. the smallest value to pass via --max-backjumps so that the solver would give a definitive answer - instead of giving up with a non-exhaustive "don't know"?)

The reason is that I want to collect such statistics in http://matrix.hackage.haskell.org in order to spot problematic cases; currently, I can only collect the rather crude metrics of runtime in seconds & memory allocations (as reported by +RTS -t) which are subject to external noise...

@kosmikus
Copy link
Contributor

I think the discussions about remaining problems and ideas for future improvements should not hold up merging this. It all seems rather good.

It's of course very interesting to understand what it is that makes the remaining cases still difficult, and to improve further on this. But I think it is pretty clear due to the testing so far that this is overall a clear improvement.

@23Skidoo
Copy link
Member

23Skidoo commented Jul 4, 2017

Since @kosmikus thinks that this PR is good to go, I'm merging it.

@23Skidoo 23Skidoo merged commit 3890185 into haskell:master Jul 4, 2017
@23Skidoo
Copy link
Member

23Skidoo commented Jul 4, 2017

Merged, thanks!

@grayjay
Copy link
Collaborator Author

grayjay commented Jul 7, 2017

Thanks everyone! I'll open issues for the other problems that we discussed when I have time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Solver doesn't always add stanza choices to the conflict set.
7 participants