123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- // Copyright 2014 Google Inc. All Rights Reserved.
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
-
- package graph
-
- import (
- "fmt"
- "io"
- "math"
- "path/filepath"
- "strings"
-
- "github.com/google/pprof/internal/measurement"
- )
-
- // DotAttributes contains details about the graph itself, giving
- // insight into how its elements should be rendered.
- type DotAttributes struct {
- Nodes map[*Node]*DotNodeAttributes // A map allowing each Node to have its own visualization option
- }
-
- // DotNodeAttributes contains Node specific visualization options.
- type DotNodeAttributes struct {
- Shape string // The optional shape of the node when rendered visually
- Bold bool // If the node should be bold or not
- Peripheries int // An optional number of borders to place around a node
- URL string // An optional url link to add to a node
- Formatter func(*NodeInfo) string // An optional formatter for the node's label
- }
-
- // DotConfig contains attributes about how a graph should be
- // constructed and how it should look.
- type DotConfig struct {
- Title string // The title of the DOT graph
- LegendURL string // The URL to link to from the legend.
- Labels []string // The labels for the DOT's legend
-
- FormatValue func(int64) string // A formatting function for values
- Total int64 // The total weight of the graph, used to compute percentages
- }
-
- const maxNodelets = 4 // Number of nodelets for labels (both numeric and non)
-
- // ComposeDot creates and writes a in the DOT format to the writer, using
- // the configurations given.
- func ComposeDot(w io.Writer, g *Graph, a *DotAttributes, c *DotConfig) {
- builder := &builder{w, a, c}
-
- // Begin constructing DOT by adding a title and legend.
- builder.start()
- defer builder.finish()
- builder.addLegend()
-
- if len(g.Nodes) == 0 {
- return
- }
-
- // Preprocess graph to get id map and find max flat.
- nodeIDMap := make(map[*Node]int)
- hasNodelets := make(map[*Node]bool)
-
- maxFlat := float64(abs64(g.Nodes[0].FlatValue()))
- for i, n := range g.Nodes {
- nodeIDMap[n] = i + 1
- if float64(abs64(n.FlatValue())) > maxFlat {
- maxFlat = float64(abs64(n.FlatValue()))
- }
- }
-
- edges := EdgeMap{}
-
- // Add nodes and nodelets to DOT builder.
- for _, n := range g.Nodes {
- builder.addNode(n, nodeIDMap[n], maxFlat)
- hasNodelets[n] = builder.addNodelets(n, nodeIDMap[n])
-
- // Collect all edges. Use a fake node to support multiple incoming edges.
- for _, e := range n.Out {
- edges[&Node{}] = e
- }
- }
-
- // Add edges to DOT builder. Sort edges by frequency as a hint to the graph layout engine.
- for _, e := range edges.Sort() {
- builder.addEdge(e, nodeIDMap[e.Src], nodeIDMap[e.Dest], hasNodelets[e.Src])
- }
- }
-
- // builder wraps an io.Writer and understands how to compose DOT formatted elements.
- type builder struct {
- io.Writer
- attributes *DotAttributes
- config *DotConfig
- }
-
- // start generates a title and initial node in DOT format.
- func (b *builder) start() {
- graphname := "unnamed"
- if b.config.Title != "" {
- graphname = b.config.Title
- }
- fmt.Fprintln(b, `digraph "`+graphname+`" {`)
- fmt.Fprintln(b, `node [style=filled fillcolor="#f8f8f8"]`)
- }
-
- // finish closes the opening curly bracket in the constructed DOT buffer.
- func (b *builder) finish() {
- fmt.Fprintln(b, "}")
- }
-
- // addLegend generates a legend in DOT format.
- func (b *builder) addLegend() {
- labels := b.config.Labels
- if len(labels) == 0 {
- return
- }
- title := labels[0]
- fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, title)
- fmt.Fprintf(b, ` label="%s\l"`, strings.Join(labels, `\l`))
- if b.config.LegendURL != "" {
- fmt.Fprintf(b, ` URL="%s" target="_blank"`, b.config.LegendURL)
- }
- if b.config.Title != "" {
- fmt.Fprintf(b, ` tooltip="%s"`, b.config.Title)
- }
- fmt.Fprintf(b, "] }\n")
- }
-
- // addNode generates a graph node in DOT format.
- func (b *builder) addNode(node *Node, nodeID int, maxFlat float64) {
- flat, cum := node.FlatValue(), node.CumValue()
- attrs := b.attributes.Nodes[node]
-
- // Populate label for node.
- var label string
- if attrs != nil && attrs.Formatter != nil {
- label = attrs.Formatter(&node.Info)
- } else {
- label = multilinePrintableName(&node.Info)
- }
-
- flatValue := b.config.FormatValue(flat)
- if flat != 0 {
- label = label + fmt.Sprintf(`%s (%s)`,
- flatValue,
- strings.TrimSpace(measurement.Percentage(flat, b.config.Total)))
- } else {
- label = label + "0"
- }
- cumValue := flatValue
- if cum != flat {
- if flat != 0 {
- label = label + `\n`
- } else {
- label = label + " "
- }
- cumValue = b.config.FormatValue(cum)
- label = label + fmt.Sprintf(`of %s (%s)`,
- cumValue,
- strings.TrimSpace(measurement.Percentage(cum, b.config.Total)))
- }
-
- // Scale font sizes from 8 to 24 based on percentage of flat frequency.
- // Use non linear growth to emphasize the size difference.
- baseFontSize, maxFontGrowth := 8, 16.0
- fontSize := baseFontSize
- if maxFlat != 0 && flat != 0 && float64(abs64(flat)) <= maxFlat {
- fontSize += int(math.Ceil(maxFontGrowth * math.Sqrt(float64(abs64(flat))/maxFlat)))
- }
-
- // Determine node shape.
- shape := "box"
- if attrs != nil && attrs.Shape != "" {
- shape = attrs.Shape
- }
-
- // Create DOT attribute for node.
- attr := fmt.Sprintf(`label="%s" id="node%d" fontsize=%d shape=%s tooltip="%s (%s)" color="%s" fillcolor="%s"`,
- label, nodeID, fontSize, shape, node.Info.PrintableName(), cumValue,
- dotColor(float64(node.CumValue())/float64(abs64(b.config.Total)), false),
- dotColor(float64(node.CumValue())/float64(abs64(b.config.Total)), true))
-
- // Add on extra attributes if provided.
- if attrs != nil {
- // Make bold if specified.
- if attrs.Bold {
- attr += ` style="bold,filled"`
- }
-
- // Add peripheries if specified.
- if attrs.Peripheries != 0 {
- attr += fmt.Sprintf(` peripheries=%d`, attrs.Peripheries)
- }
-
- // Add URL if specified. target="_blank" forces the link to open in a new tab.
- if attrs.URL != "" {
- attr += fmt.Sprintf(` URL="%s" target="_blank"`, attrs.URL)
- }
- }
-
- fmt.Fprintf(b, "N%d [%s]\n", nodeID, attr)
- }
-
- // addNodelets generates the DOT boxes for the node tags if they exist.
- func (b *builder) addNodelets(node *Node, nodeID int) bool {
- var nodelets string
-
- // Populate two Tag slices, one for LabelTags and one for NumericTags.
- var ts []*Tag
- lnts := make(map[string][]*Tag)
- for _, t := range node.LabelTags {
- ts = append(ts, t)
- }
- for l, tm := range node.NumericTags {
- for _, t := range tm {
- lnts[l] = append(lnts[l], t)
- }
- }
-
- // For leaf nodes, print cumulative tags (includes weight from
- // children that have been deleted).
- // For internal nodes, print only flat tags.
- flatTags := len(node.Out) > 0
-
- // Select the top maxNodelets alphanumeric labels by weight.
- SortTags(ts, flatTags)
- if len(ts) > maxNodelets {
- ts = ts[:maxNodelets]
- }
- for i, t := range ts {
- w := t.CumValue()
- if flatTags {
- w = t.FlatValue()
- }
- if w == 0 {
- continue
- }
- weight := b.config.FormatValue(w)
- 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)
- nodelets += fmt.Sprintf(`N%d -> N%d_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"]`+"\n", nodeID, nodeID, i, weight, weight, weight)
- if nts := lnts[t.Name]; nts != nil {
- nodelets += b.numericNodelets(nts, maxNodelets, flatTags, fmt.Sprintf(`N%d_%d`, nodeID, i))
- }
- }
-
- if nts := lnts[""]; nts != nil {
- nodelets += b.numericNodelets(nts, maxNodelets, flatTags, fmt.Sprintf(`N%d`, nodeID))
- }
-
- fmt.Fprint(b, nodelets)
- return nodelets != ""
- }
-
- func (b *builder) numericNodelets(nts []*Tag, maxNumNodelets int, flatTags bool, source string) string {
- nodelets := ""
-
- // Collapse numeric labels into maxNumNodelets buckets, of the form:
- // 1MB..2MB, 3MB..5MB, ...
- for j, t := range b.collapsedTags(nts, maxNumNodelets, flatTags) {
- w, attr := t.CumValue(), ` style="dotted"`
- if flatTags || t.FlatValue() == t.CumValue() {
- w, attr = t.FlatValue(), ""
- }
- if w != 0 {
- weight := b.config.FormatValue(w)
- 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)
- nodelets += fmt.Sprintf(`%s -> N%s_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"%s]`+"\n", source, source, j, weight, weight, weight, attr)
- }
- }
- return nodelets
- }
-
- // addEdge generates a graph edge in DOT format.
- func (b *builder) addEdge(edge *Edge, from, to int, hasNodelets bool) {
- var inline string
- if edge.Inline {
- inline = `\n (inline)`
- }
- w := b.config.FormatValue(edge.WeightValue())
- attr := fmt.Sprintf(`label=" %s%s"`, w, inline)
- if b.config.Total != 0 {
- // Note: edge.weight > b.config.Total is possible for profile diffs.
- if weight := 1 + int(min64(abs64(edge.WeightValue()*100/b.config.Total), 100)); weight > 1 {
- attr = fmt.Sprintf(`%s weight=%d`, attr, weight)
- }
- if width := 1 + int(min64(abs64(edge.WeightValue()*5/b.config.Total), 5)); width > 1 {
- attr = fmt.Sprintf(`%s penwidth=%d`, attr, width)
- }
- attr = fmt.Sprintf(`%s color="%s"`, attr,
- dotColor(float64(edge.WeightValue())/float64(abs64(b.config.Total)), false))
- }
- arrow := "->"
- if edge.Residual {
- arrow = "..."
- }
- tooltip := fmt.Sprintf(`"%s %s %s (%s)"`,
- edge.Src.Info.PrintableName(), arrow, edge.Dest.Info.PrintableName(), w)
- attr = fmt.Sprintf(`%s tooltip=%s labeltooltip=%s`, attr, tooltip, tooltip)
-
- if edge.Residual {
- attr = attr + ` style="dotted"`
- }
-
- if hasNodelets {
- // Separate children further if source has tags.
- attr = attr + " minlen=2"
- }
-
- fmt.Fprintf(b, "N%d -> N%d [%s]\n", from, to, attr)
- }
-
- // dotColor returns a color for the given score (between -1.0 and
- // 1.0), with -1.0 colored red, 0.0 colored grey, and 1.0 colored
- // green. If isBackground is true, then a light (low-saturation)
- // color is returned (suitable for use as a background color);
- // otherwise, a darker color is returned (suitable for use as a
- // foreground color).
- func dotColor(score float64, isBackground bool) string {
- // A float between 0.0 and 1.0, indicating the extent to which
- // colors should be shifted away from grey (to make positive and
- // negative values easier to distinguish, and to make more use of
- // the color range.)
- const shift = 0.7
-
- // Saturation and value (in hsv colorspace) for background colors.
- const bgSaturation = 0.1
- const bgValue = 0.93
-
- // Saturation and value (in hsv colorspace) for foreground colors.
- const fgSaturation = 1.0
- const fgValue = 0.7
-
- // Choose saturation and value based on isBackground.
- var saturation float64
- var value float64
- if isBackground {
- saturation = bgSaturation
- value = bgValue
- } else {
- saturation = fgSaturation
- value = fgValue
- }
-
- // Limit the score values to the range [-1.0, 1.0].
- score = math.Max(-1.0, math.Min(1.0, score))
-
- // Reduce saturation near score=0 (so it is colored grey, rather than yellow).
- if math.Abs(score) < 0.2 {
- saturation *= math.Abs(score) / 0.2
- }
-
- // Apply 'shift' to move scores away from 0.0 (grey).
- if score > 0.0 {
- score = math.Pow(score, (1.0 - shift))
- }
- if score < 0.0 {
- score = -math.Pow(-score, (1.0 - shift))
- }
-
- var r, g, b float64 // red, green, blue
- if score < 0.0 {
- g = value
- r = value * (1 + saturation*score)
- } else {
- r = value
- g = value * (1 - saturation*score)
- }
- b = value * (1 - saturation)
- return fmt.Sprintf("#%02x%02x%02x", uint8(r*255.0), uint8(g*255.0), uint8(b*255.0))
- }
-
- func multilinePrintableName(info *NodeInfo) string {
- infoCopy := *info
- infoCopy.Name = ShortenFunctionName(infoCopy.Name)
- infoCopy.Name = strings.Replace(infoCopy.Name, "::", `\n`, -1)
- infoCopy.Name = strings.Replace(infoCopy.Name, ".", `\n`, -1)
- if infoCopy.File != "" {
- infoCopy.File = filepath.Base(infoCopy.File)
- }
- return strings.Join(infoCopy.NameComponents(), `\n`) + `\n`
- }
-
- // collapsedTags trims and sorts a slice of tags.
- func (b *builder) collapsedTags(ts []*Tag, count int, flatTags bool) []*Tag {
- ts = SortTags(ts, flatTags)
- if len(ts) <= count {
- return ts
- }
-
- tagGroups := make([][]*Tag, count)
- for i, t := range (ts)[:count] {
- tagGroups[i] = []*Tag{t}
- }
- for _, t := range (ts)[count:] {
- g, d := 0, tagDistance(t, tagGroups[0][0])
- for i := 1; i < count; i++ {
- if nd := tagDistance(t, tagGroups[i][0]); nd < d {
- g, d = i, nd
- }
- }
- tagGroups[g] = append(tagGroups[g], t)
- }
-
- var nts []*Tag
- for _, g := range tagGroups {
- l, w, c := b.tagGroupLabel(g)
- nts = append(nts, &Tag{
- Name: l,
- Flat: w,
- Cum: c,
- })
- }
- return SortTags(nts, flatTags)
- }
-
- func tagDistance(t, u *Tag) float64 {
- v, _ := measurement.Scale(u.Value, u.Unit, t.Unit)
- if v < float64(t.Value) {
- return float64(t.Value) - v
- }
- return v - float64(t.Value)
- }
-
- func (b *builder) tagGroupLabel(g []*Tag) (label string, flat, cum int64) {
- if len(g) == 1 {
- t := g[0]
- return measurement.Label(t.Value, t.Unit), t.FlatValue(), t.CumValue()
- }
- min := g[0]
- max := g[0]
- df, f := min.FlatDiv, min.Flat
- dc, c := min.CumDiv, min.Cum
- for _, t := range g[1:] {
- if v, _ := measurement.Scale(t.Value, t.Unit, min.Unit); int64(v) < min.Value {
- min = t
- }
- if v, _ := measurement.Scale(t.Value, t.Unit, max.Unit); int64(v) > max.Value {
- max = t
- }
- f += t.Flat
- df += t.FlatDiv
- c += t.Cum
- dc += t.CumDiv
- }
- if df != 0 {
- f = f / df
- }
- if dc != 0 {
- c = c / dc
- }
-
- // Tags are not scaled with the selected output unit because tags are often
- // much smaller than other values which appear, so the range of tag sizes
- // sometimes would appear to be "0..0" when scaled to the selected output unit.
- return measurement.Label(min.Value, min.Unit) + ".." + measurement.Label(max.Value, max.Unit), f, c
- }
-
- func min64(a, b int64) int64 {
- if a < b {
- return a
- }
- return b
- }
|