Skip to content

Commit 6fa8213

Browse files
committed
runtime: timer implementation from tinygo-org#1402
1 parent b65447c commit 6fa8213

File tree

7 files changed

+245
-3
lines changed

7 files changed

+245
-3
lines changed

main_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func TestBuild(t *testing.T) {
6666
"stdlib.go",
6767
"string.go",
6868
"structs.go",
69+
"timers.go",
6970
"zeroalloc.go",
7071
}
7172
_, minor, err := goenv.GetGorootVersion(goenv.Get("GOROOT"))
@@ -181,6 +182,14 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) {
181182
}
182183

183184
for _, name := range tests {
185+
if options.GOOS == "linux" && (options.GOARCH == "arm" || options.GOARCH == "386") {
186+
switch name {
187+
case "timers.go":
188+
// Timer tests do not work because syscall.seek is implemented
189+
// as Assembly in mainline Go and causes linker failure
190+
continue
191+
}
192+
}
184193
if options.Target == "simavr" {
185194
// Not all tests are currently supported on AVR.
186195
// Skip the ones that aren't.

src/runtime/scheduler.go

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ package runtime
1212

1313
import (
1414
"internal/task"
15+
"runtime/interrupt"
1516
)
1617

1718
const schedulerDebug = false
@@ -27,6 +28,7 @@ var (
2728
runqueue task.Queue
2829
sleepQueue *task.Task
2930
sleepQueueBaseTime timeUnit
31+
timerQueue *timerNode
3032
)
3133

3234
// Simple logging, for debugging.
@@ -112,14 +114,54 @@ func addSleepTask(t *task.Task, duration timeUnit) {
112114
*q = t
113115
}
114116

117+
// addTimer adds the given timer node to the timer queue. It must not be in the
118+
// queue already.
119+
// This function is very similar to addSleepTask but for timerQueue instead of
120+
// sleepQueue.
121+
func addTimer(tim *timerNode) {
122+
mask := interrupt.Disable()
123+
124+
// Add to timer queue.
125+
q := &timerQueue
126+
for ; *q != nil; q = &(*q).next {
127+
if tim.whenTicks() < (*q).whenTicks() {
128+
// this will finish earlier than the next - insert here
129+
break
130+
}
131+
}
132+
tim.next = *q
133+
*q = tim
134+
interrupt.Restore(mask)
135+
}
136+
137+
// removeTimer is the implementation of time.stopTimer. It removes a timer from
138+
// the timer queue, returning true if the timer is present in the timer queue.
139+
func removeTimer(tim *timer) bool {
140+
removedTimer := false
141+
mask := interrupt.Disable()
142+
for t := &timerQueue; *t != nil; t = &(*t).next {
143+
if (*t).timer == tim {
144+
scheduleLog("removed timer")
145+
*t = (*t).next
146+
removedTimer = true
147+
break
148+
}
149+
}
150+
if !removedTimer {
151+
scheduleLog("did not remove timer")
152+
}
153+
interrupt.Restore(mask)
154+
return removedTimer
155+
}
156+
115157
// Run the scheduler until all tasks have finished.
116158
func scheduler() {
117159
// Main scheduler loop.
118160
var now timeUnit
119161
for !schedulerDone {
120162
scheduleLog("")
121163
scheduleLog(" schedule")
122-
if sleepQueue != nil {
164+
if sleepQueue != nil || timerQueue != nil {
123165
now = ticks()
124166
}
125167

@@ -134,22 +176,47 @@ func scheduler() {
134176
runqueue.Push(t)
135177
}
136178

179+
// Check for expired timers to trigger.
180+
if timerQueue != nil && now >= timerQueue.whenTicks() {
181+
scheduleLog("--- timer awoke")
182+
// Pop timer from queue.
183+
tn := timerQueue
184+
timerQueue = tn.next
185+
tn.next = nil
186+
// Run the callback stored in this timer node.
187+
tn.callback(tn)
188+
}
189+
137190
t := runqueue.Pop()
138191
if t == nil {
139-
if sleepQueue == nil {
192+
if sleepQueue == nil && timerQueue == nil {
140193
if asyncScheduler {
141194
// JavaScript is treated specially, see below.
142195
return
143196
}
144197
waitForEvents()
145198
continue
146199
}
147-
timeLeft := timeUnit(sleepQueue.Data) - (now - sleepQueueBaseTime)
200+
201+
var timeLeft timeUnit
202+
if sleepQueue != nil {
203+
timeLeft = timeUnit(sleepQueue.Data) - (now - sleepQueueBaseTime)
204+
}
205+
if timerQueue != nil {
206+
timeLeftForTimer := timerQueue.whenTicks() - now
207+
if sleepQueue == nil || timeLeftForTimer < timeLeft {
208+
timeLeft = timeLeftForTimer
209+
}
210+
}
211+
148212
if schedulerDebug {
149213
println(" sleeping...", sleepQueue, uint(timeLeft))
150214
for t := sleepQueue; t != nil; t = t.Next {
151215
println(" task sleeping:", t, timeUnit(t.Data))
152216
}
217+
for tim := timerQueue; tim != nil; tim = tim.next {
218+
println("--- timer waiting:", tim, tim.whenTicks())
219+
}
153220
}
154221
sleepTicks(timeLeft)
155222
if asyncScheduler {

src/runtime/time.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package runtime
2+
3+
// timerNode is an element in a linked list of timers.
4+
type timerNode struct {
5+
next *timerNode
6+
timer *timer
7+
callback func(*timerNode)
8+
}
9+
10+
// whenTicks returns the (absolute) time when this timer should trigger next.
11+
func (t *timerNode) whenTicks() timeUnit {
12+
return nanosecondsToTicks(t.timer.when)
13+
}
14+
15+
// Defined in the time package, implemented here in the runtime.
16+
//go:linkname startTimer time.startTimer
17+
func startTimer(tim *timer) {
18+
addTimer(&timerNode{
19+
timer: tim,
20+
callback: timerCallback,
21+
})
22+
scheduleLog("adding timer")
23+
}
24+
25+
// timerCallback is called when a timer expires. It makes sure to call the
26+
// callback in the time package and to re-add the timer to the queue if this is
27+
// a ticker (repeating timer).
28+
// This is intentionally used as a callback and not a direct call (even though a
29+
// direct call would be trivial), because otherwise a circular dependency
30+
// between scheduler, addTimer and timerQueue would form. Such a circular
31+
// dependency causes timerQueue not to get optimized away.
32+
// If timerQueue doesn't get optimized away, small programs (that don't call
33+
// time.NewTimer etc) would still pay the cost of these timers.
34+
func timerCallback(tn *timerNode) {
35+
// Run timer function (implemented in the time package).
36+
// The seq parameter to the f function is not used in the time
37+
// package so is left zero.
38+
tn.timer.f(tn.timer.arg, 0)
39+
40+
// If this is a periodic timer (a ticker), re-add it to the queue.
41+
if tn.timer.period != 0 {
42+
tn.timer.when += tn.timer.period
43+
addTimer(tn)
44+
}
45+
}
46+
47+
//go:linkname stopTimer time.stopTimer
48+
func stopTimer(tim *timer) bool {
49+
return removeTimer(tim)
50+
}

src/runtime/timer_go116.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//go:build !go1.18
2+
// +build !go1.18
3+
4+
package runtime
5+
6+
type puintptr uintptr
7+
8+
// Package time knows the layout of this structure.
9+
// If this struct changes, adjust ../time/sleep.go:/runtimeTimer.
10+
type timer struct {
11+
// If this timer is on a heap, which P's heap it is on.
12+
// puintptr rather than *p to match uintptr in the versions
13+
// of this struct defined in other packages.
14+
pp puintptr
15+
16+
// Timer wakes up at when, and then at when+period, ... (period > 0 only)
17+
// each time calling f(arg, now) in the timer goroutine, so f must be
18+
// a well-behaved function and not block.
19+
//
20+
// when must be positive on an active timer.
21+
when int64
22+
period int64
23+
f func(interface{}, uintptr)
24+
arg interface{}
25+
seq uintptr
26+
27+
// What to set the when field to in timerModifiedXX status.
28+
nextwhen int64
29+
30+
// The status field holds one of the values below.
31+
status uint32
32+
}

src/runtime/timer_go118.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//go:build go1.18
2+
// +build go1.18
3+
4+
package runtime
5+
6+
type puintptr uintptr
7+
8+
// Package time knows the layout of this structure.
9+
// If this struct changes, adjust ../time/sleep.go:/runtimeTimer.
10+
type timer struct {
11+
// If this timer is on a heap, which P's heap it is on.
12+
// puintptr rather than *p to match uintptr in the versions
13+
// of this struct defined in other packages.
14+
pp puintptr
15+
16+
// Timer wakes up at when, and then at when+period, ... (period > 0 only)
17+
// each time calling f(arg, now) in the timer goroutine, so f must be
18+
// a well-behaved function and not block.
19+
//
20+
// when must be positive on an active timer.
21+
when int64
22+
period int64
23+
f func(any, uintptr)
24+
arg any
25+
seq uintptr
26+
27+
// What to set the when field to in timerModifiedXX status.
28+
nextwhen int64
29+
30+
// The status field holds one of the values below.
31+
status uint32
32+
}

testdata/timers.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
import "time"
4+
5+
func main() {
6+
// Test ticker.
7+
ticker := time.NewTicker(time.Millisecond * 10)
8+
println("waiting on ticker")
9+
go func() {
10+
time.Sleep(time.Millisecond * 5)
11+
println(" - after 5ms")
12+
time.Sleep(time.Millisecond * 10)
13+
println(" - after 15ms")
14+
time.Sleep(time.Millisecond * 10)
15+
println(" - after 25ms")
16+
}()
17+
<-ticker.C
18+
println("waited on ticker at 10ms")
19+
<-ticker.C
20+
println("waited on ticker at 20ms")
21+
ticker.Stop()
22+
time.Sleep(time.Millisecond * 20)
23+
select {
24+
case <-ticker.C:
25+
println("fail: ticker should have stopped!")
26+
default:
27+
println("ticker was stopped (didn't send anything after 50ms)")
28+
}
29+
30+
timer := time.NewTimer(time.Millisecond * 10)
31+
println("waiting on timer")
32+
go func() {
33+
time.Sleep(time.Millisecond * 5)
34+
println(" - after 5ms")
35+
time.Sleep(time.Millisecond * 10)
36+
println(" - after 15ms")
37+
}()
38+
<-timer.C
39+
println("waited on timer at 10ms")
40+
time.Sleep(time.Millisecond * 10)
41+
}

testdata/timers.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
waiting on ticker
2+
- after 5ms
3+
waited on ticker at 10ms
4+
- after 15ms
5+
waited on ticker at 20ms
6+
- after 25ms
7+
ticker was stopped (didn't send anything after 50ms)
8+
waiting on timer
9+
- after 5ms
10+
waited on timer at 10ms
11+
- after 15ms

0 commit comments

Comments
 (0)