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

Publish a dual CJS/ESM package with platform-specific loaders #167

Merged
merged 21 commits into from
Dec 2, 2024

Conversation

sandhose
Copy link
Member

@sandhose sandhose commented Nov 18, 2024

This package currently ships the WASM module as a JS string, base64-encoded.

This is very inefficient for three reasons:

  • the base64 encoding is about 30% larger than the WASM module
  • it decodes on the JS side, which means dealing with a large string in JS memory, and hanging the main thread for a long time
  • it means we can't use the streaming compilation APIs

For it to be handled correctly by bundlers, it is better to provide an ESM-compatible package. Because some environments (like the JS SDK test suite) still run on CJS, I've made it a proper dual CJS/ESM package.

The loading logic is also platform-specific: on node-like environments, it needs to read the file from the disk, whereas on browser-like environments it needs to fetch the WASM file. This is why this now provides platform-specific entrypoints.

To give an idea of what performance impact this change has, here is a before/after comparison on Element Call:

image

Notice how:

  • the WASM is a lot smaller to download than the JS blob
  • it compiles during the download
  • there is a big long task on the 'before' which is just the base64-decoding. 660ms on that profiling, with a 4x CPU slowdown
  • it saves around 10MB in the JS heap memory

Same before/after on Element Web:

image

@sandhose sandhose requested a review from a team as a code owner November 18, 2024 16:11
@sandhose sandhose requested a review from andybalaam November 18, 2024 16:11
.eslintrc.cjs Outdated Show resolved Hide resolved
index.js Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
tests/device.test.js Outdated Show resolved Hide resolved
tests/tracing.test.js Outdated Show resolved Hide resolved
@sandhose sandhose marked this pull request as draft November 18, 2024 16:17
@richvdh
Copy link
Member

richvdh commented Nov 18, 2024

As discussed: unfortunately, this is going to break the Jest tests in matrix-js-sdk. We looked into trying to get Jest running on ES modules, but ultimately gave up.

tests/helper.js Outdated Show resolved Hide resolved
tests/tracing.test.js Outdated Show resolved Hide resolved
tests/device.test.js Outdated Show resolved Hide resolved
@sandhose sandhose marked this pull request as ready for review November 19, 2024 15:32
@sandhose
Copy link
Member Author

sandhose commented Nov 19, 2024

I switched gears a little bit, and instead opted for making the package have 4 entrypoints:

  • Node.js with CJS (node.cjs)
  • Node.js with ESM (node.js)
  • browser with CJS (index.cjs)
  • browser with ESM (index.js)

One annoying thing, is that the matrix-js-sdk expects the WASM to be already loaded and available, and doesn't call initAsync, so I made the Node.js versions load synchronously.

To achieve that, I used the bundler target of wasm-pack, and removed their loading logic for my own. I then used babel to convert the _bg.js ESM to CJS syntax

@sandhose sandhose requested a review from Hywan November 19, 2024 15:46
hughns added a commit to element-hq/element-call that referenced this pull request Nov 22, 2024
hughns added a commit to element-hq/element-call that referenced this pull request Nov 23, 2024
…tween deploys (#2823)

* Add explicit code split on matrix-sdk-crypto-wasm to allow caching between deploys

* Comment on removing once matrix-org/matrix-rust-sdk-crypto-wasm#167 lands
@richvdh richvdh self-requested a review November 25, 2024 15:02
Copy link
Member

@richvdh richvdh left a comment

Choose a reason for hiding this comment

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

A few questions and comments.

This is a bit of a scary-looking PR. Do you think it would be possible to break it up a bit? (For instance, could we switch to vitest in a separate PR?)

scripts/build.sh Show resolved Hide resolved
scripts/build.sh Outdated Show resolved Hide resolved
scripts/build.sh Outdated Show resolved Hide resolved
scripts/build.sh Outdated Show resolved Hide resolved
node.cjs Outdated Show resolved Hide resolved
index.cjs Outdated Show resolved Hide resolved
index.cjs Outdated Show resolved Hide resolved
index.js Outdated
export * from "./pkg/matrix_sdk_crypto_wasm_bg.js";
import * as wasm from "./pkg/matrix_sdk_crypto_wasm_bg.js";

const moduleUrl = new URL("./pkg/matrix_sdk_crypto_wasm_bg.wasm", import.meta.url);
Copy link
Member

Choose a reason for hiding this comment

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

for reasons I don't entirely understand, webpack is putting the wasm in the top-level build directory, rather than in a bundle directory. That's probably a problem in element-web's webpack config, but it would be good to fix before we ship this.

Copy link
Member Author

Choose a reason for hiding this comment

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

I also noticed that, no idea why. Element Call (so, Vite) properly handled this as regular asset.

src/lib.rs Outdated Show resolved Hide resolved
tests/device.test.js Outdated Show resolved Hide resolved
@sandhose sandhose requested a review from richvdh November 28, 2024 11:14
@sandhose
Copy link
Member Author

@richvdh I've reverted the move to vitest, now that it works correctly again in CJS environments 💃

For consumers, the bindings don't synchronously initialise themselves automatically anymore. If they need that behaviour, they need to import @matrix-org/matrix-sdk-crypto-wasm/init-sync. I haven't updated the README/changelog yet, I first want feedback on this solution

@sandhose sandhose changed the title Properly load the WASM blob asynchronously & switch to Vitest Publish a dual CJS/ESM package with platform-specific loaders Nov 28, 2024
index.mjs Outdated Show resolved Hide resolved
index.js Outdated Show resolved Hide resolved
index.mjs Outdated Show resolved Hide resolved
@sandhose
Copy link
Member Author

@richvdh I've added typechecking of JS files to avoid some of the dumb errors you've spotted previously

Copy link
Member

@richvdh richvdh left a comment

Choose a reason for hiding this comment

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

This looks like a sensible direction to me now. Some general points:

  • I've pulled out the test file changes into a separate PR (Update tests to use the public entry point to the module #171). Could you rebase on top of that?
  • There's a lot of duplicate code between the entry points. I'd like to see if we can factor a lot of that out to one or two shared modules (possibly: one module which is shared across all four entry points and one which is shared between the two node entry points.) AIUI a commonjs module can be easily imported into an ESM module?
  • I'd like to see comments on the various function definitions describing what they do, which global variables they touch, what they return, etc.

tsconfig.json Outdated Show resolved Hide resolved
@sandhose sandhose force-pushed the quenting/proper-async-loading branch from e5c1094 to accdcf7 Compare November 29, 2024 11:44
@sandhose sandhose requested a review from richvdh November 29, 2024 12:11
Copy link
Member

@richvdh richvdh left a comment

Choose a reason for hiding this comment

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

Sorry this is taking so many iterations. I think it's nearly there though.

README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
index.js Outdated Show resolved Hide resolved
index.js Outdated Show resolved Hide resolved
index.js Outdated Show resolved Hide resolved
index.mjs Outdated Show resolved Hide resolved
node.js Outdated Show resolved Hide resolved
node.js Outdated Show resolved Hide resolved
@sandhose sandhose requested a review from richvdh December 2, 2024 16:32
node.js Outdated Show resolved Hide resolved
* @param {WebAssembly.Module} mod
* @returns {typeof import("./pkg/matrix_sdk_crypto_wasm_bg.wasm.d")}
*/
function initInstance(mod) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
function initInstance(mod) {
function initInstance(mod) {
if (initialised) {
// This should be unreachable
throw new Error("initInstance called twice");
}

Copy link
Member Author

Choose a reason for hiding this comment

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

This actually throws if you call initAsync, it loads in the background, and then init it synchronously before it finishes; but I think it is fine to throw because the consumer shouldn't do that

Copy link
Member

Choose a reason for hiding this comment

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

This actually throws if you call initAsync, it loads in the background, and then init it synchronously before it finishes;

Will it? In that case, wouldn't modPromise be set, so that the synchronous init will fail early with an exception?

node.js Show resolved Hide resolved
node.js Outdated Show resolved Hide resolved
@sandhose sandhose requested a review from richvdh December 2, 2024 18:33
Copy link
Member

@richvdh richvdh left a comment

Choose a reason for hiding this comment

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

lgtm! Thanks for doing this!

CHANGELOG.md Show resolved Hide resolved
@richvdh richvdh enabled auto-merge (squash) December 2, 2024 20:30
@richvdh richvdh merged commit db1af39 into main Dec 2, 2024
3 checks passed
@richvdh richvdh deleted the quenting/proper-async-loading branch December 2, 2024 20:31
richvdh added a commit that referenced this pull request Dec 10, 2024
This fixes a problem (introduced in #167) on Chrome, which would complain about the module being too big. It also significantly simplifies the code on the browser entry points, because instantiateStreaming is widely supported.
richvdh added a commit to element-hq/element-web that referenced this pull request Dec 11, 2024
As of matrix-org/matrix-rust-sdk-crypto-wasm#167, the
wasm build of matrix-sdk-crypto is actually shipped as a `.wasm` file, rather
than base64-ed into Javascript. Our current webpack config then dumps it into
the top level of the build directory, which will be a problem on redeployment
(specifically, if you try to fetch the wasm artifact for vN after vN+1 has been
deployed, you'll get a 404 and sadness).

So, instead we use Webpack's experimental support for WASM-as-ES-module, which
makes Webpack put the wasm file in the bundle, along with everything else.

Fixes: #28632
github-merge-queue bot pushed a commit to element-hq/element-web that referenced this pull request Dec 11, 2024
As of matrix-org/matrix-rust-sdk-crypto-wasm#167, the
wasm build of matrix-sdk-crypto is actually shipped as a `.wasm` file, rather
than base64-ed into Javascript. Our current webpack config then dumps it into
the top level of the build directory, which will be a problem on redeployment
(specifically, if you try to fetch the wasm artifact for vN after vN+1 has been
deployed, you'll get a 404 and sadness).

So, instead we use Webpack's experimental support for WASM-as-ES-module, which
makes Webpack put the wasm file in the bundle, along with everything else.

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

Successfully merging this pull request may close these issues.

3 participants