diff --git a/main_test.go b/main_test.go index 6bd79347..a6925cea 100644 --- a/main_test.go +++ b/main_test.go @@ -65,6 +65,7 @@ func TestBuild(t *testing.T) { "stdlib.go", "string.go", "structs.go", + "timers.go", "zeroalloc.go", } _, minor, err := goenv.GetGorootVersion(goenv.Get("GOROOT")) @@ -180,6 +181,14 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) { } for _, name := range tests { + if options.GOOS == "linux" && (options.GOARCH == "arm" || options.GOARCH == "386") { + switch name { + case "timers.go": + // Timer tests do not work because syscall.seek is implemented + // as Assembly in mainline Go and causes linker failure + continue + } + } if options.Target == "simavr" { // Not all tests are currently supported on AVR. // Skip the ones that aren't. @@ -208,6 +217,11 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) { // CGo does not work on AVR. continue + case "timers.go": + // Doesn't compile: + // panic: compiler: could not store type code number inside interface type code + continue + default: } } diff --git a/src/runtime/scheduler.go b/src/runtime/scheduler.go index 383051ae..7367eed3 100644 --- a/src/runtime/scheduler.go +++ b/src/runtime/scheduler.go @@ -12,6 +12,7 @@ package runtime import ( "internal/task" + "runtime/interrupt" ) const schedulerDebug = false @@ -27,6 +28,7 @@ var ( runqueue task.Queue sleepQueue *task.Task sleepQueueBaseTime timeUnit + timerQueue *timerNode ) // Simple logging, for debugging. @@ -114,6 +116,46 @@ func addSleepTask(t *task.Task, duration timeUnit) { *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. @@ -121,7 +163,7 @@ func scheduler() { for !schedulerDone { scheduleLog("") scheduleLog(" schedule") - if sleepQueue != nil { + if sleepQueue != nil || timerQueue != nil { now = ticks() } @@ -136,9 +178,20 @@ func scheduler() { 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 { + if sleepQueue == nil && timerQueue == nil { if asyncScheduler { // JavaScript is treated specially, see below. return @@ -146,12 +199,26 @@ func scheduler() { waitForEvents() continue } - timeLeft := timeUnit(sleepQueue.Data) - (now - sleepQueueBaseTime) + + 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 { diff --git a/src/runtime/time.go b/src/runtime/time.go new file mode 100644 index 00000000..b14eae6a --- /dev/null +++ b/src/runtime/time.go @@ -0,0 +1,50 @@ +package runtime + +// timerNode is an element in a linked list of timers. +type timerNode struct { + next *timerNode + timer *timer + callback func(*timerNode) +} + +// whenTicks returns the (absolute) time when this timer should trigger next. +func (t *timerNode) whenTicks() timeUnit { + return nanosecondsToTicks(t.timer.when) +} + +// Defined in the time package, implemented here in the runtime. +//go:linkname startTimer time.startTimer +func startTimer(tim *timer) { + addTimer(&timerNode{ + timer: tim, + callback: timerCallback, + }) + scheduleLog("adding timer") +} + +// timerCallback is called when a timer expires. It makes sure to call the +// callback in the time package and to re-add the timer to the queue if this is +// a ticker (repeating timer). +// This is intentionally used as a callback and not a direct call (even though a +// direct call would be trivial), because otherwise a circular dependency +// between scheduler, addTimer and timerQueue would form. Such a circular +// dependency causes timerQueue not to get optimized away. +// If timerQueue doesn't get optimized away, small programs (that don't call +// time.NewTimer etc) would still pay the cost of these timers. +func timerCallback(tn *timerNode) { + // Run timer function (implemented in the time package). + // The seq parameter to the f function is not used in the time + // package so is left zero. + tn.timer.f(tn.timer.arg, 0) + + // If this is a periodic timer (a ticker), re-add it to the queue. + if tn.timer.period != 0 { + tn.timer.when += tn.timer.period + addTimer(tn) + } +} + +//go:linkname stopTimer time.stopTimer +func stopTimer(tim *timer) bool { + return removeTimer(tim) +} diff --git a/src/runtime/timer_go116.go b/src/runtime/timer_go116.go new file mode 100644 index 00000000..9ff1625a --- /dev/null +++ b/src/runtime/timer_go116.go @@ -0,0 +1,36 @@ +//go:build !go1.18 +// +build !go1.18 + +// Portions copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package runtime + +type puintptr uintptr + +// Package time knows the layout of this structure. +// If this struct changes, adjust ../time/sleep.go:/runtimeTimer. +type timer struct { + // If this timer is on a heap, which P's heap it is on. + // puintptr rather than *p to match uintptr in the versions + // of this struct defined in other packages. + pp puintptr + + // Timer wakes up at when, and then at when+period, ... (period > 0 only) + // each time calling f(arg, now) in the timer goroutine, so f must be + // a well-behaved function and not block. + // + // when must be positive on an active timer. + when int64 + period int64 + f func(interface{}, uintptr) + arg interface{} + seq uintptr + + // What to set the when field to in timerModifiedXX status. + nextwhen int64 + + // The status field holds one of the values below. + status uint32 +} diff --git a/src/runtime/timer_go118.go b/src/runtime/timer_go118.go new file mode 100644 index 00000000..894ed849 --- /dev/null +++ b/src/runtime/timer_go118.go @@ -0,0 +1,36 @@ +//go:build go1.18 +// +build go1.18 + +// Portions copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package runtime + +type puintptr uintptr + +// Package time knows the layout of this structure. +// If this struct changes, adjust ../time/sleep.go:/runtimeTimer. +type timer struct { + // If this timer is on a heap, which P's heap it is on. + // puintptr rather than *p to match uintptr in the versions + // of this struct defined in other packages. + pp puintptr + + // Timer wakes up at when, and then at when+period, ... (period > 0 only) + // each time calling f(arg, now) in the timer goroutine, so f must be + // a well-behaved function and not block. + // + // when must be positive on an active timer. + when int64 + period int64 + f func(any, uintptr) + arg any + seq uintptr + + // What to set the when field to in timerModifiedXX status. + nextwhen int64 + + // The status field holds one of the values below. + status uint32 +} diff --git a/testdata/timers.go b/testdata/timers.go new file mode 100644 index 00000000..99591c82 --- /dev/null +++ b/testdata/timers.go @@ -0,0 +1,41 @@ +package main + +import "time" + +func main() { + // Test ticker. + ticker := time.NewTicker(time.Millisecond * 160) + println("waiting on ticker") + go func() { + time.Sleep(time.Millisecond * 80) + println(" - after 80ms") + time.Sleep(time.Millisecond * 160) + println(" - after 240ms") + time.Sleep(time.Millisecond * 160) + println(" - after 400ms") + }() + <-ticker.C + println("waited on ticker at 160ms") + <-ticker.C + println("waited on ticker at 320ms") + ticker.Stop() + time.Sleep(time.Millisecond * 400) + select { + case <-ticker.C: + println("fail: ticker should have stopped!") + default: + println("ticker was stopped (didn't send anything after 400ms)") + } + + timer := time.NewTimer(time.Millisecond * 160) + println("waiting on timer") + go func() { + time.Sleep(time.Millisecond * 80) + println(" - after 80ms") + time.Sleep(time.Millisecond * 160) + println(" - after 240ms") + }() + <-timer.C + println("waited on timer at 160ms") + time.Sleep(time.Millisecond * 160) +} diff --git a/testdata/timers.txt b/testdata/timers.txt new file mode 100644 index 00000000..24142d54 --- /dev/null +++ b/testdata/timers.txt @@ -0,0 +1,11 @@ +waiting on ticker + - after 80ms +waited on ticker at 160ms + - after 240ms +waited on ticker at 320ms + - after 400ms +ticker was stopped (didn't send anything after 400ms) +waiting on timer + - after 80ms +waited on timer at 160ms + - after 240ms