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  }