Skip to content

Commit 104c293

Browse files
Jacobcherrymui
Jacob
authored andcommitted
syscall/js: allocate arg slices on stack for small numbers of args
The existing implementation causes unnecessary heap allocations for javascript syscalls: Call, Invoke, and New. The new change seeks to hint the Go compiler to allocate arg slices with length <=16 to the stack. Original Work: CL 367045 - Calling a JavaScript function with 16 arguments or fewer will not induce two additional heap allocations, at least with the current Go compiler. - Using syscall/js features with slices and strings of statically-known length will not cause them to be escaped to the heap, at least with the current Go compiler. - The reduction in allocations has the additional benefit that the garbage collector runs less often, blocking WebAssembly's one and only thread less often. Fixes #39740 Change-Id: I815047e1d4f8ada796318e2064d38d3e63f73098 GitHub-Last-Rev: 36df1b3 GitHub-Pull-Request: #66684 Reviewed-on: https://go-review.googlesource.com/c/go/+/576575 Reviewed-by: Cherry Mui <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent f31fcc7 commit 104c293

File tree

2 files changed

+170
-9
lines changed

2 files changed

+170
-9
lines changed

src/syscall/js/js.go

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,13 @@ func ValueOf(x any) Value {
210210
}
211211
}
212212

213+
// stringVal copies string x to Javascript and returns a ref.
214+
//
215+
// (noescape): This is safe because no references are maintained to the
216+
// Go string x after the syscall returns.
217+
//
213218
//go:wasmimport gojs syscall/js.stringVal
219+
//go:noescape
214220
func stringVal(x string) ref
215221

216222
// Type represents the JavaScript type of a Value.
@@ -294,7 +300,13 @@ func (v Value) Get(p string) Value {
294300
return r
295301
}
296302

303+
// valueGet returns a ref to JavaScript property p of ref v.
304+
//
305+
// (noescape): This is safe because no references are maintained to the
306+
// Go string p after the syscall returns.
307+
//
297308
//go:wasmimport gojs syscall/js.valueGet
309+
//go:noescape
298310
func valueGet(v ref, p string) ref
299311

300312
// Set sets the JavaScript property p of value v to ValueOf(x).
@@ -309,7 +321,13 @@ func (v Value) Set(p string, x any) {
309321
runtime.KeepAlive(xv)
310322
}
311323

324+
// valueSet sets property p of ref v to ref x.
325+
//
326+
// (noescape): This is safe because no references are maintained to the
327+
// Go string p after the syscall returns.
328+
//
312329
//go:wasmimport gojs syscall/js.valueSet
330+
//go:noescape
313331
func valueSet(v ref, p string, x ref)
314332

315333
// Delete deletes the JavaScript property p of value v.
@@ -322,7 +340,13 @@ func (v Value) Delete(p string) {
322340
runtime.KeepAlive(v)
323341
}
324342

343+
// valueDelete deletes the JavaScript property p of ref v.
344+
//
345+
// (noescape): This is safe because no references are maintained to the
346+
// Go string p after the syscall returns.
347+
//
325348
//go:wasmimport gojs syscall/js.valueDelete
349+
//go:noescape
326350
func valueDelete(v ref, p string)
327351

328352
// Index returns JavaScript index i of value v.
@@ -354,15 +378,36 @@ func (v Value) SetIndex(i int, x any) {
354378
//go:wasmimport gojs syscall/js.valueSetIndex
355379
func valueSetIndex(v ref, i int, x ref)
356380

357-
func makeArgs(args []any) ([]Value, []ref) {
358-
argVals := make([]Value, len(args))
359-
argRefs := make([]ref, len(args))
381+
// makeArgSlices makes two slices to hold JavaScript arg data.
382+
// It can be paired with storeArgs to make-and-store JavaScript arg slices.
383+
// However, the two functions are separated to ensure makeArgSlices is inlined
384+
// which will prevent the slices from being heap allocated for small (<=16)
385+
// numbers of args.
386+
func makeArgSlices(size int) (argVals []Value, argRefs []ref) {
387+
// value chosen for being power of two, and enough to handle all web APIs
388+
// in particular, note that WebGL2's texImage2D takes up to 10 arguments
389+
const maxStackArgs = 16
390+
if size <= maxStackArgs {
391+
// as long as makeArgs is inlined, these will be stack-allocated
392+
argVals = make([]Value, size, maxStackArgs)
393+
argRefs = make([]ref, size, maxStackArgs)
394+
} else {
395+
// allocates on the heap, but exceeding maxStackArgs should be rare
396+
argVals = make([]Value, size)
397+
argRefs = make([]ref, size)
398+
}
399+
return
400+
}
401+
402+
// storeArgs maps input args onto respective Value and ref slices.
403+
// It can be paired with makeArgSlices to make-and-store JavaScript arg slices.
404+
func storeArgs(args []any, argValsDst []Value, argRefsDst []ref) {
405+
// would go in makeArgs if the combined func was simple enough to inline
360406
for i, arg := range args {
361407
v := ValueOf(arg)
362-
argVals[i] = v
363-
argRefs[i] = v.ref
408+
argValsDst[i] = v
409+
argRefsDst[i] = v.ref
364410
}
365-
return argVals, argRefs
366411
}
367412

368413
// Length returns the JavaScript property "length" of v.
@@ -383,7 +428,8 @@ func valueLength(v ref) int
383428
// It panics if v has no method m.
384429
// The arguments get mapped to JavaScript values according to the ValueOf function.
385430
func (v Value) Call(m string, args ...any) Value {
386-
argVals, argRefs := makeArgs(args)
431+
argVals, argRefs := makeArgSlices(len(args))
432+
storeArgs(args, argVals, argRefs)
387433
res, ok := valueCall(v.ref, m, argRefs)
388434
runtime.KeepAlive(v)
389435
runtime.KeepAlive(argVals)
@@ -399,15 +445,24 @@ func (v Value) Call(m string, args ...any) Value {
399445
return makeValue(res)
400446
}
401447

448+
// valueCall does a JavaScript call to the method name m of ref v with the given arguments.
449+
//
450+
// (noescape): This is safe because no references are maintained to the
451+
// Go string m after the syscall returns. Additionally, the args slice
452+
// is only used temporarily to collect the JavaScript objects for
453+
// the JavaScript method invocation.
454+
//
402455
//go:wasmimport gojs syscall/js.valueCall
403456
//go:nosplit
457+
//go:noescape
404458
func valueCall(v ref, m string, args []ref) (ref, bool)
405459

406460
// Invoke does a JavaScript call of the value v with the given arguments.
407461
// It panics if v is not a JavaScript function.
408462
// The arguments get mapped to JavaScript values according to the ValueOf function.
409463
func (v Value) Invoke(args ...any) Value {
410-
argVals, argRefs := makeArgs(args)
464+
argVals, argRefs := makeArgSlices(len(args))
465+
storeArgs(args, argVals, argRefs)
411466
res, ok := valueInvoke(v.ref, argRefs)
412467
runtime.KeepAlive(v)
413468
runtime.KeepAlive(argVals)
@@ -420,14 +475,22 @@ func (v Value) Invoke(args ...any) Value {
420475
return makeValue(res)
421476
}
422477

478+
// valueInvoke does a JavaScript call to value v with the given arguments.
479+
//
480+
// (noescape): This is safe because the args slice is only used temporarily
481+
// to collect the JavaScript objects for the JavaScript method
482+
// invocation.
483+
//
423484
//go:wasmimport gojs syscall/js.valueInvoke
485+
//go:noescape
424486
func valueInvoke(v ref, args []ref) (ref, bool)
425487

426488
// New uses JavaScript's "new" operator with value v as constructor and the given arguments.
427489
// It panics if v is not a JavaScript function.
428490
// The arguments get mapped to JavaScript values according to the ValueOf function.
429491
func (v Value) New(args ...any) Value {
430-
argVals, argRefs := makeArgs(args)
492+
argVals, argRefs := makeArgSlices(len(args))
493+
storeArgs(args, argVals, argRefs)
431494
res, ok := valueNew(v.ref, argRefs)
432495
runtime.KeepAlive(v)
433496
runtime.KeepAlive(argVals)
@@ -440,7 +503,13 @@ func (v Value) New(args ...any) Value {
440503
return makeValue(res)
441504
}
442505

506+
// valueNew uses JavaScript's "new" operator with value v as a constructor and the given arguments.
507+
//
508+
// (noescape): This is safe because the args slice is only used temporarily
509+
// to collect the JavaScript objects for the constructor execution.
510+
//
443511
//go:wasmimport gojs syscall/js.valueNew
512+
//go:noescape
444513
func valueNew(v ref, args []ref) (ref, bool)
445514

446515
func (v Value) isNumber() bool {
@@ -543,7 +612,13 @@ func jsString(v Value) string {
543612
//go:wasmimport gojs syscall/js.valuePrepareString
544613
func valuePrepareString(v ref) (ref, int)
545614

615+
// valueLoadString loads string data located at ref v into byte slice b.
616+
//
617+
// (noescape): This is safe because the byte slice is only used as a destination
618+
// for storing the string data and references to it are not maintained.
619+
//
546620
//go:wasmimport gojs syscall/js.valueLoadString
621+
//go:noescape
547622
func valueLoadString(v ref, b []byte)
548623

549624
// InstanceOf reports whether v is an instance of type t according to JavaScript's instanceof operator.
@@ -581,7 +656,13 @@ func CopyBytesToGo(dst []byte, src Value) int {
581656
return n
582657
}
583658

659+
// copyBytesToGo copies bytes from src to dst.
660+
//
661+
// (noescape): This is safe because the dst byte slice is only used as a dst
662+
// copy buffer and no references to it are maintained.
663+
//
584664
//go:wasmimport gojs syscall/js.copyBytesToGo
665+
//go:noescape
585666
func copyBytesToGo(dst []byte, src ref) (int, bool)
586667

587668
// CopyBytesToJS copies bytes from src to dst.
@@ -596,5 +677,11 @@ func CopyBytesToJS(dst Value, src []byte) int {
596677
return n
597678
}
598679

680+
// copyBytesToJs copies bytes from src to dst.
681+
//
682+
// (noescape): This is safe because the src byte slice is only used as a src
683+
// copy buffer and no references to it are maintained.
684+
//
599685
//go:wasmimport gojs syscall/js.copyBytesToJS
686+
//go:noescape
600687
func copyBytesToJS(dst ref, src []byte) (int, bool)

src/syscall/js/js_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,80 @@ func TestGarbageCollection(t *testing.T) {
581581
}
582582
}
583583

584+
// This table is used for allocation tests. We expect a specific allocation
585+
// behavior to be seen, depending on the number of arguments applied to various
586+
// JavaScript functions.
587+
// Note: All JavaScript functions return a JavaScript array, which will cause
588+
// one allocation to be created to track the Value.gcPtr for the Value finalizer.
589+
var allocTests = []struct {
590+
argLen int // The number of arguments to use for the syscall
591+
expected int // The expected number of allocations
592+
}{
593+
// For less than or equal to 16 arguments, we expect 1 alloction:
594+
// - makeValue new(ref)
595+
{0, 1},
596+
{2, 1},
597+
{15, 1},
598+
{16, 1},
599+
// For greater than 16 arguments, we expect 3 alloction:
600+
// - makeValue: new(ref)
601+
// - makeArgSlices: argVals = make([]Value, size)
602+
// - makeArgSlices: argRefs = make([]ref, size)
603+
{17, 3},
604+
{32, 3},
605+
{42, 3},
606+
}
607+
608+
// TestCallAllocations ensures the correct allocation profile for Value.Call
609+
func TestCallAllocations(t *testing.T) {
610+
for _, test := range allocTests {
611+
args := make([]any, test.argLen)
612+
613+
tmpArray := js.Global().Get("Array").New(0)
614+
numAllocs := testing.AllocsPerRun(100, func() {
615+
tmpArray.Call("concat", args...)
616+
});
617+
618+
if numAllocs != float64(test.expected) {
619+
t.Errorf("got numAllocs %#v, want %#v", numAllocs, test.expected)
620+
}
621+
}
622+
}
623+
624+
// TestInvokeAllocations ensures the correct allocation profile for Value.Invoke
625+
func TestInvokeAllocations(t *testing.T) {
626+
for _, test := range allocTests {
627+
args := make([]any, test.argLen)
628+
629+
tmpArray := js.Global().Get("Array").New(0)
630+
concatFunc := tmpArray.Get("concat").Call("bind", tmpArray)
631+
numAllocs := testing.AllocsPerRun(100, func() {
632+
concatFunc.Invoke(args...)
633+
});
634+
635+
if numAllocs != float64(test.expected) {
636+
t.Errorf("got numAllocs %#v, want %#v", numAllocs, test.expected)
637+
}
638+
}
639+
}
640+
641+
// TestNewAllocations ensures the correct allocation profile for Value.New
642+
func TestNewAllocations(t *testing.T) {
643+
arrayConstructor := js.Global().Get("Array")
644+
645+
for _, test := range allocTests {
646+
args := make([]any, test.argLen)
647+
648+
numAllocs := testing.AllocsPerRun(100, func() {
649+
arrayConstructor.New(args...)
650+
});
651+
652+
if numAllocs != float64(test.expected) {
653+
t.Errorf("got numAllocs %#v, want %#v", numAllocs, test.expected)
654+
}
655+
}
656+
}
657+
584658
// BenchmarkDOM is a simple benchmark which emulates a webapp making DOM operations.
585659
// It creates a div, and sets its id. Then searches by that id and sets some data.
586660
// Finally it removes that div.

0 commit comments

Comments
 (0)