Skip to content
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

Improve recompilation avoidance in the presence of TH #2316

Merged
merged 8 commits into from
Mar 2, 2022
Merged

Conversation

wz1000
Copy link
Collaborator

@wz1000 wz1000 commented Oct 30, 2021

The old recompilation avoidance scheme performs quite poorly when code generation is needed. We end up needing to recompile modules basically any time anything in their transitive dependency closure changes.

Most versions of GHC we currently support don't have a working implementation of code unloading for object code, and no version of GHC supports this on certain platforms like Windows. This makes it completely infeasible for interactive use, as symbols from previous compiles will shadow over all future compiles.

This means that we need to use bytecode when generating code for Template Haskell. Unfortunately, we can't serialize bytecode, so we will always need to recompile when the IDE starts. However, we can put in place a much tighter recompilation avoidance scheme for subsequent compiles:

  1. If the source file changes, then we always need to recompile
    a. For files of interest, we will get explicit textDocument/change events that will let us invalidate our build products
    b. For files we read from disk, we can detect source file changes by comparing the mtime of the source file with the build product (.hi/.o) file on disk.

  2. If GHC's recompilation avoidance scheme based on interface file hashes says that we need to recompile, the we need to recompile.

  3. If the file in question requires code generation then, we need to recompile if we don't have the appropriate kind of build products.
    a. If we already have the build products in memory, and the conditions 1 and 2 above hold, then we don't need to recompile
    b. If we are generating object code, then we can also search for it on disk and ensure it is up to date.
    Notably, we did not previously re-use old bytecode from memory when hls-graph/shake decided to rebuild the HiFileResult for some reason

  4. If the file in question used Template Haskell on the previous compile, then we need to recompile if any Linkable in its transitive closure changed. This sounds bad, but it is possible to make some improvements.
    In particular, we only need to recompile if any of the Linkables actually used during the previous compile change.

    How can we tell if a Linkable was actually used while running some TH?

    GHC provides a hscCompileCoreExprHook which lets us intercept bytecode as it is being compiled and linked. We can inspect the bytecode to see which Linkable dependencies it requires, and record this for use in
    recompilation checking.
    We record all the home package modules of the free names that occur in the bytecode. The Linkables required are then the transitive closure of these modules in the home-package environment. This is the same scheme as used by GHC to find the correct things to link in before running bytecode.

    This works fine if we already have previous build products in memory, but what if we are reading an interface from disk? Well, we can smuggle in the necessary information (linkable Modules required as well as the time they
    were generated) using Annotations, which provide a somewhat general purpose way to serialise arbitrary information along with interface files.

    Then when deciding whether to recompile, we need to check that the versions of the linkables used during a previous compile match whatever is currently in the HPT.

The changes that were made to ghcide in order to implement this scheme include:

  1. Add RuleWithOldValue to define Rules which have access to the previous value.
    This is the magic bit that lets us re-use bytecode from previous compiles
  2. IsHiFileStable rule was removed as we don't need it with this scheme in place.
  3. Everything in the store is properly versioned with a FileVersion, not just FOIs.
  4. The VFSHandle type was removed. Instead we now take a VFS snapshot on every restart, and use this snapshot for all the Rules in that build. This ensures that Rules see a consistent version of the VFS for each run and also makes 3 possible.
    The setVirtualFileContents function was removed since it was not being used anywhere.
    If needed in the future, we can easily just modify the VFS using functions from lsp.
  5. Fix a bug with the DependencyInformation calculation, were modules at the top of the hierarchy (no incoming edges) weren't being recorded properly

A possible future improvement is to use object-code on the first load (so we have a warm cache) and use bytecode for subsequent compiles.

@wz1000 wz1000 requested a review from pepeiborra October 30, 2021 09:43
@pepeiborra
Copy link
Collaborator

This is too big to review in one go. Any chance that you could break it in smaller PRs?

@wz1000
Copy link
Collaborator Author

wz1000 commented Oct 30, 2021

I guess I could separate out the VFS and versioning changes out, but they aren't that big. The bulk of it is synergystic changes to the recompilation checking logic, it is not immediately clear to me how it could be separated out

ghcide/ghcide.cabal Outdated Show resolved Hide resolved
ghcide/ghcide.cabal Outdated Show resolved Hide resolved
@wz1000
Copy link
Collaborator Author

wz1000 commented Nov 10, 2021

@shapr you might want to try this out.

@GavinRay97
Copy link

GavinRay97 commented Nov 22, 2021

This is fantastic, thank you = D

Is this synergistic with @pepeiborra architecture changes in the v1.5 release that he discussed in his recent talk, or is there some overlap?

@Anton-Latukha
Copy link
Collaborator

Anton-Latukha commented Dec 19, 2021

Mentioned dependency #2263 - is merged.

@wz1000 would you be pursuing it further?

This PR status currently seems to be WIP, so marking it so.

@Anton-Latukha
Copy link
Collaborator

Anton-Latukha commented Dec 21, 2021

I wish rebase web UI of allowed to do it in many commits.

Many of these files seem pretty rebaseable.

@myShoggoth
Copy link

@wz1000 What is the status of this PR from your point of view? @pepeiborra explained that it was a huge perf gain for HLS use cases, and would work for previous GHCs as well, so I'd love to see it pushed through. Do you need help? Thank you for doing this!

@wz1000 wz1000 force-pushed the wip/th-recomp branch 5 times, most recently from 3c0a9f6 to e84c6ed Compare February 23, 2022 09:59
@mpickering
Copy link
Contributor

Looks like it needs a rebase and CI is failing.

wz1000 added a commit that referenced this pull request Feb 23, 2022
This patch does two things:

1. It allows us to track the versions of `Values` which don't come from the VFS, as long as those
   particular `Values` depended on the `GetModificationTime` rule
   This is necessary for the recompilation avoidance scheme implemented in #2316

2. It removes the VFSHandle type and instead relies on snapshots of the VFS state taken on every rebuild
   of the shake session to ensure that we see a consistent VFS state throughout each individual build.

With regards to 2, this is necessary because the lsp library mutates its VFS file store as changes come
in. This can lead to scenarios where the HLS build session can see inconsistent views of the VFS.
One such scenario is.

1. HLS build starts, with VFS state A
2. LSP Change request comes in and lsp updates its internal VFS state to B
3. HLS build continues, now consulting VFS state B
4. lsp calls the HLS file change handler, interrupting the build and restarting it.
   However, the build might have completed, or cached results computed using an
   inconsistent VFS state.
wz1000 added a commit that referenced this pull request Feb 23, 2022
This patch does two things:

1. It allows us to track the versions of `Values` which don't come from the VFS, as long as those
   particular `Values` depended on the `GetModificationTime` rule
   This is necessary for the recompilation avoidance scheme implemented in #2316

2. It removes the VFSHandle type and instead relies on snapshots of the VFS state taken on every rebuild
   of the shake session to ensure that we see a consistent VFS state throughout each individual build.

With regards to 2, this is necessary because the lsp library mutates its VFS file store as changes come
in. This can lead to scenarios where the HLS build session can see inconsistent views of the VFS.
One such scenario is.

1. HLS build starts, with VFS state A
2. LSP Change request comes in and lsp updates its internal VFS state to B
3. HLS build continues, now consulting VFS state B
4. lsp calls the HLS file change handler, interrupting the build and restarting it.
   However, the build might have completed, or cached results computed using an
   inconsistent VFS state.
@wz1000 wz1000 marked this pull request as ready for review February 23, 2022 12:50
@wz1000 wz1000 requested a review from jneira as a code owner February 23, 2022 12:50
@wz1000 wz1000 force-pushed the wip/th-recomp branch 4 times, most recently from 80689e9 to 35aad7b Compare February 25, 2022 23:13
@wz1000
Copy link
Collaborator Author

wz1000 commented Feb 26, 2022

My last commit tries to change the lsp-types benchmark to better showcase the improvements to recompilation avoidance because of this patch, but unfortunately lsp-types has a relatively "flat" module dependency graph.

This patch shows the most improvement when you have a "deep" module graph and you ask for recompilation of modules both at the bottom and the top of the graph - and the modules in between are a mixture of requiring and not requiring TH or linking.

Here are the benchmarks for lsp-types run locally on my machine:

version    name                       success   samples   startup              setup   userTime             delayedTime     totalTime            buildRulesBuilt   buildRulesChanged   buildRulesVisited   buildRulesTotal   buildEdges   maxResidency   allocatedBytes
upstream   edit                             True      50        1.798771925          0.0                 191.91978414899998      2.3124167000000008e-2   191.947932916        155               130                 286                 1178              9138         494MB          284073MB
HEAD       edit                             True      50        1.766769395          0.0                 48.072982939            2.1007206000000004e-2   48.099120334000006   73                43                  235                 1155              8577         368MB          59120MB
upstream   hover                            True      50        1.9401033040000002   0.0                 6.7993e-5               4.8781324029999995      4.882002377          473               472                 481                 1159              9126         258MB          8776MB
HEAD       hover                            True      50        1.584940787          0.0                 6.4439e-5               4.162078842             4.16556116           757               756                 759                 1107              8651         258MB          8652MB
upstream   hover after edit                 True      50        2.23405389           0.0                 3.1066998000000002e-2   7.622193109000003       7.669040757          13                5                   202                 1202              9126         258MB          9246MB
HEAD       hover after edit                 True      50        1.757586773          0.0                 3.0687872e-2            5.010605698000001       5.057394084          23                7                   174                 1149              8624         263MB          9269MB
upstream   getDefinition                    True      50        1.699694912          0.0                 6.879799999999997e-5    6.944418013             6.948739521          376               375                 442                 1159              9126         257MB          8630MB
HEAD       getDefinition                    True      50        2.0626965260000003   0.0                 7.986000000000002e-5    6.418572896000001       6.424054067          369               368                 405                 1107              8651         258MB          8595MB
upstream   getDefinition after edit         True      50        2.26279817           0.0                 4.507241e-2             8.454230090000001       8.515164201000001    29                8                   205                 1202              9126         258MB          9886MB
HEAD       getDefinition after edit         True      50        2.069157503          0.0                 3.2946489e-2            4.430497608000002       4.47954948           22                7                   174                 1150              8623         259MB          9308MB
upstream   completions                      True      50        2.1221533420000003   0.0                 1.0265099999999998e-4   8.334979066999995       8.342324000000001    577               576                 583                 1159              9126         259MB          8818MB
HEAD       completions                      True      50        1.600802762          0.0                 6.1072e-5               4.494912567000004       4.498352855          595               594                 599                 1107              8651         258MB          8760MB
upstream   completions after edit           True      50        2.219558311          0.0                 3.1378321e-2            7.379409227000004       7.425725392          17                7                   204                 1202              9126         258MB          9134MB
HEAD       completions after edit           True      50        1.027144498          0.0                 2.8329477999999995e-2   4.463608328000001       4.502147832          20                7                   174                 1150              8623         260MB          9303MB
upstream   code actions                     False     50        0.0                  0.0                 0.0                     0.0                     0.0                  0                 0                   0                   0                 0            118MB          1459MB
HEAD       code actions                     False     50        0.0                  0.0                 0.0                     0.0                     0.0                  0                 0                   0                   0                 0            116MB          2001MB
upstream   code actions after edit          False     50        0.0                  0.0                 0.0                     0.0                     0.0                  0                 0                   0                   0                 0            114MB          1600MB
HEAD       code actions after edit          False     50        0.0                  0.0                 0.0                     0.0                     0.0                  0                 0                   0                   0                 0            118MB          1743MB
upstream   code actions after cradle edit   False     50        2.5541577180000004   8.389741869         0.0                     0.0                     6.160159156000001    527               378                 581                 1164              9138         456MB          17603MB
HEAD       code actions after cradle edit   False     50        1.801486665          5.651785252000001   0.0                     0.0                     3.228125078          393               249                 533                 1155              8574         415MB          11408MB
upstream   documentSymbols after edit       True      50        1.613184221          0.0                 3.1915035579999995      6.702542298000001       9.904618432000001    41                9                   207                 1202              9126         272MB          13751MB
HEAD       documentSymbols after edit       True      50        1.810905519          0.0                 2.1611687269999997      7.287025813999999       9.457577542000001    11                9                   176                 1150              8623         269MB          13153MB
upstream   hole fit suggestions             False     50        0.0                  0.0                 0.0                     0.0                     0.0                  0                 0                   0                   0                 0            284MB          9693MB
HEAD       hole fit suggestions             True      50        2.0343340100000002   0.0                 95.41021875600002       2.1052176000000002e-2   95.436770108         77                46                  301                 1154              8578         388MB          99495MB

The "after edit" benchmarks show some reduction in total time and rules built.

Unfortunately some of the code action benchmarks fail (on both this patch and upstream), so I will revert the changes from the last commit in this patch.

Here is another set of benchmarks for "get definition after edit" run on https://github.com/hasura/graphql-engine which is a large project satisfying the constraints mentioned above:

version    name                       success   samples   startup              setup   userTime             delayedTime     totalTime            buildRulesBuilt   buildRulesChanged   buildRulesVisited   buildRulesTotal   buildEdges   maxResidency   allocatedBytes
HEAD       getDefinition after edit   True      20        2.4624424570000003   0.0     135.65716052999997   18.553615088    154.21270514100001   77                45                  1751                9523              390324       1115MB         363910MB
upstream   getDefinition after edit   True      20        2.21295533           0.0     115.71000826900003   287.580292801   403.291925787        198               165                 1812                9887              395673       1185MB         747853MB

@wz1000 wz1000 force-pushed the wip/th-recomp branch 3 times, most recently from cfdb552 to 0987bef Compare February 26, 2022 13:48
Copy link
Collaborator

@pepeiborra pepeiborra left a comment

Choose a reason for hiding this comment

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

Impressive results. The next HLS release is going to be so good for TH users

@michaelpj
Copy link
Collaborator

There's a lot of careful reasoning about how this scheme works in the PR description. Can we make sure that this is captured somewhere for posterity, maybe a note in the code?

@wz1000
Copy link
Collaborator Author

wz1000 commented Feb 28, 2022

There's a lot of careful reasoning about how this scheme works in the PR description. Can we make sure that this is captured somewhere for posterity, maybe a note in the code?

I've added a note to this effect

@wz1000 wz1000 added merge me Label to trigger pull request merge and removed pr: needs rebase labels Mar 1, 2022
wz1000 and others added 8 commits March 2, 2022 17:58
… at the top

of the hierarchy (no incoming edges) weren't being recorded properly
…e it.

The SourceUnmodifiedAndStable check in loadInterface wasn't doing much for us
because we use bytecode and there is no bytecode on disk so we always had to
recompile.
The old recompilation avoidance scheme performs quite poorly when code
generation is needed. We end up needed to recompile modules basically
any time anything in their transitive dependency closure changes.

Most versions of GHC we currently support don't have a working implementation of
code unloading for object code, and no version of GHC supports this on certain
platforms like Windows. This makes it completely infeasible for interactive
use, as symbols from previous compiles will shadow over all future compiles.

This means that we need to use bytecode when generating code for Template
Haskell. Unfortunately, we can't serialize bytecode, so we will always need
to recompile when the IDE starts. However, we can put in place a much tighter
recompilation avoidance scheme for subsequent compiles:

1. If the source file changes, then we always need to recompile
   a. For files of interest, we will get explicit `textDocument/change`
      events that will let us invalidate our build products
   b. For files we read from disk, we can detect source file changes
      by comparing the mtime of the source file with the build
      product (.hi/.o) file on disk.
2. If GHC's recompilation avoidance scheme based on interface file hashes
   says that we need to recompile, the we need to recompile.
3. If the file in question requires code generation then, we need to recompile
   if we don't have the appropriate kind of build products.
   a. If we already have the build products in memory, and the conditions
      1 and 2 hold, then we don't need to recompile
   b. If we are generating object code, then we can also search for it on
      disk and ensure it is up to date.
   Notably, we did _not_ previously re-use old bytecode from memory when
   hls-graph/shake decided to rebuild the 'HiFileResult' for some reason
4. If the file in question used Template Haskell on the previous compile,
   then we need to recompile if any `Linkable` in its transitive closure
   changed.
   This sounds bad, but it is possible to make some improvements.
   In particular, we only need to recompile if any of the `Linkable`s
   actually used during the previous compile change.

   How can we tell if a `Linkable` was actually used while running some
   TH?

   GHC provides a `hscCompileCoreExprHook` which lets us intercept bytecode
   as it is being compiled and linked. We can inspect the bytecode to see
   which `Linkable` dependencies it requires, and record this for use in
   recompilation checking.
   We record all the home package modules of the free names that occur in the
   bytecode. The `Linkable`s required are then the transitive closure of these
   modules in the home-package environment. This is the same scheme as used by
   GHC to find the correct things to link in before running bytecode.

   This works fine if we already have previous build products in memory, but
   what if we are reading an interface from disk? Well, we can smuggle in the
   necessary information (linkable `Module`s required as well as the time they
   were generated) using `Annotation`s, which provide a somewhat general purpose
   way to serialise arbitrary information along with interface files.

   Then when deciding whether to recompile, we need to check that the versions of
   the linkables used during a previous compile match whatever is currently in the
   HPT.

The changes that were made to `ghcide` in order to implement this scheme include:

1. Add `RuleWithOldValue` to define Rules which have access to the previous value.
   This is the magic bit that lets us re-use bytecode from previous compiles
2. `IsHiFileStable` rule was removed as we don't need it with this scheme in place.
3. Everything in the store is properly versioned with a `FileVersion`, not just
   FOIs.
4. The VFSHandle type was removed. Instead we now take a VFS snapshot on every
   restart, and use this snapshot for all the `Rules` in that build. This ensures
   that Rules see a consistent version of the VFS and also makes
   The `setVirtualFileContents` function was removed since it was not being used anywhere.
   If needed in the future, we can easily just modify the VFS using functions from
   `lsp`.
5. Fix a bug with the `DependencyInformation` calculation, were modules at the top
   of the hierarchy (no incoming edges) weren't being recorded properly

A possible future improvement is to use object-code on the first load (so we
have a warm cache) and use bytecode for subsequent compiles.
avoid coerce

redundant import

remove trace and format imports

hlints and comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merge me Label to trigger pull request merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants