go: cmd/compile: wasm code causes out of memory error on Chrome and Firefox for Android

I am very excited that Go ships now with Webassembly support. I ran wasm code generated by Go 1.11 on Chrome and Firefox on desktops (MacOS and Linux) and on Chrome, Firefox and Safari on iOS devices. Running Go generated wasm code on Android devices failed though.

Minimal example

  • Go code
    • GOOS=js GOARCH=wasm go build -o test.wasm wasm.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello")
}
  • index.html
    • wasm_exec.js from go/misc/wasm
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
    </head>
    <body>
        <script src="wasm_exec.js"></script>
        <script>
            (async function() {
                const wasmFile = "test.wasm"
                let run
                const go = new Go()
                try {
                    const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmFile), go.importObject)
                    document.querySelector('#info').innerHTML = "ready"
                    run = go.run(instance)
                } catch (err) {
                    document.querySelector('#info').innerHTML = err
                    console.log(err)
                }
            })()
        </script>
        <div id="info"></div>
    </body>
</html>
  • files are served via Nginx using adjusted mime.types

Expected behavior

  • page displays “ready” after the wasm file has been loaded and the console shows “hello”
    • this works on Desktops and iOS devices

Actual behavior

  • on Android devices the above code fails with “RangeError: WebAssembly Instantiation: Out of memory: wasm memory” (Chrome) and “out of memory” (Firefox)
    • tested on Chrome for Android (68.0.3440.91) and Firefox for Android (61.0.2). Several devices from different manufacturers were tested

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 7
  • Comments: 36 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Change https://golang.org/cl/170950 mentions this issue: runtime, cmd/link: optimize memory allocation on wasm

I hacked on the runtime library to reduce the initial allocation to ~80MB: https://github.com/golang/go/compare/master...twifkak:small

A couple of notes:

  • I have no idea if this is a good idea. The comments about keeping the heap contiguous might apply here, too? I just looked around for things that call growMemory and then lowered some constants.
  • If you use @termonio’s wams tool after compilation, then you don’t need to build a custom Go compiler. Only patch your GOROOT’s runtime dir, which will get compiled into the wasm binary.
  • No idea if this has good/bad/no effect on long-term memory usage. I’m still trying to figure that out.
  • You probably want to remove the printlns from my patch.

Update: It seems this is bad for processes that create a lot of flyweight objects. runtime/mem_js.go needs a free list or some such. Update 2: I wrote a free list. It requires GODEBUG=gcstoptheworld=1.

I wrote a small tool that can patch the memory section of a .wasm binary. This allows for easy experimenting with smaller initial page sizes without building a modified tool chain. It seems as if during instantiation quite a bit of memory is allocated by the WebAssembly runtime. When starting with 4096 pages (256MB) the runtime grows the memory on my desktop machine to 745865216 bytes (about 710MB, more than 11000 pages). As I have trouble allocating more than 7500 pages on my Android devices, this approach alone won’t help to make Go generated .wasm binaries run on Android. I am surprised that the instantiation is that expensive but I can understand now why the initial memory was set to 1GB …

I updated my fork to implement a free list, so that Go can reclaim freed memory in wasm. Using this, combined with GODEBUG=gcstoptheworld=1, I was able to run a pretty allocation-intensive workload (parsing a bunch of HTML files) with <124MB. (Using the concurrent GC, it crashes.)

Update: With GOGC=20 (arbitrary first guess), memory is <80MB.

Using some of the optimization flags mentioned here, I got my .wasm file down to ~2.6MB but still seeing OOM errors on everything but my mac 😦 I haven’t tried the fork mentioned above (I’m very new to Go) but I did try using ‘bytecoder’ with some Java for comparison…

That generates a 15KB file (also interacting with Javascript / canvas 2D) and runs great on Android g-tab2, and even an old Nexus 5 Android 6 device! and obviously the load times are vastly improved. Although still no success on my iOS devices, but that might be because I keep them on older iOS versions for development testing.

I know that wasm support is still experimental, but could this issue possibly be upgraded from ‘Performance’ ? Since currently this makes Go WASM unusable in many situations, and this probably needs fixing before WASI hits mainstream. Thanks.

It works. Thank you! @twifkak

Why does WebAssembly allocate this 1GB of (mostly unused) memory in the first place? Is there a way to limit the amount of memory it can request? (WebAssembly.Memory({initial: x, maximum: y}) comes to mind but my attempts to populate the importObject with preallocated memory did not succeed.) Edit: Dumping a wasm file with wasm-dump shows that memory is indeed set to 16384 64kB pages (=1GB): memory[0] pages: initial=16384. I am wondering whether this can be changed to a more reasonable size.

Given the market share of Android devices, this would be a major drawback …