Bez popisu

dotgraph.go 14KB

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