This commit adds support for a scheduler that runs a scheduler on all
available cores. It is meant to be used on baremetal systems with a
fixed number of cores, such as the RP2040.
The initial implementation adds support for multicore scheduling to the
riscv-qemu target as a convenient testing target. This means that this
new multicore scheduler is tested in CI, including a bunch of standard
library tests (`make tinygo-test-baremetal`). This should ensure the new
scheduler is reasonably well tested before trying to use it on
harder-to-debug targets like the RP2040.
This is more descriptive: the call is to exit a task, not to pause it.
This also makes it more obvious that there's an optimization
opportunity: to free the stack explicitly after the goroutine returns
(or to keep it as a cache for the next stack allocation).
This moves all scheduler code into a separate file that is only compiled
when there's a scheduler in use (the tasks or asyncify scheduler, which
are both cooperative). The main goal of this change is to make it easier
to add a new "scheduler" based on OS threads.
It also fixes a few subtle issues with `-gc=none`:
- Gosched() panicked. This is now fixed to just return immediately
(the only logical thing to do when there's only one goroutine).
- Timers aren't supported without a scheduler, but the relevant code
was still present and would happily add a timer to the queue. It
just never ran. So now it exits with a runtime error, similar to any
blocking operation.
With a few small modifications, all the problems with `-gc=precise` in
WebAssembly seem to have been fixed.
I didn't do any performance measurements, but this is supposed to
improve GC performance.
Blocking inside an interrupt is always unsafe and will lead to all kinds
of bad behavior (even if it might appear to work sometimes). So disallow
it, just like allocating heap memory inside an interrupt is not allowed.
I suspect this will usually be caused by channel sends, like this:
ch <- someValue
The easy workaround is to make it a non-blocking send instead:
select {
case ch <- someValue:
default:
}
This does mean the application might become a bit more complex to be
able to deal with this case, but the alternative (undefined behavior) is
IMHO much worse.
This implements the block-based GC as a partially precise GC. This means
that for most heap allocations it is known which words contain a pointer
and which don't. This should in theory make the GC faster (because it
can skip non-pointer object) and have fewer false positives in a GC
cycle. It does however use a bit more RAM to store the layout of each
object.
Right now this GC seems to be slower than the conservative GC, but
should be less likely to run out of memory as a result of false
positives.
Go 1.19 started reformatting code in a way that makes it more obvious
how it will be rendered on pkg.go.dev. It gets it almost right, but not
entirely. Therefore, I had to modify some of the comments so that they
are formatted correctly.
This change swaps the stack chain when switching goroutines, ensuring that the chain is maintained consistently.
This is only really currently necessary with asyncify on wasm.
The Cortex-M target isn't much changed, but much of the logic for the
AVR stack switcher that was previously in assembly has now been moved to
Go to make it more maintainable and in fact smaller in code size. Three
functions (tinygo_getCurrentStackPointer, tinygo_switchToTask,
tinygo_switchToScheduler) have been changed to one: tinygo_swapTask.
This reduction in assembly code should make the code more maintainable
and should make it easier to port stack switching to other
architectures.
I've also moved the assembly files to src/internal/task, which seems
like a more appropriate location to me.
This is a big change that will determine the stack size for many
goroutines automatically. Functions that aren't recursive and don't call
function pointers can in many cases have an automatically determined
worst case stack size. This is useful, as the stack size is usually much
lower than the previous hardcoded default of 1024 bytes: somewhere
around 200-500 bytes is common.
A side effect of this change is that the default stack sizes (including
the stack size for other architectures such as AVR) can now be changed
in the config JSON file, making it tunable per application.