
This commit adds support for time.NewTimer and time.NewTicker. It also adds support for the Stop() method on time.Timer, but doesn't (yet) add support for the Reset() method. The implementation has been carefully written so that programs that don't use these timers will normally not see an increase in RAM or binary size. None of the examples in the drivers repo change as a result of this commit. This comes at the cost of slightly more complex code and possibly slower execution of the timers when they are used.
261 строка
6,6 КиБ
Go
261 строка
6,6 КиБ
Go
package runtime
|
|
|
|
// This file implements the TinyGo scheduler. This scheduler is a very simple
|
|
// cooperative round robin scheduler, with a runqueue that contains a linked
|
|
// list of goroutines (tasks) that should be run next, in order of when they
|
|
// were added to the queue (first-in, first-out). It also contains a sleep queue
|
|
// with sleeping goroutines in order of when they should be re-activated.
|
|
//
|
|
// The scheduler is used both for the asyncify based scheduler and for the task
|
|
// based scheduler. In both cases, the 'internal/task.Task' type is used to represent one
|
|
// goroutine.
|
|
|
|
import (
|
|
"internal/task"
|
|
"runtime/interrupt"
|
|
)
|
|
|
|
const schedulerDebug = false
|
|
|
|
// On JavaScript, we can't do a blocking sleep. Instead we have to return and
|
|
// queue a new scheduler invocation using setTimeout.
|
|
const asyncScheduler = GOOS == "js"
|
|
|
|
var schedulerDone bool
|
|
|
|
// Queues used by the scheduler.
|
|
var (
|
|
runqueue task.Queue
|
|
sleepQueue *task.Task
|
|
sleepQueueBaseTime timeUnit
|
|
timerQueue *timerNode
|
|
)
|
|
|
|
// Simple logging, for debugging.
|
|
func scheduleLog(msg string) {
|
|
if schedulerDebug {
|
|
println("---", msg)
|
|
}
|
|
}
|
|
|
|
// Simple logging with a task pointer, for debugging.
|
|
func scheduleLogTask(msg string, t *task.Task) {
|
|
if schedulerDebug {
|
|
println("---", msg, t)
|
|
}
|
|
}
|
|
|
|
// Simple logging with a channel and task pointer.
|
|
func scheduleLogChan(msg string, ch *channel, t *task.Task) {
|
|
if schedulerDebug {
|
|
println("---", msg, ch, t)
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
//
|
|
// select{}
|
|
//
|
|
//go:noinline
|
|
func deadlock() {
|
|
// call yield without requesting a wakeup
|
|
task.Pause()
|
|
panic("unreachable")
|
|
}
|
|
|
|
// Goexit terminates the currently running goroutine. No other goroutines are affected.
|
|
//
|
|
// Unlike the main Go implementation, no deffered calls will be run.
|
|
//
|
|
//go:inline
|
|
func Goexit() {
|
|
// its really just a deadlock
|
|
deadlock()
|
|
}
|
|
|
|
// Add this task to the end of the run queue.
|
|
func runqueuePushBack(t *task.Task) {
|
|
runqueue.Push(t)
|
|
}
|
|
|
|
// Add this task to the sleep queue, assuming its state is set to sleeping.
|
|
func addSleepTask(t *task.Task, duration timeUnit) {
|
|
if schedulerDebug {
|
|
println(" set sleep:", t, duration)
|
|
if t.Next != nil {
|
|
panic("runtime: addSleepTask: expected next task to be nil")
|
|
}
|
|
}
|
|
t.Data = uint64(duration)
|
|
now := ticks()
|
|
if sleepQueue == nil {
|
|
scheduleLog(" -> sleep new queue")
|
|
|
|
// set new base time
|
|
sleepQueueBaseTime = now
|
|
}
|
|
|
|
// Add to sleep queue.
|
|
q := &sleepQueue
|
|
for ; *q != nil; q = &(*q).Next {
|
|
if t.Data < (*q).Data {
|
|
// this will finish earlier than the next - insert here
|
|
break
|
|
} else {
|
|
// this will finish later - adjust delay
|
|
t.Data -= (*q).Data
|
|
}
|
|
}
|
|
if *q != nil {
|
|
// cut delay time between this sleep task and the next
|
|
(*q).Data -= t.Data
|
|
}
|
|
t.Next = *q
|
|
*q = t
|
|
}
|
|
|
|
// addTimer adds the given timer node to the timer queue. It must not be in the
|
|
// queue already.
|
|
// This function is very similar to addSleepTask but for timerQueue instead of
|
|
// sleepQueue.
|
|
func addTimer(tim *timerNode) {
|
|
mask := interrupt.Disable()
|
|
|
|
// Add to timer queue.
|
|
q := &timerQueue
|
|
for ; *q != nil; q = &(*q).next {
|
|
if tim.whenTicks() < (*q).whenTicks() {
|
|
// this will finish earlier than the next - insert here
|
|
break
|
|
}
|
|
}
|
|
tim.next = *q
|
|
*q = tim
|
|
interrupt.Restore(mask)
|
|
}
|
|
|
|
// removeTimer is the implementation of time.stopTimer. It removes a timer from
|
|
// the timer queue, returning true if the timer is present in the timer queue.
|
|
func removeTimer(tim *timer) bool {
|
|
removedTimer := false
|
|
mask := interrupt.Disable()
|
|
for t := &timerQueue; *t != nil; t = &(*t).next {
|
|
if (*t).timer == tim {
|
|
scheduleLog("removed timer")
|
|
*t = (*t).next
|
|
removedTimer = true
|
|
break
|
|
}
|
|
}
|
|
if !removedTimer {
|
|
scheduleLog("did not remove timer")
|
|
}
|
|
interrupt.Restore(mask)
|
|
return removedTimer
|
|
}
|
|
|
|
// Run the scheduler until all tasks have finished.
|
|
func scheduler() {
|
|
// Main scheduler loop.
|
|
var now timeUnit
|
|
for !schedulerDone {
|
|
scheduleLog("")
|
|
scheduleLog(" schedule")
|
|
if sleepQueue != nil || timerQueue != nil {
|
|
now = ticks()
|
|
}
|
|
|
|
// Add tasks that are done sleeping to the end of the runqueue so they
|
|
// will be executed soon.
|
|
if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.Data) {
|
|
t := sleepQueue
|
|
scheduleLogTask(" awake:", t)
|
|
sleepQueueBaseTime += timeUnit(t.Data)
|
|
sleepQueue = t.Next
|
|
t.Next = nil
|
|
runqueue.Push(t)
|
|
}
|
|
|
|
// Check for expired timers to trigger.
|
|
if timerQueue != nil && now >= timerQueue.whenTicks() {
|
|
scheduleLog("--- timer awoke")
|
|
// Pop timer from queue.
|
|
tn := timerQueue
|
|
timerQueue = tn.next
|
|
tn.next = nil
|
|
// Run the callback stored in this timer node.
|
|
tn.callback(tn)
|
|
}
|
|
|
|
t := runqueue.Pop()
|
|
if t == nil {
|
|
if sleepQueue == nil && timerQueue == nil {
|
|
if asyncScheduler {
|
|
// JavaScript is treated specially, see below.
|
|
return
|
|
}
|
|
waitForEvents()
|
|
continue
|
|
}
|
|
|
|
var timeLeft timeUnit
|
|
if sleepQueue != nil {
|
|
timeLeft = timeUnit(sleepQueue.Data) - (now - sleepQueueBaseTime)
|
|
}
|
|
if timerQueue != nil {
|
|
timeLeftForTimer := timerQueue.whenTicks() - now
|
|
if sleepQueue == nil || timeLeftForTimer < timeLeft {
|
|
timeLeft = timeLeftForTimer
|
|
}
|
|
}
|
|
|
|
if schedulerDebug {
|
|
println(" sleeping...", sleepQueue, uint(timeLeft))
|
|
for t := sleepQueue; t != nil; t = t.Next {
|
|
println(" task sleeping:", t, timeUnit(t.Data))
|
|
}
|
|
for tim := timerQueue; tim != nil; tim = tim.next {
|
|
println("--- timer waiting:", tim, tim.whenTicks())
|
|
}
|
|
}
|
|
sleepTicks(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. So instead of sleeping, we return and expect to be
|
|
// called again.
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Run the given task.
|
|
scheduleLogTask(" run:", t)
|
|
t.Resume()
|
|
}
|
|
}
|
|
|
|
// This horrible hack exists to make WASM work properly.
|
|
// When a WASM program calls into JS which calls back into WASM, the event with which we called back in needs to be handled before returning.
|
|
// Thus there are two copies of the scheduler running at once.
|
|
// This is a reduced version of the scheduler which does not deal with the timer queue (that is a problem for the outer scheduler).
|
|
func minSched() {
|
|
scheduleLog("start nested scheduler")
|
|
for !schedulerDone {
|
|
t := runqueue.Pop()
|
|
if t == nil {
|
|
break
|
|
}
|
|
|
|
scheduleLogTask(" run:", t)
|
|
t.Resume()
|
|
}
|
|
scheduleLog("stop nested scheduler")
|
|
}
|
|
|
|
func Gosched() {
|
|
runqueue.Push(task.Current())
|
|
task.Pause()
|
|
}
|