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

Allow loading the smoldot bytecode from the worker #532

Merged
merged 4 commits into from
May 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion wasm-node/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@

## Unreleased

It is now possible to run the CPU-heavy tasks of smoldot within a worker (WebWorker, worker threads, etc.). To do so, create two ports using `new MessageChannel()`, pass one of the two ports in the `ClientOptions.portToWorker` field and send the other port to a web worker, then call `run(port)` from within that worker. The `run` function can be found by importing `import { run } from 'smoldot/worker'`. If a `portToWorker` is provided, then the `cpuRateLimit` setting applies to the worker.

It is also now possible to load the smoldot bytecode separately or within a worker. To do so, import the `compileBytecode` function using `import { compileBytecode } from 'smoldot/bytecode';`, call it, optionally send it from a worker to the main thread if necessary, then pass the object to the options of the new `startWithBytecode` function. The new `startWithBytecode` function can be imported with `import { startWithBytecode } from 'smoldot/no-auto-bytecode';`. It is equivalent to `start`, except that its configuration must contains a `bytecode` field.

See the README of the JavaScript package for more information.

### Added

- It is now possible to run the CPU-heavy tasks of smoldot within a worker (WebWorker, worker threads, etc.). To do so, create two ports using `new MessageChannel()`, pass one of the two ports in the `ClientOptions.portToWorker` field and send the other port to a web worker, then call `run(port)` from within that worker. The `run` function can be found by importing `import { run } from 'smoldot/worker'`. If a `portToWorker` is provided, then the `cpuRateLimit` setting applies to the worker. ([#529](https://github.com/smol-dot/smoldot/pull/529))
- Add `ClientOptions.portToWorker` field. ([#529](https://github.com/smol-dot/smoldot/pull/529))
- Add a new `worker` entry point to the library (for Deno: `worker-deno.ts`) containing a `run` function ([#529](https://github.com/smol-dot/smoldot/pull/529))
- Add a new `SmoldotBytecode` public interface. ([#532](https://github.com/smol-dot/smoldot/pull/532))
- Add a new `ClientOptionsWithBytecode` interface that extends `ClientOptions` with an extra `bytecode` field. ([#532](https://github.com/smol-dot/smoldot/pull/532))
- Add a new `bytecode` entry point to the library (for Deno: `bytecode-deno.ts`) containin a `comileBytecode` function. ([#532](https://github.com/smol-dot/smoldot/pull/532))
- Add a new `no-auto-bytecode` entry point to the library (for Deno: `no-auto-bytecode-deno.ts`) containin a `startWithBytecode` function. This function is equivalent to `start`, but accepts a `ClientOptionsWithBytecode` rather than a `ClientOptions`. ([#532](https://github.com/smol-dot/smoldot/pull/532))

### Changed

Expand Down
58 changes: 58 additions & 0 deletions wasm-node/javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,61 @@ chains must be passed as parameter to `addChain` as well. In situations where th
specifications passed to `addChain` are not trusted, it is important for security reasons to not
establish a parachain-relay-chain link between two chains that aren't part of the same "trust
sandbox".

## Usage with a worker

By default, calling `start()` will run smoldot entirely in the current thread. This can cause
performance issues if other CPU-heavy operations are done in that thread.

In order to help with this, it possible to use smoldot in conjunction with a worker.
To do so, you must first create a worker. Since creating a worker has some subtle differences
depending on the platform, this is outside of the responsibility of smoldot.

Once the worker is created, create two `MessagePort`s using `new MessageChannel`, and send one
of them to the worker. Then, pass one port to the `ClientOptions.portToWorker` field and the
other port to the `run()` function of smoldot, which can be imported with
`import { run } from 'smoldot/worker';` (on Deno, it is found in `worker-deno.ts`).

Another optimization that is orthogonal to but is related to running smoldot in a worker consists
in also loading the smoldot bytecode in that worker. The smoldot bytecode weights several
megabytes, and loading it in a worker rather than the main thread makes it possible to load the
UI while smoldot is still initializing. This is especially important when smoldot is included in
an application served over the web.

In order to load the smoldot bytecode in a worker, import `compileBytecode` with
`import { compileBytecode } from 'smoldot/bytecode';` (on Deno: `bytecode-deno.ts`), then call the
function and send the result to the main thread. From the main thread, rather than using the
`start` function imported from `smoldot`, use the `startWithBytecode` function that can be imported
using `import { startWithBytecode } from 'smoldot/no-auto-bytecode';` (on Deno:
`no-auto-bytecode-deno.ts`). The options provided to `startWithBytecode` are the same as the ones
passed to `start`, except for an additional `bytecode` field that must be set to the bytecode
created in the worker.

Here is an example of all this, assuming a browser environment:

```ts
import * as smoldot from 'smoldot/no-auto-bytecode';

const worker = new Worker(new URL('./worker.js', import.meta.url));

const bytecode = new Promise((resolve) => {
worker.onmessage = (event) => resolve(event.data);
});

const { port1, port2 } = new MessageChannel();
worker.postMessage(port1, [port1]);

const client = smoldot.startWithBytecode({
bytecode,
portToWorker: port2,
});


// `worker.ts`

import * as smoldot from '@substrate/smoldot-light/worker';
import { compileBytecode } from '@substrate/smoldot-light/bytecode';

compileBytecode().then((bytecode) => postMessage(bytecode))
onmessage = (msg) => smoldot.run(msg.data);
```
20 changes: 20 additions & 0 deletions wasm-node/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@
"require": "./dist/cjs/index-browser.js"
}
},
"./no-auto-bytecode": {
"node": {
"import": "./dist/mjs/no-auto-bytecode-nodejs.js",
"require": "./dist/cjs/no-auto-bytecode-nodejs.js"
},
"default": {
"import": "./dist/mjs/no-auto-bytecode-browser.js",
"require": "./dist/cjs/no-auto-bytecode-browser.js"
}
},
"./worker": {
"node": {
"import": "./dist/mjs/worker-nodejs.js",
Expand All @@ -38,6 +48,16 @@
"import": "./dist/mjs/worker-browser.js",
"require": "./dist/cjs/worker-browser.js"
}
},
"./bytecode": {
"node": {
"import": "./dist/mjs/bytecode-nodejs.js",
"require": "./dist/cjs/bytecode-nodejs.js"
},
"default": {
"import": "./dist/mjs/bytecode-browser.js",
"require": "./dist/cjs/bytecode-browser.js"
}
}
},
"scripts": {
Expand Down
10 changes: 5 additions & 5 deletions wasm-node/javascript/prepare.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ child_process.execSync(
{ 'stdio': 'inherit', 'env': { 'RUSTFLAGS': '-C target-feature=+bulk-memory,+sign-ext,+simd128', ...process.env } }
);

// The code below will write a variable number of files to the `src/internals/module` directory.
// The code below will write a variable number of files to the `src/internals/bytecode` directory.
// Start by clearing all existing files from this directory in case there are some left from past
// builds.
const filesToRemove = fs.readdirSync('./src/internals/module');
const filesToRemove = fs.readdirSync('./src/internals/bytecode');
for (const file of filesToRemove) {
if (!file.startsWith('.')) // Don't want to remove the `.gitignore` or `.npmignore` or similar
fs.unlinkSync(path.join("./src/internals/module", file));
fs.unlinkSync(path.join("./src/internals/bytecode", file));
}

// We then do an optimization pass on the Wasm file, using `wasm-opt`.
Expand Down Expand Up @@ -124,14 +124,14 @@ try {
const chunk = base64Data.slice(0, 1024 * 1024);
// We could simply export the chunk instead of a function that returns the chunk, but that
// would cause TypeScript to generate a definitions file containing a copy of the entire chunk.
fs.writeFileSync('./src/internals/module/wasm' + fileNum + '.ts', 'export default function(): string { return "' + chunk + '"; }');
fs.writeFileSync('./src/internals/bytecode/wasm' + fileNum + '.ts', 'export default function(): string { return "' + chunk + '"; }');
imports += 'import { default as wasm' + fileNum + ' } from \'./wasm' + fileNum + '.js\';\n';
chunksSum += ' + wasm' + fileNum + '()';
fileNum += 1;
base64Data = base64Data.slice(1024 * 1024);
}
fs.writeFileSync(
'./src/internals/module/wasm.ts',
'./src/internals/bytecode/wasm.ts',
imports +
'export default ' + chunksSum
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import { default as wasmBase64 } from './internals/module/wasm.js';
import { default as wasmBase64 } from './internals/bytecode/wasm.js';
import { classicDecode } from './internals/base64.js'
import { inflate } from 'pako';
import { SmoldotBytecode } from './public-types.js';

/**
* Compiles and returns the smoldot WebAssembly binary.
*/
export async function compileModule(): Promise<WebAssembly.Module> {
export async function compileBytecode(): Promise<SmoldotBytecode> {
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a
// different file.
// This is suboptimal compared to using `instantiateStreaming`, but it is the most
// cross-platform cross-bundler approach.
return WebAssembly.compile(inflate(classicDecode(wasmBase64)));
return WebAssembly.compile(inflate(classicDecode(wasmBase64)))
.then((m) => { return { wasm: m } });
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import { default as wasmBase64 } from './internals/module/wasm.js';
import { default as wasmBase64 } from './internals/bytecode/wasm.js';
import { SmoldotBytecode } from './public-types.js';

/**
* Compiles and returns the smoldot WebAssembly binary.
*/
export async function compileModule(): Promise<WebAssembly.Module> {
export async function compileBytecode(): Promise<SmoldotBytecode> {
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a
// different file.
// This is suboptimal compared to using `instantiateStreaming`, but it is the most
// cross-platform cross-bundler approach.
return zlibInflate(trustedBase64Decode(wasmBase64)).then(((bytecode) => WebAssembly.compile(bytecode)));
return zlibInflate(trustedBase64Decode(wasmBase64))
.then(((bytecode) => WebAssembly.compile(bytecode)))
.then((m) => { return { wasm: m } });
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import { default as wasmBase64 } from './internals/module/wasm.js';
import { default as wasmBase64 } from './internals/bytecode/wasm.js';
import { inflate } from 'pako';
import { SmoldotBytecode } from './public-types.js';

/**
* Compiles and returns the smoldot WebAssembly binary.
*/
export async function compileModule(): Promise<WebAssembly.Module> {
export async function compileBytecode(): Promise<SmoldotBytecode> {
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a
// different file.
// This is suboptimal compared to using `instantiateStreaming`, but it is the most
// cross-platform cross-bundler approach.
return WebAssembly.compile(inflate(Buffer.from(wasmBase64, 'base64')));
return WebAssembly.compile(inflate(Buffer.from(wasmBase64, 'base64')))
.then((m) => { return { wasm: m } });
}
Loading