mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-09-11 15:09:32 +00:00
runtime: run syscall/js finalizers on wasm without a manual GC (#5545)
* runtime: run syscall/js finalizers on wasm without a manual GC * runtime: address review feedback on finalizer idle GC * runtime: clear a finished task's args pointer so its arguments are collectable * runtime: skip the finalizer scan with a per-block registration bit * runtime: guard the finalizer registration bitmap with gcLock * testdata: cover finalizer invariants on every scheduler * main_test: limit the finalizer scheduler variants to linux and darwin * testdata: wait for the finalizer queue to drain before asserting * testdata: make the finalizer counters atomic and wait for a known drain count * runtime: add finalizer bookkeeping asserts under runtime_asserts * runtime: address finalizer GC review feedback * testdata: strengthen blocked stack finalizer test * runtime: fix finalizer cleanup edge cases * runtime: decouple wasm export scheduling from finalizers * runtime: avoid redundant wakeups for re-entrant wasm exports * runtime: simplify finalizer comments
This commit is contained in:
@@ -39,8 +39,13 @@ var (
|
||||
runqueue task.Queue
|
||||
sleepQueue *task.Task
|
||||
sleepQueueBaseTime timeUnit
|
||||
deadlockedTasks task.Queue
|
||||
)
|
||||
|
||||
// finalizerIdleGC runs pressure GC at safe points and enables asyncify stack cleanup.
|
||||
// The first finalizer installs it so unused code can be removed.
|
||||
var finalizerIdleGC func() bool
|
||||
|
||||
// deadlock is called when a goroutine cannot proceed any more, but is in theory
|
||||
// not exited (so deferred calls won't run). This can happen for example in code
|
||||
// like this, that blocks forever:
|
||||
@@ -49,12 +54,41 @@ var (
|
||||
//
|
||||
//go:noinline
|
||||
func deadlock() {
|
||||
// call yield without requesting a wakeup
|
||||
// Keep permanently blocked tasks reachable so their suspended stacks remain
|
||||
// GC roots, but never put them back on the runnable queue.
|
||||
deadlockedTasks.Push(task.Current())
|
||||
task.Pause()
|
||||
runtimeFatal("unreachable")
|
||||
}
|
||||
|
||||
// exitGoroutine ends an asyncify task that returned from its function.
|
||||
// Unlike deadlock, this task will not resume.
|
||||
func exitGoroutine() {
|
||||
if finalizerIdleGC != nil {
|
||||
task.MarkFinishing()
|
||||
}
|
||||
task.Pause()
|
||||
runtimeFatal("unreachable")
|
||||
}
|
||||
|
||||
// wasmExportExit stops the scheduler after a //go:wasmexport function returns.
|
||||
// It is not used when the scheduler is disabled.
|
||||
func wasmExportExit() {
|
||||
schedulerExit = true
|
||||
if finalizerIdleGC != nil {
|
||||
task.MarkFinishing()
|
||||
}
|
||||
|
||||
task.Pause()
|
||||
|
||||
// TODO: we could cache the allocated stack so we don't have to keep
|
||||
// allocating a new stack on every //go:wasmexport call.
|
||||
}
|
||||
|
||||
func goexit() {
|
||||
if finalizerIdleGC != nil {
|
||||
task.MarkFinishing()
|
||||
}
|
||||
task.Exit()
|
||||
}
|
||||
|
||||
@@ -183,6 +217,11 @@ func scheduler(returnAtDeadlock bool) {
|
||||
|
||||
t := runqueue.Pop()
|
||||
if t == nil {
|
||||
// Run the pressure GC only when the scheduler is idle at the top level.
|
||||
// This batches completed work and avoids collections during allocation.
|
||||
if task.Current() == nil && finalizerIdleGC != nil && finalizerIdleGC() {
|
||||
continue
|
||||
}
|
||||
if sleepQueue == nil && timerQueue == nil {
|
||||
if returnAtDeadlock {
|
||||
return
|
||||
@@ -236,6 +275,16 @@ func scheduler(returnAtDeadlock bool) {
|
||||
// //go:wasmexport function returned.
|
||||
if GOARCH == "wasm" && schedulerExit {
|
||||
schedulerExit = false // reset the signal
|
||||
if task.Current() == nil {
|
||||
if finalizerIdleGC != nil {
|
||||
finalizerIdleGC()
|
||||
}
|
||||
// Return from an export at the top level before unrelated goroutines run.
|
||||
// A nested export returns to its active outer scheduler.
|
||||
if asyncScheduler && (!runqueue.Empty() || sleepQueue != nil || timerQueue != nil) {
|
||||
sleepTicks(0)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user