github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/rtcheck/order.go (about)

     1  // Copyright 2016 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"go/token"
    11  	"html/template"
    12  	"io"
    13  	"io/ioutil"
    14  	"log"
    15  	"math/big"
    16  	"os"
    17  	"os/exec"
    18  	"path/filepath"
    19  
    20  	"golang.org/x/tools/go/ssa"
    21  )
    22  
    23  // LockOrder tracks a lock graph and reports cycles that prevent the
    24  // graph from being a partial order.
    25  type LockOrder struct {
    26  	lca  *LockClassAnalysis
    27  	fset *token.FileSet
    28  	m    map[lockOrderEdge]map[lockOrderInfo]struct{}
    29  
    30  	// cycles is the cached result of FindCycles, or nil.
    31  	cycles [][]int
    32  }
    33  
    34  type lockOrderEdge struct {
    35  	fromId, toId int
    36  }
    37  
    38  type lockOrderInfo struct {
    39  	fromStack, toStack *StackFrame // Must be interned and common trimmed
    40  }
    41  
    42  // NewLockOrder returns an empty lock graph. Source locations in
    43  // reports will be resolved using fset.
    44  func NewLockOrder(fset *token.FileSet) *LockOrder {
    45  	return &LockOrder{
    46  		lca:  nil,
    47  		fset: fset,
    48  		m:    make(map[lockOrderEdge]map[lockOrderInfo]struct{}),
    49  	}
    50  }
    51  
    52  // Add adds lock edges to the lock order, given that the locks in
    53  // locked are currently held and the locks in locking are being
    54  // acquired at stack.
    55  func (lo *LockOrder) Add(locked *LockSet, locking *LockSet, stack *StackFrame) {
    56  	lo.cycles = nil
    57  	if lo.lca == nil {
    58  		lo.lca = locked.lca
    59  	} else if locked.lca != nil && lo.lca != locked.lca {
    60  		panic("locks come from a different LockClassAnalyses")
    61  	}
    62  
    63  	for i := 0; i < locked.bits.BitLen(); i++ {
    64  		if locked.bits.Bit(i) != 0 {
    65  			for j := 0; j < locking.bits.BitLen(); j++ {
    66  				if locking.bits.Bit(j) != 0 {
    67  					// Trim the common prefix of
    68  					// the two stacks, since we
    69  					// only care about how we got
    70  					// from locked to locking.
    71  					lockedStack := locked.stacks[i]
    72  					fromStack, toStack := lockedStack.TrimCommonPrefix(stack, 1)
    73  
    74  					// Add info to edge.
    75  					edge := lockOrderEdge{i, j}
    76  					info := lockOrderInfo{
    77  						fromStack.Intern(),
    78  						toStack.Intern(),
    79  					}
    80  					infos := lo.m[edge]
    81  					if infos == nil {
    82  						infos = make(map[lockOrderInfo]struct{})
    83  						lo.m[edge] = infos
    84  					}
    85  					infos[info] = struct{}{}
    86  				}
    87  			}
    88  		}
    89  	}
    90  }
    91  
    92  // FindCycles returns a list of cycles in the lock order. Each cycle
    93  // is a list of lock IDs from the StringSpace in cycle order (without
    94  // any repetition).
    95  func (lo *LockOrder) FindCycles() [][]int {
    96  	if lo.cycles != nil {
    97  		return lo.cycles
    98  	}
    99  
   100  	// Compute out-edge adjacency list.
   101  	out := map[int][]int{}
   102  	for edge := range lo.m {
   103  		out[edge.fromId] = append(out[edge.fromId], edge.toId)
   104  	}
   105  
   106  	// Use DFS to find cycles.
   107  	//
   108  	// TODO: Implement a real cycle-finding algorithm. This one is
   109  	// terrible.
   110  	path, pathSet := []int{}, map[int]struct{}{}
   111  	cycles := [][]int{}
   112  	var dfs func(root, node int)
   113  	dfs = func(root, node int) {
   114  		if _, ok := pathSet[node]; ok {
   115  			// Only report as a cycle if we got back to
   116  			// where we started and this is the lowest
   117  			// numbered node in the cycle. This gets us
   118  			// each elementary cycle exactly once.
   119  			if node == root {
   120  				minNode := node
   121  				for _, n := range path {
   122  					if n < minNode {
   123  						minNode = n
   124  					}
   125  				}
   126  				if node == minNode {
   127  					pathCopy := append([]int(nil), path...)
   128  					cycles = append(cycles, pathCopy)
   129  				}
   130  			}
   131  			return
   132  		}
   133  		pathSet[node] = struct{}{}
   134  		path = append(path, node)
   135  		for _, next := range out[node] {
   136  			dfs(root, next)
   137  		}
   138  		path = path[:len(path)-1]
   139  		delete(pathSet, node)
   140  	}
   141  	for root := range out {
   142  		dfs(root, root)
   143  	}
   144  
   145  	// Cache the result.
   146  	lo.cycles = cycles
   147  	return cycles
   148  }
   149  
   150  // WriteToDot writes the lock graph in the dot language to w, with
   151  // cycles highlighted.
   152  func (lo *LockOrder) WriteToDot(w io.Writer) {
   153  	lo.writeToDot(w)
   154  }
   155  
   156  func (lo *LockOrder) name(id int) string {
   157  	return lo.lca.Lookup(id).String()
   158  }
   159  
   160  func (lo *LockOrder) writeToDot(w io.Writer) map[lockOrderEdge]string {
   161  	// TODO: Compute the transitive reduction (of the SCC
   162  	// condensation, I guess) to reduce noise.
   163  
   164  	// Find cycles to highlight edges.
   165  	cycles := lo.FindCycles()
   166  	cycleEdges := map[lockOrderEdge]struct{}{}
   167  	var maxStack int
   168  	for _, cycle := range cycles {
   169  		for i, fromId := range cycle {
   170  			toId := cycle[(i+1)%len(cycle)]
   171  			edge := lockOrderEdge{fromId, toId}
   172  			cycleEdges[edge] = struct{}{}
   173  			if len(lo.m[edge]) > maxStack {
   174  				maxStack = len(lo.m[edge])
   175  			}
   176  		}
   177  	}
   178  
   179  	fmt.Fprintf(w, "digraph locks {\n")
   180  	fmt.Fprintf(w, "  tooltip=\" \";\n")
   181  	var nodes big.Int
   182  	nid := func(lockId int) string {
   183  		return fmt.Sprintf("l%d", lockId)
   184  	}
   185  	// Write edges.
   186  	edgeIds := make(map[lockOrderEdge]string)
   187  	for edge, stacks := range lo.m {
   188  		var props string
   189  		if _, ok := cycleEdges[edge]; ok {
   190  			width := 1 + 6*float64(len(stacks))/float64(maxStack)
   191  			props = fmt.Sprintf(",label=%d,penwidth=%f,color=red,weight=2", len(stacks), width)
   192  		}
   193  		id := fmt.Sprintf("edge%d-%d", edge.fromId, edge.toId)
   194  		edgeIds[edge] = id
   195  		tooltip := fmt.Sprintf("%s -> %s", lo.name(edge.fromId), lo.name(edge.toId))
   196  		// We set the edge ID so Javascript can find the
   197  		// element in the SVG.
   198  		fmt.Fprintf(w, "  %s -> %s [id=%q,tooltip=%q%s];\n", nid(edge.fromId), nid(edge.toId), id, tooltip, props)
   199  		nodes.SetBit(&nodes, edge.fromId, 1)
   200  		nodes.SetBit(&nodes, edge.toId, 1)
   201  	}
   202  	// Write nodes. This excludes lone locks: these are only the
   203  	// locks that participate in some ordering
   204  	for i := 0; i < nodes.BitLen(); i++ {
   205  		if nodes.Bit(i) == 1 {
   206  			// We set the fill color to white so
   207  			// mouseovers on this node work nicely.
   208  			fmt.Fprintf(w, "  %s [label=%q,style=filled,fillcolor=white];\n", nid(i), lo.name(i))
   209  		}
   210  	}
   211  	fmt.Fprintf(w, "}\n")
   212  	return edgeIds
   213  }
   214  
   215  type renderedPath struct {
   216  	RootFn   string
   217  	From, To []renderedFrame
   218  }
   219  
   220  type renderedFrame struct {
   221  	Op  string
   222  	Pos token.Position
   223  }
   224  
   225  func (lo *LockOrder) renderInfo(edge lockOrderEdge, info lockOrderInfo) renderedPath {
   226  	fset := lo.fset
   227  	fromStack := info.fromStack.Flatten(nil)
   228  	toStack := info.toStack.Flatten(nil)
   229  	rootFn := fromStack[0].Parent()
   230  	renderStack := func(stack []ssa.Instruction, tail string) []renderedFrame {
   231  		var frames []renderedFrame
   232  		for i, call := range stack[1:] {
   233  			frames = append(frames, renderedFrame{"calls " + call.Parent().String(), fset.Position(stack[i].Pos())})
   234  		}
   235  		frames = append(frames, renderedFrame{tail, fset.Position(stack[len(stack)-1].Pos())})
   236  		return frames
   237  	}
   238  	return renderedPath{
   239  		rootFn.String(),
   240  		renderStack(fromStack, "acquires "+lo.name(edge.fromId)),
   241  		renderStack(toStack, "acquires "+lo.name(edge.toId)),
   242  	}
   243  }
   244  
   245  // Check writes a text report of lock cycles to w.
   246  //
   247  // This report is thorough, but can be quite repetitive, since a
   248  // single edge can participate in multiple cycles.
   249  func (lo *LockOrder) Check(w io.Writer) {
   250  	cycles := lo.FindCycles()
   251  
   252  	// Report cycles.
   253  	printStack := func(stack []renderedFrame) {
   254  		indent := 6
   255  		for _, fr := range stack {
   256  			fmt.Fprintf(w, "%*s%s at %s\n", indent, "", fr.Op, fr.Pos)
   257  			indent += 2
   258  		}
   259  	}
   260  	printInfo := func(rinfo renderedPath) {
   261  		fmt.Fprintf(w, "    %s\n", rinfo.RootFn)
   262  		printStack(rinfo.From)
   263  		printStack(rinfo.To)
   264  	}
   265  	for _, cycle := range cycles {
   266  		cycle = append(cycle, cycle[0])
   267  		fmt.Fprintf(w, "lock cycle: ")
   268  		for i, node := range cycle {
   269  			if i != 0 {
   270  				fmt.Fprintf(w, " -> ")
   271  			}
   272  			fmt.Fprintf(w, lo.name(node))
   273  		}
   274  		fmt.Fprintf(w, "\n")
   275  
   276  		for i := 0; i < len(cycle)-1; i++ {
   277  			edge := lockOrderEdge{cycle[i], cycle[i+1]}
   278  			infos := lo.m[edge]
   279  
   280  			fmt.Fprintf(w, "  %d path(s) acquire %s then %s:\n", len(infos), lo.name(edge.fromId), lo.name(edge.toId))
   281  			for info, _ := range infos {
   282  				rinfo := lo.renderInfo(edge, info)
   283  				printInfo(rinfo)
   284  			}
   285  			fmt.Fprintf(w, "\n")
   286  		}
   287  	}
   288  }
   289  
   290  // WriteToHTML writes a self-contained, interactive HTML lock graph
   291  // report to w. It requires dot to be in $PATH.
   292  func (lo *LockOrder) WriteToHTML(w io.Writer) {
   293  	// Generate SVG from dot graph.
   294  	cmd := exec.Command("dot", "-Tsvg")
   295  	dotin, err := cmd.StdinPipe()
   296  	if err != nil {
   297  		log.Fatal("creating pipe to dot: ", err)
   298  	}
   299  	dotDone := make(chan bool)
   300  	var edgeIds map[lockOrderEdge]string
   301  	go func() {
   302  		edgeIds = lo.writeToDot(dotin)
   303  		dotin.Close()
   304  		dotDone <- true
   305  	}()
   306  	svg, err := cmd.Output()
   307  	if err != nil {
   308  		log.Fatal("error running dot: ", err)
   309  	}
   310  	<-dotDone
   311  	// Strip stuff before the SVG tag so we can put it into HTML.
   312  	if i := bytes.Index(svg, []byte("<svg")); i > 0 {
   313  		svg = svg[i:]
   314  	}
   315  
   316  	// Construct JSON for lock graph details. This is about an
   317  	// order of magnitude smaller than the naive renderedFrames.
   318  	jsonStrings := NewStringSpace()
   319  	// To save space, we use a struct of arrays.
   320  	type jsonStack struct {
   321  		Op     []int
   322  		PathID []int `json:"P"`
   323  		Line   []int `json:"L"`
   324  	}
   325  	xFrames := func(rs []renderedFrame) jsonStack {
   326  		out := jsonStack{
   327  			make([]int, len(rs)),
   328  			make([]int, len(rs)),
   329  			make([]int, len(rs)),
   330  		}
   331  		for i, r := range rs {
   332  			out.Op[i] = jsonStrings.Intern(r.Op)
   333  			out.PathID[i] = jsonStrings.Intern(r.Pos.Filename)
   334  			out.Line[i] = r.Pos.Line
   335  		}
   336  		return out
   337  	}
   338  	type jsonPath struct {
   339  		RootFn   int
   340  		From, To jsonStack
   341  	}
   342  	xPath := func(r renderedPath) jsonPath {
   343  		return jsonPath{jsonStrings.Intern(r.RootFn), xFrames(r.From), xFrames(r.To)}
   344  	}
   345  	type jsonEdge struct {
   346  		EdgeID string
   347  		Locks  [2]string
   348  		Paths  []jsonPath
   349  	}
   350  	jsonEdges := []jsonEdge{}
   351  	for edge, infos := range lo.m {
   352  		var paths []jsonPath
   353  		for info := range infos {
   354  			paths = append(paths, xPath(lo.renderInfo(edge, info)))
   355  		}
   356  		jsonEdges = append(jsonEdges, jsonEdge{
   357  			EdgeID: edgeIds[edge],
   358  			Locks:  [2]string{lo.name(edge.fromId), lo.name(edge.toId)},
   359  			Paths:  paths,
   360  		})
   361  	}
   362  
   363  	// Find the static file path.
   364  	//
   365  	// TODO: Optionally bake these into the binary.
   366  	var static string
   367  	var found bool
   368  	for _, gopath := range filepath.SplitList(os.Getenv("GOPATH")) {
   369  		static = filepath.Join(gopath, "src/github.com/aclements/go-misc/rtcheck/static")
   370  		if _, err := os.Stat(static); err == nil {
   371  			found = true
   372  			break
   373  		}
   374  	}
   375  	if !found {
   376  		log.Fatal("unable to find HTML template in $GOPATH")
   377  	}
   378  
   379  	// Generate HTML.
   380  	tmpl, err := template.ParseFiles(filepath.Join(static, "tmpl-order.html"))
   381  	if err != nil {
   382  		log.Fatal("loading HTML templates: ", err)
   383  	}
   384  	mainJS, err := ioutil.ReadFile(filepath.Join(static, "main.js"))
   385  	if err != nil {
   386  		log.Fatal("loading main.js: ", err)
   387  	}
   388  	err = tmpl.Execute(w, map[string]interface{}{
   389  		"graph":   template.HTML(svg),
   390  		"strings": jsonStrings.s,
   391  		"edges":   jsonEdges,
   392  		"mainJS":  template.JS(mainJS),
   393  	})
   394  	if err != nil {
   395  		log.Fatal("executing HTML template: ", err)
   396  	}
   397  }