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 }