| // Copyright 2024 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 ssa |
| |
| import ( |
| "sync/atomic" |
| ) |
| |
| // Each task has two states: it is initially "active", |
| // and transitions to "done". |
| // |
| // tasks form a directed graph. An edge from x to y (with y in x.edges) |
| // indicates that the task x waits on the task y to be done. |
| // Cycles are permitted. |
| // |
| // Calling x.wait() blocks the calling goroutine until task x, |
| // and all the tasks transitively reachable from x are done. |
| // |
| // The nil *task is always considered done. |
| type task struct { |
| done chan unit // close when the task is done. |
| edges map[*task]unit // set of predecessors of this task. |
| transitive atomic.Bool // true once it is known all predecessors are done. |
| } |
| |
| func (x *task) isTransitivelyDone() bool { return x == nil || x.transitive.Load() } |
| |
| // addEdge creates an edge from x to y, indicating that |
| // x.wait() will not return before y is done. |
| // All calls to x.addEdge(...) should happen before x.markDone(). |
| func (x *task) addEdge(y *task) { |
| if x == y || y.isTransitivelyDone() { |
| return // no work remaining |
| } |
| |
| // heuristic done check |
| select { |
| case <-x.done: |
| panic("cannot add an edge to a done task") |
| default: |
| } |
| |
| if x.edges == nil { |
| x.edges = make(map[*task]unit) |
| } |
| x.edges[y] = unit{} |
| } |
| |
| // markDone changes the task's state to markDone. |
| func (x *task) markDone() { |
| if x != nil { |
| close(x.done) |
| } |
| } |
| |
| // wait blocks until x and all the tasks it can reach through edges are done. |
| func (x *task) wait() { |
| if x.isTransitivelyDone() { |
| return // already known to be done. Skip allocations. |
| } |
| |
| // Use BFS to wait on u.done to be closed, for all u transitively |
| // reachable from x via edges. |
| // |
| // This work can be repeated by multiple workers doing wait(). |
| // |
| // Note: Tarjan's SCC algorithm is able to mark SCCs as transitively done |
| // as soon as the SCC has been visited. This is theoretically faster, but is |
| // a more complex algorithm. Until we have evidence, we need the more complex |
| // algorithm, the simpler algorithm BFS is implemented. |
| // |
| // In Go 1.23, ssa/TestStdlib reaches <=3 *tasks per wait() in most schedules |
| // On some schedules, there is a cycle building net/http and internal/trace/testtrace |
| // due to slices functions. |
| work := []*task{x} |
| enqueued := map[*task]unit{x: {}} |
| for i := 0; i < len(work); i++ { |
| u := work[i] |
| if u.isTransitivelyDone() { // already transitively done |
| work[i] = nil |
| continue |
| } |
| <-u.done // wait for u to be marked done. |
| |
| for v := range u.edges { |
| if _, ok := enqueued[v]; !ok { |
| enqueued[v] = unit{} |
| work = append(work, v) |
| } |
| } |
| } |
| |
| // work is transitively closed over dependencies. |
| // u in work is done (or transitively done and skipped). |
| // u is transitively done. |
| for _, u := range work { |
| if u != nil { |
| x.transitive.Store(true) |
| } |
| } |
| } |