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