1  
     2  
     3  
     4  
     5  
     6  
     7  
     8  
     9  
    10  
    11  
    12  
    13  
    14  
    15  package graph
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"math"
    21  	"path/filepath"
    22  	"strings"
    23  
    24  	"github.com/google/pprof/internal/measurement"
    25  )
    26  
    27  
    28  
    29  type DotAttributes struct {
    30  	Nodes map[*Node]*DotNodeAttributes 
    31  }
    32  
    33  
    34  type DotNodeAttributes struct {
    35  	Shape       string                 
    36  	Bold        bool                   
    37  	Peripheries int                    
    38  	URL         string                 
    39  	Formatter   func(*NodeInfo) string 
    40  }
    41  
    42  
    43  
    44  type DotConfig struct {
    45  	Title     string   
    46  	LegendURL string   
    47  	Labels    []string 
    48  
    49  	FormatValue func(int64) string 
    50  	Total       int64              
    51  }
    52  
    53  const maxNodelets = 4 
    54  
    55  
    56  
    57  func ComposeDot(w io.Writer, g *Graph, a *DotAttributes, c *DotConfig) {
    58  	builder := &builder{w, a, c}
    59  
    60  	
    61  	builder.start()
    62  	defer builder.finish()
    63  	builder.addLegend()
    64  
    65  	if len(g.Nodes) == 0 {
    66  		return
    67  	}
    68  
    69  	
    70  	nodeIDMap := make(map[*Node]int)
    71  	hasNodelets := make(map[*Node]bool)
    72  
    73  	maxFlat := float64(abs64(g.Nodes[0].FlatValue()))
    74  	for i, n := range g.Nodes {
    75  		nodeIDMap[n] = i + 1
    76  		if float64(abs64(n.FlatValue())) > maxFlat {
    77  			maxFlat = float64(abs64(n.FlatValue()))
    78  		}
    79  	}
    80  
    81  	edges := EdgeMap{}
    82  
    83  	
    84  	for _, n := range g.Nodes {
    85  		builder.addNode(n, nodeIDMap[n], maxFlat)
    86  		hasNodelets[n] = builder.addNodelets(n, nodeIDMap[n])
    87  
    88  		
    89  		for _, e := range n.Out {
    90  			edges[&Node{}] = e
    91  		}
    92  	}
    93  
    94  	
    95  	for _, e := range edges.Sort() {
    96  		builder.addEdge(e, nodeIDMap[e.Src], nodeIDMap[e.Dest], hasNodelets[e.Src])
    97  	}
    98  }
    99  
   100  
   101  type builder struct {
   102  	io.Writer
   103  	attributes *DotAttributes
   104  	config     *DotConfig
   105  }
   106  
   107  
   108  func (b *builder) start() {
   109  	graphname := "unnamed"
   110  	if b.config.Title != "" {
   111  		graphname = b.config.Title
   112  	}
   113  	fmt.Fprintln(b, `digraph "`+graphname+`" {`)
   114  	fmt.Fprintln(b, `node [style=filled fillcolor="#f8f8f8"]`)
   115  }
   116  
   117  
   118  func (b *builder) finish() {
   119  	fmt.Fprintln(b, "}")
   120  }
   121  
   122  
   123  func (b *builder) addLegend() {
   124  	labels := b.config.Labels
   125  	if len(labels) == 0 {
   126  		return
   127  	}
   128  	title := labels[0]
   129  	fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, escapeForDot(title))
   130  	fmt.Fprintf(b, ` label="%s\l"`, strings.Join(escapeAllForDot(labels), `\l`))
   131  	if b.config.LegendURL != "" {
   132  		fmt.Fprintf(b, ` URL="%s" target="_blank"`, b.config.LegendURL)
   133  	}
   134  	if b.config.Title != "" {
   135  		fmt.Fprintf(b, ` tooltip="%s"`, b.config.Title)
   136  	}
   137  	fmt.Fprintf(b, "] }\n")
   138  }
   139  
   140  
   141  func (b *builder) addNode(node *Node, nodeID int, maxFlat float64) {
   142  	flat, cum := node.FlatValue(), node.CumValue()
   143  	attrs := b.attributes.Nodes[node]
   144  
   145  	
   146  	var label string
   147  	if attrs != nil && attrs.Formatter != nil {
   148  		label = attrs.Formatter(&node.Info)
   149  	} else {
   150  		label = multilinePrintableName(&node.Info)
   151  	}
   152  
   153  	flatValue := b.config.FormatValue(flat)
   154  	if flat != 0 {
   155  		label = label + fmt.Sprintf(`%s (%s)`,
   156  			flatValue,
   157  			strings.TrimSpace(measurement.Percentage(flat, b.config.Total)))
   158  	} else {
   159  		label = label + "0"
   160  	}
   161  	cumValue := flatValue
   162  	if cum != flat {
   163  		if flat != 0 {
   164  			label = label + `\n`
   165  		} else {
   166  			label = label + " "
   167  		}
   168  		cumValue = b.config.FormatValue(cum)
   169  		label = label + fmt.Sprintf(`of %s (%s)`,
   170  			cumValue,
   171  			strings.TrimSpace(measurement.Percentage(cum, b.config.Total)))
   172  	}
   173  
   174  	
   175  	
   176  	baseFontSize, maxFontGrowth := 8, 16.0
   177  	fontSize := baseFontSize
   178  	if maxFlat != 0 && flat != 0 && float64(abs64(flat)) <= maxFlat {
   179  		fontSize += int(math.Ceil(maxFontGrowth * math.Sqrt(float64(abs64(flat))/maxFlat)))
   180  	}
   181  
   182  	
   183  	shape := "box"
   184  	if attrs != nil && attrs.Shape != "" {
   185  		shape = attrs.Shape
   186  	}
   187  
   188  	
   189  	attr := fmt.Sprintf(`label="%s" id="node%d" fontsize=%d shape=%s tooltip="%s (%s)" color="%s" fillcolor="%s"`,
   190  		label, nodeID, fontSize, shape, escapeForDot(node.Info.PrintableName()), cumValue,
   191  		dotColor(float64(node.CumValue())/float64(abs64(b.config.Total)), false),
   192  		dotColor(float64(node.CumValue())/float64(abs64(b.config.Total)), true))
   193  
   194  	
   195  	if attrs != nil {
   196  		
   197  		if attrs.Bold {
   198  			attr += ` style="bold,filled"`
   199  		}
   200  
   201  		
   202  		if attrs.Peripheries != 0 {
   203  			attr += fmt.Sprintf(` peripheries=%d`, attrs.Peripheries)
   204  		}
   205  
   206  		
   207  		if attrs.URL != "" {
   208  			attr += fmt.Sprintf(` URL="%s" target="_blank"`, attrs.URL)
   209  		}
   210  	}
   211  
   212  	fmt.Fprintf(b, "N%d [%s]\n", nodeID, attr)
   213  }
   214  
   215  
   216  func (b *builder) addNodelets(node *Node, nodeID int) bool {
   217  	var nodelets string
   218  
   219  	
   220  	var ts []*Tag
   221  	lnts := make(map[string][]*Tag)
   222  	for _, t := range node.LabelTags {
   223  		ts = append(ts, t)
   224  	}
   225  	for l, tm := range node.NumericTags {
   226  		for _, t := range tm {
   227  			lnts[l] = append(lnts[l], t)
   228  		}
   229  	}
   230  
   231  	
   232  	
   233  	
   234  	flatTags := len(node.Out) > 0
   235  
   236  	
   237  	SortTags(ts, flatTags)
   238  	if len(ts) > maxNodelets {
   239  		ts = ts[:maxNodelets]
   240  	}
   241  	for i, t := range ts {
   242  		w := t.CumValue()
   243  		if flatTags {
   244  			w = t.FlatValue()
   245  		}
   246  		if w == 0 {
   247  			continue
   248  		}
   249  		weight := b.config.FormatValue(w)
   250  		nodelets += fmt.Sprintf(`N%d_%d [label = "%s" id="N%d_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", nodeID, i, t.Name, nodeID, i, weight)
   251  		nodelets += fmt.Sprintf(`N%d -> N%d_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"]`+"\n", nodeID, nodeID, i, weight, weight, weight)
   252  		if nts := lnts[t.Name]; nts != nil {
   253  			nodelets += b.numericNodelets(nts, maxNodelets, flatTags, fmt.Sprintf(`N%d_%d`, nodeID, i))
   254  		}
   255  	}
   256  
   257  	if nts := lnts[""]; nts != nil {
   258  		nodelets += b.numericNodelets(nts, maxNodelets, flatTags, fmt.Sprintf(`N%d`, nodeID))
   259  	}
   260  
   261  	fmt.Fprint(b, nodelets)
   262  	return nodelets != ""
   263  }
   264  
   265  func (b *builder) numericNodelets(nts []*Tag, maxNumNodelets int, flatTags bool, source string) string {
   266  	nodelets := ""
   267  
   268  	
   269  	
   270  	for j, t := range b.collapsedTags(nts, maxNumNodelets, flatTags) {
   271  		w, attr := t.CumValue(), ` style="dotted"`
   272  		if flatTags || t.FlatValue() == t.CumValue() {
   273  			w, attr = t.FlatValue(), ""
   274  		}
   275  		if w != 0 {
   276  			weight := b.config.FormatValue(w)
   277  			nodelets += fmt.Sprintf(`N%s_%d [label = "%s" id="N%s_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", source, j, t.Name, source, j, weight)
   278  			nodelets += fmt.Sprintf(`%s -> N%s_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"%s]`+"\n", source, source, j, weight, weight, weight, attr)
   279  		}
   280  	}
   281  	return nodelets
   282  }
   283  
   284  
   285  func (b *builder) addEdge(edge *Edge, from, to int, hasNodelets bool) {
   286  	var inline string
   287  	if edge.Inline {
   288  		inline = `\n (inline)`
   289  	}
   290  	w := b.config.FormatValue(edge.WeightValue())
   291  	attr := fmt.Sprintf(`label=" %s%s"`, w, inline)
   292  	if b.config.Total != 0 {
   293  		
   294  		if weight := 1 + int(min64(abs64(edge.WeightValue()*100/b.config.Total), 100)); weight > 1 {
   295  			attr = fmt.Sprintf(`%s weight=%d`, attr, weight)
   296  		}
   297  		if width := 1 + int(min64(abs64(edge.WeightValue()*5/b.config.Total), 5)); width > 1 {
   298  			attr = fmt.Sprintf(`%s penwidth=%d`, attr, width)
   299  		}
   300  		attr = fmt.Sprintf(`%s color="%s"`, attr,
   301  			dotColor(float64(edge.WeightValue())/float64(abs64(b.config.Total)), false))
   302  	}
   303  	arrow := "->"
   304  	if edge.Residual {
   305  		arrow = "..."
   306  	}
   307  	tooltip := fmt.Sprintf(`"%s %s %s (%s)"`,
   308  		escapeForDot(edge.Src.Info.PrintableName()), arrow,
   309  		escapeForDot(edge.Dest.Info.PrintableName()), w)
   310  	attr = fmt.Sprintf(`%s tooltip=%s labeltooltip=%s`, attr, tooltip, tooltip)
   311  
   312  	if edge.Residual {
   313  		attr = attr + ` style="dotted"`
   314  	}
   315  
   316  	if hasNodelets {
   317  		
   318  		attr = attr + " minlen=2"
   319  	}
   320  
   321  	fmt.Fprintf(b, "N%d -> N%d [%s]\n", from, to, attr)
   322  }
   323  
   324  
   325  
   326  
   327  
   328  
   329  
   330  func dotColor(score float64, isBackground bool) string {
   331  	
   332  	
   333  	
   334  	
   335  	const shift = 0.7
   336  
   337  	
   338  	const bgSaturation = 0.1
   339  	const bgValue = 0.93
   340  
   341  	
   342  	const fgSaturation = 1.0
   343  	const fgValue = 0.7
   344  
   345  	
   346  	var saturation float64
   347  	var value float64
   348  	if isBackground {
   349  		saturation = bgSaturation
   350  		value = bgValue
   351  	} else {
   352  		saturation = fgSaturation
   353  		value = fgValue
   354  	}
   355  
   356  	
   357  	score = math.Max(-1.0, math.Min(1.0, score))
   358  
   359  	
   360  	if math.Abs(score) < 0.2 {
   361  		saturation *= math.Abs(score) / 0.2
   362  	}
   363  
   364  	
   365  	if score > 0.0 {
   366  		score = math.Pow(score, (1.0 - shift))
   367  	}
   368  	if score < 0.0 {
   369  		score = -math.Pow(-score, (1.0 - shift))
   370  	}
   371  
   372  	var r, g, b float64 
   373  	if score < 0.0 {
   374  		g = value
   375  		r = value * (1 + saturation*score)
   376  	} else {
   377  		r = value
   378  		g = value * (1 - saturation*score)
   379  	}
   380  	b = value * (1 - saturation)
   381  	return fmt.Sprintf("#%02x%02x%02x", uint8(r*255.0), uint8(g*255.0), uint8(b*255.0))
   382  }
   383  
   384  func multilinePrintableName(info *NodeInfo) string {
   385  	infoCopy := *info
   386  	infoCopy.Name = escapeForDot(ShortenFunctionName(infoCopy.Name))
   387  	infoCopy.Name = strings.Replace(infoCopy.Name, "::", `\n`, -1)
   388  	
   389  	
   390  	infoCopy.Name = strings.Replace(infoCopy.Name, "[...]", "[…]", -1)
   391  	infoCopy.Name = strings.Replace(infoCopy.Name, ".", `\n`, -1)
   392  	if infoCopy.File != "" {
   393  		infoCopy.File = filepath.Base(infoCopy.File)
   394  	}
   395  	return strings.Join(infoCopy.NameComponents(), `\n`) + `\n`
   396  }
   397  
   398  
   399  func (b *builder) collapsedTags(ts []*Tag, count int, flatTags bool) []*Tag {
   400  	ts = SortTags(ts, flatTags)
   401  	if len(ts) <= count {
   402  		return ts
   403  	}
   404  
   405  	tagGroups := make([][]*Tag, count)
   406  	for i, t := range (ts)[:count] {
   407  		tagGroups[i] = []*Tag{t}
   408  	}
   409  	for _, t := range (ts)[count:] {
   410  		g, d := 0, tagDistance(t, tagGroups[0][0])
   411  		for i := 1; i < count; i++ {
   412  			if nd := tagDistance(t, tagGroups[i][0]); nd < d {
   413  				g, d = i, nd
   414  			}
   415  		}
   416  		tagGroups[g] = append(tagGroups[g], t)
   417  	}
   418  
   419  	var nts []*Tag
   420  	for _, g := range tagGroups {
   421  		l, w, c := b.tagGroupLabel(g)
   422  		nts = append(nts, &Tag{
   423  			Name: l,
   424  			Flat: w,
   425  			Cum:  c,
   426  		})
   427  	}
   428  	return SortTags(nts, flatTags)
   429  }
   430  
   431  func tagDistance(t, u *Tag) float64 {
   432  	v, _ := measurement.Scale(u.Value, u.Unit, t.Unit)
   433  	if v < float64(t.Value) {
   434  		return float64(t.Value) - v
   435  	}
   436  	return v - float64(t.Value)
   437  }
   438  
   439  func (b *builder) tagGroupLabel(g []*Tag) (label string, flat, cum int64) {
   440  	if len(g) == 1 {
   441  		t := g[0]
   442  		return measurement.Label(t.Value, t.Unit), t.FlatValue(), t.CumValue()
   443  	}
   444  	min := g[0]
   445  	max := g[0]
   446  	df, f := min.FlatDiv, min.Flat
   447  	dc, c := min.CumDiv, min.Cum
   448  	for _, t := range g[1:] {
   449  		if v, _ := measurement.Scale(t.Value, t.Unit, min.Unit); int64(v) < min.Value {
   450  			min = t
   451  		}
   452  		if v, _ := measurement.Scale(t.Value, t.Unit, max.Unit); int64(v) > max.Value {
   453  			max = t
   454  		}
   455  		f += t.Flat
   456  		df += t.FlatDiv
   457  		c += t.Cum
   458  		dc += t.CumDiv
   459  	}
   460  	if df != 0 {
   461  		f = f / df
   462  	}
   463  	if dc != 0 {
   464  		c = c / dc
   465  	}
   466  
   467  	
   468  	
   469  	
   470  	return measurement.Label(min.Value, min.Unit) + ".." + measurement.Label(max.Value, max.Unit), f, c
   471  }
   472  
   473  func min64(a, b int64) int64 {
   474  	if a < b {
   475  		return a
   476  	}
   477  	return b
   478  }
   479  
   480  
   481  func escapeAllForDot(in []string) []string {
   482  	var out = make([]string, len(in))
   483  	for i := range in {
   484  		out[i] = escapeForDot(in[i])
   485  	}
   486  	return out
   487  }
   488  
   489  
   490  
   491  
   492  func escapeForDot(str string) string {
   493  	return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(str, `\`, `\\`), `"`, `\"`), "\n", `\l`)
   494  }
   495  
View as plain text