all: rewrite goroutine lowering

Before this commit, goroutine support was spread through the compiler.
This commit changes this support, so that the compiler itself only
generates simple intrinsics and leaves the real support to a compiler
pass that runs as one of the TinyGo-specific optimization passes.

The biggest change, that was done together with the rewrite, was support
for goroutines in WebAssembly for JavaScript. The challenge in
JavaScript is that in general no blocking operations are allowed, which
means that programs that call time.Sleep() but do not start goroutines
also have to be scheduled by the scheduler.
This commit is contained in:
Ayke van Laethem
2019-01-10 16:54:09 +01:00
parent 072ef603fe
commit 602c264749
24 changed files with 807 additions and 483 deletions
+5 -5
View File
@@ -10,8 +10,8 @@ const Compiler = "tgo"
// package.
func initAll()
// The compiler will insert the call to main.main() here, depending on whether
// the scheduler is necessary.
// A function call to this function is replaced withone of the following,
// depending on whether the scheduler is necessary:
//
// Without scheduler:
//
@@ -19,9 +19,9 @@ func initAll()
//
// With scheduler:
//
// coroutine := main.main(nil)
// scheduler(coroutine)
func mainWrapper()
// main.main()
// scheduler()
func callMain()
func GOMAXPROCS(n int) int {
// Note: setting GOMAXPROCS is ignored.
+3 -1
View File
@@ -15,7 +15,7 @@ type timeUnit int64
func main() {
preinit()
initAll()
mainWrapper()
callMain()
abort()
}
@@ -238,6 +238,8 @@ type isrFlag bool
var timerWakeup isrFlag
const asyncScheduler = false
// sleepTicks should sleep for d number of microseconds.
func sleepTicks(d timeUnit) {
for d != 0 {
+3 -1
View File
@@ -41,7 +41,7 @@ func main() {
preinit()
initAll()
postinit()
mainWrapper()
callMain()
abort()
}
@@ -71,6 +71,8 @@ func putchar(c byte) {
machine.UART0.WriteByte(c)
}
const asyncScheduler = false
// Sleep this number of ticks of 16ms.
//
// TODO: not very accurate. Improve accuracy by calibrating on startup and every
+3 -1
View File
@@ -20,7 +20,7 @@ func main() {
systemInit()
preinit()
initAll()
mainWrapper()
callMain()
abort()
}
@@ -50,6 +50,8 @@ func putchar(c byte) {
machine.UART0.WriteByte(c)
}
const asyncScheduler = false
func sleepTicks(d timeUnit) {
for d != 0 {
ticks() // update timestamp
+3 -1
View File
@@ -20,11 +20,13 @@ var timestamp timeUnit
func main() {
preinit()
initAll()
mainWrapper()
callMain()
arm.SemihostingCall(arm.SemihostingReportException, arm.SemihostingApplicationExit)
abort()
}
const asyncScheduler = false
func sleepTicks(d timeUnit) {
// TODO: actually sleep here for the given time.
timestamp += d
+1 -1
View File
@@ -8,6 +8,6 @@ type timeUnit int64
func main() {
preinit()
initAll()
mainWrapper()
callMain()
abort()
}
+2
View File
@@ -107,6 +107,8 @@ func initTIM() {
arm.EnableIRQ(stm32.IRQ_TIM3)
}
const asyncScheduler = false
// sleepTicks should sleep for specific number of microseconds.
func sleepTicks(d timeUnit) {
for d != 0 {
+4 -2
View File
@@ -46,8 +46,8 @@ func main() int {
// Run initializers of all packages.
initAll()
// Compiler-generated wrapper to main.main().
mainWrapper()
// Compiler-generated call to main.main().
callMain()
// For libc compatibility.
return 0
@@ -57,6 +57,8 @@ func putchar(c byte) {
_putchar(int(c))
}
const asyncScheduler = false
func sleepTicks(d timeUnit) {
usleep(uint(d) / 1000)
}
+15 -11
View File
@@ -6,11 +6,9 @@ import (
"unsafe"
)
type timeUnit int64
type timeUnit float64 // time in milliseconds, just like Date.now() in JavaScript
const tickMicros = 1
var timestamp timeUnit
const tickMicros = 1000000
//go:export io_get_stdout
func io_get_stdout() int32
@@ -32,21 +30,27 @@ func _start() {
//go:export cwa_main
func cwa_main() {
initAll() // _start is not called by olin/cwa so has to be called here
mainWrapper()
callMain()
}
func putchar(c byte) {
resource_write(stdout, &c, 1)
}
func sleepTicks(d timeUnit) {
// TODO: actually sleep here for the given time.
timestamp += d
//go:export go_scheduler
func go_scheduler() {
scheduler()
}
func ticks() timeUnit {
return timestamp
}
const asyncScheduler = true
// This function is called by the scheduler.
// Schedule a call to runtime.scheduler, do not actually sleep.
//go:export runtime.sleepTicks
func sleepTicks(d timeUnit)
//go:export runtime.ticks
func ticks() timeUnit
// Abort executes the wasm 'unreachable' instruction.
func abort() {
+29 -68
View File
@@ -9,14 +9,13 @@ package runtime
// * A blocking function that calls a non-blocking function is called as
// usual.
// * A blocking function that calls a blocking function passes its own
// coroutine handle as a parameter to the subroutine and will make sure it's
// own coroutine is removed from the scheduler. When the subroutine returns,
// it will re-insert the parent into the scheduler.
// coroutine handle as a parameter to the subroutine. When the subroutine
// returns, it will re-insert the parent into the scheduler.
// Note that a goroutine is generally called a 'task' for brevity and because
// that's the more common term among RTOSes. But a goroutine and a task are
// basically the same thing. Although, the code often uses the word 'task' to
// refer to both a coroutine and a goroutine, as most of the scheduler isn't
// aware of the difference.
// refer to both a coroutine and a goroutine, as most of the scheduler doesn't
// care about the difference.
//
// For more background on coroutines in LLVM:
// https://llvm.org/docs/Coroutines.html
@@ -45,25 +44,19 @@ func (t *coroutine) _promise(alignment int32, from bool) unsafe.Pointer
// Get the promise belonging to a task.
func (t *coroutine) promise() *taskState {
return (*taskState)(t._promise(4, false))
return (*taskState)(t._promise(int32(unsafe.Alignof(taskState{})), false))
}
func makeGoroutine(*uint8) *uint8
// State/promise of a task. Internally represented as:
//
// {i8 state, i32 data, i8* next}
// {i8* next, i32/i64 data}
type taskState struct {
state uint8
data uint32
next *coroutine
next *coroutine
data uint
}
// Various states a task can be in.
const (
TASK_STATE_RUNNABLE = iota
TASK_STATE_SLEEP
TASK_STATE_CALL // waiting for a sub-coroutine
)
// Queues used by the scheduler.
//
// TODO: runqueueFront can be removed by making the run queue a circular linked
@@ -89,48 +82,28 @@ func scheduleLogTask(msg string, t *coroutine) {
}
}
// Set the task state to sleep for a given time.
// Set the task to sleep for a given time.
//
// This is a compiler intrinsic.
func sleepTask(caller *coroutine, duration int64) {
if schedulerDebug {
println(" set state sleep:", caller, uint32(duration/tickMicros))
println(" set sleep:", caller, uint(duration/tickMicros))
}
promise := caller.promise()
promise.state = TASK_STATE_SLEEP
promise.data = uint32(duration / tickMicros) // TODO: longer durations
promise.data = uint(duration / tickMicros) // TODO: longer durations
addSleepTask(caller)
}
// Wait for the result of an async call. This means that the parent goroutine
// will be removed from the runqueue and be rescheduled by the callee.
// Add a non-queued task to the run queue.
//
// This is a compiler intrinsic.
func waitForAsyncCall(caller *coroutine) {
scheduleLogTask(" set state call:", caller)
promise := caller.promise()
promise.state = TASK_STATE_CALL
}
// Add a task to the runnable or sleep queue, depending on the state.
//
// This is a compiler intrinsic.
func yieldToScheduler(t *coroutine) {
if t == nil {
// This is a compiler intrinsic, and is called from a callee to reactivate the
// caller.
func activateTask(task *coroutine) {
if task == nil {
return
}
// See what we should do with this task: try to execute it directly
// again or let it sleep for a bit.
promise := t.promise()
if promise.state == TASK_STATE_CALL {
scheduleLogTask(" set waiting for call:", t)
return // calling an async task, the subroutine will re-active the parent
} else if promise.state == TASK_STATE_SLEEP && promise.data != 0 {
scheduleLogTask(" set sleeping:", t)
addSleepTask(t)
} else {
scheduleLogTask(" set runnable:", t)
runqueuePushBack(t)
}
scheduleLogTask(" set runnable:", task)
runqueuePushBack(task)
}
// Add this task to the end of the run queue. May also destroy the task if it's
@@ -145,9 +118,6 @@ func runqueuePushBack(t *coroutine) {
if t.promise().next != nil {
panic("runtime: runqueuePushBack: expected next task to be nil")
}
if t.promise().state != TASK_STATE_RUNNABLE {
panic("runtime: runqueuePushBack: expected task state to be runnable")
}
}
if runqueueBack == nil { // empty runqueue
scheduleLogTask(" add to runqueue front:", t)
@@ -169,10 +139,6 @@ func runqueuePopFront() *coroutine {
}
if schedulerDebug {
println(" runqueuePopFront:", t)
// Sanity checking.
if t.promise().state != TASK_STATE_RUNNABLE {
panic("runtime: runqueuePopFront: task not runnable")
}
}
promise := t.promise()
runqueueFront = promise.next
@@ -190,9 +156,6 @@ func addSleepTask(t *coroutine) {
if t.promise().next != nil {
panic("runtime: addSleepTask: expected next task to be nil")
}
if t.promise().state != TASK_STATE_SLEEP {
panic("runtime: addSleepTask: task not sleeping")
}
}
now := ticks()
if sleepQueue == nil {
@@ -236,11 +199,7 @@ func addSleepTask(t *coroutine) {
}
// Run the scheduler until all tasks have finished.
// It takes an initial task (main.main) to bootstrap.
func scheduler(main *coroutine) {
// Initial task.
yieldToScheduler(main)
func scheduler() {
// Main scheduler loop.
for {
scheduleLog("\n schedule")
@@ -254,7 +213,6 @@ func scheduler(main *coroutine) {
promise := t.promise()
sleepQueueBaseTime += timeUnit(promise.data)
sleepQueue = promise.next
promise.state = TASK_STATE_RUNNABLE
promise.next = nil
runqueuePushBack(t)
}
@@ -271,9 +229,15 @@ func scheduler(main *coroutine) {
}
timeLeft := timeUnit(sleepQueue.promise().data) - (now - sleepQueueBaseTime)
if schedulerDebug {
println(" sleeping...", sleepQueue, uint32(timeLeft))
println(" sleeping...", sleepQueue, uint(timeLeft))
}
sleepTicks(timeUnit(timeLeft))
if asyncScheduler {
// The sleepTicks function above only sets a timeout at which
// point the scheduler will be called again. It does not really
// sleep.
break
}
continue
}
@@ -281,8 +245,5 @@ func scheduler(main *coroutine) {
scheduleLog(" <- runqueuePopFront")
scheduleLogTask(" run:", t)
t.resume()
// Add the just resumed task to the run queue or the sleep queue.
yieldToScheduler(t)
}
}