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  }