mirror of https://github.com/tinygo-org/tinygo.git
Ayke van Laethem
7 years ago
10 changed files with 796 additions and 59 deletions
@ -0,0 +1,161 @@ |
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"golang.org/x/tools/go/ssa" |
|||
) |
|||
|
|||
// Analysis results over a whole program.
|
|||
type Analysis struct { |
|||
functions map[*ssa.Function]*FuncMeta |
|||
needsScheduler bool |
|||
goCalls []*ssa.Go |
|||
} |
|||
|
|||
// Some analysis results of a single function.
|
|||
type FuncMeta struct { |
|||
f *ssa.Function |
|||
blocking bool |
|||
parents []*ssa.Function // calculated by AnalyseCallgraph
|
|||
children []*ssa.Function |
|||
} |
|||
|
|||
// Return a new Analysis object.
|
|||
func NewAnalysis() *Analysis { |
|||
return &Analysis{ |
|||
functions: make(map[*ssa.Function]*FuncMeta), |
|||
} |
|||
} |
|||
|
|||
// Add a given package to the analyzer, to be analyzed later.
|
|||
func (a *Analysis) AddPackage(pkg *ssa.Package) { |
|||
for _, member := range pkg.Members { |
|||
switch member := member.(type) { |
|||
case *ssa.Function: |
|||
a.addFunction(member) |
|||
case *ssa.Type: |
|||
ms := pkg.Prog.MethodSets.MethodSet(member.Type()) |
|||
for i := 0; i < ms.Len(); i++ { |
|||
a.addFunction(pkg.Prog.MethodValue(ms.At(i))) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Analyze the given function quickly without any recursion, and add it to the
|
|||
// list of functions in the analyzer.
|
|||
func (a *Analysis) addFunction(f *ssa.Function) { |
|||
fm := &FuncMeta{} |
|||
for _, block := range f.Blocks { |
|||
for _, instr := range block.Instrs { |
|||
switch instr := instr.(type) { |
|||
case *ssa.Call: |
|||
switch call := instr.Call.Value.(type) { |
|||
case *ssa.Function: |
|||
name := getFunctionName(call, false) |
|||
if name == "runtime.Sleep" { |
|||
fm.blocking = true |
|||
} |
|||
fm.children = append(fm.children, call) |
|||
} |
|||
case *ssa.Go: |
|||
a.goCalls = append(a.goCalls, instr) |
|||
} |
|||
} |
|||
} |
|||
a.functions[f] = fm |
|||
|
|||
for _, child := range f.AnonFuncs { |
|||
a.addFunction(child) |
|||
} |
|||
} |
|||
|
|||
// Fill in parents of all functions.
|
|||
//
|
|||
// All packages need to be added before this pass can run, or it will produce
|
|||
// incorrect results.
|
|||
func (a *Analysis) AnalyseCallgraph() { |
|||
for f, fm := range a.functions { |
|||
for _, child := range fm.children { |
|||
childRes, ok := a.functions[child] |
|||
if !ok { |
|||
print("child not found: " + child.Pkg.Pkg.Path() + "." + child.Name() + ", function: " + f.Name()) |
|||
continue |
|||
} |
|||
childRes.parents = append(childRes.parents, f) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Analyse which functions are recursively blocking.
|
|||
//
|
|||
// Depends on AnalyseCallgraph.
|
|||
func (a *Analysis) AnalyseBlockingRecursive() { |
|||
worklist := make([]*FuncMeta, 0) |
|||
|
|||
// Fill worklist with directly blocking functions.
|
|||
for _, fm := range a.functions { |
|||
if fm.blocking { |
|||
worklist = append(worklist, fm) |
|||
} |
|||
} |
|||
|
|||
// Keep reducing this worklist by marking a function as recursively blocking
|
|||
// from the worklist and pushing all its parents that are non-blocking.
|
|||
// This is somewhat similar to a worklist in a mark-sweep garbage collector.
|
|||
// The work items are then grey objects.
|
|||
for len(worklist) != 0 { |
|||
// Pick the topmost.
|
|||
fm := worklist[len(worklist)-1] |
|||
worklist = worklist[:len(worklist)-1] |
|||
for _, parent := range fm.parents { |
|||
parentfm := a.functions[parent] |
|||
if !parentfm.blocking { |
|||
parentfm.blocking = true |
|||
worklist = append(worklist, parentfm) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Check whether we need a scheduler. This is only necessary when there are go
|
|||
// calls that start blocking functions (if they're not blocking, the go function
|
|||
// can be turned into a regular function call).
|
|||
//
|
|||
// Depends on AnalyseBlockingRecursive.
|
|||
func (a *Analysis) AnalyseGoCalls() { |
|||
for _, instr := range a.goCalls { |
|||
if a.isBlocking(instr.Call.Value) { |
|||
a.needsScheduler = true |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Whether this function needs a scheduler.
|
|||
//
|
|||
// Depends on AnalyseGoCalls.
|
|||
func (a *Analysis) NeedsScheduler() bool { |
|||
return a.needsScheduler |
|||
} |
|||
|
|||
// Whether this function blocks. Builtins are also accepted for convenience.
|
|||
// They will always be non-blocking.
|
|||
//
|
|||
// Depends on AnalyseBlockingRecursive.
|
|||
func (a *Analysis) IsBlocking(f ssa.Value) bool { |
|||
if !a.needsScheduler { |
|||
return false |
|||
} |
|||
return a.isBlocking(f) |
|||
} |
|||
|
|||
func (a *Analysis) isBlocking(f ssa.Value) bool { |
|||
switch f := f.(type) { |
|||
case *ssa.Builtin: |
|||
return false |
|||
case *ssa.Function: |
|||
return a.functions[f].blocking |
|||
default: |
|||
panic("Analysis.IsBlocking on unknown type") |
|||
} |
|||
} |
@ -0,0 +1,249 @@ |
|||
|
|||
package runtime |
|||
|
|||
// This file implements the Go scheduler using coroutines.
|
|||
// A goroutine contains a whole stack. A coroutine is just a single function.
|
|||
// How do we use coroutines for goroutines, then?
|
|||
// * Every function that contains a blocking call (like sleep) is marked
|
|||
// blocking, and all it's parents (callers) are marked blocking as well
|
|||
// transitively until the root (main.main or a go statement).
|
|||
// * 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.
|
|||
// 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.
|
|||
//
|
|||
// For more background on coroutines in LLVM:
|
|||
// https://llvm.org/docs/Coroutines.html
|
|||
|
|||
import ( |
|||
"unsafe" |
|||
) |
|||
|
|||
// State/promise of a task. Internally represented as:
|
|||
//
|
|||
// {i8 state, i32 data, i8* next}
|
|||
type taskState struct { |
|||
state uint8 |
|||
data uint32 |
|||
next taskInstance |
|||
} |
|||
|
|||
// Pointer to a task. Wrap unsafe.Pointer to provide some sort of type safety.
|
|||
type taskInstance unsafe.Pointer |
|||
|
|||
// Various states a task can be in. Not always updated (especially
|
|||
// TASK_STATE_RUNNABLE).
|
|||
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
|
|||
// list. The runqueueBack will simply refer to the front in the 'next' pointer.
|
|||
var ( |
|||
runqueueFront taskInstance |
|||
runqueueBack taskInstance |
|||
sleepQueue taskInstance |
|||
sleepQueueBaseTime uint64 |
|||
) |
|||
|
|||
// Translated to void @llvm.coro.resume(i8*).
|
|||
func _llvm_coro_resume(taskInstance) |
|||
|
|||
// Translated to void @llvm.coro.destroy(i8*).
|
|||
func _llvm_coro_destroy(taskInstance) |
|||
|
|||
// Translated to i1 @llvm.coro.done(i8*).
|
|||
func _llvm_coro_done(taskInstance) bool |
|||
|
|||
// Translated to i8* @llvm.coro.promise(i8*, i32, i1).
|
|||
func _llvm_coro_promise(taskInstance, int32, bool) unsafe.Pointer |
|||
|
|||
// Get the promise belonging to a task.
|
|||
func taskPromise(t taskInstance) *taskState { |
|||
return (*taskState)(_llvm_coro_promise(t, 4, false)) |
|||
} |
|||
|
|||
// Simple logging, for debugging.
|
|||
func scheduleLog(msg string) { |
|||
//println(msg)
|
|||
} |
|||
|
|||
// Simple logging with a task pointer, for debugging.
|
|||
func scheduleLogTask(msg string, t taskInstance) { |
|||
//println(msg, t)
|
|||
} |
|||
|
|||
// Set the task state to sleep for a given time.
|
|||
//
|
|||
// This is a compiler intrinsic.
|
|||
func sleepTask(caller taskInstance, duration Duration) { |
|||
promise := taskPromise(caller) |
|||
promise.state = TASK_STATE_SLEEP |
|||
promise.data = uint32(duration) // TODO: longer durations
|
|||
} |
|||
|
|||
// 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.
|
|||
//
|
|||
// This is a compiler intrinsic.
|
|||
func waitForAsyncCall(caller taskInstance) { |
|||
promise := taskPromise(caller) |
|||
promise.state = TASK_STATE_CALL |
|||
} |
|||
|
|||
// Add a task to the runnable or sleep queue, depending on the state.
|
|||
//
|
|||
// This is a compiler intrinsic.
|
|||
func scheduleTask(t taskInstance) { |
|||
if t == nil { |
|||
return |
|||
} |
|||
scheduleLogTask(" schedule task:", t) |
|||
// See what we should do with this task: try to execute it directly
|
|||
// again or let it sleep for a bit.
|
|||
promise := taskPromise(t) |
|||
if promise.state == TASK_STATE_CALL { |
|||
return // calling an async task, the subroutine will re-active the parent
|
|||
} else if promise.state == TASK_STATE_SLEEP && promise.data != 0 { |
|||
addSleepTask(t) |
|||
} else { |
|||
pushTask(t) |
|||
} |
|||
} |
|||
|
|||
// Add this task to the end of the run queue. May also destroy the task if it's
|
|||
// done.
|
|||
func pushTask(t taskInstance) { |
|||
if _llvm_coro_done(t) { |
|||
scheduleLogTask(" destroy task:", t) |
|||
_llvm_coro_destroy(t) |
|||
return |
|||
} |
|||
if runqueueBack == nil { // empty runqueue
|
|||
runqueueBack = t |
|||
runqueueFront = t |
|||
} else { |
|||
lastTaskPromise := taskPromise(runqueueBack) |
|||
lastTaskPromise.next = t |
|||
runqueueBack = t |
|||
} |
|||
} |
|||
|
|||
// Get a task from the front of the run queue. May return nil if there is none.
|
|||
func popTask() taskInstance { |
|||
t := runqueueFront |
|||
if t == nil { |
|||
return nil |
|||
} |
|||
scheduleLogTask(" popTask:", t) |
|||
promise := taskPromise(t) |
|||
runqueueFront = promise.next |
|||
if runqueueFront == nil { |
|||
runqueueBack = nil |
|||
} |
|||
promise.next = nil |
|||
return t |
|||
} |
|||
|
|||
// Add this task to the sleep queue, assuming its state is set to sleeping.
|
|||
func addSleepTask(t taskInstance) { |
|||
now := monotime() |
|||
if sleepQueue == nil { |
|||
scheduleLog(" -> sleep new queue") |
|||
// Create new linked list for the sleep queue.
|
|||
sleepQueue = t |
|||
sleepQueueBaseTime = now |
|||
return |
|||
} |
|||
|
|||
// Make sure promise.data is relative to the queue time base.
|
|||
promise := taskPromise(t) |
|||
|
|||
// Insert at front of sleep queue.
|
|||
if promise.data < taskPromise(sleepQueue).data { |
|||
scheduleLog(" -> sleep at start") |
|||
taskPromise(sleepQueue).data -= promise.data |
|||
promise.next = sleepQueue |
|||
sleepQueue = t |
|||
return |
|||
} |
|||
|
|||
// Add to sleep queue (in the middle or at the end).
|
|||
queueIndex := sleepQueue |
|||
for { |
|||
promise.data -= taskPromise(queueIndex).data |
|||
if taskPromise(queueIndex).next == nil || taskPromise(queueIndex).data > promise.data { |
|||
if taskPromise(queueIndex).next == nil { |
|||
scheduleLog(" -> sleep at end") |
|||
promise.next = nil |
|||
} else { |
|||
scheduleLog(" -> sleep in middle") |
|||
promise.next = taskPromise(queueIndex).next |
|||
taskPromise(promise.next).data -= promise.data |
|||
} |
|||
taskPromise(queueIndex).next = t |
|||
break |
|||
} |
|||
queueIndex = taskPromise(queueIndex).next |
|||
} |
|||
} |
|||
|
|||
// Run the scheduler until all tasks have finished.
|
|||
// It takes an initial task (main.main) to bootstrap.
|
|||
func scheduler(main taskInstance) { |
|||
// Initial task.
|
|||
scheduleTask(main) |
|||
|
|||
// Main scheduler loop.
|
|||
for { |
|||
scheduleLog("\n schedule") |
|||
now := monotime() |
|||
|
|||
// Add tasks that are done sleeping to the end of the runqueue so they
|
|||
// will be executed soon.
|
|||
if sleepQueue != nil && now - sleepQueueBaseTime >= uint64(taskPromise(sleepQueue).data) { |
|||
scheduleLog(" run <- sleep") |
|||
t := sleepQueue |
|||
promise := taskPromise(t) |
|||
sleepQueueBaseTime += uint64(promise.data) |
|||
sleepQueue = promise.next |
|||
promise.next = nil |
|||
pushTask(t) |
|||
} |
|||
|
|||
scheduleLog(" <- popTask") |
|||
t := popTask() |
|||
if t == nil { |
|||
if sleepQueue == nil { |
|||
// No more tasks to execute.
|
|||
// It would be nice if we could detect deadlocks here, because
|
|||
// there might still be functions waiting on each other in a
|
|||
// deadlock.
|
|||
scheduleLog(" no tasks left!") |
|||
return |
|||
} |
|||
scheduleLog(" sleeping...") |
|||
timeLeft := uint64(taskPromise(sleepQueue).data) - (now - sleepQueueBaseTime) |
|||
sleep(Duration(timeLeft)) |
|||
continue |
|||
} |
|||
|
|||
// Run the given task.
|
|||
scheduleLogTask(" run:", t) |
|||
_llvm_coro_resume(t) |
|||
|
|||
// Add the just resumed task to the run queue or the sleep queue.
|
|||
scheduleTask(t) |
|||
} |
|||
} |
Loading…
Reference in new issue