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

syscall/js: performance considerations #32591

Open
dmitshur opened this issue Jun 13, 2019 · 9 comments

Comments

Projects
None yet
6 participants
@dmitshur
Copy link
Member

commented Jun 13, 2019

I was porting some frontend Go code to be compiled to WebAssembly instead of GopherJS, and noticed the performance was noticeably reduced. The Go code in question makes a lot of DOM manipulation calls and queries, so I decided to benchmark the performance of making calls from WebAssembly to the JavaScript APIs via syscall/js.

I found it's approximately 10x slower than native JavaScript.

Results of running a benchmark in Chrome 75.0.3770.80 on macOS 10.14.5:

  131.212518 ms/op - WebAssembly via syscall/js
   61.850000 ms/op - GopherJS via syscall/js
   12.040000 ms/op - GopherJS via github.com/gopherjs/gopherjs/js
   11.320000 ms/op - native JavaScript

Here's the benchmark code I used, written to be self-contained:

Source Code

main.go

package main

import (
	"fmt"
	"runtime"
	"syscall/js"
	"testing"
	"time"

	"honnef.co/go/js/dom/v2"
)

var document = dom.GetWindow().Document().(dom.HTMLDocument)

func main() {
	loaded := make(chan struct{})
	switch readyState := document.ReadyState(); readyState {
	case "loading":
		document.AddEventListener("DOMContentLoaded", false, func(dom.Event) { close(loaded) })
	case "interactive", "complete":
		close(loaded)
	default:
		panic(fmt.Errorf("internal error: unexpected document.ReadyState value: %v", readyState))
	}
	<-loaded

	for i := 0; i < 10000; i++ {
		div := document.CreateElement("div")
		div.SetInnerHTML(fmt.Sprintf("foo <strong>bar</strong> baz %d", i))
		document.Body().AppendChild(div)
	}

	time.Sleep(time.Second)

	runBench(BenchmarkGoSyscallJS, WasmOrGJS+" via syscall/js")
	if runtime.GOARCH == "js" { // GopherJS-only benchmark.
		runBench(BenchmarkGoGopherJS, "GopherJS via github.com/gopherjs/gopherjs/js")
	}
	runBench(BenchmarkNativeJavaScript, "native JavaScript")

	document.Body().Style().SetProperty("background-color", "lightgreen", "")
}

func runBench(f func(*testing.B), desc string) {
	r := testing.Benchmark(f)
	msPerOp := float64(r.T) * 1e-6 / float64(r.N)
	fmt.Printf("%f ms/op - %s\n", msPerOp, desc)
}

func BenchmarkGoSyscallJS(b *testing.B) {
	var total float64
	for i := 0; i < b.N; i++ {
		total = 0
		divs := js.Global().Get("document").Call("getElementsByTagName", "div")
		for j := 0; j < divs.Length(); j++ {
			total += divs.Index(j).Call("getBoundingClientRect").Get("top").Float()
		}
	}
	_ = total
}

func BenchmarkNativeJavaScript(b *testing.B) {
	js.Global().Set("NativeJavaScript", js.Global().Call("eval", nativeJavaScript))
	b.ResetTimer()
	js.Global().Get("NativeJavaScript").Invoke(b.N)
}

const nativeJavaScript = `(function(N) {
	var i, j, total;
	for (i = 0; i < N; i++) {
		total = 0;
		var divs = document.getElementsByTagName("div");
		for (j = 0; j < divs.length; j++) {
			total += divs[j].getBoundingClientRect().top;
		}
	}
	var _ = total;
})`

wasm.go

// +build wasm

package main

import "testing"

const WasmOrGJS = "WebAssembly"

func BenchmarkGoGopherJS(b *testing.B) {}

gopherjs.go

// +build !wasm

package main

import (
	"testing"

	"github.com/gopherjs/gopherjs/js"
)

const WasmOrGJS = "GopherJS"

func BenchmarkGoGopherJS(b *testing.B) {
	var total float64
	for i := 0; i < b.N; i++ {
		total = 0
		divs := js.Global.Get("document").Call("getElementsByTagName", "div")
		for j := 0; j < divs.Length(); j++ {
			total += divs.Index(j).Call("getBoundingClientRect").Get("top").Float()
		}
	}
	_ = total
}

I know syscall/js is documented as "Its current scope is only to allow tests to run, but not yet to provide a comprehensive API for users", but I wanted to open this issue to discuss the future. Performance is important for Go applications that need to make a lot of calls into the JavaScript world.

What is the current state of syscall/js performance, and are there known opportunities to improve it?

/cc @neelance @cherrymui @hajimehoshi

@agnivade

This comment has been minimized.

Copy link
Member

commented Jun 13, 2019

It would be also good to benchmark with Firefox and see the results.

IIUC, you are just benchmarking DOM manipulation. And since DOM manipulation anyways happens outside wasm, it is just about the price of context jump from wasm land to browser land and back. In that case, I wonder if it is even within the control of syscall/js and not the underlying wasm engine.

Would be also good to benchmark equivalent code using Rust and C and compare the benchmarks. I think that may be a better apples-apples comparison just to compare syscall/js performance with other languages.

@cherrymui

This comment has been minimized.

Copy link
Contributor

commented Jun 13, 2019

As @agnivade said, probably worth trying Firefox. V8 is known to have some performance problems with the Wasm code generated by the Go compiler.

@dmitshur

This comment has been minimized.

Copy link
Member Author

commented Jun 13, 2019

It would be also good to benchmark with Firefox and see the results.

Agreed. I'll do this later and share results.

IIUC, you are just benchmarking DOM manipulation. And since DOM manipulation anyways happens outside wasm, it is just about the price of context jump from wasm land to browser land and back. In that case, I wonder if it is even within the control of syscall/js and not the underlying wasm engine.

Yes. When I said syscall/js, I meant the entire performance cost of jumping from Wasm to the browser APIs and back. It's what the user sees when they use the API to interact with the JavaScript world.

Would be also good to benchmark equivalent code using Rust and C and compare the benchmarks. I think that may be a better apples-apples comparison just to compare syscall/js performance with other languages.

Agreed, that would be good and more representative of the actual WebAssembly <-> JS call overhead. Doing that would give us more information. I won't have a chance to do this, but if someone else can, it'd be helpful.

@eliasnaur

This comment has been minimized.

Copy link
Contributor

commented Jun 13, 2019

Perhaps it's not worth doing anything substantial here before something like WASI is standardized. @neelance even did a WIP implementation at #31105.

@dmitshur

This comment has been minimized.

Copy link
Member Author

commented Jun 14, 2019

I've tried the benchmark again with recent development versions of 3 browsers:

Chrome Canary
Version 77.0.3824.0 (Official Build) canary (64-bit)

    114.154496 ms/op - WebAssembly via syscall/js
     63.350000 ms/op - GopherJS via syscall/js
     11.740000 ms/op - GopherJS via github.com/gopherjs/gopherjs/js
     11.360000 ms/op - native JavaScript

Firefox Nightly
69.0a1 (2019-06-13) (64-bit)

     94.150003 ms/op - WebAssembly via syscall/js
     85.300000 ms/op - GopherJS via syscall/js
      7.695000 ms/op - GopherJS via github.com/gopherjs/gopherjs/js
      7.405000 ms/op - native JavaScript

Safari Technology Preview
Release 85 (Safari 13.0, WebKit 14608.1.28.1)

     57.249996 ms/op - WebAssembly via syscall/js
     42.866666 ms/op - GopherJS via syscall/js
      5.536666 ms/op - GopherJS via github.com/gopherjs/gopherjs/js
      5.073333 ms/op - native JavaScript

The results are pretty consistent across the 3 browsers in that doing lots of DOM queries via WebAssembly was about 10x slower than with pure JavaScript.

@hajimehoshi

This comment has been minimized.

Copy link
Contributor

commented Jun 14, 2019

Could you share the code to take the benchmark to output the values [s/op]?

@agnivade

This comment has been minimized.

Copy link
Member

commented Jun 14, 2019

Thanks for the tests @dmitshur. I would have thought that after https://hacks.mozilla.org/2018/10/calls-between-javascript-and-webassembly-are-finally-fast-%F0%9F%8E%89/, the DOM access overhead would have reduced in Firefox. And interesting that Safari is much faster for DOM access than Firefox.

The tests with Rust/C should give us a better idea on what exactly can be improved from Go side. If anybody can post results for that, that'll be great.

@dmitshur

This comment has been minimized.

Copy link
Member Author

commented Jun 14, 2019

@hajimehoshi Sure. I've updated the source code in the original post.

@gopherbot

This comment has been minimized.

Copy link

commented Jun 22, 2019

Change https://golang.org/cl/183457 mentions this issue: runtime,syscall/js: reuse wasm memory DataView

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.