Commit 54e6ba67 authored by Richard Musiol's avatar Richard Musiol Committed by Brad Fitzpatrick

syscall/js: garbage collect references to JavaScript values

The js.Value struct now contains a pointer, so a finalizer can
determine if the value is not referenced by Go any more.

Unfortunately this breaks Go's == operator with js.Value. This change
adds a new Equal method to check for the equality of two Values.
This is a breaking change. The == operator is now disallowed to
not silently break code.

Additionally the helper methods IsUndefined, IsNull and IsNaN got added.

Fixes #35111

Change-Id: I58a50ca18f477bf51a259c668a8ba15bfa76c955
Reviewed-on: https://go-review.googlesource.com/c/go/+/203600
Run-TryBot: Richard Musiol <neelance@gmail.com>
Reviewed-by: default avatarCherry Zhang <cherryyz@google.com>
Reviewed-by: default avatarBrad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent 063d0f11
......@@ -205,26 +205,31 @@
return;
}
let ref = this._refs.get(v);
if (ref === undefined) {
ref = this._values.length;
this._values.push(v);
this._refs.set(v, ref);
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
let typeFlag = 0;
this._goRefCounts[id]++;
let typeFlag = 1;
switch (typeof v) {
case "string":
typeFlag = 1;
typeFlag = 2;
break;
case "symbol":
typeFlag = 2;
typeFlag = 3;
break;
case "function":
typeFlag = 3;
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, ref, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
......@@ -263,7 +268,9 @@
this.exited = true;
delete this._inst;
delete this._values;
delete this._refs;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
......@@ -323,6 +330,18 @@
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
storeValue(sp + 24, loadString(sp + 8));
......@@ -462,7 +481,7 @@
async run(instance) {
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // TODO: garbage collection
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
......@@ -471,8 +490,10 @@
global,
this,
];
this._refs = new Map();
this.exited = false;
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map(); // mapping from JS values to reference ids
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
......
......@@ -41,7 +41,7 @@ const jsFetchCreds = "js.fetch:credentials"
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
const jsFetchRedirect = "js.fetch:redirect"
var useFakeNetwork = js.Global().Get("fetch") == js.Undefined()
var useFakeNetwork = js.Global().Get("fetch").IsUndefined()
// RoundTrip implements the RoundTripper interface using the WHATWG Fetch API.
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
......@@ -50,7 +50,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
}
ac := js.Global().Get("AbortController")
if ac != js.Undefined() {
if !ac.IsUndefined() {
// Some browsers that support WASM don't necessarily support
// the AbortController. See
// https://developer.mozilla.org/en-US/docs/Web/API/AbortController#Browser_compatibility.
......@@ -74,7 +74,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
opt.Set("redirect", h)
req.Header.Del(jsFetchRedirect)
}
if ac != js.Undefined() {
if !ac.IsUndefined() {
opt.Set("signal", ac.Get("signal"))
}
headers := js.Global().Get("Headers").New()
......@@ -132,7 +132,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
var body io.ReadCloser
// The body is undefined when the browser does not support streaming response bodies (Firefox),
// and null in certain error cases, i.e. when the request is blocked because of CORS settings.
if b != js.Undefined() && b != js.Null() {
if !b.IsUndefined() && !b.IsNull() {
body = &streamReader{stream: b.Call("getReader")}
} else {
// Fall back to using ArrayBuffer
......@@ -168,7 +168,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
respPromise.Call("then", success, failure)
select {
case <-req.Context().Done():
if ac != js.Undefined() {
if !ac.IsUndefined() {
// Abort the Fetch request
ac.Call("abort")
}
......
......@@ -259,7 +259,7 @@ func Lchown(path string, uid, gid int) error {
if err := checkPath(path); err != nil {
return err
}
if jsFS.Get("lchown") == js.Undefined() {
if jsFS.Get("lchown").IsUndefined() {
// fs.lchown is unavailable on Linux until Node.js 10.6.0
// TODO(neelance): remove when we require at least this Node.js version
return ENOSYS
......@@ -497,7 +497,7 @@ func fsCall(name string, args ...interface{}) (js.Value, error) {
var res callResult
if len(args) >= 1 { // on Node.js 8, fs.utimes calls the callback without any arguments
if jsErr := args[0]; jsErr != js.Null() {
if jsErr := args[0]; !jsErr.IsNull() {
res.err = mapJSError(jsErr)
}
}
......
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build js,wasm
package js
var JSGo = jsGo
......@@ -64,7 +64,7 @@ func init() {
func handleEvent() {
cb := jsGo.Get("_pendingEvent")
if cb == Null() {
if cb.IsNull() {
return
}
jsGo.Set("_pendingEvent", Null())
......
This diff is collapsed.
......@@ -4,6 +4,10 @@
#include "textflag.h"
TEXT ·finalizeRef(SB), NOSPLIT, $0
CallImport
RET
TEXT ·stringVal(SB), NOSPLIT, $0
CallImport
RET
......
......@@ -18,6 +18,7 @@ package js_test
import (
"fmt"
"math"
"runtime"
"syscall/js"
"testing"
)
......@@ -53,7 +54,7 @@ func TestBool(t *testing.T) {
if got := dummys.Get("otherBool").Bool(); got != want {
t.Errorf("got %#v, want %#v", got, want)
}
if dummys.Get("someBool") != dummys.Get("someBool") {
if !dummys.Get("someBool").Equal(dummys.Get("someBool")) {
t.Errorf("same value not equal")
}
}
......@@ -68,7 +69,7 @@ func TestString(t *testing.T) {
if got := dummys.Get("otherString").String(); got != want {
t.Errorf("got %#v, want %#v", got, want)
}
if dummys.Get("someString") != dummys.Get("someString") {
if !dummys.Get("someString").Equal(dummys.Get("someString")) {
t.Errorf("same value not equal")
}
......@@ -105,7 +106,7 @@ func TestInt(t *testing.T) {
if got := dummys.Get("otherInt").Int(); got != want {
t.Errorf("got %#v, want %#v", got, want)
}
if dummys.Get("someInt") != dummys.Get("someInt") {
if !dummys.Get("someInt").Equal(dummys.Get("someInt")) {
t.Errorf("same value not equal")
}
if got := dummys.Get("zero").Int(); got != 0 {
......@@ -141,20 +142,20 @@ func TestFloat(t *testing.T) {
if got := dummys.Get("otherFloat").Float(); got != want {
t.Errorf("got %#v, want %#v", got, want)
}
if dummys.Get("someFloat") != dummys.Get("someFloat") {
if !dummys.Get("someFloat").Equal(dummys.Get("someFloat")) {
t.Errorf("same value not equal")
}
}
func TestObject(t *testing.T) {
if dummys.Get("someArray") != dummys.Get("someArray") {
if !dummys.Get("someArray").Equal(dummys.Get("someArray")) {
t.Errorf("same value not equal")
}
// An object and its prototype should not be equal.
proto := js.Global().Get("Object").Get("prototype")
o := js.Global().Call("eval", "new Object()")
if proto == o {
if proto.Equal(o) {
t.Errorf("object equals to its prototype")
}
}
......@@ -167,26 +168,66 @@ func TestFrozenObject(t *testing.T) {
}
}
func TestEqual(t *testing.T) {
if !dummys.Get("someFloat").Equal(dummys.Get("someFloat")) {
t.Errorf("same float is not equal")
}
if !dummys.Get("emptyObj").Equal(dummys.Get("emptyObj")) {
t.Errorf("same object is not equal")
}
if dummys.Get("someFloat").Equal(dummys.Get("someInt")) {
t.Errorf("different values are not unequal")
}
}
func TestNaN(t *testing.T) {
want := js.ValueOf(math.NaN())
got := dummys.Get("NaN")
if got != want {
t.Errorf("got %#v, want %#v", got, want)
if !dummys.Get("NaN").IsNaN() {
t.Errorf("JS NaN is not NaN")
}
if !js.ValueOf(math.NaN()).IsNaN() {
t.Errorf("Go NaN is not NaN")
}
if dummys.Get("NaN").Equal(dummys.Get("NaN")) {
t.Errorf("NaN is equal to NaN")
}
}
func TestUndefined(t *testing.T) {
dummys.Set("test", js.Undefined())
if dummys == js.Undefined() || dummys.Get("test") != js.Undefined() || dummys.Get("xyz") != js.Undefined() {
t.Errorf("js.Undefined expected")
if !js.Undefined().IsUndefined() {
t.Errorf("undefined is not undefined")
}
if !js.Undefined().Equal(js.Undefined()) {
t.Errorf("undefined is not equal to undefined")
}
if dummys.IsUndefined() {
t.Errorf("object is undefined")
}
if js.Undefined().IsNull() {
t.Errorf("undefined is null")
}
if dummys.Set("test", js.Undefined()); !dummys.Get("test").IsUndefined() {
t.Errorf("could not set undefined")
}
}
func TestNull(t *testing.T) {
dummys.Set("test1", nil)
dummys.Set("test2", js.Null())
if dummys == js.Null() || dummys.Get("test1") != js.Null() || dummys.Get("test2") != js.Null() {
t.Errorf("js.Null expected")
if !js.Null().IsNull() {
t.Errorf("null is not null")
}
if !js.Null().Equal(js.Null()) {
t.Errorf("null is not equal to null")
}
if dummys.IsNull() {
t.Errorf("object is null")
}
if js.Null().IsUndefined() {
t.Errorf("null is undefined")
}
if dummys.Set("test", js.Null()); !dummys.Get("test").IsNull() {
t.Errorf("could not set null")
}
if dummys.Set("test", nil); !dummys.Get("test").IsNull() {
t.Errorf("could not set nil")
}
}
......@@ -340,7 +381,7 @@ func TestValueOf(t *testing.T) {
func TestZeroValue(t *testing.T) {
var v js.Value
if v != js.Undefined() {
if !v.IsUndefined() {
t.Error("zero js.Value is not js.Undefined()")
}
}
......@@ -497,12 +538,24 @@ func TestCopyBytesToJS(t *testing.T) {
}
}
func TestGarbageCollection(t *testing.T) {
before := js.JSGo.Get("_values").Length()
for i := 0; i < 1000; i++ {
_ = js.Global().Get("Object").New().Call("toString").String()
runtime.GC()
}
after := js.JSGo.Get("_values").Length()
if after-before > 500 {
t.Errorf("garbage collection ineffective")
}
}
// BenchmarkDOM is a simple benchmark which emulates a webapp making DOM operations.
// It creates a div, and sets its id. Then searches by that id and sets some data.
// Finally it removes that div.
func BenchmarkDOM(b *testing.B) {
document := js.Global().Get("document")
if document == js.Undefined() {
if document.IsUndefined() {
b.Skip("Not a browser environment. Skipping.")
}
const data = "someString"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment