Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/site/source/docs/optimizing/Module-Splitting.rst
4154 views
.. _Module-Splitting:

================
Module Splitting
================

*wasm-split and the SPLIT_MODULE Emscripten integration are both in active
development and may change and gain new features frequently. This page will be
kept up-to-date with the latest changes.*

Large codebases often contain a lot of code that is very rarely used in practice
or is never used early in the application's life cycle. Loading that unused code
can noticeably delay application startup, so it would be good to defer loading
that code until after the application has already started. One excellent
solution for this is to use dynamic linking, but that requires refactoring an
application into shared libraries and also comes with some performance overhead,
so it is not always feasible. Module splitting is another approach where a
module is split into separate pieces, the primary and secondary modules, after
it is built normally. The primary module is loaded first and contains the code
necessary to start the application, while the secondary module contains code
that will be needed later or not at all. The secondary module will automatically
be loaded on demand.

wasm-split is a Binaryen tool that performs module splitting. After running
wasm-split, the primary module has all the same imports and exports as the
original module and is meant to be a drop-in replacement for it. However, it
also imports a placeholder function for each secondary function that was split
out into the secondary module. Before the secondary module is loaded, calls of
secondary functions will call the appropriate placeholder function instead. The
placeholder functions are responsible for loading and instantiating the
secondary module, which automatically replaces all the placeholder functions
with the original secondary functions when it is instantiated. After the
secondary module is loaded, the placeholder function that loaded it is also
responsible for calling its corresponding newly-loaded secondary function and
returning the result to its caller. The loading of the secondary module is
therefore completely transparent to the primary module; it just looks like a
function call took a long time to return.

Currently the only workflow for splitting modules involves instrumenting the
original module to collect a profile of what functions are run, running that
instrumented module with a number of interesting workloads, and using the
resulting profiles to determine how to split the module. wasm-split will leave
any function that was run during any of the profiled workloads in the primary
module and will split all other functions out into the secondary module.

Emscripten has a prototype integration with wasm-split enabled by the
``-sSPLIT_MODULE`` option. This option will emit the original module with the
wasm-split instrumentation applied so it is ready to collect profiles. It will
also insert the placeholder functions responsible for loading a secondary module
into the emitted JS. The developer is then responsible for running appropriate
workloads, collecting the profiles, and using the wasm-split tool to perform the
splitting. After the module is split, everything will work correctly with no
further changes to the JS produced by the initial compilation.

Basic Example
-------------

Let’s run through a basic example of using SPLIT_MODULE with Node. Later in the
"Running on the Web" section we will discuss how to adapt the example to run on
the Web as well.

Here’s our application code::

  // application.c

  #include <stdio.h>
  #include <emscripten.h>

  void foo() {
    printf("foo\n");
  }

  void bar() {
    printf("bar\n");
  }

  void unsupported(int i) {
    printf("%d is not supported!\n", i);
  }

  EM_JS(int, get_number, (), {
    if (typeof prompt === 'undefined') {
      prompt = require('prompt-sync')();
    }
    return parseInt(prompt('Give me 0 or 1: '));
  });

  int main() {
    int i = get_number();
    if (i == 0) {
      foo();
    } else if (i == 1) {
      bar();
    } else {
      unsupported(i);
    }
  }

This application prompts the user for some input and executes different
functions depending on what the user provides. It uses the prompt-sync npm
module to make the prompting behavior portable between Node and the Web. We will
see that the input we provide during profiling will determine how our functions
are split between the primary and secondary modules.

We can compile our application with ``-sSPLIT_MODULE``::

  $ emcc application.c -o application.js -sSPLIT_MODULE

In addition to the typical application.wasm and application.js files, this also
produces an application.wasm.orig file. application.wasm.orig is the original,
unmodified module that a normal Emscripten build would produce, while
application.wasm has been instrumented by wasm-split to collect profiles.

The instrumented module has an additional exported function,
``__write_profile``, that takes as arguments a pointer and length for an
in-memory buffer to which it will write the profile. ``__write_profile`` returns
the length of the profile, and only writes the data if the supplied buffer is
large enough. ``__write_profile`` can be called externally from JS or
internally, from the application itself. For simplicity, we will just call it at
the end of our main function here, but note that this will mean that any
functions called after main, such as destructors for global objects, will not be
included in the profile.

Here’s the function to write the profile and our new main function::

  EM_JS(void, write_profile, (), {
    var __write_profile = wasmExports.__write_profile;
    if (!__write_profile) {
      return;
    }

    // Get the size of the profile and allocate a buffer for it.
    var len = __write_profile(0, 0);
    var ptr = _malloc(len);

    // Write the profile data to the buffer.
    __write_profile(ptr, len);

    // Write the profile file.
    var profile_data = HEAPU8.subarray(ptr, ptr + len);
    const fs = require("fs");
    fs.writeFileSync('profile.data', profile_data);

    // Free the buffer.
    _free(ptr);
  });

  int main() {
    int i = get_number();
    if (i == 0) {
      foo();
    } else if (i == 1) {
      bar();
    } else {
      unsupported(i);
    }
    write_profile();
  }

Note that we only try to write the profile if the ``__write_profile`` export
exists. This is important because only the instrumented, unsplit module exports
``__write_profile``. The split modules will not include the profiling
instrumentation or this export.

Our new write_profile function depends on malloc and free being available to JS,
so we need to explicitly export them on the command line::

  $ emcc application.c -o application.js -sSPLIT_MODULE -sEXPORTED_FUNCTIONS=_malloc,_free,_main

Now we can run our application, which produces a profile.data file. The next
step is to use wasm-split and the profile to split the original module,
application.wasm::

  $ wasm-split --enable-mutable-globals --export-prefix=% application.wasm.orig -o1 application.wasm -o2 application.deferred.wasm --profile=profile.data

Let’s break down what all those options are for.

``--enable-mutable-globals``
  This option enables the mutable-global target feature, which allows mutable
  Wasm globals (as opposed to C/C++ globals) to be imported and exported.
  wasm-split has to share mutable globals between the primary and secondary
  modules, so it requires this feature to be enabled.

``--export-prefix=%``
  This is a prefix added to all the new exports wasm-split creates to share
  module elements from the primary module to the secondary module. The prefix
  can be used to differentiate "true" exports from those that only exist to be
  consumed by the secondary module. Emscripten’s wasm-split integration expects
  “%” in particular to be used as the prefix.

``-o1 application.wasm``
  Write the primary module to application.wasm. Note that this will overwrite
  the instrumented module previously produced by Emscripten, so the application
  will now use the split modules rather than the instrumented module.

``-o2 application.deferred.wasm``
  Write the secondary module to application.deferred.wasm. Emscripten expects
  the name of the secondary module to be the same as the name of the primary
  module with “.wasm” replaced with “.deferred.wasm”.

``--profile=profile.data``
  Directs wasm-split to use the profile in profile.data to guide the splitting.

Running application.js in node again, we can see that the application works just
as it did before, but if we execute any code path besides the one used in the
profiled workload, the application will print a console message about a
placeholder function being called and the deferred module being loaded.

Profiling Multiple Workloads
----------------------------

wasm-split supports merging profiles from multiple profiling workloads into a
single profile to guide splitting. Any function that was run in any of the
workloads will be kept in the primary module and all other functions will be
split out into the secondary module.

This command will merge any number of profiles (here just profile1.data and
profile2.data) into a single profile::

  $ wasm-split --merge-profiles profile1.data profile2.data -o profile.data

Multithreaded Programs
----------------------

By default, the data gathered by the wasm-split instrumentation is stored in
Wasm globals, so it is thread local. But in a multithreaded program, it is
important to collect profile information from all threads. To do so, you can
tell wasm-split to collect shared profile information in shared memory using the
``--in-memory`` wasm-split flag. This will use memory starting at address zero
to store the profile information, so you must also pass ``-sGLOBAL_BASE=N`` to
Emscripten, where ``N`` is at least the number of functions in the module, to
prevent the program from clobbering that memory region.

After splitting, multithreaded applications will currently fetch and compile the
secondary module separately on each thread. The compiled secondary module is not
postmessaged to each thread the way the Emscripten postmessages the primary
module to the threads. This is not as bad as it sounds because downloads of the
secondary module from workers will be serviced from the cache if the appropriate
Cache-Control headers are set, but improving this is an area for future work.

Running on the Web
------------------

One complication to keep in mind when using SPLIT_MODULE for Web applications is
that the secondary module cannot be loaded both lazily and asynchronously, which
means it cannot be loaded lazily on the main browser thread. The reason is that
the placeholder functions need to be completely transparent to the functions in
the primary module, so they can’t return until they have synchronously loaded
and called the correct secondary function.

One workaround for this limitation would be to eagerly load and instantiate the
secondary module and ensure that no secondary functions can possibly be called
before it has been instantiated on the main browser thread. This may be
difficult to ensure, though. Another fix would be to run the Asyncify
transformation on the primary module to allow placeholder functions to return to
the JS event loop while waiting for the secondary module to load asynchronously.
This is on the wasm-split roadmap, although we do not yet know what the size and
performance overhead of this solution will be.

This limitation on lazy loading means that the best way to run applications with
SPLIT_MODULE is in a worker thread, for example using ``-sPROXY_TO_PTHREAD``. In
PROXY_TO_PTHREAD mode, it is important to collect a profile for the browser main
thread in addition to the application main thread because the browser main
thread runs some functions not run in the application main thread, such as the
shim that wraps the proxied main function and the functions involved in handling
calls proxied back to the main browser thread. See the previous section for how
to collect profiles from multiple threads.

Another minor complication is that the profile data cannot be immediately
written to a file from inside the browser. The data must instead be transmitted
to developer machines some other way, such as posting it to the dev server or
copying a base64 encoding of it from the console.

Here’s code implementing the base64 solution::

  var profile_data = HEAPU8.subarray(ptr, ptr + len);
  var binary = '';
  for (var i = 0; i < profile_data.length; i++) {
      binary += String.fromCharCode(profile_data[i]);
  }
  console.log("===BEGIN===");
  console.log(window.btoa(binary));
  console.log("===END===");

Then the profile file can be created by by running::

  $ echo [pasted base64] | base64 --decode > profile.data

or::

  $ base64 --decode [base64 file] > profile.data

Usage with Dynamic Linking
--------------------------

Module splitting can be used in conjunction with dynamic linking, but making the
two features work correctly together requires some developer intervention.
wasm-split often needs to grow the table to make space for placeholder
functions, but that means that the instrumented and split modules would have
different table sizes. Normally this is not a problem, but
MAIN_MODULE/SIDE_MODULE dynamic linking support currently requires the table
size to be baked into the JS Emscripten emits, so the table size needs to be
stable.

To ensure that the table size is the same between the instrumented module and
the split modules, use the ``-sINITIAL_TABLE=N`` Emscripten setting, where ``N``
is the desired table size. Then, when using wasm-split to perform the splitting,
pass ``--initial-table=N`` to wasm-split to ensure that the split modules have
the correct table size as well.

If the specified table size is too small, you will get an error message when the
primary module is loaded after splitting. Adjust the table size you specify
until it is large enough. Besides taking up extra space at runtime, there is no
downside to specifying a table size that is larger than necessary.

Custom Loading of the Secondary Module
--------------------------------------

The default logic for lazily loading the secondary module can be overridden by
implementing the "loadSplitModule" custom hook function. The hook is called from
placeholder functions and is responsible for returning the [instance, module]
pair for the secondary module. The hook takes as arguments the name of the file
to load (e.g. “my_program.deferred.wasm”), the imports object to instantiate the
module with, and the property corresponding to the called placeholder function.
Here is an example implementation that does the same thing as the default
implementation with some extra logging::

  Module["loadSplitModule"] = function(deferred, imports, prop) {
      console.log('Custom handler for loading split module.');
      console.log('Called with placeholder ', prop);

      return instantiateSync(deferred, imports);
  }

If the module was eagerly loaded, then this hook could simply instantiate the
module rather than fetching and compiling it as well. However, if the eagerly
loaded module is instantiated eagerly as well, the placeholder functions will be
patched out and never called in the first place, so this custom hook will never
be called either.

When eagerly instantiating the secondary module, the imports object should be::

  {'primary': wasmExports}

Debugging
---------

wasm-split has several options to make debugging split modules easier.

``-v``
  When splitting, print the primary and secondary functions. When merging
  profiles, print profiles that do not contribute to the merged profile.

``-g``
  Preserve names in both the primary and secondary modules. Without this option,
  wasm-split will strip the names instead.

``--emit-module-names``
  Generate and emit module names to differentiate the primary and secondary
  module in stack traces, even if -g is not used.

``--symbolmap``
  Emit separate map files for the primary and secondary modules, mapping
  function indices to function names. When combined with --emit-module-names,
  these maps can be used to re-symbolify stack traces. To ensure that the
  function names are available for wasm-split to emit into the maps,
  pass --profiling-funcs to Emscripten.

``--placeholdermap``
  Emit a map file mapping placeholder function indices to their corresponding
  secondary functions. This can be useful for figuring out what function caused
  the secondary module to be loaded.


Upcoming Changes
----------------

*A list of changes and new features that have not yet been incorporated into
this documentation.*

Work is planned on an integration with the Asyncify instrumentation that will
allow the secondary module to be asynchronously loaded on the main browser
thread.