1 // Copyright 2014 Google Inc. All Rights Reserved. 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 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 // DotAttributes contains details about the graph itself, giving 28 // insight into how its elements should be rendered. 29 type DotAttributes struct { 30 Nodes map[*Node]*DotNodeAttributes // A map allowing each Node to have its own visualization option 31 } 32 33 // DotNodeAttributes contains Node specific visualization options. 34 type DotNodeAttributes struct { 35 Shape string // The optional shape of the node when rendered visually 36 Bold bool // If the node should be bold or not 37 Peripheries int // An optional number of borders to place around a node 38 URL string // An optional url link to add to a node 39 Formatter func(*NodeInfo) string // An optional formatter for the node's label 40 } 41 42 // DotConfig contains attributes about how a graph should be 43 // constructed and how it should look. 44 type DotConfig struct { 45 Title string // The title of the DOT graph 46 LegendURL string // The URL to link to from the legend. 47 Labels []string // The labels for the DOT's legend 48 49 FormatValue func(int64) string // A formatting function for values 50 Total int64 // The total weight of the graph, used to compute percentages 51 } 52 53 const maxNodelets = 4 // Number of nodelets for labels (both numeric and non) 54 55 // ComposeDot creates and writes a in the DOT format to the writer, using 56 // the configurations given. 57 func ComposeDot(w io.Writer, g *Graph, a *DotAttributes, c *DotConfig) { 58 builder := &builder{w, a, c} 59 60 // Begin constructing DOT by adding a title and legend. 61 builder.start() 62 defer builder.finish() 63 builder.addLegend() 64 65 if len(g.Nodes) == 0 { 66 return 67 } 68 69 // Preprocess graph to get id map and find max flat. 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 // Add nodes and nodelets to DOT builder. 84 for _, n := range g.Nodes { 85 builder.addNode(n, nodeIDMap[n], maxFlat) 86 hasNodelets[n] = builder.addNodelets(n, nodeIDMap[n]) 87 88 // Collect all edges. Use a fake node to support multiple incoming edges. 89 for _, e := range n.Out { 90 edges[&Node{}] = e 91 } 92 } 93 94 // Add edges to DOT builder. Sort edges by frequency as a hint to the graph layout engine. 95 for _, e := range edges.Sort() { 96 builder.addEdge(e, nodeIDMap[e.Src], nodeIDMap[e.Dest], hasNodelets[e.Src]) 97 } 98 } 99 100 // builder wraps an io.Writer and understands how to compose DOT formatted elements. 101 type builder struct { 102 io.Writer 103 attributes *DotAttributes 104 config *DotConfig 105 } 106 107 // start generates a title and initial node in DOT format. 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 // finish closes the opening curly bracket in the constructed DOT buffer. 118 func (b *builder) finish() { 119 fmt.Fprintln(b, "}") 120 } 121 122 // addLegend generates a legend in DOT format. 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`, title) 130 fmt.Fprintf(b, ` label="%s\l"`, strings.Join(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 // addNode generates a graph node in DOT format. 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 // Populate label for node. 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(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(percentage(cum, b.config.Total))) 172 } 173 174 // Scale font sizes from 8 to 24 based on percentage of flat frequency. 175 // Use non linear growth to emphasize the size difference. 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 // Determine node shape. 183 shape := "box" 184 if attrs != nil && attrs.Shape != "" { 185 shape = attrs.Shape 186 } 187 188 // Create DOT attribute for node. 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, 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 // Add on extra attributes if provided. 195 if attrs != nil { 196 // Make bold if specified. 197 if attrs.Bold { 198 attr += ` style="bold,filled"` 199 } 200 201 // Add peripheries if specified. 202 if attrs.Peripheries != 0 { 203 attr += fmt.Sprintf(` peripheries=%d`, attrs.Peripheries) 204 } 205 206 // Add URL if specified. target="_blank" forces the link to open in a new tab. 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 // addNodelets generates the DOT boxes for the node tags if they exist. 216 func (b *builder) addNodelets(node *Node, nodeID int) bool { 217 var nodelets string 218 219 // Populate two Tag slices, one for LabelTags and one for NumericTags. 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 // For leaf nodes, print cumulative tags (includes weight from 232 // children that have been deleted). 233 // For internal nodes, print only flat tags. 234 flatTags := len(node.Out) > 0 235 236 // Select the top maxNodelets alphanumeric labels by weight. 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 // Collapse numeric labels into maxNumNodelets buckets, of the form: 269 // 1MB..2MB, 3MB..5MB, ... 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 // addEdge generates a graph edge in DOT format. 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 // Note: edge.weight > b.config.Total is possible for profile diffs. 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 edge.Src.Info.PrintableName(), arrow, edge.Dest.Info.PrintableName(), w) 309 attr = fmt.Sprintf(`%s tooltip=%s labeltooltip=%s`, attr, tooltip, tooltip) 310 311 if edge.Residual { 312 attr = attr + ` style="dotted"` 313 } 314 315 if hasNodelets { 316 // Separate children further if source has tags. 317 attr = attr + " minlen=2" 318 } 319 320 fmt.Fprintf(b, "N%d -> N%d [%s]\n", from, to, attr) 321 } 322 323 // dotColor returns a color for the given score (between -1.0 and 324 // 1.0), with -1.0 colored red, 0.0 colored grey, and 1.0 colored 325 // green. If isBackground is true, then a light (low-saturation) 326 // color is returned (suitable for use as a background color); 327 // otherwise, a darker color is returned (suitable for use as a 328 // foreground color). 329 func dotColor(score float64, isBackground bool) string { 330 // A float between 0.0 and 1.0, indicating the extent to which 331 // colors should be shifted away from grey (to make positive and 332 // negative values easier to distinguish, and to make more use of 333 // the color range.) 334 const shift = 0.7 335 336 // Saturation and value (in hsv colorspace) for background colors. 337 const bgSaturation = 0.1 338 const bgValue = 0.93 339 340 // Saturation and value (in hsv colorspace) for foreground colors. 341 const fgSaturation = 1.0 342 const fgValue = 0.7 343 344 // Choose saturation and value based on isBackground. 345 var saturation float64 346 var value float64 347 if isBackground { 348 saturation = bgSaturation 349 value = bgValue 350 } else { 351 saturation = fgSaturation 352 value = fgValue 353 } 354 355 // Limit the score values to the range [-1.0, 1.0]. 356 score = math.Max(-1.0, math.Min(1.0, score)) 357 358 // Reduce saturation near score=0 (so it is colored grey, rather than yellow). 359 if math.Abs(score) < 0.2 { 360 saturation *= math.Abs(score) / 0.2 361 } 362 363 // Apply 'shift' to move scores away from 0.0 (grey). 364 if score > 0.0 { 365 score = math.Pow(score, (1.0 - shift)) 366 } 367 if score < 0.0 { 368 score = -math.Pow(-score, (1.0 - shift)) 369 } 370 371 var r, g, b float64 // red, green, blue 372 if score < 0.0 { 373 g = value 374 r = value * (1 + saturation*score) 375 } else { 376 r = value 377 g = value * (1 - saturation*score) 378 } 379 b = value * (1 - saturation) 380 return fmt.Sprintf("#%02x%02x%02x", uint8(r*255.0), uint8(g*255.0), uint8(b*255.0)) 381 } 382 383 // percentage computes the percentage of total of a value, and encodes 384 // it as a string. At least two digits of precision are printed. 385 func percentage(value, total int64) string { 386 var ratio float64 387 if total != 0 { 388 ratio = math.Abs(float64(value)/float64(total)) * 100 389 } 390 switch { 391 case math.Abs(ratio) >= 99.95 && math.Abs(ratio) <= 100.05: 392 return " 100%" 393 case math.Abs(ratio) >= 1.0: 394 return fmt.Sprintf("%5.2f%%", ratio) 395 default: 396 return fmt.Sprintf("%5.2g%%", ratio) 397 } 398 } 399 400 func multilinePrintableName(info *NodeInfo) string { 401 infoCopy := *info 402 infoCopy.Name = strings.Replace(infoCopy.Name, "::", `\n`, -1) 403 infoCopy.Name = strings.Replace(infoCopy.Name, ".", `\n`, -1) 404 if infoCopy.File != "" { 405 infoCopy.File = filepath.Base(infoCopy.File) 406 } 407 return strings.Join(infoCopy.NameComponents(), `\n`) + `\n` 408 } 409 410 // collapsedTags trims and sorts a slice of tags. 411 func (b *builder) collapsedTags(ts []*Tag, count int, flatTags bool) []*Tag { 412 ts = SortTags(ts, flatTags) 413 if len(ts) <= count { 414 return ts 415 } 416 417 tagGroups := make([][]*Tag, count) 418 for i, t := range (ts)[:count] { 419 tagGroups[i] = []*Tag{t} 420 } 421 for _, t := range (ts)[count:] { 422 g, d := 0, tagDistance(t, tagGroups[0][0]) 423 for i := 1; i < count; i++ { 424 if nd := tagDistance(t, tagGroups[i][0]); nd < d { 425 g, d = i, nd 426 } 427 } 428 tagGroups[g] = append(tagGroups[g], t) 429 } 430 431 var nts []*Tag 432 for _, g := range tagGroups { 433 l, w, c := b.tagGroupLabel(g) 434 nts = append(nts, &Tag{ 435 Name: l, 436 Flat: w, 437 Cum: c, 438 }) 439 } 440 return SortTags(nts, flatTags) 441 } 442 443 func tagDistance(t, u *Tag) float64 { 444 v, _ := measurement.Scale(u.Value, u.Unit, t.Unit) 445 if v < float64(t.Value) { 446 return float64(t.Value) - v 447 } 448 return v - float64(t.Value) 449 } 450 451 func (b *builder) tagGroupLabel(g []*Tag) (label string, flat, cum int64) { 452 if len(g) == 1 { 453 t := g[0] 454 return measurement.Label(t.Value, t.Unit), t.FlatValue(), t.CumValue() 455 } 456 min := g[0] 457 max := g[0] 458 df, f := min.FlatDiv, min.Flat 459 dc, c := min.CumDiv, min.Cum 460 for _, t := range g[1:] { 461 if v, _ := measurement.Scale(t.Value, t.Unit, min.Unit); int64(v) < min.Value { 462 min = t 463 } 464 if v, _ := measurement.Scale(t.Value, t.Unit, max.Unit); int64(v) > max.Value { 465 max = t 466 } 467 f += t.Flat 468 df += t.FlatDiv 469 c += t.Cum 470 dc += t.CumDiv 471 } 472 if df != 0 { 473 f = f / df 474 } 475 if dc != 0 { 476 c = c / dc 477 } 478 479 // Tags are not scaled with the selected output unit because tags are often 480 // much smaller than other values which appear, so the range of tag sizes 481 // sometimes would appear to be "0..0" when scaled to the selected output unit. 482 return measurement.Label(min.Value, min.Unit) + ".." + measurement.Label(max.Value, max.Unit), f, c 483 } 484 485 func min64(a, b int64) int64 { 486 if a < b { 487 return a 488 } 489 return b 490 } 491