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

Use elm-solve-deps-wasm instead of elm-json solve #558

Merged
merged 39 commits into from
Apr 6, 2022

Conversation

mpizenberg
Copy link
Contributor

Hey, I discussed this a little bit over the incremental elm discord, and this is what using elm-solve-deps-wasm inside node-test-runner would look like.

elm-solve-deps-wasm basically is a WebAssembly port of the dependency solver inside elm-test-rs. Advantages over using elm-json as a binary are the versatility of the solver that you can customize to your needs (can work with lamdera for example), and lightweight compared to downloading a binary. The wasm crate is 100kb compressed and 280kb total package uncompressed.

On the downsides, it's currently not much tested, has worse error messages than elm-json, and customizability implies a bit more code (cf dependency-provider-offline.js provided as starting point).

Let me know what you think of this.

@mpizenberg
Copy link
Contributor Author

mpizenberg commented Dec 28, 2021

PS: it's expected that tests are failing since the starter dependency-provider-offline.js file only works if dependencies are already available offline. Tests are passing on my machine when I run npm test.

@harrysarson harrysarson added status: discussion Next step is not crystal clear. Let's discuss. status: waiting for review Pull request needs a review labels Dec 28, 2021
@mpizenberg
Copy link
Contributor Author

mpizenberg commented Dec 29, 2021

To complement dependency-provider-offline.js I've implemented another dependency-provider.js that contains both the offline and online logic, and basically reproduces the full logic of the solver in elm-test-rs in JS. I think I got it right but JS is so painfully unforgiving that there are probably some bugs left.

I've left untouched the dependency-provider-offline.js because it's much easier to read first, to get a grasp of the API, and then read the more complete dependency-provider.js, which has additional online behavior and also caches.

Since I haven't figured out (yet) how to interact with async functions and wasm, I've made the wasm interfaces sync, and thus needed to have sync versions of file reading and http requests. This means I've added sync-request to the dependencies. If you know how to prevent that, let me know.

There are still a few console logs for now, that would eventually be removed or replaced by proper logging.

@mpizenberg
Copy link
Contributor Author

I think I've figured out a way to use async functions with wasm and rust, but unfortunately, due to the contaminating nature of async, it requires changes all the way through elm-solve-deps to the pubgrub rust package. And there are other WIP in pubgrub that are more prioritized than async, so I wouldn't expect to be able to use elm-solve-deps-wasm in an async way for a few months.

It’s used to install Elm and elm-format for development of
node-test-runner.

I re-added elm-tooling to devDependencies (not dependencies) since it’s
only used during development now.
Copy link
Collaborator

@lydell lydell left a comment

Choose a reason for hiding this comment

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

I pushed some modifications directly to your branch, I hope that’s ok!

lib/DependencyProvider.js Outdated Show resolved Hide resolved
lib/DependencyProvider.js Outdated Show resolved Hide resolved
lib/DependencyProvider.js Show resolved Hide resolved
lib/DependencyProvider.js Outdated Show resolved Hide resolved
@mpizenberg
Copy link
Contributor Author

mpizenberg commented Jan 2, 2022 via email

@lydell
Copy link
Collaborator

lydell commented Jan 2, 2022

The way the cache in Solve.js works is this:

  1. Try to read elm-stuff/generated-code/elm-community/elm-test/0.19.1-revision6/dependencies.SHA256.json from disk.
  2. If it exists, use that (it contains the "dependencies" field value for the generated Elm application used to run the tests).
  3. Otherwise compute that.

Reading that json file is very fast.

The cache key is a SHA256 hash of the user’s elm.json, so it is only invalidated if the user changes their elm.json which usually isn’t that often.

I know that elm-json stores things in ~/.elm, but I’m not sure how that affects things the times elm-json is actually invoked.

Given the above, do you think the extra caching you built is needed? If so, can you give an example scenario?

@mpizenberg
Copy link
Contributor Author

mpizenberg commented Jan 2, 2022 via email

@lydell
Copy link
Collaborator

lydell commented Jan 2, 2022

So, what’s an example scenario where the extra caching leads to a faster experience?

@mpizenberg
Copy link
Contributor Author

mpizenberg commented Jan 3, 2022

So, what’s an example scenario where the extra caching leads to a faster experience?

There are few scenarios I have in mind such as:

Create another project with the same dependencies than the current project

Let's say you use some project as a starter for another project. Change a few things like the package name, or the source location. But the dependencies stay unchanged. All files needed to solve the dependencies will already be available offline, so no need to make a request to the package server.

Add a dependency

With the caches, most of what is needed is already on disk. Most of the time, only few new requests will be needed instead of 1 + 1 per dependency. Sometimes even 1 request will be enough.

If the offline solver fails, there is one mandatory request done to https://.../all-packages/since/${count} where count is the number of package versions already known. Which are the ones we knew since last time we did a .../all-packages/since/${lastCount} request. So this requests will be quite fast as it just lists the last few published packages since last time we updated this. It will look something like follows.

// curl -L https://package.elm-lang.org/all-packages/since/12970 | jq
[
  "bChiquet/[email protected]",
  "newmana/[email protected]",
  "newmana/[email protected]"
]

Then only when needing to fetch the dependencies of a given package for the first time, inside fetchElmJson we need to make a call to the package server like https://package.elm-lang.org/packages/elm/random/1.0.0/elm.json. But sometimes, that package elm json was already downloaded for solving dependencies in another project so no request is needed at all.

@harrysarson
Copy link
Collaborator

I think I like the direction of this, using a library (even if it is wasm) is much neater than shelling out to a different program. It has the green light from me in principle.

@mpizenberg
Copy link
Contributor Author

mpizenberg commented Jan 4, 2022 via email

@avh4
Copy link
Collaborator

avh4 commented Mar 26, 2022

FYI, I have a large app I can test it on. (Feel free to ping or @ me directly if needed.)

Also, re node 12, imo I think it'd be fine to just drop it now, so long as there's a clear error message explaining that you need node 14+ if you try to run it on an earlier version (or does npm even have a way to mark the package as requiring a certain node version?) Anyone committed to using node 12 can just lock to the current elm-test version.

@lydell
Copy link
Collaborator

lydell commented Mar 26, 2022

re node 12, imo I think it'd be fine to just drop it now, so long as there's a clear error message explaining that you need node 14+ if you try to run it on an earlier version (or does npm even have a way to mark the package as requiring a certain node version?)

Yes, package.json has the engines field: #465 (comment)

@mpizenberg
Copy link
Contributor Author

I don't think there is much left to discuss since the discussion happened on the other PR by @harrysarson . Now I guess we only need a bit of testing from people with big code base.
After that, for Jeroen (and others) it might be nice to extract the JS overlay on top of the wasm package that we ended up with here so that there is a JS package with just a couple function ready to be used as-is. Otherwise everyone will end with a slightly different JS overlay on top of the wasm package. We might want it for flexibility but it's more update and fix maintainability burden if issues are raised.

@mpizenberg
Copy link
Contributor Author

One argument to keep compatibility with node 12 is that debian bullseye, which is the current stable debian uses Node 12.22.

I have had issues raised about node 10 compatibility for elm-test-rs for the previous stable debian, so it might become an issue for elm-test too if we remove node 12 too early. Locking an elm-test version is an option, but it's possible they need to update for a security patch. I don't know, might be simpler to release a security patch for an older release.

I think all should work with a minimum version of 12.20 (for the sync http requests via worker) so if we can keep it the minimum version let's keep it that way.

@harrysarson
Copy link
Collaborator

agree about node 12. We should update the minimum version in package.json (currently it is 12.13 I think) and possibly also ensure that our CI checks node 12.20

@mpizenberg
Copy link
Contributor Author

I added 12.20.0 to the list of node versions tested in the test.yml action. Not sure if that works since I can't trigger CI myself. Maybe there is also a way to extract that from the node engine property but I haven't looked at how if possible.

@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
node-version: [12.x, 14.x, 16.x, 17.x]
node-version: [12.20.0, 12.x, 14.x, 16.x, 17.x]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it worth testing both 12.20.0 and latest 12.x, or would it be enough with just 12.20.0?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Felt like both the explicit minimum version, and the likely version are worth checking but 🤷

@mpizenberg
Copy link
Contributor Author

Maybe there is also a way to extract that from the node engine property but I haven't looked at how if possible.

Most likely it's not a problem to have that duplicate because if we change the minimum version in the code it will fail on CI and we will change it there too.

@avh4
Copy link
Collaborator

avh4 commented Mar 30, 2022

Trying this out on my work project(s) with npm install -g mpizenberg/node-test-runner#elm-solve-deps-wasm (in a docker image):

Everything seems to work when using npm 6.14.16!

But when using npm 8.5.5 (node 14.19.1), npm install -g mpizenberg/node-test-runner#elm-solve-deps-wasm fails with:

npm ERR! npm ERR! code 127
npm ERR! npm ERR! path /root/.npm/_cacache/tmp/git-cloneXJGWfj
npm ERR! npm ERR! command failed
npm ERR! npm ERR! command sh -c elm-tooling install
npm ERR! npm ERR! sh: 1: elm-tooling: not found
npm ERR!
npm ERR! npm ERR! A complete log of this run can be found in: ...

Seemingly when trying to run the prepare hook?

@mpizenberg
Copy link
Contributor Author

@avh4 It seems that npm v6 does not run prepare for global installs, while npm v8 does.

Then I suppose the error stems from the fact that elm-tooling is in the dev dependencies, so I suppose it is not installed and not accessible for a global install when prepare is run after install. I'm not sure cause I don't do much JS and node.

@avh4
Copy link
Collaborator

avh4 commented Mar 31, 2022

ah yeah, that analysis sounds correct then. I'm not seeing any npm hooks that would be appropriate for this (post-install script that is only for when dev dependencies are installed). Maybe the solution is to change the prepare script to be if which elm-tooling; then elm-tooling install; fi ?

@mpizenberg
Copy link
Contributor Author

Maybe the solution is to change the prepare script to be if which elm-tooling; then elm-tooling install; fi ?

That would not be windows-compatible would it?

@lydell
Copy link
Collaborator

lydell commented Mar 31, 2022

@avh4 You can try adding --ignore-scripts to your npm install command.

@avh4
Copy link
Collaborator

avh4 commented Mar 31, 2022

Oh, npm scripts still don't get sh on Windows?

I'll see if --ignore-scripts works; that would be fine for me, since it's gonna be in a Dockerfile. (Also, if it weren't for building a docker image, I wouldn't be using global install anyway.)

@lydell
Copy link
Collaborator

lydell commented Mar 31, 2022

But it’s still a good point of prepare. If npm 8 runs prepare on global installs, that’s bad. People are going to do that. So I should look into running elm-tooling for development in this repo some other way (and update the elm-tooling docs). However, it’s unrelated to this PR.

@mpizenberg
Copy link
Contributor Author

So it seems we have the green light to merge, and the elm-tooling prepare stuff can be fixed later in another PR since it's already impacting the current version of node-test-runner.

Anything else you want to check @harrysarson ?

@harrysarson
Copy link
Collaborator

There are a couple of changes I might suggest (in a future PR) so let's give this 1 month or so on master before cutting the new release

@harrysarson
Copy link
Collaborator

@mpizenberg do you want to draft a commit message for the squashed commit?

@mpizenberg
Copy link
Contributor Author

Use a custom dependency solver library instead of the elm-json binary.

This PR introduces a custom dependency solver, extracted from the solver in elm-test-rs, improving the compatibility with other elm-based platforms such as Lamdera.

The core algorithmic part of the solver, called "pubgrub", is rooted in the same logic than used in elm-json but implemented differently in the rust package pubgrub-rs.
That core algorithmic logic has been ported into a WebAssembly package called elm-solve-deps-wasm dedicated to the constraints of the elm package ecosystem and providing the following solve_deps wasm function where the two functions passed as arguments js_fetch_elm_json and js_list_available_versions are two functions injected via the JS call.

pub fn solve_deps(
    project_elm_json_str: &str,
    use_test: bool,
    // additional_constraints_str: &HashMap<String, Constraint>,
    additional_constraints_str: JsValue,
    // js_fetch_elm_json(pkg: &str, version: &str) -> String;
    js_fetch_elm_json: js_sys::Function,
    // js_list_available_versions(pkg: &str) -> Vec<String>;
    js_list_available_versions: js_sys::Function,
) -> Result<JsValue, JsValue> {

In this PR, the JS overlay implementing those two js_fetch_elm_json and js_list_available_versions is taken care of in the form of a DependencyProvider class providing two functions solveOffline and solveOnline. In the case of the offline solver, listing of existing versions and discovery of dependencies is exclusively performed from the packages installed inside the elm home. In the case of the online solver, some information may be retrieved from the package server.

// same interface for solveOnline
solveOffline( 
  elmJson /*: string */,
  useTest /*: boolean */,
  extra /*: { [string]: string } */
) /*: string */ { ... }

Remark: since requests for the online solver need to perform http requests in a synchronous manner, an approach based on a web worker thread is used, requiring a minimum node version of 12.20.0.

@mpizenberg
Copy link
Contributor Author

Above is an attempt at a summary of this PR for a commit message. Feel free to change/remove/add anything.

@harrysarson harrysarson merged commit fd52f04 into rtfeldman:master Apr 6, 2022
@harrysarson
Copy link
Collaborator

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: discussion Next step is not crystal clear. Let's discuss. status: waiting for review Pull request needs a review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants