Нет описания

dotgraph.go 14KB

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