mirror of
https://github.com/langhuihui/monibuca.git
synced 2026-04-25 08:06:56 +08:00
feat: add pprof
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
// 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"
|
||||
|
||||
"m7s.live/v5/plugin/debug/pkg/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`, escapeForDot(title))
|
||||
fmt.Fprintf(b, ` label="%s\l"`, strings.Join(escapeAllForDot(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, escapeForDot(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)"`,
|
||||
escapeForDot(edge.Src.Info.PrintableName()), arrow,
|
||||
escapeForDot(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 green, 0.0 colored grey, and 1.0 colored
|
||||
// red. 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 = escapeForDot(ShortenFunctionName(infoCopy.Name))
|
||||
infoCopy.Name = strings.Replace(infoCopy.Name, "::", `\n`, -1)
|
||||
// Go type parameters are reported as "[...]" by Go pprof profiles.
|
||||
// Keep this ellipsis rather than replacing with newlines below.
|
||||
infoCopy.Name = strings.Replace(infoCopy.Name, "[...]", "[…]", -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
|
||||
}
|
||||
|
||||
// escapeAllForDot applies escapeForDot to all strings in the given slice.
|
||||
func escapeAllForDot(in []string) []string {
|
||||
var out = make([]string, len(in))
|
||||
for i := range in {
|
||||
out[i] = escapeForDot(in[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// escapeForDot escapes double quotes and backslashes, and replaces Graphviz's
|
||||
// "center" character (\n) with a left-justified character.
|
||||
// See https://graphviz.org/docs/attr-types/escString/ for more info.
|
||||
func escapeForDot(str string) string {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(str, `\`, `\\`), `"`, `\"`), "\n", `\l`)
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"m7s.live/v5/plugin/debug/pkg/internal/proftest"
|
||||
)
|
||||
|
||||
var updateFlag = flag.Bool("update", false, "Update the golden files")
|
||||
|
||||
func TestComposeWithStandardGraph(t *testing.T) {
|
||||
g := baseGraph()
|
||||
a, c := baseAttrsAndConfig()
|
||||
|
||||
var buf bytes.Buffer
|
||||
ComposeDot(&buf, g, a, c)
|
||||
|
||||
compareGraphs(t, buf.Bytes(), "compose1.dot")
|
||||
}
|
||||
|
||||
func TestComposeWithNodeAttributesAndZeroFlat(t *testing.T) {
|
||||
g := baseGraph()
|
||||
a, c := baseAttrsAndConfig()
|
||||
|
||||
// Set NodeAttributes for Node 1.
|
||||
a.Nodes[g.Nodes[0]] = &DotNodeAttributes{
|
||||
Shape: "folder",
|
||||
Bold: true,
|
||||
Peripheries: 2,
|
||||
URL: "www.google.com",
|
||||
Formatter: func(ni *NodeInfo) string {
|
||||
return strings.ToUpper(ni.Name)
|
||||
},
|
||||
}
|
||||
|
||||
// Set Flat value to zero on Node 2.
|
||||
g.Nodes[1].Flat = 0
|
||||
|
||||
var buf bytes.Buffer
|
||||
ComposeDot(&buf, g, a, c)
|
||||
|
||||
compareGraphs(t, buf.Bytes(), "compose2.dot")
|
||||
}
|
||||
|
||||
func TestComposeWithTagsAndResidualEdge(t *testing.T) {
|
||||
g := baseGraph()
|
||||
a, c := baseAttrsAndConfig()
|
||||
|
||||
// Add tags to Node 1.
|
||||
g.Nodes[0].LabelTags["a"] = &Tag{
|
||||
Name: "tag1",
|
||||
Cum: 10,
|
||||
Flat: 10,
|
||||
}
|
||||
g.Nodes[0].NumericTags[""] = TagMap{
|
||||
"b": &Tag{
|
||||
Name: "tag2",
|
||||
Cum: 20,
|
||||
Flat: 20,
|
||||
Unit: "ms",
|
||||
},
|
||||
}
|
||||
|
||||
// Set edge to be Residual.
|
||||
g.Nodes[0].Out[g.Nodes[1]].Residual = true
|
||||
|
||||
var buf bytes.Buffer
|
||||
ComposeDot(&buf, g, a, c)
|
||||
|
||||
compareGraphs(t, buf.Bytes(), "compose3.dot")
|
||||
}
|
||||
|
||||
func TestComposeWithNestedTags(t *testing.T) {
|
||||
g := baseGraph()
|
||||
a, c := baseAttrsAndConfig()
|
||||
|
||||
// Add tags to Node 1.
|
||||
g.Nodes[0].LabelTags["tag1"] = &Tag{
|
||||
Name: "tag1",
|
||||
Cum: 10,
|
||||
Flat: 10,
|
||||
}
|
||||
g.Nodes[0].NumericTags["tag1"] = TagMap{
|
||||
"tag2": &Tag{
|
||||
Name: "tag2",
|
||||
Cum: 20,
|
||||
Flat: 20,
|
||||
Unit: "ms",
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
ComposeDot(&buf, g, a, c)
|
||||
|
||||
compareGraphs(t, buf.Bytes(), "compose5.dot")
|
||||
}
|
||||
|
||||
func TestComposeWithEmptyGraph(t *testing.T) {
|
||||
g := &Graph{}
|
||||
a, c := baseAttrsAndConfig()
|
||||
|
||||
var buf bytes.Buffer
|
||||
ComposeDot(&buf, g, a, c)
|
||||
|
||||
compareGraphs(t, buf.Bytes(), "compose4.dot")
|
||||
}
|
||||
|
||||
func TestComposeWithStandardGraphAndURL(t *testing.T) {
|
||||
g := baseGraph()
|
||||
a, c := baseAttrsAndConfig()
|
||||
c.LegendURL = "http://example.com"
|
||||
|
||||
var buf bytes.Buffer
|
||||
ComposeDot(&buf, g, a, c)
|
||||
|
||||
compareGraphs(t, buf.Bytes(), "compose6.dot")
|
||||
}
|
||||
|
||||
func TestComposeWithNamesThatNeedEscaping(t *testing.T) {
|
||||
g := baseGraph()
|
||||
a, c := baseAttrsAndConfig()
|
||||
g.Nodes[0].Info = NodeInfo{Name: `var"src"`}
|
||||
g.Nodes[1].Info = NodeInfo{Name: `var"#dest#"`}
|
||||
|
||||
var buf bytes.Buffer
|
||||
ComposeDot(&buf, g, a, c)
|
||||
|
||||
compareGraphs(t, buf.Bytes(), "compose7.dot")
|
||||
}
|
||||
|
||||
func TestComposeWithCommentsWithNewlines(t *testing.T) {
|
||||
g := baseGraph()
|
||||
a, c := baseAttrsAndConfig()
|
||||
// comments that could be added with the -add_comment command line tool
|
||||
// the first label is used as the dot "node name"; the others are escaped as labels
|
||||
c.Labels = []string{"comment line 1\ncomment line 2 \"unterminated double quote", `second comment "double quote"`}
|
||||
|
||||
var buf bytes.Buffer
|
||||
ComposeDot(&buf, g, a, c)
|
||||
|
||||
compareGraphs(t, buf.Bytes(), "compose9.dot")
|
||||
}
|
||||
|
||||
func baseGraph() *Graph {
|
||||
src := &Node{
|
||||
Info: NodeInfo{Name: "src"},
|
||||
Flat: 10,
|
||||
Cum: 25,
|
||||
In: make(EdgeMap),
|
||||
Out: make(EdgeMap),
|
||||
LabelTags: make(TagMap),
|
||||
NumericTags: make(map[string]TagMap),
|
||||
}
|
||||
dest := &Node{
|
||||
Info: NodeInfo{Name: "dest"},
|
||||
Flat: 15,
|
||||
Cum: 25,
|
||||
In: make(EdgeMap),
|
||||
Out: make(EdgeMap),
|
||||
LabelTags: make(TagMap),
|
||||
NumericTags: make(map[string]TagMap),
|
||||
}
|
||||
edge := &Edge{
|
||||
Src: src,
|
||||
Dest: dest,
|
||||
Weight: 10,
|
||||
}
|
||||
src.Out[dest] = edge
|
||||
src.In[src] = edge
|
||||
return &Graph{
|
||||
Nodes: Nodes{
|
||||
src,
|
||||
dest,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func baseAttrsAndConfig() (*DotAttributes, *DotConfig) {
|
||||
a := &DotAttributes{
|
||||
Nodes: make(map[*Node]*DotNodeAttributes),
|
||||
}
|
||||
c := &DotConfig{
|
||||
Title: "testtitle",
|
||||
Labels: []string{"label1", "label2", `label3: "foo"`},
|
||||
Total: 100,
|
||||
FormatValue: func(v int64) string {
|
||||
return strconv.FormatInt(v, 10)
|
||||
},
|
||||
}
|
||||
return a, c
|
||||
}
|
||||
|
||||
func compareGraphs(t *testing.T, got []byte, wantFile string) {
|
||||
wantFile = filepath.Join("testdata", wantFile)
|
||||
want, err := os.ReadFile(wantFile)
|
||||
if err != nil {
|
||||
t.Fatalf("error reading test file %s: %v", wantFile, err)
|
||||
}
|
||||
|
||||
if string(got) != string(want) {
|
||||
d, err := proftest.Diff(got, want)
|
||||
if err != nil {
|
||||
t.Fatalf("error finding diff: %v", err)
|
||||
}
|
||||
t.Errorf("Compose incorrectly wrote %s", string(d))
|
||||
if *updateFlag {
|
||||
err := os.WriteFile(wantFile, got, 0644)
|
||||
if err != nil {
|
||||
t.Errorf("failed to update the golden file %q: %v", wantFile, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeletCountCapping(t *testing.T) {
|
||||
labelTags := make(TagMap)
|
||||
for i := 0; i < 10; i++ {
|
||||
name := fmt.Sprintf("tag-%d", i)
|
||||
labelTags[name] = &Tag{
|
||||
Name: name,
|
||||
Flat: 10,
|
||||
Cum: 10,
|
||||
}
|
||||
}
|
||||
numTags := make(TagMap)
|
||||
for i := 0; i < 10; i++ {
|
||||
name := fmt.Sprintf("num-tag-%d", i)
|
||||
numTags[name] = &Tag{
|
||||
Name: name,
|
||||
Unit: "mb",
|
||||
Value: 16,
|
||||
Flat: 10,
|
||||
Cum: 10,
|
||||
}
|
||||
}
|
||||
node1 := &Node{
|
||||
Info: NodeInfo{Name: "node1-with-tags"},
|
||||
Flat: 10,
|
||||
Cum: 10,
|
||||
NumericTags: map[string]TagMap{"": numTags},
|
||||
LabelTags: labelTags,
|
||||
}
|
||||
node2 := &Node{
|
||||
Info: NodeInfo{Name: "node2"},
|
||||
Flat: 15,
|
||||
Cum: 15,
|
||||
}
|
||||
node3 := &Node{
|
||||
Info: NodeInfo{Name: "node3"},
|
||||
Flat: 15,
|
||||
Cum: 15,
|
||||
}
|
||||
g := &Graph{
|
||||
Nodes: Nodes{
|
||||
node1,
|
||||
node2,
|
||||
node3,
|
||||
},
|
||||
}
|
||||
for n := 1; n <= 3; n++ {
|
||||
input := maxNodelets + n
|
||||
if got, want := len(g.SelectTopNodes(input, true)), n; got != want {
|
||||
t.Errorf("SelectTopNodes(%d): got %d nodes, want %d", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultilinePrintableName(t *testing.T) {
|
||||
ni := &NodeInfo{
|
||||
Name: "test1.test2::test3",
|
||||
File: "src/file.cc",
|
||||
Address: 123,
|
||||
Lineno: 999,
|
||||
}
|
||||
|
||||
want := fmt.Sprintf(`%016x\ntest1\ntest2\ntest3\nfile.cc:999\n`, 123)
|
||||
if got := multilinePrintableName(ni); got != want {
|
||||
t.Errorf("multilinePrintableName(%#v) == %q, want %q", ni, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagCollapse(t *testing.T) {
|
||||
|
||||
makeTag := func(name, unit string, value, flat, cum int64) *Tag {
|
||||
return &Tag{name, unit, value, flat, 0, cum, 0}
|
||||
}
|
||||
|
||||
tagSource := []*Tag{
|
||||
makeTag("12mb", "mb", 12, 100, 100),
|
||||
makeTag("1kb", "kb", 1, 1, 1),
|
||||
makeTag("1mb", "mb", 1, 1000, 1000),
|
||||
makeTag("2048mb", "mb", 2048, 1000, 1000),
|
||||
makeTag("1b", "b", 1, 100, 100),
|
||||
makeTag("2b", "b", 2, 100, 100),
|
||||
makeTag("7b", "b", 7, 100, 100),
|
||||
}
|
||||
|
||||
tagWant := [][]*Tag{
|
||||
{
|
||||
makeTag("1B..2GB", "", 0, 2401, 2401),
|
||||
},
|
||||
{
|
||||
makeTag("2GB", "", 0, 1000, 1000),
|
||||
makeTag("1B..12MB", "", 0, 1401, 1401),
|
||||
},
|
||||
{
|
||||
makeTag("2GB", "", 0, 1000, 1000),
|
||||
makeTag("12MB", "", 0, 100, 100),
|
||||
makeTag("1B..1MB", "", 0, 1301, 1301),
|
||||
},
|
||||
{
|
||||
makeTag("2GB", "", 0, 1000, 1000),
|
||||
makeTag("1MB", "", 0, 1000, 1000),
|
||||
makeTag("2B..1kB", "", 0, 201, 201),
|
||||
makeTag("1B", "", 0, 100, 100),
|
||||
makeTag("12MB", "", 0, 100, 100),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tagWant {
|
||||
var got, want []*Tag
|
||||
b := builder{nil, &DotAttributes{}, &DotConfig{}}
|
||||
got = b.collapsedTags(tagSource, len(tc), true)
|
||||
want = SortTags(tc, true)
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("collapse to %d, got:\n%v\nwant:\n%v", len(tc), tagString(got), tagString(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeForDot(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
desc string
|
||||
input []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
desc: "with multiple doubles quotes",
|
||||
input: []string{`label: "foo" and "bar"`},
|
||||
want: []string{`label: \"foo\" and \"bar\"`},
|
||||
},
|
||||
{
|
||||
desc: "with graphviz center line character",
|
||||
input: []string{"label: foo \n bar"},
|
||||
want: []string{`label: foo \l bar`},
|
||||
},
|
||||
{
|
||||
desc: "with two backslashes",
|
||||
input: []string{`label: \\`},
|
||||
want: []string{`label: \\\\`},
|
||||
},
|
||||
{
|
||||
desc: "with two double quotes together",
|
||||
input: []string{`label: ""`},
|
||||
want: []string{`label: \"\"`},
|
||||
},
|
||||
{
|
||||
desc: "with multiple labels",
|
||||
input: []string{`label1: "foo"`, `label2: "bar"`},
|
||||
want: []string{`label1: \"foo\"`, `label2: \"bar\"`},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if got := escapeAllForDot(tc.input); !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("escapeAllForDot(%s) = %s, want %s", tc.input, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func tagString(t []*Tag) string {
|
||||
var ret []string
|
||||
for _, s := range t {
|
||||
ret = append(ret, fmt.Sprintln(s))
|
||||
}
|
||||
return strings.Join(ret, ":")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,547 @@
|
||||
// Copyright 2016 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"
|
||||
"testing"
|
||||
|
||||
"m7s.live/v5/plugin/debug/pkg/profile"
|
||||
)
|
||||
|
||||
func edgeDebugString(edge *Edge) string {
|
||||
debug := ""
|
||||
debug += fmt.Sprintf("\t\tSrc: %p\n", edge.Src)
|
||||
debug += fmt.Sprintf("\t\tDest: %p\n", edge.Dest)
|
||||
debug += fmt.Sprintf("\t\tWeight: %d\n", edge.Weight)
|
||||
debug += fmt.Sprintf("\t\tResidual: %t\n", edge.Residual)
|
||||
debug += fmt.Sprintf("\t\tInline: %t\n", edge.Inline)
|
||||
return debug
|
||||
}
|
||||
|
||||
func edgeMapsDebugString(in, out EdgeMap) string {
|
||||
debug := ""
|
||||
debug += "In Edges:\n"
|
||||
for parent, edge := range in {
|
||||
debug += fmt.Sprintf("\tParent: %p\n", parent)
|
||||
debug += edgeDebugString(edge)
|
||||
}
|
||||
debug += "Out Edges:\n"
|
||||
for child, edge := range out {
|
||||
debug += fmt.Sprintf("\tChild: %p\n", child)
|
||||
debug += edgeDebugString(edge)
|
||||
}
|
||||
return debug
|
||||
}
|
||||
|
||||
func graphDebugString(graph *Graph) string {
|
||||
debug := ""
|
||||
for i, node := range graph.Nodes {
|
||||
debug += fmt.Sprintf("Node %d: %p\n", i, node)
|
||||
}
|
||||
|
||||
for i, node := range graph.Nodes {
|
||||
debug += "\n"
|
||||
debug += fmt.Sprintf("=== Node %d: %p ===\n", i, node)
|
||||
debug += edgeMapsDebugString(node.In, node.Out)
|
||||
}
|
||||
return debug
|
||||
}
|
||||
|
||||
func expectedNodesDebugString(expected []expectedNode) string {
|
||||
debug := ""
|
||||
for i, node := range expected {
|
||||
debug += fmt.Sprintf("Node %d: %p\n", i, node.node)
|
||||
}
|
||||
|
||||
for i, node := range expected {
|
||||
debug += "\n"
|
||||
debug += fmt.Sprintf("=== Node %d: %p ===\n", i, node.node)
|
||||
debug += edgeMapsDebugString(node.in, node.out)
|
||||
}
|
||||
return debug
|
||||
}
|
||||
|
||||
// edgeMapsEqual checks if all the edges in this equal all the edges in that.
|
||||
func edgeMapsEqual(this, that EdgeMap) bool {
|
||||
if len(this) != len(that) {
|
||||
return false
|
||||
}
|
||||
for node, thisEdge := range this {
|
||||
if *thisEdge != *that[node] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// nodesEqual checks if node is equal to expected.
|
||||
func nodesEqual(node *Node, expected expectedNode) bool {
|
||||
return node == expected.node && edgeMapsEqual(node.In, expected.in) &&
|
||||
edgeMapsEqual(node.Out, expected.out)
|
||||
}
|
||||
|
||||
// graphsEqual checks if graph is equivalent to the graph templated by expected.
|
||||
func graphsEqual(graph *Graph, expected []expectedNode) bool {
|
||||
if len(graph.Nodes) != len(expected) {
|
||||
return false
|
||||
}
|
||||
expectedSet := make(map[*Node]expectedNode)
|
||||
for i := range expected {
|
||||
expectedSet[expected[i].node] = expected[i]
|
||||
}
|
||||
|
||||
for _, node := range graph.Nodes {
|
||||
expectedNode, found := expectedSet[node]
|
||||
if !found || !nodesEqual(node, expectedNode) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type expectedNode struct {
|
||||
node *Node
|
||||
in, out EdgeMap
|
||||
}
|
||||
|
||||
type trimTreeTestcase struct {
|
||||
initial *Graph
|
||||
expected []expectedNode
|
||||
keep NodePtrSet
|
||||
}
|
||||
|
||||
// makeExpectedEdgeResidual makes the edge from parent to child residual.
|
||||
func makeExpectedEdgeResidual(parent, child expectedNode) {
|
||||
parent.out[child.node].Residual = true
|
||||
child.in[parent.node].Residual = true
|
||||
}
|
||||
|
||||
func makeEdgeInline(edgeMap EdgeMap, node *Node) {
|
||||
edgeMap[node].Inline = true
|
||||
}
|
||||
|
||||
func setEdgeWeight(edgeMap EdgeMap, node *Node, weight int64) {
|
||||
edgeMap[node].Weight = weight
|
||||
}
|
||||
|
||||
// createEdges creates directed edges from the parent to each of the children.
|
||||
func createEdges(parent *Node, children ...*Node) {
|
||||
for _, child := range children {
|
||||
edge := &Edge{
|
||||
Src: parent,
|
||||
Dest: child,
|
||||
}
|
||||
parent.Out[child] = edge
|
||||
child.In[parent] = edge
|
||||
}
|
||||
}
|
||||
|
||||
// createEmptyNode creates a node without any edges.
|
||||
func createEmptyNode() *Node {
|
||||
return &Node{
|
||||
In: make(EdgeMap),
|
||||
Out: make(EdgeMap),
|
||||
}
|
||||
}
|
||||
|
||||
// createExpectedNodes creates a slice of expectedNodes from nodes.
|
||||
func createExpectedNodes(nodes ...*Node) ([]expectedNode, NodePtrSet) {
|
||||
expected := make([]expectedNode, len(nodes))
|
||||
keep := make(NodePtrSet, len(nodes))
|
||||
|
||||
for i, node := range nodes {
|
||||
expected[i] = expectedNode{
|
||||
node: node,
|
||||
in: make(EdgeMap),
|
||||
out: make(EdgeMap),
|
||||
}
|
||||
keep[node] = true
|
||||
}
|
||||
|
||||
return expected, keep
|
||||
}
|
||||
|
||||
// createExpectedEdges creates directed edges from the parent to each of the
|
||||
// children.
|
||||
func createExpectedEdges(parent expectedNode, children ...expectedNode) {
|
||||
for _, child := range children {
|
||||
edge := &Edge{
|
||||
Src: parent.node,
|
||||
Dest: child.node,
|
||||
}
|
||||
parent.out[child.node] = edge
|
||||
child.in[parent.node] = edge
|
||||
}
|
||||
}
|
||||
|
||||
// createTestCase1 creates a test case that initially looks like:
|
||||
//
|
||||
// 0
|
||||
// |(5)
|
||||
// 1
|
||||
// (3)/ \(4)
|
||||
// 2 3.
|
||||
//
|
||||
// After keeping 0, 2, and 3, it expects the graph:
|
||||
//
|
||||
// 0
|
||||
// (3)/ \(4)
|
||||
// 2 3.
|
||||
func createTestCase1() trimTreeTestcase {
|
||||
// Create initial graph
|
||||
graph := &Graph{make(Nodes, 4)}
|
||||
nodes := graph.Nodes
|
||||
for i := range nodes {
|
||||
nodes[i] = createEmptyNode()
|
||||
}
|
||||
createEdges(nodes[0], nodes[1])
|
||||
createEdges(nodes[1], nodes[2], nodes[3])
|
||||
makeEdgeInline(nodes[0].Out, nodes[1])
|
||||
makeEdgeInline(nodes[1].Out, nodes[2])
|
||||
setEdgeWeight(nodes[0].Out, nodes[1], 5)
|
||||
setEdgeWeight(nodes[1].Out, nodes[2], 3)
|
||||
setEdgeWeight(nodes[1].Out, nodes[3], 4)
|
||||
|
||||
// Create expected graph
|
||||
expected, keep := createExpectedNodes(nodes[0], nodes[2], nodes[3])
|
||||
createExpectedEdges(expected[0], expected[1], expected[2])
|
||||
makeEdgeInline(expected[0].out, expected[1].node)
|
||||
makeExpectedEdgeResidual(expected[0], expected[1])
|
||||
makeExpectedEdgeResidual(expected[0], expected[2])
|
||||
setEdgeWeight(expected[0].out, expected[1].node, 3)
|
||||
setEdgeWeight(expected[0].out, expected[2].node, 4)
|
||||
return trimTreeTestcase{
|
||||
initial: graph,
|
||||
expected: expected,
|
||||
keep: keep,
|
||||
}
|
||||
}
|
||||
|
||||
// createTestCase2 creates a test case that initially looks like:
|
||||
//
|
||||
// 3
|
||||
// | (12)
|
||||
// 1
|
||||
// | (8)
|
||||
// 2
|
||||
// | (15)
|
||||
// 0
|
||||
// | (10)
|
||||
// 4.
|
||||
//
|
||||
// After keeping 3 and 4, it expects the graph:
|
||||
//
|
||||
// 3
|
||||
// | (10)
|
||||
// 4.
|
||||
func createTestCase2() trimTreeTestcase {
|
||||
// Create initial graph
|
||||
graph := &Graph{make(Nodes, 5)}
|
||||
nodes := graph.Nodes
|
||||
for i := range nodes {
|
||||
nodes[i] = createEmptyNode()
|
||||
}
|
||||
createEdges(nodes[3], nodes[1])
|
||||
createEdges(nodes[1], nodes[2])
|
||||
createEdges(nodes[2], nodes[0])
|
||||
createEdges(nodes[0], nodes[4])
|
||||
setEdgeWeight(nodes[3].Out, nodes[1], 12)
|
||||
setEdgeWeight(nodes[1].Out, nodes[2], 8)
|
||||
setEdgeWeight(nodes[2].Out, nodes[0], 15)
|
||||
setEdgeWeight(nodes[0].Out, nodes[4], 10)
|
||||
|
||||
// Create expected graph
|
||||
expected, keep := createExpectedNodes(nodes[3], nodes[4])
|
||||
createExpectedEdges(expected[0], expected[1])
|
||||
makeExpectedEdgeResidual(expected[0], expected[1])
|
||||
setEdgeWeight(expected[0].out, expected[1].node, 10)
|
||||
return trimTreeTestcase{
|
||||
initial: graph,
|
||||
expected: expected,
|
||||
keep: keep,
|
||||
}
|
||||
}
|
||||
|
||||
// createTestCase3 creates an initially empty graph and expects an empty graph
|
||||
// after trimming.
|
||||
func createTestCase3() trimTreeTestcase {
|
||||
graph := &Graph{make(Nodes, 0)}
|
||||
expected, keep := createExpectedNodes()
|
||||
return trimTreeTestcase{
|
||||
initial: graph,
|
||||
expected: expected,
|
||||
keep: keep,
|
||||
}
|
||||
}
|
||||
|
||||
// createTestCase4 creates a test case that initially looks like:
|
||||
//
|
||||
// 0.
|
||||
//
|
||||
// After keeping 0, it expects the graph:
|
||||
//
|
||||
// 0.
|
||||
func createTestCase4() trimTreeTestcase {
|
||||
graph := &Graph{make(Nodes, 1)}
|
||||
nodes := graph.Nodes
|
||||
for i := range nodes {
|
||||
nodes[i] = createEmptyNode()
|
||||
}
|
||||
expected, keep := createExpectedNodes(nodes[0])
|
||||
return trimTreeTestcase{
|
||||
initial: graph,
|
||||
expected: expected,
|
||||
keep: keep,
|
||||
}
|
||||
}
|
||||
|
||||
func createTrimTreeTestCases() []trimTreeTestcase {
|
||||
caseGenerators := []func() trimTreeTestcase{
|
||||
createTestCase1,
|
||||
createTestCase2,
|
||||
createTestCase3,
|
||||
createTestCase4,
|
||||
}
|
||||
cases := make([]trimTreeTestcase, len(caseGenerators))
|
||||
for i, gen := range caseGenerators {
|
||||
cases[i] = gen()
|
||||
}
|
||||
return cases
|
||||
}
|
||||
|
||||
func TestTrimTree(t *testing.T) {
|
||||
tests := createTrimTreeTestCases()
|
||||
for _, test := range tests {
|
||||
graph := test.initial
|
||||
graph.TrimTree(test.keep)
|
||||
if !graphsEqual(graph, test.expected) {
|
||||
t.Fatalf("Graphs do not match.\nExpected: %s\nFound: %s\n",
|
||||
expectedNodesDebugString(test.expected),
|
||||
graphDebugString(graph))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nodeTestProfile() *profile.Profile {
|
||||
mappings := []*profile.Mapping{
|
||||
{
|
||||
ID: 1,
|
||||
File: "symbolized_binary",
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
File: "unsymbolized_library_1",
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
File: "unsymbolized_library_2",
|
||||
},
|
||||
}
|
||||
functions := []*profile.Function{
|
||||
{ID: 1, Name: "symname"},
|
||||
{ID: 2},
|
||||
}
|
||||
locations := []*profile.Location{
|
||||
{
|
||||
ID: 1,
|
||||
Mapping: mappings[0],
|
||||
Line: []profile.Line{
|
||||
{Function: functions[0]},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Mapping: mappings[1],
|
||||
Line: []profile.Line{
|
||||
{Function: functions[1]},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Mapping: mappings[2],
|
||||
},
|
||||
}
|
||||
return &profile.Profile{
|
||||
PeriodType: &profile.ValueType{Type: "cpu", Unit: "milliseconds"},
|
||||
SampleType: []*profile.ValueType{
|
||||
{Type: "type", Unit: "unit"},
|
||||
},
|
||||
Sample: []*profile.Sample{
|
||||
{
|
||||
Location: []*profile.Location{locations[0]},
|
||||
Value: []int64{1},
|
||||
},
|
||||
{
|
||||
Location: []*profile.Location{locations[1]},
|
||||
Value: []int64{1},
|
||||
},
|
||||
{
|
||||
Location: []*profile.Location{locations[2]},
|
||||
Value: []int64{1},
|
||||
},
|
||||
},
|
||||
Location: locations,
|
||||
Function: functions,
|
||||
Mapping: mappings,
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateNodes checks that nodes are properly created for a simple profile.
|
||||
func TestCreateNodes(t *testing.T) {
|
||||
testProfile := nodeTestProfile()
|
||||
wantNodeSet := NodeSet{
|
||||
{Name: "symname"}: true,
|
||||
{Objfile: "unsymbolized_library_1"}: true,
|
||||
{Objfile: "unsymbolized_library_2"}: true,
|
||||
}
|
||||
|
||||
nodes, _ := CreateNodes(testProfile, &Options{})
|
||||
if len(nodes) != len(wantNodeSet) {
|
||||
t.Errorf("got %d nodes, want %d", len(nodes), len(wantNodeSet))
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if !wantNodeSet[node.Info] {
|
||||
t.Errorf("unexpected node %v", node.Info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortenFunctionName(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
want string
|
||||
}
|
||||
testcases := []testCase{
|
||||
{
|
||||
"root",
|
||||
"root",
|
||||
},
|
||||
{
|
||||
"syscall.Syscall",
|
||||
"syscall.Syscall",
|
||||
},
|
||||
{
|
||||
"net/http.(*conn).serve",
|
||||
"http.(*conn).serve",
|
||||
},
|
||||
{
|
||||
"github.com/blahBlah/foo.Foo",
|
||||
"foo.Foo",
|
||||
},
|
||||
{
|
||||
"github.com/BlahBlah/foo.Foo",
|
||||
"foo.Foo",
|
||||
},
|
||||
{
|
||||
"github.com/BlahBlah/foo.Foo[...]",
|
||||
"foo.Foo[...]",
|
||||
},
|
||||
{
|
||||
"github.com/blah-blah/foo_bar.(*FooBar).Foo",
|
||||
"foo_bar.(*FooBar).Foo",
|
||||
},
|
||||
{
|
||||
"encoding/json.(*structEncoder).(encoding/json.encode)-fm",
|
||||
"json.(*structEncoder).(encoding/json.encode)-fm",
|
||||
},
|
||||
{
|
||||
"github.com/blah/blah/vendor/gopkg.in/redis.v3.(*baseClient).(github.com/blah/blah/vendor/gopkg.in/redis.v3.process)-fm",
|
||||
"redis.v3.(*baseClient).(github.com/blah/blah/vendor/gopkg.in/redis.v3.process)-fm",
|
||||
},
|
||||
{
|
||||
"github.com/foo/bar/v4.(*Foo).Bar",
|
||||
"bar.(*Foo).Bar",
|
||||
},
|
||||
{
|
||||
"github.com/foo/bar/v4/baz.Foo.Bar",
|
||||
"baz.Foo.Bar",
|
||||
},
|
||||
{
|
||||
"github.com/foo/bar/v123.(*Foo).Bar",
|
||||
"bar.(*Foo).Bar",
|
||||
},
|
||||
{
|
||||
"github.com/foobar/v0.(*Foo).Bar",
|
||||
"v0.(*Foo).Bar",
|
||||
},
|
||||
{
|
||||
"github.com/foobar/v1.(*Foo).Bar",
|
||||
"v1.(*Foo).Bar",
|
||||
},
|
||||
{
|
||||
"example.org/v2xyz.Foo",
|
||||
"v2xyz.Foo",
|
||||
},
|
||||
{
|
||||
"github.com/foo/bar/v4/v4.(*Foo).Bar",
|
||||
"v4.(*Foo).Bar",
|
||||
},
|
||||
{
|
||||
"github.com/foo/bar/v4/foo/bar/v4.(*Foo).Bar",
|
||||
"v4.(*Foo).Bar",
|
||||
},
|
||||
{
|
||||
"java.util.concurrent.ThreadPoolExecutor$Worker.run",
|
||||
"ThreadPoolExecutor$Worker.run",
|
||||
},
|
||||
{
|
||||
"java.bar.foo.FooBar.run(java.lang.Runnable)",
|
||||
"FooBar.run",
|
||||
},
|
||||
{
|
||||
"(anonymous namespace)::Bar::Foo",
|
||||
"Bar::Foo",
|
||||
},
|
||||
{
|
||||
"(anonymous namespace)::foo",
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
"cpp::namespace::Class::method()::$_100::operator()",
|
||||
"Class::method",
|
||||
},
|
||||
{
|
||||
"foo_bar::Foo::bar",
|
||||
"Foo::bar",
|
||||
},
|
||||
{
|
||||
"cpp::namespace::Class::method<float, long, int>()",
|
||||
"Class::method<float, long, int>",
|
||||
},
|
||||
{
|
||||
"foo",
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
"foo/xyz",
|
||||
"foo/xyz",
|
||||
},
|
||||
{
|
||||
"com.google.perftools.gwp.benchmark.FloatBench.lambda$run$0",
|
||||
"FloatBench.lambda$run$0",
|
||||
},
|
||||
{
|
||||
"java.bar.foo.FooBar.run$0",
|
||||
"FooBar.run$0",
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
name := ShortenFunctionName(tc.name)
|
||||
if got, want := name, tc.want; got != want {
|
||||
t.Errorf("ShortenFunctionName(%q) = %q, want %q", tc.name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
digraph "testtitle" {
|
||||
node [style=filled fillcolor="#f8f8f8"]
|
||||
subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
|
||||
N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
digraph "testtitle" {
|
||||
node [style=filled fillcolor="#f8f8f8"]
|
||||
subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
|
||||
N1 [label="SRC10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=24 shape=folder tooltip="src (25)" color="#b23c00" fillcolor="#edddd5" style="bold,filled" peripheries=2 URL="www.google.com" target="_blank"]
|
||||
N2 [label="dest\n0 of 25 (25.00%)" id="node2" fontsize=8 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
digraph "testtitle" {
|
||||
node [style=filled fillcolor="#f8f8f8"]
|
||||
subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
|
||||
N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N1_0 [label = "tag1" id="N1_0" fontsize=8 shape=box3d tooltip="10"]
|
||||
N1 -> N1_0 [label=" 10" weight=100 tooltip="10" labeltooltip="10"]
|
||||
NN1_0 [label = "tag2" id="NN1_0" fontsize=8 shape=box3d tooltip="20"]
|
||||
N1 -> NN1_0 [label=" 20" weight=100 tooltip="20" labeltooltip="20"]
|
||||
N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src ... dest (10)" labeltooltip="src ... dest (10)" style="dotted" minlen=2]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
digraph "testtitle" {
|
||||
node [style=filled fillcolor="#f8f8f8"]
|
||||
subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
digraph "testtitle" {
|
||||
node [style=filled fillcolor="#f8f8f8"]
|
||||
subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
|
||||
N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N1_0 [label = "tag1" id="N1_0" fontsize=8 shape=box3d tooltip="10"]
|
||||
N1 -> N1_0 [label=" 10" weight=100 tooltip="10" labeltooltip="10"]
|
||||
NN1_0_0 [label = "tag2" id="NN1_0_0" fontsize=8 shape=box3d tooltip="20"]
|
||||
N1_0 -> NN1_0_0 [label=" 20" weight=100 tooltip="20" labeltooltip="20"]
|
||||
N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)" minlen=2]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
digraph "testtitle" {
|
||||
node [style=filled fillcolor="#f8f8f8"]
|
||||
subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" URL="http://example.com" target="_blank" tooltip="testtitle"] }
|
||||
N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
digraph "testtitle" {
|
||||
node [style=filled fillcolor="#f8f8f8"]
|
||||
subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
|
||||
N1 [label="var\"src\"\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="var\"src\" (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N2 [label="var\"#dest#\"\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="var\"#dest#\" (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="var\"src\" -> var\"#dest#\" (10)" labeltooltip="var\"src\" -> var\"#dest#\" (10)"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
digraph "testtitle" {
|
||||
node [style=filled fillcolor="#f8f8f8"]
|
||||
subgraph cluster_L { "comment line 1\lcomment line 2 \"unterminated double quote" [shape=box fontsize=16 label="comment line 1\lcomment line 2 \"unterminated double quote\lsecond comment \"double quote\"\l" tooltip="testtitle"] }
|
||||
N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
|
||||
N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)"]
|
||||
}
|
||||
Reference in New Issue
Block a user