Skip to content

Attempt to speed solving when test is enable projectwide #7490

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
Aug 11, 2021

Conversation

gbaz
Copy link
Collaborator

@gbaz gbaz commented Jul 23, 2021

Resolves #7472

The good news is that these two changes should be simple and easy to understand, and resolve the issue presented. They also should be (to the extent I understand the solver) unilaterally safe, and almost certainly improvements.

The bad news is that it is not clear to me how much performance win we get from this -- in the small test case, the affect on solving only seemed to cut out some very small local backjumps, nothing that could account for geometric behavior.

The key notion here is "stanza preferences" -- these only affect ordering of attempts to try stanzas off vs. on, so switching preferences should not hurt correctness, only modify the order in which certain things are tried.

The first change takes account of stanza preferences not only for top level packages, but also when they are used as dependencies of other packages in the project. It makes sense to do this, since we need to match how we pick stanzas throughout.

The second change modifies the "default preferences" to always suggest trying first with all stanzas enabled. Before, it had the counterintuitive and probably conceptually wrong behavior of trying first with all stanzas enabled except those the user specifically constrained to be enabled. Because a backjump would occur in such a case anyway, there was no correctness problem, but there was certainly a small inefficiency in exploring the tree.

So both changes appear to be simple, understandable, and obviously more correct. However, again, I'm not sure if the effect on solving will be noticeable in the large projects that prompted this ticket.

@gbaz
Copy link
Collaborator Author

gbaz commented Jul 23, 2021

As far as correctness, would be interested in thoughts from @ezyang @grayjay or @dcoutts in particular (as those familiar with the solver).

@grayjay
Copy link
Collaborator

grayjay commented Jul 24, 2021

I need to look more closely at the original issue this PR is fixing, but I had some initial comments. I believe that the reason that constraints do not reorder nodes is that every time a constraint would reject a stanza or version choice, the solver should be able to backtrack in the next step, so reordering nodes shouldn't be significantly faster. Additionally, using the original node order gives better error messages, because rejecting the disallowed version/stanza choice creates a solver log message describing why the choice was rejected (including the source of any constraint).

From the log message in the issue ("rejecting: C:B:exe.A:!test (dependencies not linked: ..."), it looks like the performance issue is caused by the solver's preference for reusing the same instance of A for both C's indirect build tool dependency and the requirement to build all local packages. Those are different solver goals (A vs C:B:exe.A), so they are not required to make the same choices for enabling tests. The two goals are only required to make the same choices for enabling tests if they are linked, and the solver should be able to backtrack further to unlink them if necessary.

I'm not familiar with why ProjectPlanning.hs adds a stanza preference only when there is no constraint. I would have expected cabal to add the same preferences to all local packages.

@gbaz
Copy link
Collaborator Author

gbaz commented Jul 24, 2021

Thanks for the comments! Its not clear to me if backtracking due to test stanzas being enabled or not ever was a performance issue. @michaelpj raised it as one, but that was simply one explanation possible for why on a large project enabling tests reduces solver speed. It could well be that enabling tests just makes the solvers life much harder -- i.e. it introduces a lot more dependencies with a lot more constraints. I've noticed, for example, that when people bump constraints on a package they often don't bump them on the test stanza as frequently/eagerly since it matters less to end users (I've been lazy and done this myself!). In such situations, enabling test stanzas means that the constraint problem becomes much trickier and more computationally expensive to solve.

As is, I don't think the repro case given suffices to sort this out -- it lets us see that backtracking occurs, but since the backtracking is (as your comment describes) extremely local, it isn't clear to me that this is the root cause of any observed slowdown, and the repro case doesn't let us observe the slowdown itself.

@grayjay
Copy link
Collaborator

grayjay commented Jul 27, 2021

Yeah, solving for the test dependencies could be very time consuming, though I also wonder if the performance problem only appears when there are more packages in the chain of dependencies related to the build tool.

I can see one reason for only applying test/bench preferences when there are no constraints. It could lead to better error messages, because the solver log shows when a constraint rules out a choice, but it doesn't show when preferences prevent a choice from being considered. I think that it would be better to apply the same preferences to all local packages for consistency, though.

@newhoggy
Copy link

newhoggy commented Aug 3, 2021

I tried this branch and got the following failure:

$ git clone https://github.com/input-output-hk/cardano-node
$ cd cardano-node
$ git checkout dc87b20197a134fae5be2181f20af8404bcb029f
$ cabal-gbaz build all
HEAD is now at 95e5d74 Merge pull request #21 from effectfully/effectfully/drop-the-overlapping-list-of-char-instance
HEAD is now at 3825d3a Merge pull request #9 from input-output-hk/coot/createNamedPipe-error
HEAD is now at c028519 adding Keccak256 to cardano-crypto (#221)
HEAD is now at 07397f0 Merge pull request #77 from input-output-hk/upgrade-to-cabal-3.4.0.0
HEAD is now at bc5e54f06 Merge pull request #2394 from input-output-hk/redxaxder/injective-type-families
HEAD is now at fd773f7 Merge pull request #159 from input-output-hk/upgrade-to-cabal-3.4.0.0
HEAD is now at cde90a2 Re-enable support for GHC 8.6.5
HEAD is now at edf6945 Module re-exports (#5)
HEAD is now at 808724f Merge #620
remote: Enumerating objects: 1758, done.
remote: Counting objects: 100% (1749/1749), done.
remote: Compressing objects: 100% (560/560), done.
remote: Total 1042 (delta 998), reused 500 (delta 477), pack-reused 0
Receiving objects: 100% (1042/1042), 4.22 MiB | 4.95 MiB/s, done.
Resolving deltas: 100% (998/998), completed with 549 local objects.
From https://github.com/input-output-hk/ouroboros-network
 + 7b7d3370c...18cb8fb45 coot/ppTrace            -> origin/coot/ppTrace  (forced update)
   d80802472..b147e4d47  dougwilson/utxo-db-docs -> origin/dougwilson/utxo-db-docs
   130648129..54fcd8a45  gh-pages                -> origin/gh-pages
HEAD is now at f026e71e8 Merge #3278
remote: Enumerating objects: 145, done.
remote: Counting objects: 100% (145/145), done.
remote: Compressing objects: 100% (41/41), done.
remote: Total 145 (delta 99), reused 142 (delta 97), pack-reused 0
Receiving objects: 100% (145/145), 22.75 KiB | 121.00 KiB/s, done.
Resolving deltas: 100% (99/99), completed with 73 local objects.
From https://github.com/input-output-hk/plutus
 * [new branch]          anemish/wrap-contracerror               -> origin/anemish/wrap-contracerror
   0c549b4ef..ec03c3e43  hkm/windows-cross                       -> origin/hkm/windows-cross
   2b82b2beb..f43607479  kwxm/add-blake-2b-to-R                  -> origin/kwxm/add-blake-2b-to-R
   bc8aa656e..a5c768900  master                                  -> origin/master
 + 0c3d053a6...f9a923128 scp-2404-pab-update-internal-state      -> origin/scp-2404-pab-update-internal-state  (forced update)
 + 3e9843d98...7e1638b82 scp-2565-chain-index-query-effect-types -> origin/scp-2565-chain-index-query-effect-types  (forced update)
HEAD is now at 826c2514a SCP-2462: Use BuiltinData in the ledger etc.
Warning: The package list for 'hackage.haskell.org' is 15 days old.
Run 'cabal update' to get the latest list of available packages.
Warning: Requested index-state 2021-04-30T00:00:00Z is newer than
'hackage.haskell.org'! Falling back to older state (2021-04-29T23:07:55Z).
Resolving dependencies...
Assertion failed
CallStack (from HasCallStack):
  assert, called at src/Distribution/Client/InstallPlan.hs:361:7 in cabal-install-3.5.0.0-inplace:Distribution.Client.InstallPlan

@Mikolaj
Copy link
Member

Mikolaj commented Aug 3, 2021

@newhoggy: I tried to reproduce using cabal's master branch (to verify that the regression is from this PR), but failed due to lack of libsodium and probably other things (BTW, the instructions say that GHC >= 8.10.4 is fine, but GHC 9.0.1 fails due some bounds on base somewhere).

Would it be a problem for you to run the test above with cabal master branch? Another speedup by @gbaz is already merged into master (a few related expensive assertions disabled for non-CI builds), so you'd also have a chance to measure the impact, if you have any cabal benchmarks ready.

@gbaz
Copy link
Collaborator Author

gbaz commented Aug 3, 2021

@newhoggy fixed the bug well enough so you can test this branch with --tests-enabled.

This branch still fails on a related bug with --tests-disabled.

I believe this is because the following is incorrect:

In particular localPackagesEnabledStanzas seems to calculate tests enabled for local packages even with a global tests: False -- i.e. it disregards that global flag. I suspect this is a longstanding bug/quirk that other weirdness in our handling papered over until now?

In any case, even without it fixed, please give this branch a try vs plain master (I just rebased to master too) and see if you get any improvement?

@newhoggy
Copy link

newhoggy commented Aug 4, 2021

@gbaz your new commit works for me.

Is there a way to force cabal to solve again rather than use the cached solution? And is there a way to measure the solving time without all the other things that cabal does?

@newhoggy
Copy link

newhoggy commented Aug 4, 2021

To help with the testing, the following Dockerfile can be used to get a build environment for building the project:

FROM ubuntu:18.04

RUN apt-get -y update
RUN apt-get -y install build-essential curl git libffi-dev libffi6 libgmp-dev libgmp10 libncurses-dev libncurses5 libtinfo5
RUN apt-get -y install libsodium-dev zlib1g-dev libsystemd-dev pkg-config
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
ENV PATH /root/.ghcup/bin:$PATH
RUN ghcup install ghc 8.10.4 && ghcup set ghc 8.10.4

Contrary to documentation, ghc-8.10.4 must be used.

Note this is not how the project is supposed to be built (specifically we use a libsodium fork, not the standard one), but it should be adequate for testing builds.

As for building the project:

git clone https://github.com/input-output-hk/cardano-node
cd cardano-node
cp .github/workflows/cabal.project.local.Linux cabal.project.local
cabal build all --enable-tests

@gbaz
Copy link
Collaborator Author

gbaz commented Aug 4, 2021

Is there a way to force cabal to solve again rather than use the cached solution? And is there a way to measure the solving time without all the other things that cabal does?

To clear the cached solution, deleting the /dist-newstyle/cache dir works.

Passing --dry-run will measure solving and also generating configuration and build-plan, but that's not too bad, I suppose.

@newhoggy
Copy link

newhoggy commented Aug 9, 2021

Before:

cabal build all --dry-run  115.47s user 1.27s system 99% cpu 1:57.38 total

After:

cabal-gbaz build all --dry-run --enable-tests  76.62s user 0.69s system 99% cpu 1:17.43 total

This is a big improvement, but there is still quite a way to the point of parity with deleting build-tool-depends declarations that were already in the build-plan:

cabal build all --dry-run --enable-tests  12.43s user 0.53s system 99% cpu 13.050 total

The last run is based on this diff:

diff --git a/cardano-cli/cardano-cli.cabal b/cardano-cli/cardano-cli.cabal
index ea7409d6a..7d0a7b2d0 100644
--- a/cardano-cli/cardano-cli.cabal
+++ b/cardano-cli/cardano-cli.cabal
@@ -222,7 +222,6 @@ test-suite cardano-cli-golden
                       , text
                       , time
                       , unordered-containers
-  build-tool-depends:   cardano-cli:cardano-cli

   other-modules:        Test.Golden.Byron.SigningKeys
                         Test.Golden.Byron.Tx
diff --git a/cardano-node-chairman/cardano-node-chairman.cabal b/cardano-node-chairman/cardano-node-chairman.cabal
index a8589bda3..15242b105 100644
--- a/cardano-node-chairman/cardano-node-chairman.cabal
+++ b/cardano-node-chairman/cardano-node-chairman.cabal
@@ -83,7 +83,3 @@ test-suite chairman-tests
                         Spec.Network

   ghc-options:          -threaded -rtsopts -with-rtsopts=-N -with-rtsopts=-T
-
-  build-tool-depends:   cardano-node:cardano-node
-                      , cardano-cli:cardano-cli
-                      , cardano-node-chairman:cardano-node-chairman
diff --git a/cardano-testnet/cardano-testnet.cabal b/cardano-testnet/cardano-testnet.cabal
index 3e503e4c8..bf4961805 100644
--- a/cardano-testnet/cardano-testnet.cabal
+++ b/cardano-testnet/cardano-testnet.cabal
@@ -116,6 +116,3 @@ test-suite cardano-testnet-tests
                         Spec.Plutus.SubmitApi.TxInLockingPlutus

   ghc-options:          -threaded -rtsopts -with-rtsopts=-N -with-rtsopts=-T
-
-  build-tool-depends:   cardano-node:cardano-node
-                      , cardano-cli:cardano-cli

@gbaz
Copy link
Collaborator Author

gbaz commented Aug 9, 2021

Wow, I'm really pleased this commit made a difference after all -- I was worried about if it would! As discussed in the other thread, I think there may still be more to be done. I'll try to fix the localEnabledStanzas issue so that this can be merged, as a start.

@gbaz
Copy link
Collaborator Author

gbaz commented Aug 9, 2021

Found and fixed the bug, which was naturally in my own code, and not elsewhere. Given that this seems to bring about a performance improvement in large use cases, and that it doesn't hurt correctness (and makes logs more readable) I'd like some reviews so we can proceed to a merge.

Copy link
Member

@Mikolaj Mikolaj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Low confidence though: I'm able to follow the argument, but I wasn't able to find any of the intermittent bugs in previous versions of this PR.

@@ -1024,13 +1024,13 @@ planPackages verbosity comp platform solver SolverSettings{..}
| (pc, src) <- solverSettingConstraints ]

. addPreferences
-- enable stanza preference where the user did not specify
-- enable stanza preference unilaterally, even when the user asked to enable as well, to help hint the solver.
Copy link
Member

@Mikolaj Mikolaj Aug 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make it clearer (future readers won't have the previous version of this code as context):

     -- enable stanza preference unilaterally, regardless if the user asked
     -- accordingly or expressed no preference, to help hint the solver

@grayjay
Copy link
Collaborator

grayjay commented Aug 10, 2021

I'm not completely sure I understand how this PR is supposed to change the behavior of the solver. I think there may be some inaccuracies in the comments.

The comment in Preference.hs says that the preferences are applied to all uses of a package because the stanza choices must be compatible. However, the solver can make different choices for different goals involving the same package (with different qualifiers). For example, it could enable tests for package x when it is used as a build-depends dependency of a build target but disable tests for x when it is used as a setup dependency. Making different stanza choices is less common than making different choices for the version of a package, though, since users usually only enable tests for local packages, which can only have once instance (due to #6459).

The comment in ProjectPlanning.hs says that applying stanza preferences to local packages that also have tests enabled hints the solver. These preferences should have almost no effect on solver performance, because the constraint would already cause the solver to backtrack immediately.

@Mikolaj
Copy link
Member

Mikolaj commented Aug 10, 2021

Experimentally, I guess we could disable one half of this PR and benchmark to see from which part the significnt benefit comes.

@gbaz
Copy link
Collaborator Author

gbaz commented Aug 10, 2021

The comment in Preference.hs says that the preferences are applied to all uses of a package because the stanza choices must be compatible. However, the solver can make different choices for different goals involving the same package (with different qualifiers).

The comment could be improved. That said, my understanding of the situation is as follows. The solver can make different choices involving the same package. However, stanza preferences are only applied to local packages, and since we have a constraint that we can't make use of two installs of the same version of a package, and since a local package is constrained to be at a single version, as you note, then for the case of stanza preferences in particular, we really can't make different choices involving the same package.

The comment in ProjectPlanning.hs says that applying stanza preferences to local packages that also have tests enabled hints the solver. These preferences should have almost no effect on solver performance, because the constraint would already cause the solver to backtrack immediately.

Well that's what we thought -- but the example of cardano-node as you investigated in
#7472 -- reveals that there's times when the backjump is expensive -- especially I think when the build dependency is solved before the package at top level.

The other approach could be to try to take a topological sort of the build dependency order of local packages and apply that to the tree before solving. (And arguably that's better for things like flags, etc, which can be more complex), but since that interacts with version solving, and its a full other pass, that could be a trickier heuristic to write...

@gbaz
Copy link
Collaborator Author

gbaz commented Aug 10, 2021

@Mikolaj let me try to explain the two stupid bugs I introduced and then fixed to help your confidence level.

The first bug (fixed by 624b8dc) was that it turned on stanzas always, even when they were explicitly disabled, and even when the package was not local. (I figured the constraints would suffice to override the preferences, but they did not).

The second bug (fixed by a5d0445) was a fix to my previous fix -- which still turned on test when it was explicitly disabled, as long as benchmark was not explicitly disabled, and vice versa.

Both these cases didn't hit errors in the solver, but only triggered sanity checks in the build-plan generation, and to trigger the latter required a cabal.project file which has explicit test disabled flag for specific packages.

@grayjay
Copy link
Collaborator

grayjay commented Aug 10, 2021

@gbaz Thanks for the explanation. It looks like the PR improves performance because it sets stanza preferences on local packages when they are used as build tool and setup dependencies, which do not have stanza constraints applied.

I still think that the best way to solve #7472 is to implement #6459. That feature would allow the solver to perform well regardless of the stanza constraints and preferences that it receives as inputs. Then other parts of cabal could pass in the constraints and preferences to produce the desired behavior, without being concerned with performance.

I also think that #6459 could be difficult to implement, so it may be necessary to first fix the performance issue in a simpler, more targeted way. I think that a more direct way to prevent the backtracking in #7472 is to apply stanza constraints (not preferences) to the build tool and setup dependencies in ProjectPlanning.hs. Controlling the qualifiers that constraints apply to is already implemented, so it wouldn't require any changes to the solver. I think that would only require changing scopeToplevel to ScopeAnyQualifier in

(PackageConstraint (scopeToplevel pkgname)
.

@gbaz
Copy link
Collaborator Author

gbaz commented Aug 10, 2021

I also think that applying stanza preferences as I do in this PR makes sense as well. Shall I amend this PR to apply stanza constraints at all levels too?

I think flag constraints are maybe a bit trickier, since they can apply to packages that are not local as well. Perhaps we should also apply those at all levels but only in the case when the package is local? This speeds up cases like here, without causing any semantic changes that would harm the solver otherwise...

@grayjay
Copy link
Collaborator

grayjay commented Aug 10, 2021

I also think that applying stanza preferences as I do in this PR makes sense as well. Shall I amend this PR to apply stanza constraints at all levels too?

Yes, I think that also modifying the stanza preferences is fine. Then stanza preferences and constraints will be applied consistently. It would also lead to more predictable install plans when tests are not explicitly enabled or disabled.

I think flag constraints are maybe a bit trickier, since they can apply to packages that are not local as well. Perhaps we should also apply those at all levels but only in the case when the package is local? This speeds up cases like here, without causing any semantic changes that would harm the solver otherwise...

Are you referring to the flags in #7472 (comment)? I think that is a different issue, because it is more about the semantics of cabal.project, especially for the case of non-local packages. For local packages, it raises the question of whether we should force each local package to only be built once, even after #6459 is fixed.

@gbaz
Copy link
Collaborator Author

gbaz commented Aug 11, 2021

On reflection, I'm not going to add constraints, just leave it at preferences. That way when #6459 is fixed, there's no changes that need to be reverted here. Added a patch to touchup the comments for clarity, and will merge.

@gbaz gbaz merged commit dab09b2 into master Aug 11, 2021
@michaelpj
Copy link
Collaborator

Thanks, looking forward to trying this out!

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.

Slow solving with build-tool-depends on local packages with tests
5 participants