Skip to content

syscall/js: remove Wrapper type to avoid extreme allocations and improve performance #44006

@finnbear

Description

@finnbear

Introduction

It is truly remarkable what can be accomplished by compiling Go to WebAssembly (wasm), but virtually all such applications are being held back by the combination of two factors:

  1. Every function call to JS that passes arguments makes multiple, unavoidable Go heap allocations
  2. The Go garbage collector then blocks the main, and only, thread for a large fraction of a second

As the title of this proposal suggests, the performance problems can be solved if the js.Wrapper type is removed. Removal of existing features from the standard library should not be taken lightly, but this particular removal is both allowable (syscall/js is exempt from Go's compatibility promise) and necessary given the data.

Why Wrapper matters

At the core of every call to JS, syscall/js uses its ValueOf function to convert Go arguments to their JS equivalents. This conversion is implemented as a switch statement:

func ValueOf(x interface{}) Value {
	switch x := x.(type) { // some cases omitted
	case Value:
		return x
	case Wrapper:
		return x.JSValue() // the problem
	case nil:
		return valueNull
	case bool:
		if x {
			return valueTrue
		} else {
			return valueFalse
		}
	case int:
		return floatValue(float64(x))
	case unsafe.Pointer:
		return floatValue(float64(uintptr(x)))
	case float64:
		return floatValue(x)
	case string:
		return makeValue(stringVal(x))
	default:
		panic("ValueOf: invalid value")
	}
}

One of the cases is the Wrapper interface. This was intended to allow any type implementing the JSValue method to be easily passed as an argument to a JS method. However, as JSValue may consist of an arbitrary pointer receiver, the compiler must assume the worst:

type BadWrapper struct {
    Value js.Value
}

var escapeRoute *BadWrapper

// Implements js.Wrapper
func (this *BadWrapper) JSValue() js.Value {
    escapeRoute = this // escape to heap
    return this.Value
}

This means that the argument to ValueOf, which includes any argument passed to any JS function, is guaranteed to escape to the heap.

Data

Data is reported as number of heap allocations per unit of work.

Optimizations (cumulative) Frame of game (best case) Frame of game (worst case) Typical canvas/WebGL call []byte copy to JS string manipulation Garbage collector effect on gameplay
Control (no optimizations) 858 40000 3 1 3 Frequent stutters
//go:noescape 829 untested 3 0 3 Frequent stutters
makeArgs slices on stack (See #39740) 383 untested 1 0 3 Less-frequent stutters
Unexport Wrapper.JSValue() 383 untested 1 0 3 No change
Remove Wrapper type 62* 300* 0 0 2 Not bad, especially after other allocations are reduced

*on the order of the number of allocations that happen unrelated to syscall/js

Data collection method

  1. Debug output from my online game, implemented in Go
  2. Micro-benchmarks: main.go
  3. Entire patch to syscall/js/js.go: js.go.patch

Context

Preliminary discussion of this proposal has has already taken place in #39740. The consensus there was that removing Wrapper is the only way to sustainably call JS functions in a performance-critical setting. Given that syscall/js is a low level package, it make sense to prioritize performance over the ease of use afforded by Wrapper.

More context on my actual use case

  1. My game's client is written in Go, compiled to WebAssembly, uses WebGL 2.0 APIs, and is intended to run at 60 frames per second.
  2. The WebGL API requires many function calls, each mainly taking integer and floating point parameters.
  3. I have observed to Go garbage collector running every 3-5 seconds under normal circumstances.
  4. I have observed the Go garbage collector running over 10 times in a single frame in a worst-case-scenario
  5. I have observed the Go garbage collector taking 300+ milliseconds to run

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions