Skip to content

Commit

Permalink
initial WASM code splitting
Browse files Browse the repository at this point in the history
  • Loading branch information
mmomtchev committed Oct 22, 2024
1 parent d9b7d20 commit 21b9f65
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 38 deletions.
83 changes: 46 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,6 @@ Alpha quality. Most basic functions work as intended. Mostly tested for leaks on

Keep in mind that I am only a very occasional user of a very small fraction of `PROJ` and my main interest is JavaScript bindings - Node.js and browser - for C/C++ projects. If you find methods that are not usable in the current version and submit unit tests for it, I will make them work.

# Try it yourself

```shell
# Checkout from git
git clone https://github.com/mmomtchev/proj.js.git

# Install all the npm dependencies
cd proj.js
npm install
npx xpm install

# If you do not have SWIG JSE installed, download the SWIG generated files
# from a recent GHA run: https://github.com/mmomtchev/proj.js/actions
# (download swig-generated and unzip it in proj.js/swig)
mkdir -p swig && cd swig && unzip ~/Downloads/swig-generated.zip

# If you have SWIG JSE installed, generate the wrappers yourself
npm run swig

# Build the native version (requires a working C++ compiler)
npm run build:native

# Build the WASM version (requires emsdk in path)
npm run build:wasm

# Alternatively, get the compiled binaries from a recent GHA run
mkdir -p lib/binding && cd lib/binding && unzip -x ~/Downloads/native-ubuntu-latest-tiff.zip && unzip -x ~/Downloads/wasm-external-no_tiff.zip

# Run the tests (Node.js and browser)
npm test

# Run the web demo (should work on all OS if you have the WASM version)
cd test/browser && npx webpack serve --mode=production
# then open http://localhost:8030/
```

# Usage

This package is a `magickwand.js`-style `npm` package with an automatic import that resolves to either the native module or the WASM module depending on the environment.
Expand Down Expand Up @@ -87,13 +51,46 @@ if (!PROJ.proj_js_inline_projdb) {
}
```

# Developer build

```shell
npm install
# Checkout from git
git clone https://github.com/mmomtchev/proj.js.git

# Install all the npm dependencies (this will also pull the latest prebuilt binaries)
cd proj.js
npm install

# If you do not have SWIG JSE installed, download the SWIG generated files
# from a recent GHA run: https://github.com/mmomtchev/proj.js/actions
# (download swig-generated and unzip it in proj.js/swig)
mkdir -p swig && cd swig && unzip ~/Downloads/swig-generated.zip

# If you have SWIG JSE installed, generate the wrappers yourself
npm run swig

# Build the native version (requires a working C++ compiler)
npx xpm run prepare --config native && npx xpm run build --config native

# Build the WASM version (requires emsdk in path)
npx xpm run prepare --config wasm && npx xpm run build --config wasm

# Run the tests (Node.js and browser)
npm test

# Run the web demo
cd test/browser && npx webpack serve --mode=production
# then open http://localhost:8030/
```

# WASM size considerations

When using WASM, `proj.db` can either be inlined in the WASM bundle or it can be loaded from an `Uint8Array` before use.

Currently, the bundle size remains an issue.

| Component | raw | brotli | brotli
| Component | raw | brotli |
| --- | --- | --- |
| `proj.wasm` w/ TIFF w/o `proj.db` | 8593K | 1735K |
| `proj.wasm` w/o TIFF w/o `proj.db` | 7082K | 1302K |
Expand All @@ -107,6 +104,18 @@ Linking with my own `sqlite-wasm-http` project to access a remote `proj.db`, usi

Currently the biggest contributor to raw code size is SWIG JSE which produces large amounts of identical code for each function. This may me improved in a future version, but bear in mind that SWIG-generated code has the best compression ratio. It is also worth investigating what can be gained from modularization of the SWIG wrappers and if it is really necessary to wrap separately all derived classes.

## WASM Splitting

Starting from version 0.9.1, `proj.js` supports WASM code splitting. This allows to split the module in two parts, a main part that is loaded when the module is initially imported and a secondary part that is lazy-loaded when any function not present in the main part is called. The splitting is performed along the lines of a JavaScript program - any function called by that program will be part of the main part, everything else will remain the secondary module. A sample JavaScript program that performs only the quickstart is included, using this program results a very decent 40%:60% split:

| Component | raw | brotli |
| --- | --- | --- |
| `proj.wasm` w/ TIFF w/o `proj.db` | 4518K | 766K |
| `proj.deferred.wasm` w/ TIFF w/o `proj.db` | 5152K | 1213K |
| `proj.wasm` w/o TIFF w/o `proj.db` | 3859K | 716K |
| `proj.deferred.wasm` w/o TIFF w/o `proj.db` | 4278K | 829K |
| `proj.db` | 9240K | 1320K |

# Performance

Initial crude benchmarks, tested on i7 9700K @ 3.6 GHz with the C++ [quickstart](https://proj.org/en/latest/development/quickstart_cpp.html):
Expand Down
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ wasm = meson.get_compiler('cpp').get_id() == 'emscripten'
if wasm
add_project_arguments([ '-Wno-documentation' ], language: [ 'cpp', 'c' ])
add_project_arguments([ '-Wno-potentially-evaluated-expression' ], language: [ 'cpp' ])
add_project_link_arguments([ '-sBINARYEN_EXTRA_PASSES="--enable-bulk-memory,--enable-threads,--converge,-Oz"' ], language: [ 'cpp', 'c' ])
add_project_link_arguments([ '-sBINARYEN_EXTRA_PASSES="--enable-bulk-memory,--enable-threads,--converge,-Oz"', '-sSPLIT_MODULE' ], language: [ 'cpp', 'c' ])
endif

# Build PROJ and its dependencies
Expand Down
29 changes: 29 additions & 0 deletions src/gen_splitting_profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import qPROJ from 'proj.js/wasm';
import assert from 'node:assert';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';

const PROJ = await qPROJ;

const proj_db_path = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'lib', 'binding', 'proj', 'proj.db');
const proj_db = readFileSync(proj_db_path);
if (!PROJ.proj_js_inline_projdb)
PROJ.loadDatabase(proj_db);

const dbContext = PROJ.DatabaseContext.create();
const authFactory = PROJ.AuthorityFactory.create(dbContext, 'string');
const coord_op_ctxt = PROJ.CoordinateOperationContext.create(authFactory, null, 0);
const authFactoryEPSG = PROJ.AuthorityFactory.create(dbContext, 'EPSG');
const sourceCRS = authFactoryEPSG.createCoordinateReferenceSystem('4326');
const targetCRS = PROJ.createFromUserInput('+proj=utm +zone=31 +datum=WGS84 +type=crs', dbContext);
const list = PROJ.CoordinateOperationFactory.create().createOperations(sourceCRS, targetCRS, coord_op_ctxt);

const transformer = list[0].coordinateTransformer();
const c0 = new PROJ.PJ_COORD;
c0.v = [49, 2, 0, 0];
const c1 = transformer.transform(c0);
assert(Math.abs(c1.v[0] - 426857.988) < 1e-3);
assert(Math.abs(c1.v[1] - 5427937.523) < 1e-3);

PROJ.swig_em_write_profile();
1 change: 1 addition & 0 deletions src/proj.i
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ using namespace NS_PROJ;
%include "coordinatesystem.i"
%include "crs.i"
%include "factory.i"
%include "splitting.i"

// SWIG can't deduce the type of PROJ_VERSION_NUMBER
#pragma SWIG nowarn=304
Expand Down
30 changes: 30 additions & 0 deletions src/splitting.i
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
void swig_em_write_profile();

%{
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
// Write the execution profile used for WASM code splitting
EM_JS(void, swig_em_write_profile, (), {
var __write_profile = wasmExports.__write_profile;
if (!__write_profile) {
console.error('__write_profile not exported');
return;
}

var len = __write_profile(0, 0);
var ptr = _malloc(len);
__write_profile(ptr, len);

var profile_data = HEAPU8.subarray(ptr, ptr + len);
console.log(`writing profile.data, ${len} bytes`);
const fs = require('fs');
fs.writeFileSync('profile.data', profile_data);

_free(ptr);
});
#else
void swig_em_write_profile() {
throw std::exception{"Not a WASM build"};
}
#endif
%}

0 comments on commit 21b9f65

Please sign in to comment.