You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

475 lines
14 KiB

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