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

Add -s MINIMAL_RUNTIME=1 option. #7923

Merged
merged 4 commits into from Feb 5, 2019

Conversation

Projects
None yet
6 participants
@juj
Copy link
Collaborator

juj commented Jan 24, 2019

This PR is the main block of work for a new minimal sized runtime. It adds a build option -s MINIMAL_RUNTIME=1, which provides a new runtime (preamble+shell+postamble+other adjustments) that scales to smaller build sizes. To achieve that, in MINIMAL_RUNTIME a number of features from the default runtime are dropped to provide a simpler shell:

  • The concepts of runtime being initialized or shut down does not exist (except for debug assertions),
  • The runtime does not take responsibility of XHRing files, but the external shell HTML does that,
  • The startup concept of preinit/prerun/etc do not exist,
  • The use Module object is now almost gone. All existing mechanisms that use Module for a random variety of runtime options or parameters, such as Module.canvas, Module.preloadFile etc., are going out, because those mechanisms do not follow linking aware approach that would maintain code size when unused,
  • JS libraries that resulted in suboptimal code size are dropped and not linked in. Basically this stems from the $Browser object, and its older use of canvas/fullscreen/pointerlock functionality that is not easily linkable, so library_browser.js and other JS libs that interact with this are disabled,
  • The ENVIRONMENT_IS_* machinery at runtime does not exist. Idea is that this kind of support should be implemented via a strictly link time approach e.g. with developer say -s ENVIRONMENT=web or -s ENVIRONMENT=web,node and linker should then emit code that works in the targeted environments.
  • As a temporary restriction, a number of features are currently removed, to be reintroduced later in a form that does not require DCEing away in order to work. (these include Node.js/shell support, multithreading, memory growth, dynamic linking, MEMFS that come to mind) - there is too much work to do in one PR.

In MINIMAL_RUNTIME code size is king, and no features should be added that increase code size when it is not needed. Two "hero tests", one for a minimal console log, and another for minimal WebGL demo, are added to test_other, and their sizes are strictly tracked so that size regressions do not happen.

Other smaller changes that are done are annotated inline in the PR.

The idea is that MINIMAL_RUNTIME would become the default runtime in a distant future.

Size comparison between default runtime and minimal runtime:

minimal console log wasm:

  • default runtime: 6337+6992+133 = 13462 bytes,
  • MINIMAL_RUNTIME: 747+712+86 = 1545 bytes, -88.5% smaller.

minimal console log asm.js:

  • default runtime: 10830+12962+14 = 23806 bytes,
  • MINIMAL_RUNTIME: 789+557+952+6 = 2304 bytes, -90.3% smaller.

minimal WebGL wasm:

  • default runtime: 6352 + 13395 + 9205 = 28952 bytes
  • MINIMAL_RUNTIME: 747 + 6875 + 9175 = 16797 bytes, -42.0% smaller.

minimal WebGL asm.js:

  • default runtime: 10850 + 25223 + 404 = 36477 bytes
  • MINIMAL_RUNTIME: 789 + 6808 + 404 + 11773 = 19774 bytes, -45.8% smaller.

@juj juj added the code size label Jan 24, 2019

@@ -1043,6 +1043,7 @@ def check(input_file):
assert not (not shared.Settings.DYNAMIC_EXECUTION and options.use_closure_compiler), 'cannot have both NO_DYNAMIC_EXECUTION and closure compiler enabled at the same time'

if options.emrun:
assert not shared.Settings.MINIMAL_RUNTIME, '--emrun is not compatible with -s MINIMAL_RUNTIME=1'

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

--emrun support could be added later

@@ -1123,7 +1124,7 @@ def check(input_file):
shared.Settings.EXPORTED_FUNCTIONS += ['___cxa_demangle']
forced_stdlibs += ['libc++abi']

if not shared.Settings.ONLY_MY_CODE:
if not shared.Settings.ONLY_MY_CODE and not shared.Settings.MINIMAL_RUNTIME:
# Always need malloc and free to be kept alive and exported, for internal use and other modules
shared.Settings.EXPORTED_FUNCTIONS += ['_malloc', '_free']

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

Unconditionally adding malloc and free should not occur, but it should only happen based on linking decisions dictating that they are needed.

if shared.Settings.MINIMAL_RUNTIME:
# Minimal runtime uses a different default shell file
if options.shell_path == shared.path_from_root('src', 'shell.html'):
options.shell_path = shared.path_from_root('src', 'shell_minimal_runtime.html')

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

A new default shell file for MINIMAL_RUNTIME shows a simple example of how the user should XHR in the compiled code files. The model of how asm.js/wasm/mem file glues together with the main JS file stays very much the same.

options.shell_path = shared.path_from_root('src', 'shell_minimal_runtime.html')

# Remove the default exported functions 'memcpy', 'memset', 'malloc', 'free', etc. - those should only be linked in if used
shared.Settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE = []

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

By default don't export anything extra, but user should explicitly opt in to whatever is needed.


# In asm.js always use memory init file to get the best code size, other modes are not supported.
if not shared.Settings.WASM:
options.memory_init_file = True

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

The code size suboptimal memory initializer modes are not supported (if someone would like to integrate those back in, would be ok as long as it occurs in a way that does not require Closure to clean up bloat caused by them)

emcc.py Outdated
if shared.Settings.BINARYEN_ASYNC_COMPILATION == 1:
# async compilation requires a swappable module - we swap it in when it's ready
if shared.Settings.BINARYEN_ASYNC_COMPILATION == 1 and not shared.Settings.MINIMAL_RUNTIME:
# async compilation requires a swappable module - we swap it in when it's ready (MINIMAL_RUNTIME does not need to utilize the swappable module concept)

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

The way MINIMAL_RUNTIME JS file is written, the Wasm is always asynchronously compiled, and the SWAPPABLE_ASM_MODULE mechanism does need to be used

# MINIMAL_RUNTIME always use separate .asm.js file for best performance and memory usage
if shared.Settings.MINIMAL_RUNTIME and not shared.Settings.WASM:
options.separate_asm = True

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

Always use separate .asm.js files, so that it can be asynchronously loaded, debugging the main JS is feasible when the compiled asm.js code is out of sight, and Firefox can unload asm.js script content from memory to disk.

# Independent of whether user is doing -o a.html or -o a.js, generate the mem init file as a.mem (and not as a.html.mem or a.js.mem)
memfile = target.replace('.html', '.mem').replace('.js', '.mem')
else:
memfile = target + '.mem'

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

This is a simplification/bugfix to allow not having to deal with two suffixed files, depending on if one compiled to JS or HTML. Also a.mem is shorter than a.html/js.mem

This comment has been minimized.

@kripken

kripken Jan 24, 2019

Member

How about doing this in all modes? (in a separate PR) There is a slight annoyance for existing users that would need to look at a different file, but I think that's a one-time annoyance, while if changing modes to MINIMAL_RUNTIME changes a filename, that would be more disruptive in the long term.

# Process .asm.js file
if not shared.Settings.WASM:
shared.run_process([shared.PYTHON, shared.path_from_root('tools', 'hacky_postprocess_around_closure_limitations.py'), asm_target])

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

These are big hacks, but useful since Closure has limits on what it can do. They make the minimal console log application considerably smaller, so there is benefit.

if shared.Settings.MINIMAL_RUNTIME:
replacement = shared.Settings.EXPORT_NAME
else:
replacement = "typeof %(EXPORT_NAME)s !== 'undefined' ? %(EXPORT_NAME)s : {}" % {"EXPORT_NAME": shared.Settings.EXPORT_NAME}

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

In MINIMAL_RUNTIME, the Module object is always present to provide the .asm.js/.wasm content, so pulling it in is simpler.

This comment has been minimized.

@kripken

kripken Jan 24, 2019

Member

Please add that in a comment in the source.

funcs += ['stackAlloc', 'stackSave', 'stackRestore', 'establishStackSpace']
if shared.Settings.SAFE_HEAP:
funcs += ['setDynamicTop']
if not (shared.Settings.WASM and shared.Settings.SIDE_MODULE):
if not (shared.Settings.WASM and shared.Settings.SIDE_MODULE) and not shared.Settings.MINIMAL_RUNTIME:

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

These will need to use a different model of being added in.

@@ -1441,7 +1450,10 @@ def create_basic_funcs(function_table_sigs, invoke_function_names):


def create_basic_vars(exported_implemented_functions, forwarded_json, metadata):
basic_vars = ['DYNAMICTOP_PTR', 'tempDoublePtr']
basic_vars = ['tempDoublePtr']

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

This is something that does bloat up needlessly, should track and only link when actually used.

else:
receiving += 'Module["asm"] = asm;\n' + ';\n'.join(['var ' + s + ' = Module["' + s + '"] = function() {' + runtime_assertions + ' return Module["asm"]["' + s + '"].apply(null, arguments) }' for s in module_exports])
receiving += ';\n'
receiving += 'Module["asm"] = asm;\n' + '\n'.join(['var ' + s + ' = Module["' + s + '"] = function() {' + runtime_assertions + ' return Module["asm"]["' + s + '"].apply(null, arguments) };' for s in module_exports]) + '\n'

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

In MINIMAL_RUNTIME we do not unconditionally export everything to Module. (Instead, in the future we should have a more explicit model that generates an export object from only those symbols that need it. Currently exports are not supported)

This comment has been minimized.

@kripken

kripken Jan 24, 2019

Member

What does "exports are not supported" mean?

@juj juj force-pushed the juj:minimal_runtime branch from cb70726 to 227b611 Jan 24, 2019

}
// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)
// {{PRE_JSES}}

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

In MINIMAL_RUNTIME, one customizes stdout/stderr printing and automatic vs manual startup by overriding out, err, or ready in a --pre-js file. Closure can optimize the pattern

function out(text) {
  console.log(text);
}

...

function out(text) {
  document.getElementById('my_console_out').value += text;
}

to remove the first function. Also, Closure is able to optimize the pattern

function out(text) {
  console.log(text);
}

...

function out(text) {
  /* no-op*/
}

to take away all calls to out() in call sites, so this is a nice way to delete away default logging functionality.

One can override ready() to get a signal of when the runtime is ready, and then either choose to run the code immediately, or call run() later when one wants to.

This comment has been minimized.

@kripken

kripken Jan 24, 2019

Member

Worth mentioning the closure benefits in a comment in the code, I think.

if (mapping[name]) {
value[2][1] = mapping[name];
}
}

This comment has been minimized.

@juj

juj Jan 24, 2019

Author Collaborator

In Wasm exports come in form of

var _free, _llvm_minnum_f32, _main, _malloc, _memcpy, _sbrk, dynCall_idi;
WebAssembly.instantiate(Module["wasm"], imports).then((function(output) {
 var asm = output.instance.exports;
 _free = asm["N"];
 _llvm_minnum_f32 = asm["O"];
 _main = asm["P"];
 _malloc = asm["Q"];
 _memcpy = asm["R"];
 _sbrk = asm["S"];
 dynCall_idi = asm["T"];
 for (var i in __ATINIT__) __ATINIT__[i].func();
 ready();
}));

and in asm.js they look like

var asm =Module["asm"](asmGlobalArg, Module.asmLibraryArg, buffer);
var _free = asm["_free"];
var _main = asm["_main"];
var _malloc = asm["_malloc"];

hence the changes to JS optimizer to recognize the different syntaxes.

@juj

This comment has been minimized.

Copy link
Collaborator Author

juj commented Jan 24, 2019

For illustration, here is what minimal hello looks like in asm.js:

#include <emscripten/html5.h>
int main()
{
	emscripten_console_log("hello!");
}
emcc small_hello_world.c -o a.html  -s RUNTIME_FUNCS_TO_IMPORT=[] -s USES_DYNAMIC_ALLOC=0 -s MINIMAL_RUNTIME=2 -s AGGRESSIVE_VARIABLE_ELIMINATION=1 -s ENVIRONMENT=web -s TEXTDECODER=2 -s ABORTING_MALLOC=0 -s ALLOW_MEMORY_GROWTH=0 -s SUPPORT_ERRNO=0 -s DECLARE_ASM_MODULE_EXPORTS=1 --output_eol linux -s WASM=0 --separate-asm -s ELIMINATE_DUPLICATE_FUNCTIONS=1 --memory-init-file 1 -O3 --closure 1

a.html

<!doctype html><html lang="en-us"><head><meta charset="utf-8"></head>
<body>
<canvas id='canvas' style='display:block; margin:auto;'></canvas>
<script>
  var Module = {};

  function script(url, cb) {
    var s = document.createElement('script');
    s.src = url;
    s.onload = cb;
    document.body.appendChild(s);
  }

  function binary(url, cb) {
    var x = new XMLHttpRequest();
    x.open('GET', url, true);
    x.responseType = 'arraybuffer';
    x.onload = function() { cb(x.response); }
    x.send(null);
  }

  binary('a.mem', function(mem) {
    Module.mem = mem;
    script('a.asm.js', function() {
      script('a.js');
    });
  });

</script>
</body>
</html>

a.js

var a = Module;
var c = new TextDecoder("utf8");

function f(b) {
    for (var d = g, e = b; d[e];) ++e;
    return c.decode(d.subarray ? d.subarray(b, e) : new Uint8Array(d.slice(b, e)))
}
var h = new ArrayBuffer(16777216);
var g = new Uint8Array(h);
var k = {
    Math: Math,
    Int8Array: Int8Array,
    Int16Array: Int16Array,
    Int32Array: Int32Array,
    Uint8Array: Uint8Array,
    Uint16Array: Uint16Array,
    Uint32Array: Uint32Array,
    Float32Array: Float32Array,
    Float64Array: Float64Array,
    NaN: NaN,
    Infinity: Infinity
};
a.c = {
    a: function(b) {
        console.log(b ? f(b) : "")
    },
    b: 256
};
var l = a["asm"](k, a.c, h)._main;
g.set(new Uint8Array(a.mem), 8);
l();

a.asm.js

Module["asm"] = (function(global, env, buffer) {
    "use asm";
    var a = new global.Int8Array(buffer);
    var b = new global.Int16Array(buffer);
    var c = new global.Int32Array(buffer);
    var d = new global.Uint8Array(buffer);
    var e = new global.Uint16Array(buffer);
    var f = new global.Uint32Array(buffer);
    var g = new global.Float32Array(buffer);
    var h = new global.Float64Array(buffer);
    var i = env.b | 0;
    var j = 0;
    var k = 0;
    var l = 0;
    var m = 0;
    var n = global.NaN,
        o = global.Infinity;
    var p = 0,
        q = 0,
        r = 0,
        s = 0,
        t = 0.0;
    var u = global.Math.floor;
    var v = global.Math.abs;
    var w = global.Math.sqrt;
    var x = global.Math.pow;
    var y = global.Math.cos;
    var z = global.Math.sin;
    var A = global.Math.tan;
    var B = global.Math.acos;
    var C = global.Math.asin;
    var D = global.Math.atan;
    var E = global.Math.atan2;
    var F = global.Math.exp;
    var G = global.Math.log;
    var H = global.Math.ceil;
    var I = global.Math.imul;
    var J = global.Math.min;
    var K = global.Math.max;
    var L = global.Math.clz32;
    var M = env.a;
    var N = 272;
    var O = 5243152;
    var P = 0.0;

    function Q() {
        M(8);
        return 0
    }
    return {
        _main: Q
    }
});

Further optimizations are possible to get down to a few lines only.

@juj juj force-pushed the juj:minimal_runtime branch 12 times, most recently from c28b9c1 to 30b8063 Jan 24, 2019

@juj juj force-pushed the juj:minimal_runtime branch from 41930d5 to 35ab6c4 Feb 5, 2019

@juj

This comment has been minimized.

Copy link
Collaborator Author

juj commented Feb 5, 2019

Thanks, sounds good. Updated to disable v8 and SpiderMonkey, waiting for CI to run on it, and landing then. This is a good starting point to iterate further.

juj added some commits Feb 5, 2019

@juj juj merged commit 63810da into emscripten-core:incoming Feb 5, 2019

26 of 28 checks passed

ci/circleci: test-browser-chrome Your tests failed on CircleCI
Details
ci/circleci: test-browser-firefox CircleCI is running your tests
Details
ci/circleci: build Your tests passed on CircleCI!
Details
ci/circleci: build-docs Your tests passed on CircleCI!
Details
ci/circleci: build-upstream Your tests passed on CircleCI!
Details
ci/circleci: flake8 Your tests passed on CircleCI!
Details
ci/circleci: test-ab Your tests passed on CircleCI!
Details
ci/circleci: test-binaryen0 Your tests passed on CircleCI!
Details
ci/circleci: test-binaryen1 Your tests passed on CircleCI!
Details
ci/circleci: test-binaryen2 Your tests passed on CircleCI!
Details
ci/circleci: test-binaryen3 Your tests passed on CircleCI!
Details
ci/circleci: test-c Your tests passed on CircleCI!
Details
ci/circleci: test-d Your tests passed on CircleCI!
Details
ci/circleci: test-e Your tests passed on CircleCI!
Details
ci/circleci: test-f Your tests passed on CircleCI!
Details
ci/circleci: test-ghi Your tests passed on CircleCI!
Details
ci/circleci: test-jklmno Your tests passed on CircleCI!
Details
ci/circleci: test-other Your tests passed on CircleCI!
Details
ci/circleci: test-p Your tests passed on CircleCI!
Details
ci/circleci: test-qrst Your tests passed on CircleCI!
Details
ci/circleci: test-sanity Your tests passed on CircleCI!
Details
ci/circleci: test-upstream-binaryen0 Your tests passed on CircleCI!
Details
ci/circleci: test-upstream-binaryen2 Your tests passed on CircleCI!
Details
ci/circleci: test-upstream-browser-chrome Your tests passed on CircleCI!
Details
ci/circleci: test-upstream-other Your tests passed on CircleCI!
Details
ci/circleci: test-upstream-wasmobj0 Your tests passed on CircleCI!
Details
ci/circleci: test-upstream-wasmobj2 Your tests passed on CircleCI!
Details
ci/circleci: test-uvwxyz Your tests passed on CircleCI!
Details

@juj juj deleted the juj:minimal_runtime branch Feb 6, 2019

@trzecieu

This comment has been minimized.

Copy link
Contributor

trzecieu commented Feb 6, 2019

Just tested from curiosity this feature and we've got -30% of js file. Which is awesome!
I've tried to reconstruct output to working state with MINIMAL_RUNTIME=1 by adding missing libraries (gl, glut, openal...) and I have missing two symbols not linked: _emscripten_push_uncounted_main_loop_blocker and $Browser. Both of those are provided by library_browser.jswhich is added only if MINIMAL_RUNTIME isn't set.

Might be a strange question, but would it make sense to provide a way to include library_browser.js even if MINIMAL_RUNTIME is set?

@juj

This comment has been minimized.

Copy link
Collaborator Author

juj commented Feb 7, 2019

Really nice that you were able to test!

The main loop blocker mechanisn is something that will not be supported at least the way it exists in library_browser.js, because it contains items that do not behave well with respect to codegen. You can manually force library_browser.js in by specifying it at cmdline with --js-library directive if you really wish, though the idea is to drive developers to reimplement the functionality in other ways that are slim and dce well. In this case, there is emscripten_set_request_animation_frame_loop, and you can implement the blocking aspect inside c/c++, for smaller code size and multithreading awareness.

Cool to hear the numbers, -30% sounds sizable!

@floooh

This comment has been minimized.

Copy link
Contributor

floooh commented Feb 7, 2019

Hi @trzecieu, did you have to do anything else to get GL working besides linking with 'GL'? I've got the unresolved GL symbols resolved with this (nice that linking is explicit now btw, makes it more similar to other platforms).

The remaining problem is now that GLctx is null when the first GL functions are called.

Before calling GL function I use the emscripten_webgl_* function to setup the context:

    ....
    emscripten_webgl_init_context_attributes(&attrs);
    ....
    ctx = emscripten_webgl_create_context(0, &attrs);
    ....
    emscripten_webgl_make_context_current(ctx);

With printf-debugging I'm seeing that emscripten_webgl_create_context() returns 0 without further indication what went wrong (I also tested without any context setup attributes).

Does that sound familiar? :)

@trzecieu

This comment has been minimized.

Copy link
Contributor

trzecieu commented Feb 7, 2019

Hi @floooh
I haven't manage to run application yet, I can just see that I don't have unresolved symbols that are gl* related (please note that my project uses -s FULL_ES2=1 that might pull some extra linking objects)

@juj

This comment has been minimized.

Copy link
Collaborator Author

juj commented Feb 7, 2019

@floooh try building with -s ASSERTIONS=1 to get more debug info.

One thing to try is to do

ctx = emscripten_webgl_create_context("canvas", &attrs);

or

ctx = emscripten_webgl_create_context("#canvas", &attrs);

instead of passing in 0. The behavior with "magic 0" meaning a default canvas is going to go away at least in MINIMAL_RUNTIME mode, check out #7977 for that. Target 0 used to mean "whatever is set in Module.canvas", but in MINIMAL_RUNTIME, the Module object is also going away, so there's no Module.canvas that MINIMAL_RUNTIME can read. I think using the string "canvas" there should work.

@floooh

This comment has been minimized.

Copy link
Contributor

floooh commented Feb 7, 2019

Ok, thanks for the hints where to look. I'll try to give it a bit more time in the evening and report back when I got it working. So far it looks like in my minimal sokol-samples the .js file goes from 74 KBytes to 39 KBytes (uncompressed), very cool :)

@floooh

This comment has been minimized.

Copy link
Contributor

floooh commented Feb 7, 2019

Ok, intermediate followup (@juj):

Trying both "canvas" and "#canvas" didn't work with the default shell_minimal_runtime.html: Using "#canvas" seems to resolve to Module['canvas'] in JSEvents.findEventTarget, which is undefined, see here:

findEventTarget: function(target) {
try {
// The sensible "default" target varies between events, but use window as the default
// since DOM events mostly can default to that. Specific callback registrations
// override their own defaults.
if (!target) return window;
if (typeof target === "number") target = UTF8ToString(target);
if (target === '#window') return window;
else if (target === '#document') return document;
else if (target === '#screen') return window.screen;
else if (target === '#canvas') return Module['canvas'];
return (typeof target === 'string') ? document.getElementById(target) : target;
} catch(e) {
// In Web Workers, some objects above, such as '#document' do not exist. Gracefully
// return null for them.
return null;
}

Using "canvas" also fails, because the canvas in shell_minimal_runtime.html doesn't have an id:

<canvas style='display:block; margin:auto;'></canvas>

Hacking id='canvas' into shell_minimal_runtime.html and creating the GL context with "canvas" fixes the GL problems.

Next problem when starting the app is missing function: emscripten_set_main_loop, but I haven't looked into this yet, just wanted to give a quick update ;)

@juj

This comment has been minimized.

Copy link
Collaborator Author

juj commented Feb 7, 2019

Using "canvas" also fails,

Hacking id='canvas' into shell_minimal_runtime.html and creating the GL context with "canvas" fixes the GL problems.

Doh, sorry about that, PRs are still in flight. Before PR #7977, previous lookup behavior has been

  • "canvas" would look up DOM element with ID "canvas"
  • "#canvas" and 0 would look up Module.canvas

After PR #7977, new lookup behavior is identical to how CSS selectors work, so familiar to web developers:

  • "canvas" looks up first found DOM element with element name <canvas>,
  • "#canvas" looks up DOM element with ID "canvas",
  • 0 is an error

The file shell_minimal_runtime.html was already written as if the new behavior will have landed, to keep PR churn to minimum (I run with all the PRs layered together, and then splice them off to separate PRs)

As for emscripten_set_main_loop being gone, take a peek at the new functions

extern long emscripten_request_animation_frame(EM_BOOL (*cb)(double time, void *userData), void *userData);
extern void emscripten_cancel_animation_frame(long requestAnimationFrameId);
extern void emscripten_request_animation_frame_loop(EM_BOOL (*cb)(double time, void *userData), void *userData);

in #include <emscripten/html5.h>

emscripten_request_animation_frame_loop() is the close equivalent to emscripten_set_main_loop(). Docs at https://emscripten.org/docs/api_reference/html5.h.html#id98

@floooh

This comment has been minimized.

Copy link
Contributor

floooh commented Feb 7, 2019

Ok, success! I got the simple sokol-samples working now :)

screen shot 2019-02-07 at 7 09 08 pm

My sizes in the above comments are way off (I think I had asserts enabled and/or debug mode without minification).

The actual (uncompressed) sizes for cube demo in the screenshots are:

With traditional runtime: 21 KBytes, with minimal runtime: 14 KBytes, so this is in line with the 30% reduction @trzecieu is seeing.

I'm still seeing other problems in the more complex Dear ImGui sample (maybe because there are hidden IO calls in ImGui, it's something about FS and errno), but I'll figure that out I guess :)

This is also without the ccall/cwrap stuff.

I'll also need to do some minor changes to the sokol-headers to remove the deprecated functions. I'll try to move the sokol-samples (https://floooh.github.io/sokol-html5/) and if time allows the Tiny Emulators (https://floooh.github.io/tiny8bit/) over to the MINIMAL_RUNTIME over the weekend. Especially the Tiny Emulators should cover quite a few areas.

The changes in my code so far have been:

  • explicitely link with GL
  • don't use shell_minimal.html but the default shell (although I'm using my own shell.html replacement anyway which I need to fix for the minimal runtime)
  • canvas object wasn't found (I've been using either '0' or '#canvas'), I probably won't wait for the related PRs to land but simply lookup by id since I'm using my own shell.html anyway
  • replace emscripten_set_main_loop with emscripten_request_animation_frame_loop

I think that's it so far, at least for the minimalistic demos. What I haven't looked into yet is input events and my own streaming audio-buffer lib (sokol_audio.h), I'm currently assuming that a global Module object exists, but that's trivial to fix.

For the canvas-not-found problem I had several cases where simply nothing was rendered, and also no error messages on the JS console, despite assertions enabled. Not sure if that's intended behaviour.

Alright that's all for today. That whole MINIMAL_RUNTIME thing is very very cool :)

@juj

This comment has been minimized.

Copy link
Collaborator Author

juj commented Feb 7, 2019

* don't use shell_minimal.html but the default shell (although I'm using my own shell.html replacement anyway which I need to fix for the minimal runtime)

Ohh, there's src/shell_minimal_runtime.html that was added for this purpose to show a default loader for MINIMAL_RUNTIME. The src/shell.html and src/shell_minimal.html files are incompatible - I should probably make the build error out about this.

I'm currently assuming that a global Module object exists, but that's trivial to fix.

The global Module object does still exist, but it is only used as an imports object to bridge in loading the page. It currently recognizes the following uses:

  • Module['asm'] contains the asm.js module if targeting asm.js.
  • Module['mem'] contains the memory initializer
  • Module['wasm'] contains the wasm module

i.e. it is only used to glue together the needed parts a site developer has to XHR in to start up the page.

For the canvas-not-found problem I had several cases where simply nothing was rendered, and also no error messages on the JS console, despite assertions enabled. Not sure if that's intended behaviour.

I think the assertions messages should get better when #7977 lands.

That whole MINIMAL_RUNTIME thing is very very cool :)

Thanks, really warm feeling to read this feedback!

@floooh

This comment has been minimized.

Copy link
Contributor

floooh commented Feb 7, 2019

Ohh, there's src/shell_minimal_runtime.html that was added for this purpose to show a default loader for MINIMAL_RUNTIME. The src/shell.html and src/shell_minimal.html files are incompatible - I should probably make the build error out about this.

I might be wrong, but I think that shell.html is already automatically replaced with shell_minimal_runtime.html when linking with -s MINIMAL_RUNTIME? At least I think that's what I'm doing in my cmake toolchain file and it seems to work.

What definitely didn't work was explicitly using shell_minimal.html (that was my old default). This simply rendered a white page (I think the {{{ SCRIPT }}} wasn't even replaced).

I think the first case is fine, but the second case should probably create an error.

@juj

This comment has been minimized.

Copy link
Collaborator Author

juj commented Feb 7, 2019

I might be wrong, but I think that shell.html is already automatically replaced with shell_minimal_runtime.html when linking with -s MINIMAL_RUNTIME? At least I think that's what I'm doing in my cmake toolchain file and it seems to work.

Yeah, that's right, that's the intent. The buggy part is exactly when users submit their own non-minimal-runtime-compatible shells that have that {{{ SCRIPT }}} tag - since the MINIMAL_RUNTIME shells are managed a bit differently, that tag is not used. I'll push a PR for this second case.

@floooh

This comment has been minimized.

Copy link
Contributor

floooh commented Feb 9, 2019

Hi it's a me again :)

While porting my remaining sokol-samples I'm stuck in 2 more places:

  • in _emscripten_get_now_is_monotonic there's a check for the environment (ENVIRONMENT_IS_WEB is undefined etc...) at first I thought "yeah, easy, I need to add -s ENVIRONMENT=web, but now I'm just getting the other error that ENVIRONMENT_IS_NODE is undefined, the code in question is here:

emscripten/src/library.js

Lines 4409 to 4415 in 0e45f72

emscripten_get_now_is_monotonic__deps: ['emscripten_get_now'],
emscripten_get_now_is_monotonic: function() {
// return whether emscripten_get_now is guaranteed monotonic; the Date.now
// implementation is not :(
return ENVIRONMENT_IS_NODE || (typeof dateNow !== 'undefined') ||
((ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) && self['performance'] && self['performance']['now']);
},

  • the other problem is input event related, in library_html5.js/_fillMouseEventData(), mouse positions (0,0) are returned when there's no Module["canvas"] (and from looking around in the vicinity there seem to be a few other places which depend on Module["canvas"]), here's the fillMouseEventData code:

if (Module['canvas']) {
var rect = Module['canvas'].getBoundingClientRect();
{{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenMouseEvent.canvasX, 'e.clientX - rect.left', 'i32') }}};
{{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenMouseEvent.canvasY, 'e.clientY - rect.top', 'i32') }}};
} else { // Canvas is not initialized, return 0.
{{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenMouseEvent.canvasX, '0', 'i32') }}};
{{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenMouseEvent.canvasY, '0', 'i32') }}};
}

Both of these happen in the imgui-emsc sample, this is the most complex of the sokol-samples that directly calls emscripten-API-functions (the next step is then the cross-platform samples using the sokol_app.h header, but conceptually this isn't different from the direct emscripten samples). The imgui samples is also the last sample to be converted, all the others (mostly WebGL/WebGL2 rendering) are working.

After that I'll move on to the Tiny Emulator samples, those use the cwrap/ccall stuff, and file loading (however this is not implemented through the emscripten functions, but with a minimal inlined JS function which does an XHR).

@juj

This comment has been minimized.

Copy link
Collaborator Author

juj commented Feb 9, 2019

Fix for first one in #8048.

The second issue is a designed limitation. Module.canvas no longer exists, so canvasX and canvasY will be going away from the input event fields. Using targetX and targetY is the intended replacement. That may require manual coordinate system recomputation, like is done in library_html5.js. Not yet sure if it might make sense to provide some kind of helper to compute that.

@floooh

This comment has been minimized.

Copy link
Contributor

floooh commented Feb 9, 2019

Ok, I'll try the targetX/Y approach and complain if it is too much hassle ;)

@floooh

This comment has been minimized.

Copy link
Contributor

floooh commented Feb 9, 2019

Ok, that worked, thanks again for the hints :) I also found a bug in my sokol headers which caused closure to throw up (a single-character variable name in my embedded JS code).

I have updated the sokol-samples page with the MINIMAL_RUNTIME samples:

https://floooh.github.io/sokol-html5/

The old and new sizes are as follows (ALL-xxx is the entire compressed download size when clicking on a demo as shown in Chrome, and JS-xxx is just the Javascript wrapper, also compressed, the difference there is so big because closure works now, this is responsible for about half the size savings, sizes are in KBytes btw):

                ALL-OLD     ALL-MINI    JS-OLD  JS-MINI
clear:          32.9        15.3        23.2    5.6
triangle:       37.8        20.0        24.4    6.6
quad:           38.1        20.3        24.4    6.6
bufferoffsets:  38.4        20.4        24.5    6.7
cube:           41.8        23.7        24.9    7.0
noninterleaved: 41.8        23.6        24.9    7.0
texcube:        43.8        24.8        25.6    7.4
offscreen:      45.9        27.3        26.0    7.7
instancing:     41.3        23.4        24.8    7.0
mrt:            47.0        28.4        26.0    7.7
arraytex:       43.9        25.5        25.6    7.5
dyntex:         44.3        25.8        25.7    7.5
mipmap:         45.0        26.5        25.8    7.6
blend:          41.2        23.4        24.7    7.0
imgui:          265.0       247.0       26.6    7.9
imgui-highdpi:  523.0       505.0       26.4    7.9
saudio:         34.2        16.0        23.7    6.0

(JS-OLD is without closure compiler run, because of a
bug in sokol_app.h, so about half of the size savings
are actually from closure)

(imgui-highdpi is mostly embedded font data)

One sample which is using a complex 3rd party lib (libmodplug) doesn't work yet, I still need to investigate this.

Ah, and I also have temporarily removed the calls to the timing function.

I'm very happy with the results so far, and I think I don't even use all the emscripten options to minimize the runtime even more (I've seen stuff like not supporting errno in your emscripten tests for the MINIMAL_RUNTIME, I'll also try this in the next few days).

@floooh

This comment has been minimized.

Copy link
Contributor

floooh commented Feb 10, 2019

FYI after updating emscripten I'm now getting an error warnOnce is not defined from the findTarget function (warning about the deprecated canvas stuff).

For the libmodplug library the problem seems to be related to global C++ constructors, the error is getMemory is not defined:

Right at startup asm['globalCtors'] is called, this calls the WASM function ___emscripten_environ_constructor, and this in turn calls the JS runtime function ___buildEnvironment which has 2 calls to getMemory (one getMemory(TOTAL_ENV_SIZE); and one getMemory(MAX_ENV_VALUES * 4).

I think I'll wait a couple of days before continuing to let things settle down a bit :)

@juj

This comment has been minimized.

Copy link
Collaborator Author

juj commented Feb 10, 2019

FYI after updating emscripten I'm now getting an error warnOnce is not defined from the findTarget function (warning about the deprecated canvas stuff).

Oh, that is something that was missing from a previous PR. To fix, pass -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 to migrate to the new event lookup rules, see #8055. (Or alternatively comment out the warnOnce)

@juj

This comment has been minimized.

Copy link
Collaborator Author

juj commented Feb 10, 2019

Right at startup asm['globalCtors'] is called, this calls the WASM function ___emscripten_environ_constructor, and this in turn calls the JS runtime function ___buildEnvironment which has 2 calls to getMemory (one getMemory(TOTAL_ENV_SIZE); and one getMemory(MAX_ENV_VALUES * 4).

This looks like can be categorized as environment variable handling not being supported yet. Fixing will need adapting a little, I'll look into it. If you don't need env. vars there, as hotfix I think you can try removing the

      // Allocate memory.
      poolPtr = getMemory(TOTAL_ENV_SIZE);
      envPtr = getMemory(MAX_ENV_VALUES * {{{ Runtime.POINTER_SIZE }}});
      {{{ makeSetValue('envPtr', '0', 'poolPtr', 'i8*') }}};
      {{{ makeSetValue('environ', 0, 'envPtr', 'i8*') }}};

block in src/library.js in __buildEnvironment altogether.

@zeux

This comment has been minimized.

Copy link

zeux commented Feb 17, 2019

This is fantastic, it's a big help for shipping really small emscripten-compiled libraries. Thanks for working on this!

I know MODULARIZE is being worked on in #8057 - I'm curious, are there plans to maintain support for ALLOW_MEMORY_GROWTH and SINGLE_FILE? I'm currently using emscripten with these options and it would be great to be able to use the new mode with them.

@juj

This comment has been minimized.

Copy link
Collaborator Author

juj commented Feb 17, 2019

Thank you, much appreciated!

ALLOW_MEMORY_GROWTH is something that will come in soon in short term. SINGLE_FILE is something that I have not currently planned, contributions there would definitely be welcome. The feature itself is not incompatible that it could not be added to MINIMAL_RUNTIME, although the implementation will need to be addressed so that it will not regress size of non-SINGLE_FILE builds.

@WolfieWerewolf

This comment has been minimized.

Copy link

WolfieWerewolf commented Feb 18, 2019

@juj I just wanted to confirm that using incoming, as of last evening, I was able to produce a standalone (cmake / c++) version of your minimal_webgl2 UT. Note that the poor frame rate in the recording is the result of my gif recorder, not the actual rendering which is fluid.

To make the generated code readable I turned off the optimizations and excluded the closure compiler so my args are:

-s MINIMAL_RUNTIME=2 \
-O0 \
-s AGGRESSIVE_VARIABLE_ELIMINATION=1 \
-s ENVIRONMENT=web \
--js-library ./library_js.js \
-s TEXTDECODER=2 \
-s ABORTING_MALLOC=0 \
-s ALLOW_MEMORY_GROWTH=0 \
-s SUPPORT_ERRNO=0 \
-s DECLARE_ASM_MODULE_EXPORTS=1 \
-s MALLOC=emmalloc \
-s GL_EMULATE_GLES_VERSION_STRING_FORMAT=0 \
-s GL_EXTENSIONS_IN_PREFIXED_FORMAT=0 \
-s GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS=0 \
-s GL_TRACK_ERRORS=0 \
-s GL_SUPPORT_EXPLICIT_SWAP_CONTROL=0 \
-s GL_POOL_TEMP_BUFFERS=0 \
-s FAST_UNROLLED_MEMCPY_AND_MEMSET=0 \
-s RUNTIME_FUNCS_TO_IMPORT=[] \
-s USES_DYNAMIC_ALLOC=2 \
--output_eol linux \
-ffast-math \
-lGL \
-ffast-math \
-s USE_WEBGL2=1 \
-s WASM=1"

snow1

A 30 second trace of this running in Chrome looks like this:
image

The initial memory footprint in Chrome is about 50K
I have this in a github repo which is private currently but if anyone wants to take a look at it themselves just ping me... I'll make it public and post the link on here.

Very nice work mate... I really appreciate you doing this.
Kind Regards
/W
Almost forgot... size on disk:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.