cuelang.org/go@v0.13.0/internal/core/adt/debug.go (about) 1 // Copyright 2023 CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package adt 16 17 import ( 18 "bytes" 19 "fmt" 20 "html/template" 21 "io" 22 "log" 23 "os" 24 "os/exec" 25 "path/filepath" 26 "runtime" 27 "strings" 28 ) 29 30 // RecordDebugGraph records debug output in ctx if there was an anomaly 31 // discovered. 32 func RecordDebugGraph(ctx *OpContext, v *Vertex, name string) { 33 graph, hasError := CreateMermaidGraph(ctx, v, true) 34 if hasError { 35 if ctx.ErrorGraphs == nil { 36 ctx.ErrorGraphs = map[string]string{} 37 } 38 path := ctx.PathToString(v.Path()) 39 ctx.ErrorGraphs[path] = graph 40 } 41 } 42 43 var ( 44 // DebugDeps enables dependency tracking for debugging purposes. 45 // It is off by default, as it adds a significant overhead. 46 // 47 // TODO: hook this init CUE_DEBUG, once we have set this up as a single 48 // environment variable. For instance, CUE_DEBUG=matchdeps=1. 49 DebugDeps = false 50 51 OpenGraphs = false 52 53 // MaxGraphs is the maximum number of debug graphs to be opened. To avoid 54 // confusion, a panic will be raised if this number is exceeded. 55 MaxGraphs = 10 56 57 numberOpened = 0 58 ) 59 60 // OpenNodeGraph takes a given mermaid graph and opens it in the system default 61 // browser. 62 func OpenNodeGraph(title, path, code, out, graph string) { 63 if !OpenGraphs { 64 return 65 } 66 if numberOpened > MaxGraphs { 67 panic("too many debug graphs opened") 68 } 69 numberOpened++ 70 71 err := os.MkdirAll(path, 0777) 72 if err != nil { 73 log.Fatal(err) 74 } 75 url := filepath.Join(path, "graph.html") 76 77 w, err := os.Create(url) 78 if err != nil { 79 log.Fatal(err) 80 } 81 defer w.Close() 82 83 data := struct { 84 Title string 85 Code string 86 Out string 87 Graph string 88 }{ 89 Title: title, 90 Code: code, 91 Out: out, 92 Graph: graph, 93 } 94 95 tmpl := template.Must(template.New("").Parse(` 96 <!DOCTYPE html> 97 <html> 98 <head> 99 <title>{{.Title}}</title> 100 <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> 101 <script>mermaid.initialize({startOnLoad:true});</script> 102 <style> 103 .container { 104 display: flex; 105 flex-direction: column; 106 align-items: stretch; 107 } 108 .row { 109 display: flex; 110 flex-direction: row; 111 } 112 // ... 113 </style> 114 </head> 115 <body> 116 <div class="mermaid">{{.Graph}}</div> 117 <div class="row"> 118 <div class="column"> 119 <h1><b>Input</b></h1> 120 <pre>{{.Code}}</pre> 121 </div> 122 <div class="column"> 123 <h1><b>Output</b></h1> 124 <pre>{{.Out}}</pre> 125 </div> 126 </div> 127 </body> 128 </html> 129 `)) 130 131 err = tmpl.Execute(w, data) 132 if err != nil { 133 log.Fatal(err) 134 } 135 136 openBrowser(url) 137 } 138 139 // openDebugGraph opens a browser with a graph of the state of the given Vertex 140 // and all its dependencies that have not completed processing. 141 // DO NOT DELETE: this is used to insert during debugging of the evaluator 142 // to inspect a node. 143 func openDebugGraph(ctx *OpContext, v *Vertex, name string) { 144 if !OpenGraphs { 145 return 146 } 147 graph, _ := CreateMermaidGraph(ctx, v, true) 148 path := filepath.Join(".debug", "TestX", name, fmt.Sprintf("%v", v.Path())) 149 OpenNodeGraph(name, path, "in", "out", graph) 150 } 151 152 // mermaidContext is used to create a dependency analysis for a node. 153 type mermaidContext struct { 154 ctx *OpContext 155 v *Vertex 156 157 all bool 158 159 hasError bool 160 161 // roots maps a Vertex to the analysis data for that Vertex. 162 roots map[*Vertex]*mermaidVertex 163 164 w io.Writer 165 166 // vertices lists an analysis of all nodes related to the analyzed node. 167 // The first node is the node being analyzed itself. 168 vertices []*mermaidVertex 169 } 170 171 type mermaidVertex struct { 172 vertex *Vertex 173 f Feature 174 w *bytes.Buffer 175 tasks *bytes.Buffer 176 intra *bytes.Buffer 177 processed bool 178 } 179 180 // CreateMermaidGraph creates an analysis of relations and values involved in 181 // nodes with unbalanced increments. The graph is in Mermaid format. 182 func CreateMermaidGraph(ctx *OpContext, v *Vertex, all bool) (graph string, hasError bool) { 183 buf := &strings.Builder{} 184 185 m := &mermaidContext{ 186 ctx: ctx, 187 v: v, 188 roots: map[*Vertex]*mermaidVertex{}, 189 w: buf, 190 all: all, 191 } 192 193 io.WriteString(m.w, "graph TD\n") 194 io.WriteString(m.w, " classDef err fill:#e01010,stroke:#000000,stroke-width:3,font-size:medium\n") 195 fmt.Fprintf(m.w, " title[<b>%v</b>]\n", ctx.disjunctInfo()) 196 197 indent(m.w, 1) 198 fmt.Fprintf(m.w, "style %s stroke-width:5\n\n", m.vertexID(v)) 199 // Trigger descent on first vertex. This may include other vertices when 200 // traversing closeContexts if they have dependencies on such vertices. 201 m.vertex(v, true) 202 203 // get parent context, if there is relevant closedness information. 204 root := v.Parent 205 for p := root; p != nil; p = p.Parent { 206 n := p.state 207 if n == nil { 208 continue 209 } 210 if len(n.reqDefIDs) > 0 { 211 root = p.Parent 212 } 213 } 214 for p := v.Parent; p != root; p = p.Parent { 215 m.vertex(p, true) // only render relevant child 216 } 217 218 // Close and flush all collected vertices. 219 for _, v := range m.vertices { 220 v.closeVertex() 221 m.w.Write(v.w.Bytes()) 222 } 223 224 s := buf.String() 225 226 return s, m.hasError 227 } 228 229 // vertex creates a blob of Mermaid graph representing one vertex. It has 230 // the following shape (where ptr(x) means pointer of x): 231 // 232 // subgraph ptr(v) 233 // %% root note if ROOT has not been decremented. 234 // root((cc1)) -|R|-> ptr(cc1) 235 // 236 // %% closedness graph dependencies 237 // ptr(cc1) 238 // ptr(cc2) -|P|-> ptr(cc1) 239 // ptr(cc2) -|E|-> ptr(cc1) %% mid schedule 240 // 241 // %% tasks 242 // subgraph tasks 243 // ptr(cc3) 244 // ptr(cc4) 245 // ptr(cc5) 246 // end 247 // 248 // %% outstanding tasks and the contexts they depend on 249 // ptr(cc3) -|T|-> ptr(cc2) 250 // 251 // subgraph notifications 252 // ptr(cc6) 253 // ptr(cc7) 254 // end 255 // end 256 // %% arcs from nodes to nodes in other vertices 257 // ptr(cc1) -|A|-> ptr(cc10) 258 // ptr(vx) -|N|-> ptr(cc11) 259 // 260 // 261 // A vertex has the following name: path(v); done 262 // 263 // Each closeContext has the following info: ptr(cc); cc.count 264 func (m *mermaidContext) vertex(v *Vertex, recursive bool) *mermaidVertex { 265 vc := m.roots[v] 266 if vc != nil { 267 return vc 268 } 269 270 vc = &mermaidVertex{ 271 vertex: v, 272 f: v.Label, 273 w: &bytes.Buffer{}, 274 intra: &bytes.Buffer{}, 275 } 276 m.vertices = append(m.vertices, vc) 277 278 m.roots[v] = vc 279 w := vc.w 280 281 var status string 282 switch { 283 case v.Status() == finalized: 284 status = "finalized" 285 case v.state == nil: 286 status = "ready" 287 default: 288 status = v.state.scheduler.state.String() 289 } 290 path := m.vertexPath(v) 291 if v.ArcType != ArcMember { 292 path += fmt.Sprintf("/%v", v.ArcType) 293 } 294 295 indentOnNewline(w, 1) 296 fmt.Fprintf(w, "subgraph %s[%s: %s]\n", m.vertexID(v), path, status) 297 298 m.vertexInfo(vc, recursive) 299 300 return vc 301 } 302 303 func (v *mermaidVertex) closeVertex() { 304 w := v.w 305 306 if v.tasks != nil { 307 indent(v.tasks, 2) 308 fmt.Fprintf(v.tasks, "end\n") 309 w.Write(v.tasks.Bytes()) 310 } 311 312 // TODO: write all notification sources (or is this just the node?) 313 314 indent(w, 1) 315 fmt.Fprintf(w, "\nend\n") 316 } 317 318 func (m *mermaidContext) task(vc *mermaidVertex, t *task, id int) string { 319 v := vc.vertex 320 321 if vc.tasks == nil { 322 vc.tasks = &bytes.Buffer{} 323 indentOnNewline(vc.tasks, 2) 324 fmt.Fprintf(vc.tasks, "subgraph %s_tasks[tasks]\n", m.vertexID(v)) 325 } 326 327 if t != nil && v != t.node.node { 328 panic("inconsistent task") 329 } 330 taskID := fmt.Sprintf("%s_%d", m.vertexID(v), id) 331 var state string 332 var completes condition 333 var kind string 334 if t != nil { 335 state = t.state.String()[:2] 336 completes = t.completes 337 kind = t.run.name 338 } 339 indentOnNewline(vc.tasks, 3) 340 fmt.Fprintf(vc.tasks, "%s(%d", taskID, id) 341 indentOnNewline(vc.tasks, 4) 342 io.WriteString(vc.tasks, state) 343 indentOnNewline(vc.tasks, 4) 344 io.WriteString(vc.tasks, kind) 345 indentOnNewline(vc.tasks, 4) 346 fmt.Fprintf(vc.tasks, "%x)\n", completes) 347 348 if s := t.blockedOn; s != nil { 349 m.vertex(s.node.node, false) 350 fmt.Fprintf(m.w, "%s_tasks == BLOCKED ==> %s\n", m.vertexID(s.node.node), taskID) 351 } 352 353 return taskID 354 } 355 356 func (m *mermaidContext) vertexInfo(vc *mermaidVertex, recursive bool) { 357 if vc.processed { 358 return 359 } 360 vc.processed = true 361 362 v := vc.vertex 363 364 // This must already exist. 365 366 // Dependencies at different scope levels. 367 global := m.w 368 node := vc.w 369 370 if s := v.state; s != nil { 371 for i, t := range s.tasks { 372 taskID := m.task(vc, t, i) 373 name := fmt.Sprintf("%s((%d))", taskID, 1) 374 _ = name 375 // dst := m.pstr(cc) 376 // indent(w, indentLevel) 377 // fmt.Fprintf(w, "%s %s %s\n", name, link, dst) 378 } 379 } 380 381 indentOnNewline(node, 2) 382 if n := v.state; n != nil { 383 for i, d := range n.reqDefIDs { 384 indentOnNewline(node, 2) 385 var id any = d.id 386 if d.v != nil && d.v.ClosedNonRecursive && d.id == 0 { 387 id = "once" 388 } 389 reqID := fmt.Sprintf("%s_req_%d", m.vertexID(v), i) 390 arrow := "%s == R ==> %s\n" 391 format := "%s((%d%s))\n" 392 if d.ignore { 393 arrow = "%s -. R .-> %s\n" 394 format = "%s((<s><i>%d%si</i></s>))\n" 395 } 396 flags := "" 397 if d.isOuterStruct { 398 flags += "S" 399 } 400 if d.exclude != 0 { 401 flags += fmt.Sprintf("-%d", d.exclude) 402 } 403 404 fmt.Fprintf(node, format, reqID, id, flags) 405 m.vertex(d.v, false) 406 fmt.Fprintf(global, arrow, reqID, m.vertexID(d.v)) 407 } 408 indentOnNewline(node, 2) 409 410 // fmt.Fprintf(node, "subgraph %s_conjuncts[conjunctInfo]\n", m.vertexID(v)) 411 fmt.Fprintf(node, "subgraph %s_conjuncts[conjuncts]\n", m.vertexID(v)) 412 for i, conj := range n.conjunctInfo { 413 indentOnNewline(node, 3) 414 kind := conj.kind.String() 415 if kind == "_|_" { 416 kind = "error" 417 } 418 x := conj.flags 419 flags := "" 420 if x != 0 { 421 flags = " " 422 } 423 if x&cHasTop != 0 { 424 flags += "_" 425 } 426 if x&cHasStruct != 0 { 427 flags += "s" 428 } 429 if x&cHasEllipsis != 0 { 430 flags += "." 431 } 432 if x&cHasOpenValidator != 0 { 433 flags += "o" 434 } 435 fmt.Fprintf(node, "%s_conj_%d((%v\n%d%s))", m.vertexID(v), i, kind, conj.id, flags) 436 } 437 indentOnNewline(node, 2) 438 fmt.Fprintln(node, "end") 439 440 if len(n.replaceIDs) > 0 { 441 fmt.Fprintf(node, "subgraph %s_drop[replace]\n", m.vertexID(v)) 442 for i, r := range n.replaceIDs { 443 indentOnNewline(node, 3) 444 dropID := fmt.Sprintf("%s_drop_%d", m.vertexID(v), i) 445 flags := "" 446 if r.add { 447 flags = "+" 448 } 449 fmt.Fprintf(node, "%s((%d->%d%s))\n", dropID, r.from, r.to, flags) 450 } 451 indentOnNewline(node, 2) 452 // fmt.Fprintf(node, "end\n") 453 fmt.Fprintln(node, "end") 454 } 455 } 456 457 if v.Parent != nil { 458 m.vertex(v.Parent, false) // ensure the arc is also processed 459 indentOnNewline(node, 2) 460 fmt.Fprintf(global, "%s --> %s\n", m.vertexID(v.Parent), m.vertexID(v)) 461 } 462 if recursive { 463 for _, arc := range v.Arcs { 464 m.vertex(arc, true) // ensure the arc is also processed 465 } 466 } 467 } 468 469 func (m *mermaidContext) vertexPath(v *Vertex) string { 470 path := m.ctx.PathToString(v.Path()) 471 if path == "" { 472 return "_" 473 } 474 return path 475 } 476 477 const sigPtrLen = 6 478 479 func (m *mermaidContext) vertexID(v *Vertex) string { 480 s := fmt.Sprintf("%p", v) 481 return "v" + s[len(s)-sigPtrLen:] 482 } 483 484 func indentOnNewline(w io.Writer, level int) { 485 w.Write([]byte{'\n'}) 486 indent(w, level) 487 } 488 489 func indent(w io.Writer, level int) { 490 for i := 0; i < level; i++ { 491 io.WriteString(w, " ") 492 } 493 } 494 495 // openBrowser opens the given URL in the default browser. 496 func openBrowser(url string) { 497 var cmd *exec.Cmd 498 499 switch runtime.GOOS { 500 case "windows": 501 cmd = exec.Command("cmd", "/c", "start", url) 502 case "darwin": 503 cmd = exec.Command("open", url) 504 default: 505 cmd = exec.Command("xdg-open", url) 506 } 507 508 err := cmd.Start() 509 if err != nil { 510 log.Fatal(err) 511 } 512 go cmd.Wait() 513 }