golang.org/toolchain@v0.0.1-go1.9rc2.windows-amd64/src/cmd/vendor/github.com/google/pprof/internal/graph/dotgraph.go (about) 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 Labels []string // The labels for the DOT's legend 47 48 FormatValue func(int64) string // A formatting function for values 49 FormatTag func(int64, string) string // A formatting function for numeric tags 50 Total int64 // The total weight of the graph, used to compute percentages 51 } 52 53 // Compose creates and writes a in the DOT format to the writer, using 54 // the configurations given. 55 func ComposeDot(w io.Writer, g *Graph, a *DotAttributes, c *DotConfig) { 56 builder := &builder{w, a, c} 57 58 // Begin constructing DOT by adding a title and legend. 59 builder.start() 60 defer builder.finish() 61 builder.addLegend() 62 63 if len(g.Nodes) == 0 { 64 return 65 } 66 67 // Preprocess graph to get id map and find max flat. 68 nodeIDMap := make(map[*Node]int) 69 hasNodelets := make(map[*Node]bool) 70 71 maxFlat := float64(abs64(g.Nodes[0].FlatValue())) 72 for i, n := range g.Nodes { 73 nodeIDMap[n] = i + 1 74 if float64(abs64(n.FlatValue())) > maxFlat { 75 maxFlat = float64(abs64(n.FlatValue())) 76 } 77 } 78 79 edges := EdgeMap{} 80 81 // Add nodes and nodelets to DOT builder. 82 for _, n := range g.Nodes { 83 builder.addNode(n, nodeIDMap[n], maxFlat) 84 hasNodelets[n] = builder.addNodelets(n, nodeIDMap[n]) 85 86 // Collect all edges. Use a fake node to support multiple incoming edges. 87 for _, e := range n.Out { 88 edges[&Node{}] = e 89 } 90 } 91 92 // Add edges to DOT builder. Sort edges by frequency as a hint to the graph layout engine. 93 for _, e := range edges.Sort() { 94 builder.addEdge(e, nodeIDMap[e.Src], nodeIDMap[e.Dest], hasNodelets[e.Src]) 95 } 96 } 97 98 // builder wraps an io.Writer and understands how to compose DOT formatted elements. 99 type builder struct { 100 io.Writer 101 attributes *DotAttributes 102 config *DotConfig 103 } 104 105 // start generates a title and initial node in DOT format. 106 func (b *builder) start() { 107 graphname := "unnamed" 108 if b.config.Title != "" { 109 graphname = b.config.Title 110 } 111 fmt.Fprintln(b, `digraph "`+graphname+`" {`) 112 fmt.Fprintln(b, `node [style=filled fillcolor="#f8f8f8"]`) 113 } 114 115 // finish closes the opening curly bracket in the constructed DOT buffer. 116 func (b *builder) finish() { 117 fmt.Fprintln(b, "}") 118 } 119 120 // addLegend generates a legend in DOT format. 121 func (b *builder) addLegend() { 122 labels := b.config.Labels 123 var title string 124 if len(labels) > 0 { 125 title = labels[0] 126 } 127 fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16 label="%s\l"] }`+"\n", title, strings.Join(labels, `\l`)) 128 } 129 130 // addNode generates a graph node in DOT format. 131 func (b *builder) addNode(node *Node, nodeID int, maxFlat float64) { 132 flat, cum := node.FlatValue(), node.CumValue() 133 attrs := b.attributes.Nodes[node] 134 135 // Populate label for node. 136 var label string 137 if attrs != nil && attrs.Formatter != nil { 138 label = attrs.Formatter(&node.Info) 139 } else { 140 label = multilinePrintableName(&node.Info) 141 } 142 143 flatValue := b.config.FormatValue(flat) 144 if flat != 0 { 145 label = label + fmt.Sprintf(`%s (%s)`, 146 flatValue, 147 strings.TrimSpace(percentage(flat, b.config.Total))) 148 } else { 149 label = label + "0" 150 } 151 cumValue := flatValue 152 if cum != flat { 153 if flat != 0 { 154 label = label + `\n` 155 } else { 156 label = label + " " 157 } 158 cumValue = b.config.FormatValue(cum) 159 label = label + fmt.Sprintf(`of %s (%s)`, 160 cumValue, 161 strings.TrimSpace(percentage(cum, b.config.Total))) 162 } 163 164 // Scale font sizes from 8 to 24 based on percentage of flat frequency. 165 // Use non linear growth to emphasize the size difference. 166 baseFontSize, maxFontGrowth := 8, 16.0 167 fontSize := baseFontSize 168 if maxFlat != 0 && flat != 0 && float64(abs64(flat)) <= maxFlat { 169 fontSize += int(math.Ceil(maxFontGrowth * math.Sqrt(float64(abs64(flat))/maxFlat))) 170 } 171 172 // Determine node shape. 173 shape := "box" 174 if attrs != nil && attrs.Shape != "" { 175 shape = attrs.Shape 176 } 177 178 // Create DOT attribute for node. 179 attr := fmt.Sprintf(`label="%s" fontsize=%d shape=%s tooltip="%s (%s)" color="%s" fillcolor="%s"`, 180 label, fontSize, shape, node.Info.PrintableName(), cumValue, 181 dotColor(float64(node.CumValue())/float64(abs64(b.config.Total)), false), 182 dotColor(float64(node.CumValue())/float64(abs64(b.config.Total)), true)) 183 184 // Add on extra attributes if provided. 185 if attrs != nil { 186 // Make bold if specified. 187 if attrs.Bold { 188 attr += ` style="bold,filled"` 189 } 190 191 // Add peripheries if specified. 192 if attrs.Peripheries != 0 { 193 attr += fmt.Sprintf(` peripheries=%d`, attrs.Peripheries) 194 } 195 196 // Add URL if specified. target="_blank" forces the link to open in a new tab. 197 if attrs.URL != "" { 198 attr += fmt.Sprintf(` URL="%s" target="_blank"`, attrs.URL) 199 } 200 } 201 202 fmt.Fprintf(b, "N%d [%s]\n", nodeID, attr) 203 } 204 205 // addNodelets generates the DOT boxes for the node tags if they exist. 206 func (b *builder) addNodelets(node *Node, nodeID int) bool { 207 const maxNodelets = 4 // Number of nodelets for alphanumeric labels 208 const maxNumNodelets = 4 // Number of nodelets for numeric labels 209 var nodelets string 210 211 // Populate two Tag slices, one for LabelTags and one for NumericTags. 212 var ts []*Tag 213 lnts := make(map[string][]*Tag, 0) 214 for _, t := range node.LabelTags { 215 ts = append(ts, t) 216 } 217 for l, tm := range node.NumericTags { 218 for _, t := range tm { 219 lnts[l] = append(lnts[l], t) 220 } 221 } 222 223 // For leaf nodes, print cumulative tags (includes weight from 224 // children that have been deleted). 225 // For internal nodes, print only flat tags. 226 flatTags := len(node.Out) > 0 227 228 // Select the top maxNodelets alphanumeric labels by weight. 229 SortTags(ts, flatTags) 230 if len(ts) > maxNodelets { 231 ts = ts[:maxNodelets] 232 } 233 for i, t := range ts { 234 w := t.CumValue() 235 if flatTags { 236 w = t.FlatValue() 237 } 238 if w == 0 { 239 continue 240 } 241 weight := b.config.FormatValue(w) 242 nodelets += fmt.Sprintf(`N%d_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", nodeID, i, t.Name, weight) 243 nodelets += fmt.Sprintf(`N%d -> N%d_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"]`+"\n", nodeID, nodeID, i, weight, weight, weight) 244 if nts := lnts[t.Name]; nts != nil { 245 nodelets += b.numericNodelets(nts, maxNumNodelets, flatTags, fmt.Sprintf(`N%d_%d`, nodeID, i)) 246 } 247 } 248 249 if nts := lnts[""]; nts != nil { 250 nodelets += b.numericNodelets(nts, maxNumNodelets, flatTags, fmt.Sprintf(`N%d`, nodeID)) 251 } 252 253 fmt.Fprint(b, nodelets) 254 return nodelets != "" 255 } 256 257 func (b *builder) numericNodelets(nts []*Tag, maxNumNodelets int, flatTags bool, source string) string { 258 nodelets := "" 259 260 // Collapse numeric labels into maxNumNodelets buckets, of the form: 261 // 1MB..2MB, 3MB..5MB, ... 262 for j, t := range b.collapsedTags(nts, maxNumNodelets, flatTags) { 263 w, attr := t.CumValue(), ` style="dotted"` 264 if flatTags || t.FlatValue() == t.CumValue() { 265 w, attr = t.FlatValue(), "" 266 } 267 if w != 0 { 268 weight := b.config.FormatValue(w) 269 nodelets += fmt.Sprintf(`N%s_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", source, j, t.Name, weight) 270 nodelets += fmt.Sprintf(`%s -> N%s_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"%s]`+"\n", source, source, j, weight, weight, weight, attr) 271 } 272 } 273 return nodelets 274 } 275 276 // addEdge generates a graph edge in DOT format. 277 func (b *builder) addEdge(edge *Edge, from, to int, hasNodelets bool) { 278 var inline string 279 if edge.Inline { 280 inline = `\n (inline)` 281 } 282 w := b.config.FormatValue(edge.WeightValue()) 283 attr := fmt.Sprintf(`label=" %s%s"`, w, inline) 284 if b.config.Total != 0 { 285 // Note: edge.weight > b.config.Total is possible for profile diffs. 286 if weight := 1 + int(min64(abs64(edge.WeightValue()*100/b.config.Total), 100)); weight > 1 { 287 attr = fmt.Sprintf(`%s weight=%d`, attr, weight) 288 } 289 if width := 1 + int(min64(abs64(edge.WeightValue()*5/b.config.Total), 5)); width > 1 { 290 attr = fmt.Sprintf(`%s penwidth=%d`, attr, width) 291 } 292 attr = fmt.Sprintf(`%s color="%s"`, attr, 293 dotColor(float64(edge.WeightValue())/float64(abs64(b.config.Total)), false)) 294 } 295 arrow := "->" 296 if edge.Residual { 297 arrow = "..." 298 } 299 tooltip := fmt.Sprintf(`"%s %s %s (%s)"`, 300 edge.Src.Info.PrintableName(), arrow, edge.Dest.Info.PrintableName(), w) 301 attr = fmt.Sprintf(`%s tooltip=%s labeltooltip=%s`, attr, tooltip, tooltip) 302 303 if edge.Residual { 304 attr = attr + ` style="dotted"` 305 } 306 307 if hasNodelets { 308 // Separate children further if source has tags. 309 attr = attr + " minlen=2" 310 } 311 312 fmt.Fprintf(b, "N%d -> N%d [%s]\n", from, to, attr) 313 } 314 315 // dotColor returns a color for the given score (between -1.0 and 316 // 1.0), with -1.0 colored red, 0.0 colored grey, and 1.0 colored 317 // green. If isBackground is true, then a light (low-saturation) 318 // color is returned (suitable for use as a background color); 319 // otherwise, a darker color is returned (suitable for use as a 320 // foreground color). 321 func dotColor(score float64, isBackground bool) string { 322 // A float between 0.0 and 1.0, indicating the extent to which 323 // colors should be shifted away from grey (to make positive and 324 // negative values easier to distinguish, and to make more use of 325 // the color range.) 326 const shift = 0.7 327 328 // Saturation and value (in hsv colorspace) for background colors. 329 const bgSaturation = 0.1 330 const bgValue = 0.93 331 332 // Saturation and value (in hsv colorspace) for foreground colors. 333 const fgSaturation = 1.0 334 const fgValue = 0.7 335 336 // Choose saturation and value based on isBackground. 337 var saturation float64 338 var value float64 339 if isBackground { 340 saturation = bgSaturation 341 value = bgValue 342 } else { 343 saturation = fgSaturation 344 value = fgValue 345 } 346 347 // Limit the score values to the range [-1.0, 1.0]. 348 score = math.Max(-1.0, math.Min(1.0, score)) 349 350 // Reduce saturation near score=0 (so it is colored grey, rather than yellow). 351 if math.Abs(score) < 0.2 { 352 saturation *= math.Abs(score) / 0.2 353 } 354 355 // Apply 'shift' to move scores away from 0.0 (grey). 356 if score > 0.0 { 357 score = math.Pow(score, (1.0 - shift)) 358 } 359 if score < 0.0 { 360 score = -math.Pow(-score, (1.0 - shift)) 361 } 362 363 var r, g, b float64 // red, green, blue 364 if score < 0.0 { 365 g = value 366 r = value * (1 + saturation*score) 367 } else { 368 r = value 369 g = value * (1 - saturation*score) 370 } 371 b = value * (1 - saturation) 372 return fmt.Sprintf("#%02x%02x%02x", uint8(r*255.0), uint8(g*255.0), uint8(b*255.0)) 373 } 374 375 // percentage computes the percentage of total of a value, and encodes 376 // it as a string. At least two digits of precision are printed. 377 func percentage(value, total int64) string { 378 var ratio float64 379 if total != 0 { 380 ratio = math.Abs(float64(value)/float64(total)) * 100 381 } 382 switch { 383 case math.Abs(ratio) >= 99.95 && math.Abs(ratio) <= 100.05: 384 return " 100%" 385 case math.Abs(ratio) >= 1.0: 386 return fmt.Sprintf("%5.2f%%", ratio) 387 default: 388 return fmt.Sprintf("%5.2g%%", ratio) 389 } 390 } 391 392 func multilinePrintableName(info *NodeInfo) string { 393 infoCopy := *info 394 infoCopy.Name = strings.Replace(infoCopy.Name, "::", `\n`, -1) 395 infoCopy.Name = strings.Replace(infoCopy.Name, ".", `\n`, -1) 396 if infoCopy.File != "" { 397 infoCopy.File = filepath.Base(infoCopy.File) 398 } 399 return strings.Join(infoCopy.NameComponents(), `\n`) + `\n` 400 } 401 402 // collapsedTags trims and sorts a slice of tags. 403 func (b *builder) collapsedTags(ts []*Tag, count int, flatTags bool) []*Tag { 404 ts = SortTags(ts, flatTags) 405 if len(ts) <= count { 406 return ts 407 } 408 409 tagGroups := make([][]*Tag, count) 410 for i, t := range (ts)[:count] { 411 tagGroups[i] = []*Tag{t} 412 } 413 for _, t := range (ts)[count:] { 414 g, d := 0, tagDistance(t, tagGroups[0][0]) 415 for i := 1; i < count; i++ { 416 if nd := tagDistance(t, tagGroups[i][0]); nd < d { 417 g, d = i, nd 418 } 419 } 420 tagGroups[g] = append(tagGroups[g], t) 421 } 422 423 var nts []*Tag 424 for _, g := range tagGroups { 425 l, w, c := b.tagGroupLabel(g) 426 nts = append(nts, &Tag{ 427 Name: l, 428 Flat: w, 429 Cum: c, 430 }) 431 } 432 return SortTags(nts, flatTags) 433 } 434 435 func tagDistance(t, u *Tag) float64 { 436 v, _ := measurement.Scale(u.Value, u.Unit, t.Unit) 437 if v < float64(t.Value) { 438 return float64(t.Value) - v 439 } 440 return v - float64(t.Value) 441 } 442 443 func (b *builder) tagGroupLabel(g []*Tag) (label string, flat, cum int64) { 444 formatTag := b.config.FormatTag 445 if formatTag == nil { 446 formatTag = measurement.Label 447 } 448 449 if len(g) == 1 { 450 t := g[0] 451 return formatTag(t.Value, t.Unit), t.FlatValue(), t.CumValue() 452 } 453 min := g[0] 454 max := g[0] 455 df, f := min.FlatDiv, min.Flat 456 dc, c := min.CumDiv, min.Cum 457 for _, t := range g[1:] { 458 if v, _ := measurement.Scale(t.Value, t.Unit, min.Unit); int64(v) < min.Value { 459 min = t 460 } 461 if v, _ := measurement.Scale(t.Value, t.Unit, max.Unit); int64(v) > max.Value { 462 max = t 463 } 464 f += t.Flat 465 df += t.FlatDiv 466 c += t.Cum 467 dc += t.CumDiv 468 } 469 if df != 0 { 470 f = f / df 471 } 472 if dc != 0 { 473 c = c / dc 474 } 475 return formatTag(min.Value, min.Unit) + ".." + formatTag(max.Value, max.Unit), f, c 476 } 477 478 func min64(a, b int64) int64 { 479 if a < b { 480 return a 481 } 482 return b 483 }