Repository: go
Issue: runtime: wasm GC causes stop the world for a very long time
Labels: Performance,NeedsInvestigation,arch-wasm,compiler/runtime

### What version of Go are you using (`go version`)?

<pre>
$ go version
go version go1.19 linux/amd64
</pre>

### Does this issue reproduce with the latest release?

Yes

### What operating system and processor architecture are you using (`go env`)?

<details><summary><code>go env</code> Output</summary><br><pre>
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/user/.cache/go-build"
GOENV="/home/user/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/user/.go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/user/.go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go-1.19"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go-1.19/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.19"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/dev/null"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build306969192=/tmp/go-build -gno-record-gcc-switches"
</pre></details>

### What did you do?

I am working on a hobby game engine that has both Native (OpenGL) and WASM (WebGL2) support.

When running a larger scene in WebAssembly I notice that whenever a GC is triggered it causes everything to freeze for about a second or more.

Actually, it happens with smaller scenes too but just less often (due to GC triggering less often). I think that the duration of the freeze is proportional to the number of active pointer references but this needs more testing to verify.

I try not to allocate much memory but it happens that once a frame I might allocate a few KBs here and there (I have a custom react-style Go UI framework that is hard to make allocation-free due to tree invalidation logic and besides, one should be able to make use of Go's memory management). This means that after 15s-60s everything just freezes.

I managed to write a dummy program that reproduces the issue:

```go
var (
	statsRunning int32
	gcStats      = new(debug.GCStats)
	cached       []byte
)

func main() {
	// Force the GC to trigger more often. If you remove this, you might need
	// to wait up to a minute in some cases.
	debug.SetGCPercent(5)

	aLotOfData := make([]int, 20_000_000)
	aLotOfRefData := make([]*int, 20_000_000)
	for i := range aLotOfRefData {
		aLotOfRefData[i] = &aLotOfData[i]
	}

	callbackFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
		frameStart := time.Now()

		// Check stats once in a while.
		if atomic.CompareAndSwapInt32(&statsRunning, 0, 1) {
			go func() {
				debug.ReadGCStats(gcStats)
				if len(gcStats.Pause) > 0 {
					log.Printf("GC STATS! Total: %d; Time since last GC: %s; Pause total: %s; Pause last: %s",
						gcStats.NumGC,
						time.Since(gcStats.LastGC),
						gcStats.PauseTotal,
						gcStats.Pause[0],
					)
				}
				atomic.StoreInt32(&statsRunning, 0)
			}()
		}

		processingTime := time.Now()

		// Do some processing so that the frame takes some time and so that the
		// runtime is forced to go through a lot of pointers (not sure if relevant).
		for i := 0; i < len(aLotOfRefData); i++ {
			*aLotOfRefData[i]++
		}

		// Couldn't figure out a better way to force the compiler not to optimize
		// the slice allocation. Hence keep it in a variable and use it next frame.
		if cached == nil {
			cached = make([]byte, 1_000_000)
		} else {
			for j := range cached {
				cached[j]++
			}
			cached = nil
		}

		processingDuration := time.Since(processingTime)

		log.Printf("Frame time: %s (processing: %s)", time.Since(frameStart), processingDuration)
		return nil
	})

	intervalFunc := js.Global().Get("setInterval")
	intervalFunc.Invoke(callbackFunc, 1)

	<-make(chan struct{})
}
```

### What did you expect to see?

Smooth and consistent logging in the Browser Console.

### What did you see instead?

A stop the world situation once in a while (every 2 seconds or so). The `Total` counter (indicating the number of GC runs) always increases immediatelly after the freeze, showing that it is related to GC.

What's interesting is that none of the stats (even the frame time) indicate that GC took more time than indicated but the fact is that the Browser freezes. Could it be that Go is doing something that JS or the Browser does not like and needs to adjust to once setInterval callback has completed (e.g. memory shuffling or something of the kind)?

This is reproducible with both Chrome and Firefox. Have not tried other browsers.