Skip to content

Implement time.NewTimer and time.NewTicker #1402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 39 additions & 15 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ func init() {
// The TinyGo import path.
const tinygoPath = "github.com/tinygo-org/tinygo"

// runtimeTypeNames lists all types used in getLLVMRuntimeType. They should be
// declared before the rest of the module is compiled, to make
// getLLVMRuntimeType work correctly.
//
// Type names are grouped together by shared functionality.
var runtimeTypeNames = []string{
"channel", "chanSelectState", "channelBlockedList",
"_defer",
"funcValue", "funcValueWithSignature",
"hashmap", "hashmapIterator",
"_interface", "typeInInterface", "typecodeID", "interfaceMethodInfo",
"_string", "stringIterator",
"structField",
}

// compilerContext contains function-independent data that should still be
// available while compiling every function. It is not strictly read-only, but
// must not contain function-dependent data such as an IR builder.
Expand All @@ -51,6 +66,7 @@ type compilerContext struct {
ir *ir.Program
diagnostics []error
astComments map[string]*ast.CommentGroup
runtimeTypes map[string]llvm.Type
}

// builder contains all information relevant to build a single function.
Expand Down Expand Up @@ -234,14 +250,10 @@ func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Con
// TODO: lazily create runtime types in getLLVMRuntimeType when they are
// needed. Eventually this will be required anyway, when packages are
// compiled independently (and the runtime types are not available).
for _, member := range c.ir.Program.ImportedPackage("runtime").Members {
if member, ok := member.(*ssa.Type); ok {
if typ, ok := member.Type().(*types.Named); ok {
if _, ok := typ.Underlying().(*types.Struct); ok {
c.getLLVMType(typ)
}
}
}
c.runtimeTypes = make(map[string]llvm.Type)
for _, name := range runtimeTypeNames {
member := c.ir.Program.ImportedPackage("runtime").Members[name].(*ssa.Type)
c.runtimeTypes[name] = c.getLLVMType(member.Type())
}

// Declare all functions.
Expand Down Expand Up @@ -363,16 +375,15 @@ func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Con
}

// getLLVMRuntimeType obtains a named type from the runtime package and returns
// it as a LLVM type, creating it if necessary. It is a shorthand for
// it as a LLVM type. Getting a type this way should be faster than with
// getLLVMType(getRuntimeType(name)).
func (c *compilerContext) getLLVMRuntimeType(name string) llvm.Type {
fullName := "runtime." + name
typ := c.mod.GetTypeByName(fullName)
if typ.IsNil() {
println(c.mod.String())
panic("could not find runtime type: " + fullName)
if typ, ok := c.runtimeTypes[name]; ok {
return typ
}
return typ
// Note: if a type is not found (when a new runtime type is added for
// example), it may need to be added to runtimeTypeNames at the top.
panic("could not find runtime type: runtime." + name)
}

// getLLVMType creates and returns a LLVM type for a Go type. In the case of
Expand Down Expand Up @@ -427,6 +438,19 @@ func (c *compilerContext) getLLVMType(goType types.Type) llvm.Type {
// LLVM. This is because it is otherwise impossible to create
// self-referencing types such as linked lists.
llvmName := typ.Obj().Pkg().Path() + "." + typ.Obj().Name()
if llvmName == "runtime.timer" {
// This is a hack. The types time.runtimeTimer and runtime.timer
// are structurally identical, but because they are not exposed
// they can't be the same named Go type. However, functions such
// as time.startTimer expect their definitions to be provided by
// the runtime (with a *time.runtimeTimer parameter). LLVM
// doesn't allow such a mismatched parameter, unfortunately (at
// least not until opaque pointer types are supported).
// I dislike this hack, but the alternative would involve much
// more invasive changes to the compiler.
// Hopefully this is the only type that needs such a treatment.
llvmName = "time.runtimeTimer"
}
llvmType := c.mod.GetTypeByName(llvmName)
if llvmType.IsNil() {
llvmType = c.ctx.StructCreateNamed(llvmName)
Expand Down
7 changes: 7 additions & 0 deletions src/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ func nanotime() int64 {
return ticksToNanoseconds(ticks())
}

// Go 1.11 compatibility.
// See: https://github.com/golang/go/commit/ff51353c3887b9d83130d958fb503ff1f2291fde
//go:linkname timeRuntimeNano time.runtimeNano
func timeRuntimeNano() int64 {
return nanotime()
}

// timeOffset is how long the monotonic clock started after the Unix epoch. It
// should be a positive integer under normal operation or zero when it has not
// been set.
Expand Down
75 changes: 70 additions & 5 deletions src/runtime/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package runtime

import (
"internal/task"
"runtime/interrupt"
)

const schedulerDebug = false
Expand All @@ -27,6 +28,7 @@ var (
runqueue task.Queue
sleepQueue *task.Task
sleepQueueBaseTime timeUnit
timerQueue *timerNode
)

// Simple logging, for debugging.
Expand Down Expand Up @@ -112,14 +114,54 @@ 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.
var now timeUnit
for !schedulerDone {
scheduleLog("")
scheduleLog(" schedule")
if sleepQueue != nil {
if sleepQueue != nil || timerQueue != nil {
now = ticks()
}

Expand All @@ -134,20 +176,43 @@ 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is really safe with coroutine lowering right now?

Any user code can define a function value of the type func(interface{}, uintptr) which I think is sufficient to break this?

Copy link
Member Author

@aykevl aykevl Sep 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid you're right. I'll have to think a bit about how to fix this. It's not trivial, as the signature is set by the time package.

Thank you for taking a look. I hadn't thought of this.

Copy link
Member

@niaow niaow Oct 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So on coroutines we should do go tn.callback(tn). I dunno, there is probably a way to improve this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, nevermind, you fixed it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I didn't, I haven't really thought about how to do this in a way that works with the coroutines scheduler.

}

t := runqueue.Pop()
if t == nil {
if sleepQueue == nil {
if sleepQueue == nil && timerQueue == nil {
if asyncScheduler {
return
}
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))
println("--- sleeping...", uint(timeLeft))
for t := sleepQueue; t != nil; t = t.Next {
println(" task sleeping:", t, timeUnit(t.Data))
println("--- task sleeping:", t, timeUnit(t.Data))
}
for tim := timerQueue; tim != nil; tim = tim.next {
println("--- timer waiting:", tim, tim.whenTicks())
}
}
sleepTicks(timeLeft)
Expand Down
50 changes: 50 additions & 0 deletions src/runtime/timer.go
Original file line number Diff line number Diff line change
@@ -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)
}
23 changes: 23 additions & 0 deletions src/runtime/timer_go113.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// +build !go1.14

package runtime

// Runtime timer, must be kept in sync with src/time/sleep.go of the Go stdlib.
// The layout changed in Go 1.14, so this is only supported on Go 1.13 and
// below.
//
// The fields used by the time package are:
// when: time to wake up (in nanoseconds)
// period: if not 0, a repeating time (of the given nanoseconds)
// f: the function called when the timer expires
// arg: parameter to f
type timer struct {
tb uintptr
i int

when int64
period int64
f func(interface{}, uintptr)
arg interface{}
seq uintptr
}
23 changes: 23 additions & 0 deletions src/runtime/timer_go114.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// +build go1.14

package runtime

// Runtime timer, must be kept in sync with src/time/sleep.go of the Go stdlib.
// The layout changed in Go 1.14, so this is only supported on Go 1.14 and
// above.
//
// The fields used by the time package are:
// when: time to wake up (in nanoseconds)
// period: if not 0, a repeating time (of the given nanoseconds)
// f: the function called when the timer expires
// arg: parameter to f
type timer struct {
pp uintptr
when int64
period int64
f func(interface{}, uintptr)
arg interface{}
seq uintptr
nextwhen int64
status uint32
}
36 changes: 36 additions & 0 deletions testdata/coroutines.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,42 @@ func main() {
startSimpleFunc(emptyFunc)

time.Sleep(2 * time.Millisecond)

// Test ticker.
ticker := time.NewTicker(time.Millisecond * 10)
println("waiting on ticker")
go func() {
time.Sleep(time.Millisecond * 5)
println(" - after 5ms")
time.Sleep(time.Millisecond * 10)
println(" - after 15ms")
time.Sleep(time.Millisecond * 10)
println(" - after 25ms")
}()
<-ticker.C
println("waited on ticker at 10ms")
<-ticker.C
println("waited on ticker at 20ms")
ticker.Stop()
time.Sleep(time.Millisecond * 20)
select {
case <-ticker.C:
println("fail: ticker should have stopped!")
default:
println("ticker was stopped (didn't send anything after 50ms)")
}

timer := time.NewTimer(time.Millisecond * 10)
println("waiting on timer")
go func() {
time.Sleep(time.Millisecond * 5)
println(" - after 5ms")
time.Sleep(time.Millisecond * 10)
println(" - after 15ms")
}()
<-timer.C
println("waited on timer at 10ms")
time.Sleep(time.Millisecond * 10)
}

func acquire(m *sync.Mutex) {
Expand Down
11 changes: 11 additions & 0 deletions testdata/coroutines.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,14 @@ acquired mutex from goroutine
released mutex from goroutine
re-acquired mutex
done
waiting on ticker
- after 5ms
waited on ticker at 10ms
- after 15ms
waited on ticker at 20ms
- after 25ms
ticker was stopped (didn't send anything after 50ms)
waiting on timer
- after 5ms
waited on timer at 10ms
- after 15ms