Skip to content
Snippets Groups Projects
Commit 2b8a9a48 authored by Austin Clements's avatar Austin Clements
Browse files

runtime: generate the lock ranking from a DAG description

Currently, the runtime lock rank graph is maintained manually in a
large set of arrays that give the partial order and a manual
topological sort of this partial order. Any changes to the rank graph
are difficult to reason about and hard to review, as well as likely to
cause merge conflicts. Furthermore, because the partial order is
manually maintained, it's not actually transitively closed (though
it's close), meaning there are many cases where rank a can be acquired
before b and b before c, but a cannot be acquired before c. While this
isn't technically wrong, it's very strange in the context of lock
ordering.

Replace all of this with a much more compact, readable, and
maintainable description of the rank graph written in the internal/dag
graph language. We statically generate the runtime structures from
this description, which has the advantage that the parser doesn't have
to run during runtime initialization and the structures can live in
static data where they can be accessed from any point during runtime
init.

The current description was automatically generated from the existing
partial order, combined with a transitive reduction. This ensures it's
correct, but it could use some manual messaging to call out the
logical layers and add some structure.

We do lose the ad hoc string names of the lock ranks in this
translation, which could mostly be derived from the rank constant
names, but not always. I may bring those back but in a more uniform
way.

We no longer need the tests in lockrank_test.go because they were
checking that we manually maintained the structures correctly.

Fixes #53789.

Change-Id: I54451d561b22e61150aff7e9b8602ba9737e1b9b
Reviewed-on: https://go-review.googlesource.com/c/go/+/418715


Run-TryBot: Austin Clements <austin@google.com>
Reviewed-by: default avatarMichael Pratt <mpratt@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
parent ddfd6394
Branches
Tags
No related merge requests found
// Copyright 2022 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 dag
// Transpose reverses all edges in g.
func (g *Graph) Transpose() {
old := g.edges
g.edges = make(map[string]map[string]bool)
for _, n := range g.Nodes {
g.edges[n] = make(map[string]bool)
}
for from, tos := range old {
for to := range tos {
g.edges[to][from] = true
}
}
}
// Topo returns a topological sort of g. This function is deterministic.
func (g *Graph) Topo() []string {
topo := make([]string, 0, len(g.Nodes))
marks := make(map[string]bool)
var visit func(n string)
visit = func(n string) {
if marks[n] {
return
}
for _, to := range g.Edges(n) {
visit(to)
}
marks[n] = true
topo = append(topo, n)
}
for _, root := range g.Nodes {
visit(root)
}
for i, j := 0, len(topo)-1; i < j; i, j = i+1, j-1 {
topo[i], topo[j] = topo[j], topo[i]
}
return topo
}
// TransitiveReduction removes edges from g that are transitively
// reachable. g must be transitively closed.
func (g *Graph) TransitiveReduction() {
// For i -> j -> k, if i -> k exists, delete it.
for _, i := range g.Nodes {
for _, j := range g.Nodes {
if g.HasEdge(i, j) {
for _, k := range g.Nodes {
if g.HasEdge(j, k) {
g.DelEdge(i, k)
}
}
}
}
}
}
// Copyright 2022 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 dag
import (
"reflect"
"strings"
"testing"
)
func TestTranspose(t *testing.T) {
g := mustParse(t, diamond)
g.Transpose()
wantEdges(t, g, "a->b a->c a->d b->d c->d")
}
func TestTopo(t *testing.T) {
g := mustParse(t, diamond)
got := g.Topo()
// "d" is the root, so it's first.
//
// "c" and "b" could be in either order, but Topo is
// deterministic in reverse node definition order.
//
// "a" is a leaf.
wantNodes := strings.Fields("d c b a")
if !reflect.DeepEqual(wantNodes, got) {
t.Fatalf("want topo sort %v, got %v", wantNodes, got)
}
}
func TestTransitiveReduction(t *testing.T) {
t.Run("diamond", func(t *testing.T) {
g := mustParse(t, diamond)
g.TransitiveReduction()
wantEdges(t, g, "b->a c->a d->b d->c")
})
t.Run("chain", func(t *testing.T) {
const chain = `NONE < a < b < c < d; a, d < e;`
g := mustParse(t, chain)
g.TransitiveReduction()
wantEdges(t, g, "e->d d->c c->b b->a")
})
}
......@@ -71,6 +71,10 @@ func (g *Graph) AddEdge(from, to string) {
g.edges[from][to] = true
}
func (g *Graph) DelEdge(from, to string) {
delete(g.edges[from], to)
}
func (g *Graph) HasEdge(from, to string) bool {
return g.edges[from] != nil && g.edges[from][to]
}
......
This diff is collapsed.
......@@ -24,6 +24,9 @@ type lockRankStruct struct {
pad int
}
// lockInit(l *mutex, rank int) sets the rank of lock before it is used.
// If there is no clear place to initialize a lock, then the rank of a lock can be
// specified during the lock call itself via lockWithRank(l *mutex, rank int).
func lockInit(l *mutex, rank lockRank) {
l.rank = rank
}
......
// Copyright 2021 The Go Authors. All rights reserved.
// Copyright 2022 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_test
import (
. "runtime"
"bytes"
"internal/testenv"
"os"
"os/exec"
"testing"
)
// Check that the partial order in lockPartialOrder fits within the total order
// determined by the order of the lockRank constants.
func TestLockRankPartialOrder(t *testing.T) {
for r, list := range LockPartialOrder {
rank := LockRank(r)
for _, e := range list {
entry := LockRank(e)
if entry > rank {
t.Errorf("lockPartialOrder row %v entry %v is inconsistent with total lock ranking order", rank, entry)
}
}
// Test that the generated code for the lock rank graph is up-to-date.
func TestLockRankGenerated(t *testing.T) {
testenv.MustHaveGoRun(t)
want, err := testenv.CleanCmdEnv(exec.Command(testenv.GoToolPath(t), "run", "mklockrank.go")).CombinedOutput()
if err != nil {
t.Fatal(err)
}
}
// Verify that partial order lists are kept sorted. This is a purely cosemetic
// check to make manual reviews simpler. It does not affect correctness, unlike
// the above test.
func TestLockRankPartialOrderSortedEntries(t *testing.T) {
for r, list := range LockPartialOrder {
rank := LockRank(r)
var prev LockRank
for _, e := range list {
entry := LockRank(e)
if entry <= prev {
t.Errorf("Partial order for rank %v out of order: %v <= %v in %v", rank, entry, prev, list)
}
prev = entry
}
got, err := os.ReadFile("lockrank.go")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(want, got) {
t.Fatalf("lockrank.go is out of date. Please run go generate.")
}
}
// Copyright 2022 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.
//go:build ignore
// mklockrank records the static rank graph of the locks in the
// runtime and generates the rank checking structures in lockrank.go.
package main
import (
"bytes"
"flag"
"fmt"
"go/format"
"internal/dag"
"io"
"log"
"os"
"strings"
)
// ranks describes the lock rank graph. See "go doc internal/dag" for
// the syntax.
//
// "a < b" means a must be acquired before b if both are held
// (or, if b is held, a cannot be acquired).
//
// "NONE < a" means no locks may be held when a is acquired.
//
// If a lock is not given a rank, then it is assumed to be a leaf
// lock, which means no other lock can be acquired while it is held.
// Therefore, leaf locks do not need to be given an explicit rank.
//
// TODO: It's often hard to correlate rank names to locks. Change
// these to be more consistent with the locks they label.
const ranks = `
NONE < sysmon, sweepWaiters, assistQueue, cpuprof, sweep, pollDesc, deadlock, itab, notifyList, root, rwmutexW, defer;
sysmon < scavenge, forcegc;
assistQueue, cpuprof, forcegc, pollDesc, scavenge, sweep, sweepWaiters < sched;
sched < allg, allp;
allp < timers;
itab < reflectOffs;
scavenge, sweep < hchan;
scavenge < traceBuf;
allg, hchan, reflectOffs, timers, traceBuf < fin;
traceBuf < traceStrings;
allg, hchan, notifyList, reflectOffs, timers, traceStrings < mspanSpecial, profInsert, profBlock, profMemActive, gcBitsArenas, spanSetSpine, mheapSpecial;
profMemActive < profMemFuture;
hchan, root, sched, traceStrings < trace;
fin, notifyList, trace < traceStackTab;
timers < netpollInit;
rwmutexW, sysmon < rwmutexR;
gcBitsArenas, netpollInit, profBlock, profInsert, profMemFuture, spanSetSpine, traceStackTab < gscan;
gscan, rwmutexR < stackpool;
gscan < stackLarge;
# Generally, hchan must be acquired before gscan. But in one specific
# case (in syncadjustsudogs from markroot after the g has been suspended
# by suspendG), we allow gscan to be acquired, and then an hchan lock. To
# allow this case, we use this hchanLeaf rank in syncadjustsudogs(),
# rather than hchan. By using this special rank, we don't allow any further
# locks to be acquired other than more hchan locks.
gscan < hchanLeaf;
hchan, notifyList < sudog;
defer, gscan, mspanSpecial, sudog < wbufSpans;
stackLarge, stackpool, wbufSpans < mheap;
mheap, mheapSpecial < globalAlloc;
# panic is handled specially. It is implicitly below all other locks.
deadlock < panic;
`
// cyclicRanks lists lock ranks that allow multiple locks of the same
// rank to be acquired simultaneously. The runtime enforces ordering
// within these ranks using a separate mechanism.
var cyclicRanks = map[string]bool{
// Multiple timers are locked simultaneously in destroy().
"timers": true,
// Multiple hchans are acquired in hchan.sortkey() order in
// select.
"hchan": true,
// Multiple hchanLeafs are acquired in hchan.sortkey() order in
// syncadjustsudogs().
"hchanLeaf": true,
}
func main() {
flagO := flag.String("o", "", "write to `file` instead of stdout")
flagDot := flag.Bool("dot", false, "emit graphviz output instead of Go")
flag.Parse()
if flag.NArg() != 0 {
fmt.Fprintf(os.Stderr, "too many arguments")
os.Exit(2)
}
g, err := dag.Parse(ranks)
if err != nil {
log.Fatal(err)
}
var out []byte
if *flagDot {
var b bytes.Buffer
g.TransitiveReduction()
// Add cyclic edges for visualization.
for k := range cyclicRanks {
g.AddEdge(k, k)
}
// Reverse the graph. It's much easier to read this as
// a "<" partial order than a ">" partial order. This
// ways, locks are acquired from the top going down
// and time moves forward over the edges instead of
// backward.
g.Transpose()
generateDot(&b, g)
out = b.Bytes()
} else {
var b bytes.Buffer
generateGo(&b, g)
out, err = format.Source(b.Bytes())
if err != nil {
log.Fatal(err)
}
}
if *flagO != "" {
err = os.WriteFile(*flagO, out, 0666)
} else {
_, err = os.Stdout.Write(out)
}
if err != nil {
log.Fatal(err)
}
}
func generateGo(w io.Writer, g *dag.Graph) {
fmt.Fprintf(w, `// Code generated by mklockrank.go; DO NOT EDIT.
package runtime
type lockRank int
`)
// Create numeric ranks.
topo := g.Topo()
for i, j := 0, len(topo)-1; i < j; i, j = i+1, j-1 {
topo[i], topo[j] = topo[j], topo[i]
}
fmt.Fprintf(w, `
// Constants representing the ranks of all non-leaf runtime locks, in rank order.
// Locks with lower rank must be taken before locks with higher rank,
// in addition to satisfying the partial order in lockPartialOrder.
// A few ranks allow self-cycles, which are specified in lockPartialOrder.
const (
lockRankUnknown lockRank = iota
`)
for _, rank := range topo {
fmt.Fprintf(w, "\t%s\n", cname(rank))
}
fmt.Fprintf(w, `)
// lockRankLeafRank is the rank of lock that does not have a declared rank,
// and hence is a leaf lock.
const lockRankLeafRank lockRank = 1000
`)
// Create string table.
fmt.Fprintf(w, `
// lockNames gives the names associated with each of the above ranks.
var lockNames = []string{
`)
for _, rank := range topo {
fmt.Fprintf(w, "\t%s: %q,\n", cname(rank), rank)
}
fmt.Fprintf(w, `}
func (rank lockRank) String() string {
if rank == 0 {
return "UNKNOWN"
}
if rank == lockRankLeafRank {
return "LEAF"
}
if rank < 0 || int(rank) >= len(lockNames) {
return "BAD RANK"
}
return lockNames[rank]
}
`)
// Create partial order structure.
fmt.Fprintf(w, `
// lockPartialOrder is the transitive closure of the lock rank graph.
// An entry for rank X lists all of the ranks that can already be held
// when rank X is acquired.
//
// Lock ranks that allow self-cycles list themselves.
var lockPartialOrder [][]lockRank = [][]lockRank{
`)
for _, rank := range topo {
list := []string{}
for _, before := range g.Edges(rank) {
list = append(list, cname(before))
}
if cyclicRanks[rank] {
list = append(list, cname(rank))
}
fmt.Fprintf(w, "\t%s: {%s},\n", cname(rank), strings.Join(list, ", "))
}
fmt.Fprintf(w, "}\n")
}
// cname returns the Go const name for the given lock rank label.
func cname(label string) string {
return "lockRank" + strings.ToUpper(label[:1]) + label[1:]
}
// generateDot emits a Graphviz dot representation of g to w.
func generateDot(w io.Writer, g *dag.Graph) {
fmt.Fprintf(w, "digraph g {\n")
// Define all nodes.
for _, node := range g.Nodes {
fmt.Fprintf(w, "%q;\n", node)
}
// Create edges.
for _, node := range g.Nodes {
for _, to := range g.Edges(node) {
fmt.Fprintf(w, "%q -> %q;\n", node, to)
}
}
fmt.Fprintf(w, "}\n")
}
......@@ -12,6 +12,7 @@ import (
//go:generate go run wincallback.go
//go:generate go run mkduff.go
//go:generate go run mkfastlog2table.go
//go:generate go run mklockrank.go -o lockrank.go
var ticks ticksType
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment