runtime, testing: support Goexit, SkipNow, FailNow#5493
Conversation
05448f8 to
493bcb5
Compare
There was a problem hiding this comment.
Pull request overview
Adds end-to-end support for runtime.Goexit in TinyGo, including integrating it with the testing package’s fatal/skip paths and ensuring thread-based schedulers can terminate goroutines cleanly.
Changes:
- Implement
Goexitas a special unwind/termination path in the panic/defer machinery and schedulers. - Update
testing/benchmarkrunners to execute test bodies in goroutines soFailNow/SkipNowcan terminate viaGoexitwithout killing the parent runner. - Add/extend test coverage and golden outputs for Goexit + fatal/skip behaviors.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| testdata/testing.txt | Updates expected output for new fatal subtest behavior. |
| testdata/testing.go | Adds t.Fatal/t.SkipNow subtests with arch/OS gating. |
| testdata/testing-wasm.txt | New expected output variant for wasm / windows-386 gating. |
| testdata/recover.txt | Extends expected output for Goexit + recovered deferred panics. |
| testdata/recover.go | Adds Goexit scenarios covering recovered/nested/loop defers. |
| testdata/goexit.go | New standalone Goexit scenario program for crash/deadlock assertions. |
| src/testing/testing.go | Implements FailNow/SkipNow via runtime.Goexit; runs subtests in goroutine. |
| src/testing/benchmark.go | Runs benchmarks in goroutine to support Goexit semantics. |
| src/runtime/scheduler_threads.go | Adds scheduler goexit() path for thread-backed tasks. |
| src/runtime/scheduler_none.go | Adds goexit() implementation for no-scheduler builds. |
| src/runtime/scheduler_cores.go | Adds goexit() for multicore scheduler builds. |
| src/runtime/scheduler_cooperative.go | Adds goexit() for tasks/asyncify cooperative scheduler. |
| src/runtime/runtime_windows.go | Defines abort() on Windows instead of relying on an external symbol. |
| src/runtime/panic.go | Tracks pending-Goexit across recovered deferred panics; calls goexit() on Goexit completion. |
| src/internal/task/task_threads.go | Adds task-level Exit() and refactors deadlock tracking for thread scheduler. |
| src/internal/task/task_threads.c | Adds tinygo_task_exit() using pthread_exit. |
| src/internal/task/task_stack.go | Hooks task lifecycle accounting into stack-based schedulers. |
| src/internal/task/task_exit.go | Introduces task lifecycle/deadlock logic for tasks/asyncify/cores schedulers. |
| src/internal/task/task_asyncify.go | Hooks task lifecycle accounting into asyncify scheduler start. |
| main_test.go | Adds Goexit crash/deadlock regression test; adjusts expected-output routing for testing.go on wasm/win386. |
| GNUmakefile | Adds crypto/ecdh to Linux test packages; updates skip rationale comment. |
| compiler/testdata/defer-cortex-m-qemu.ll | Updates deferFrame layout expectations for new Goexit flag field. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import "sync/atomic" | ||
|
|
||
| var ( | ||
| mainTask *Task | ||
| liveTasks uint32 | ||
| mainExitedByGoexit uint32 | ||
| ) | ||
|
|
||
| func addLiveTask(t *Task) { | ||
| if mainTask == nil { | ||
| mainTask = t | ||
| } | ||
| atomic.AddUint32(&liveTasks, 1) | ||
| } | ||
|
|
||
| // Exit exits the current task because runtime.Goexit was called. | ||
| func Exit() { | ||
| exit(true) | ||
| } | ||
|
|
||
| func exit(goexit bool) { | ||
| t := Current() | ||
| remaining := atomic.AddUint32(&liveTasks, ^uint32(0)) | ||
| if t == mainTask { |
There was a problem hiding this comment.
I don't think so? the main task is the main task, so it should only happen once, first.
1918a97 to
d073e86
Compare
| done := make(chan struct{}) | ||
| go func() { | ||
| defer close(done) | ||
| tRunner(&sub, f) | ||
| }() | ||
| <-done |
There was a problem hiding this comment.
The fact that this needs to be done is why I had to add more memory to the encoding/xml tests.
Now that tests run on a new goroutine, their stacks are way smaller. On my mac (where I was debugging this), the main thread has something like 8MB, but the new goroutine only has like 512KB or something. The xml tests have a big test that seems to take somewhere between 4MB and 8MB based on what worked in CI.
e631e70 to
429be4b
Compare
39ed9eb to
ee3f918
Compare
Keep a pending Goexit separate from the active panic state so recovered deferred panics do not cancel the original Goexit unwind. Goexit should also ignore -panic=trap, since it is not a panic. Wire testing.FailNow and SkipNow through runtime.Goexit, and run tests and benchmarks in goroutines. This lets Goexit terminate the user function without skipping test bookkeeping. Add Goexit/recover coverage and enable crypto/ecdh in the Linux stdlib test list.
The threads scheduler handled runtime.Goexit using the generic deadlock path, which parked the current pthread forever. Add a scheduler-specific goexit hook so it can remove the current task and exit the pthread while ordinary blocking deadlocks still block. Track main goroutine Goexit so the last remaining goroutine reports the expected deadlock instead of letting the process exit successfully. Expand the crash coverage with the Goexit cases covered by Big Go's runtime tests.
Top-level tests now run in goroutines, which puts stdlib tests on TinyGo's default host goroutine stack. encoding/xml's depth-limit test needs a larger stack on Linux and Darwin, so run just that package with a larger stack while leaving the rest of the host stdlib tests on the default stack.
Treat panicState as a bitmask so a recovered panic during Goexit can clear the panic bit while preserving the pending Goexit bit. This removes the separate deferFrame Goexit field and restores the previous defer frame size.
| movl %ebx, 8(%ebx) | ||
| movl %esi, 12(%ebx) | ||
| movl %edi, 16(%ebx) | ||
| movl %ebp, 20(%ebx) | ||
| 1:` | ||
| constraints = "={eax},{ebx},~{ebx},~{ecx},~{edx},~{esi},~{edi},~{ebp},~{xmm0},~{xmm1},~{xmm2},~{xmm3},~{xmm4},~{xmm5},~{xmm6},~{xmm7},~{fpsr},~{fpcr},~{flags},~{dirflag},~{memory}" | ||
| constraints = "={eax},{ebx},~{ecx},~{edx},~{xmm0},~{xmm1},~{xmm2},~{xmm3},~{xmm4},~{xmm5},~{xmm6},~{xmm7},~{fpsr},~{fpcr},~{flags},~{dirflag},~{memory}" |
There was a problem hiding this comment.
I'll be honest and say I don't have a great understanding of this; sort of like how I had to deal with clobbering problems in my previous PR, the crash is also clobbering, but seemingly so much so that I can't make LLVM do the right thing, and instead need to manually save everything.
In #5438 I expanded out panic/recover behavior to cover a lot of (all?) cases.
I forgot about
Goexit.This PR adds support for it, including the work to make it exit pthreads.