| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
I haven't looked at the code yet, but I wonder: why a new PR instead of building on top of #4339? That code has been running in production for months now. |
Sorry, something went wrong.
This PR uses mostly a different approach. For example, it doesn't assume changes in externs format, etc. It provides an overview of concrete changes and explains those decisions. Also, if I'm not mistaken #4339 was refered as abandoned lately by the author. |
Sorry, something went wrong.
|
Sorry. I've failed to explain it properly, because multiple people have looked at the code in that PR without reading the comment thread first. I'd be more than happy for you to take a 4th stab at this, with or without the code I've written. The important thing is that we get a faster compiler. https://github.com/drathier/purserl is where the caching fork lives, since about 6 months. It's a rewrite of the linked PR into something that's more likely to be mergable, but I've given up on getting it merged. Plus I've merged in the purerl code gen into the same binary, halfing compile times for erlang projects. We're using it daily at work and it's very useful. Merging in main every release is much less work than getting it merged, so that's what I'm doing. |
Sorry, something went wrong.
No problem, I understand that you are using your solution in your fork, I just read that you said that code in PR itself was abandoned. I've been working on this for quite a while, even before I saw #4339. I first started working on this because I wanted to cut off the loading of unnecessary extern files during re-builds, then I saw that I could cut off the build plan itself. In #4339 I saw a solution and design decisions that were not clear to me (something that is called caching, and chanding the externs format) so I decided to finish my work with the approach that seemed more correct to me.
You could build you version on top of this PR and compare the results. @drathier And you also are probably the person who can competently review this PR. |
Sorry, something went wrong.
|
Yes, I'll try to find the time to review it this week. The main idea behind the caching strategy we're using is:
Step 1 and 2 are very hard to validate, as the information is lost during compilation (e.g. how do you know what type alises are you depending on when they've all been desugared away?). https://youtu.be/BQVT6wiwCxM?t=762 is a useful high-level reference table for different caching strategies. It's also in the paper https://www.microsoft.com/en-us/research/uploads/prod/2018/03/build-systems.pdf . |
Sorry, something went wrong.
|
If to explain the strategy I use in a couple of sentences: When we have externs of a module then know all the things that dependants can use, and by comparing with previous externs results we know what exported things have been changed/removed/added. When we decide if to recompile a module, we have all this information about its dependencies, so we need to determine if the module uses any of those things (exported from dependencies) that have changed. Cached things that we already have here: previously built products with externs files. |
Sorry, something went wrong.
There was a problem hiding this comment.
I'm sure this will be better than all of the previous attempts, and I'm looking forward to getting this merged into main. The base approach seems to be the same as the one I was using, and lots of changes overlap between the two. There's lots of tests, which makes me confident that it's going to be fairly stable from day one. Main focus of this review at this stage is to make the code easier to read, so that others can understand it quickly later.
In academic software literature, a constant review speed of 200 lines an hour across all languages sure sounds slow, but here I am, spending 5 hours reviewing 1240 lines of code, some of which are renames. It keeps being spot on.
Sorry, something went wrong.
| -- | Traverses imports and returns a set of refs to be searched though the | ||
| -- module. Returns Nothing if removed refs found in imports (no need to search | ||
| -- through the module). If an empty set is returned then no changes apply to the | ||
| -- module. |
There was a problem hiding this comment.
"no need to search through the module" please be explicit about why
Sorry, something went wrong.
There was a problem hiding this comment.
not done
Sorry, something went wrong.
| -- | Check if type name is a type class dictionary name. | ||
| isDictName :: P.ProperName a -> Bool | ||
| isDictName = | ||
| T.isInfixOf "$" . P.runProperName |
There was a problem hiding this comment.
is this stable over time? perhaps add a comment where the $ is inserted into dict names explaining that this part of the code relies on it being there?
Sorry, something went wrong.
There was a problem hiding this comment.
code seems to be deleted from pr? so it's probably done
Sorry, something went wrong.
| -- recompiled. | ||
| go optsCorefnOnly `shouldReturn` moduleNames ["Module"] | ||
| go optsCoreFnOnly `shouldReturn` moduleNames ["Module"] | ||
|
|
There was a problem hiding this comment.
Lots of new unit tests, which is nice. Please steal as many as you can from https://github.com/drathier/purserl/blob/single-binary/tests/TestMake.hs . Some of them are from live bugs in that implementation, such as tracking transitive type alises across partially failed builds, or type class definition dependencies kinda not being a breaking change until you expand dicts.
Sorry, something went wrong.
There was a problem hiding this comment.
I want to go through this more in depth later but some quick thoughts now:
Sorry, something went wrong.
| let modulePath = sourcesDir </> "Module.purs" | ||
| let mPath = sourcesDir </> "Module.purs" |
There was a problem hiding this comment.
This is already a large-ish PR, and I would prefer it if renames like this weren't included, at least as part of this work. (I'm not arguing that one or the other name is better; just that it's not so bad the way that it is that it needs changing.)
Sorry, something went wrong.
| writeFileWithTimestamp :: FilePath -> UTCTime -> T.Text -> IO () | ||
| writeFileWithTimestamp path mtime contents = do | ||
| writeFile :: FilePath -> UTCTime -> T.Text -> IO () | ||
| writeFile path mtime contents = do |
There was a problem hiding this comment.
Why was this renamed?
Sorry, something went wrong.
| -- again. | ||
| -- | ||
| -- This version will collect an return all externs of all passed modules. | ||
| make' :: forall m. (MonadBaseControl IO m, MonadError MultipleErrors m, MonadWriter MultipleErrors m) |
There was a problem hiding this comment.
Doesn't this case correspond to the original behavior of make? If so, this function should be called make and the other one makeAndReturnUsed (or something); I don't think the ' is clear enough in this case to distinguish the two behaviors, particularly if the ' is the old behavior and the bare make is the new one.
I would also be fine with having makeImp be the new make and using one or the other of the constructors of ExternsFileCollectionPolicy (per Filip's suggestion; or whatever name you end up using) at every call site, instead of going through these two auxiliary functions.
Sorry, something went wrong.
|
❗ The thing to discuss: currently I have added a progress message Skipping My.ModuleB for skipped modules (along with Compiling My.ModuleA). We need to decide what to do with this message, should we print it out or not. Also, there is question maybe we should somehow output the reason why the module is not skipped (output a thing changed used) - don't know if it is feasable. I think this is needed just for debug purposes. |
Sorry, something went wrong.
I don't think messages about skipping are particularly useful outside of debugging. Compiling is useful because it's an indicator of work being done. |
Sorry, something went wrong.
|
Printing the reason why something recompiled helps people debug slow compile times. Likewise printing if compilation of a particular module took more than e.g. 10s, or printing the 10 slowest-to-compile modules. If it's easy to add something like this in, I think we should do it. Otherwise, it's a nice change in its own PR. We could add lots of debug info to the output and enable that feature using an envvar or cli flag, for example. |
Sorry, something went wrong.
|
Make method API question for discussion. Currenlty from Language.PureScript.Make only one method make exposed that returns the list of ALL externs, that were preloaded and rebuilt. To speed up the process, we may skip loading/parsing of significant parts of externs that are not needed for the build (preload externs only for updated modules and their transitive dependencies), which is implemented in this PR and there is an option to run make that will preload and return externs for all the modules. Currently make method is used in these places:
Here is where returned externs are used:
The question is what would be the better API to accommodate existing cases. Here are some options:
Upd:
|
Sorry, something went wrong.
|
Lmk when you want another review pass :) Please fix or reply to all review comments before |
Sorry, something went wrong.
I believe I answered all the comments, not sure if I skipped some. I made updates to the code so you may review it. |
Sorry, something went wrong.
|
Hey all. Once the row error message improvements get in, this is something else I plan to look at. |
Sorry, something went wrong.
|
Sorry about the late response. Life happened.
Other than that, there's a bunch of code quality stuff, but I would be happy to adress those in another PR and get this merged and beta-shipped. Rewriting history really messes with code review change tracking, so it looks like there's 30 or so comments which are incorrectly marked as outdated. Please don't rebase/squash going forward, because of this problem |
Sorry, something went wrong.
I don't remember squashing or rebasing after the comments were made, I added a couple of new commits to address the problems. |
Sorry, something went wrong.
|
To clarify, in case there was a misunderstanding. In order to merge this, I'd like at least the linked tests to be ported over, as they're actual regressions seen in that other implementation. @wclr There are many comments without replies, which I'd be happy to see a reply to too, but the regression tests are the minimum imo. |
Sorry, something went wrong.
|
Yes, I need to review the tests, as I mentioned in some reply to comments all the tests from your fork have passed, I will look closer at those you linked above.
I think I addressed all the meaningful problems mentioned in the comments, maybe I accidentally missed some. You can probably review your comments again and resolve those that are not actual anymore. |
Sorry, something went wrong.
|
@wclr I did a pass over my review comments to see which ones were done. I'm apparently not allowed to mark my own comments as resolved, and it felt weird deleting comments instead, so I've commented on them again pointing out if they're fixed or not. Main thing missing is porting the regression tests linked here: #4477 (comment) After that, I'd be happy to merge this, and I'm really looking forward to having much better caching in the mainline compiler! |
Sorry, something went wrong.
|
@wclr Should this be reviewed again? |
Sorry, something went wrong.
|
I believe @MonoidMusician had started reviewing this recently. From reading above I think there's still an outstanding point about some ported regression tests |
Sorry, something went wrong.
|
Yup I've been looking at this. @drathier are you still unable to resolve your own comments / would you like me to mark them as resolved? |
Sorry, something went wrong.
There was a problem hiding this comment.
I'm still going through some of the logic, but it's looking good to me so far.
A few specific code comments inline.
Sorry, something went wrong.
| stripCtorType x = x | ||
|
|
||
| searches' = S.map (map stripCtorType) searches | ||
| check = (\x -> [x | x]) . flip S.member searches' . toSearched |
There was a problem hiding this comment.
I believe this code should use Any instead of [Bool]
Sorry, something went wrong.
|
@MonoidMusician Thanks for reviewing this. Regarding your comments/suggestions:
I'm not against such a log. To do this we need to decide on the information needed in this file. I may propose something like this: Module.A compiled - changed export: myFn # list things that changed - changed export: MyType Module.B compiled - changed imported: Module.A myFn # the reason for rebuild Module.C compiled - changed import: Module.A MyType Module.D skipped To help further review, I will point out two main reasons why cut/off may fall short:
These are the points that should be thoroughly tested, though eventually it should not be difficult to have a complete set of cases, the current set should be reviewed/revised.
Cache-db contains information on source modules, timestamps and hashes so we can check if we have already performed the build for the current module state. Timestamps on externs are used to determine previous result compilation time, so we can check if dependencies where built after their parents or not. This may be the case, for example, when purs-ide performs fast-rebuild of a changed module, and then during the build, although the hash for the module in cache-db is valid, we should invalidate all its dependencies.
It will produce the results for some modules and cache-db won't be changed, the next build will fix everything, but all the modules need to be rebuilt again. This leads to the question of necessity and convenience of a single cache-db file. I think we can eventually deprecate it in favor of per module meta files. Though we will probably need a single build result summary file for other purposes (like ide checks). This is just a thought for the future.
That's right, after the build with errors failed modules are removed from cache-db as well as their dependencies (as they are treated as skipped jobs). So the next time we run the build it will invalidate all the modules that are not in cache-db. It is an important case that I didn't address, but I did encounter it periodically. This should be easy to fix though, we will just not remove those entries from cache-db and will treat them as successful compilations if the module is up-to-date. |
Sorry, something went wrong.
|
I would love to get this PR merged as in our team a large chunk of every day is spent waiting for recompilation. Is there anything we can do to help get it over the line? |
Sorry, something went wrong.
|
@roryc89 Have you tried to use it for your everyday work? |
Sorry, something went wrong.
|
@wclr Not yet but happy to try and add any issues we find with it here. |
Sorry, something went wrong.
No. Why not build on your own? |
Sorry, something went wrong.
|
It's been a while since the last update, there are notable ones with the commit. To sum up the changes from the current master:
Minor changes in behaviour:
Tests are fully refactored, though I have not yet completed the suite for testing all the cut off cases. Some questions:
Also worth noting: changes in this PR along with updates to Purescript.Ide (and Language Server) allow to implement full fledged real-time diagnostics and incremental compilation while development. Instead of calling singleton rebuildModule it is possible to use make (that compiles changed modules with the downstream) and produce a consistent result as a Full Build does but much faster. I've been succesfully using it for quite a while. |
Sorry, something went wrong.
|
For those who want to try it out, you may download binaries here: It builds in CI from https://github.com/wclr/purescript/tree/wclr/release branch. It also publishes NPM package: npm i @wclr/purescript that will download binaries for you. What to expect from running the build (e.g. spago build, purs compile):
|
Sorry, something went wrong.
|
Thanks for making this so easy to test. It seems to be such a massive improvement! Especially after I rebased to include this pr. The only thing that stands out as a regression is the compilation progress log. On master the numbering is generally sequential but with cutoff I get wildly out of sync numbering, e.g. [1 of 10] Compiling ..., [6 of 10] ..., [3 of 10] .... As things scale this gets funny: [1410 of 7437] Compiling Options.Applicative.Builder.Completer [1913 of 7437] Compiling Text.Email.Parser [5489 of 7437] Compiling AffiliationField [ 886 of 7437] Compiling Routing.Parser [1447 of 7437] Compiling Routing.Match Ideally the progress logger would maintain a count that's separate from however the files are designated a number. As it stands it's pretty hard to estimate remaining time. |
Sorry, something went wrong.
|
@finnhodgkin Thanks for trying this out, I'll fix the index ordering. I've also included the PR you mentioned, as promised it gives ~30% boost. |
Sorry, something went wrong.
|
I've started reviewing this again. I think it is close to mergeable, with a few things that need to be fixed. Thanks for adding ./output/compile.log. I think having that record of the last build be always available will be helpful if users run into miscompilations.
Those changes are available on my branch. I've been testing it with those changes, comparing the outputs between incremental builds and clean builds. So far I've seen two sources of minor diffs:
Also I ran into this error once, possibly as a result of incrementally building from output that was produced by the main compiler not this branch: purs: An internal error occurred during compilation: make: getBuildReason no barrier Please report this at https://github.com/purescript/purescript/issues CallStack (from HasCallStack): error, called at src/Language/PureScript/Crash.hs:10:3 in purescript-0.15.15-8WFym9lhKoSJtoPXLBrRrY:Language.PureScript.Crash internalError, called at src/Language/PureScript/Make/BuildPlan.hs:108:21 in purescript-0.15.15-8WFym9lhKoSJtoPXLBrRrY:Language.PureScript.Make.BuildPlan Does this case need to be an error, can it just return a build reason to force the module to rebuild? |
Sorry, something went wrong.
|
I had some time today to look at this see if it feels mergeable based on where @MonoidMusician left off. In short, I think it is, and while I found a few soundness issues, I've fixed them on trh/make-cutoff-ready, which is based on her branch and the changes below. In addition, I brought in @drathier's regression tests, added several more tests. My changes on that branch are almost all test additions, and a couple of small fixes; the core of what @wclr has built and @MonoidMusician extended I think is well-worth merging essentially as-is with these fixes. My changes include one potentially controversial addition: a --no-cutoff flag which rebuilds everything downstream of a change — basically, the old compiler behavior — which can be used as an escape hatch for the first release(s) carrying this just in case something isn't right. It's a handful of lines of centralized code we can take out if we want, as I know generally purs doesn't carry many flags. Here are the ported regression tests:
I also restored the cut-off cases dropped in the test refactor and added coverage for re-exports, hiding imports, operators, kinds/roles, and desugaring. Next, I did a few rounds of thorough verification comparing incremental builds to clean builds and found a few soundness gaps. Each of these failed on @MonoidMusician's branch, but are fixed here.
Specifically I used registry-dev to do a bunch of clean vs. incremental builds, which has about 742 modules across 132 packages, and then compared the externs.cbor, corefn.json, index.js against clean builds after performing a thorough scan editing every module and comparing the clean vs. incremental results to try and produce some stale build. That's where the gaps above were found, and after fixing them I couldn't find a way to perform an edit, build, and have an incorrectly-stale result. I think this clears @drathier's merge bar and the review points above. Of course, happy to remove --no-cutoff if we don't want it. |
Sorry, something went wrong.
…tests updated and commented.
|
Hi, sorry for my a delayed reaction, I postponed this issue for myself as I have been busy with other projects. But as this change got even more traction now I hope we will be able to wrap it up eventually. I'm fairly interested in getting it right.
I think I was acting from assumption that failed modules should and do not update their info in cache db. So they should either just be removed from new cache db or as a small optimization kept from previous cache db, then rebuild could be avoided in case of the module is fixed and its hash stays the same as a previous working version.
Right, I didn't do load of all the warnings while make. Though it can definitely flood build output seems more correct to show all the warnings for all the modules. Not sure if it should be a subject for flag --all-warnings or --only-build-warnings. Probably front-end tools like spago could have options to manage this.
I kind of missed this. It seems strange to me why we need to have those filepaths in externs files, more over in each SPAN, I guess this is also the main contributor to bloated extern's disk size esp. for large modules. For warnings files I even cleaned them and populated while loading with actual filepath. The problem with changed filename is essential, we have output artefacts (externs, warnings, corefn, sourcemaps, docs) coupled with module's filepath so when it is changed, even if the module it self is not required to be rebuild, we should re-generate or update those artifacts. I see a few ways for dealing with this in the current state:
At first, I was inclined for the first option as it doesn't require additional logic and seems not a big omission, but now it seems completing the second is not difficult. Sidenote: Ideally, I would like build artifacts be decoupled from source's filepath, though for example source maps should contain link to source file inside by standards. Some custom procedures for handing such case would be required anyway.
What line numbers do you mean, why should they be updated?
Can you elaborate on this. Is there reproducible workflow?
I'm in favor of adding a flag that would run build without performing externs diffs (I proposed the same in previous posts), probably it should be named more specifically, e.g. --no-diff-check. Thanks for your efforts, I will look how it is better to integrate the latest changes and tests into this PR. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
This is an attempt to address the problem concerned in #3724. It was aslo addressed lately in #4339. With this change, the compiler will only rebuild downstream modules if they are affected by changes introduced after the previous compilation. As this is a significant build infrastructure related change I will explain the meaningful details and design decisions below.
How build/make process currently works
Proposed changes in the make/build process
While constructing the build plan, avoid loading/parsing extern files that are not needed. We just gather timestamps (compilation time) that are needed to determine what modules have to be built.
Then preload only needed externs: transitive dependencies of modules that need to be rebuilt, and also previously built results for modules to be rebuilt (that will be used if the module does need to be recompiled or for checking if externs have changed).
While module's rebuild phase check if externs of any of its dependencies have effectively changed, if not then (if the previously built result is available) we skip the compilation step.
Avoiding loading exessive externs files
Currently externs for all the modules are loaded during construction of a build plan. Though loading/parsing those extern files may require significant (perceived) time though they are not even required for the build environment.
I noticed this problem when I added to my project a dependency with large externs files with lots of definitions (externs ~30MB in size). After just adding this package, the empty build running time (without any compilation needed) increased to more than 3 sec. With the change introduced (when those externs are not loaded) empty buuid for the described case runs for ~1 sec now.
Changes to make API
There is a small change required (because of the above change) that adds two different make methods, that returns only updated/used externs and externs for all modules (which is needed for PSCI).
CacheDb and illegal compiler version invalidation
Currently using info from cache-db we invalidate changed files (using timestamp/hash). Build products of incorrect compiler versions are invalidated while loading extern files. However, in the proposed changes, we also cut off the loading of not needed externs files, and we need a way to invalideate wrong compiler version products. The proposed implemented method does this by placing a compiler version in the cache-db file.
Externs Diff
This is probably the most intricate part of the change, though essentially it is quite obvious. To know if the module has some changes, we compare externs and store the difference in so called ExternsDiff. When building a module we check if any of its dependencies have changed and if it uses any of those changes, if it does we rebuild it, if not we skip it. So, there are two problems to solve:
correctly determine the changes in externs - this is done by comparing structure of old and new extern's entries and considering special cases of indirect dependencies such as reexports and changes in type instances.
correctly check if any of the changes in dependencies affect the module - to solve this we check if the module effectively uses any of the changed elements from the imported dependencies by traversing the module's content and searching for the first occurrence of a changed element.
Tests
To test this change comprehensively it is needed for all the elements that can be exported to check that their changes cause the rebuild for the downstream. Also check that non-effective changes do not cause the rebuild.
Another thing that needs to be tested is that while traversing the module it doesn't skip the usage of a changed element.
Existing tests have been updated to make them a bit more compact and readable.
Status
The PR is ready for review and testing. The test spec for the change is probably quite complete, though it will need a further clean up and probably some additions considering the findings and suggestions.
Checklist: