diff --git a/internal/graph/dotgraph.go b/internal/graph/dotgraph.go index cde648f20..26c1c9e4d 100644 --- a/internal/graph/dotgraph.go +++ b/internal/graph/dotgraph.go @@ -127,7 +127,7 @@ func (b *builder) addLegend() { } title := labels[0] fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, title) - fmt.Fprintf(b, ` label="%s\l"`, strings.Join(escapeForDot(labels), `\l`)) + fmt.Fprintf(b, ` label="%s\l"`, strings.Join(escapeAllForDot(labels, leftJustify), `\l`)) if b.config.LegendURL != "" { fmt.Fprintf(b, ` URL="%s" target="_blank"`, b.config.LegendURL) } @@ -187,7 +187,7 @@ func (b *builder) addNode(node *Node, nodeID int, maxFlat float64) { // Create DOT attribute for node. attr := fmt.Sprintf(`label="%s" id="node%d" fontsize=%d shape=%s tooltip="%s (%s)" color="%s" fillcolor="%s"`, - label, nodeID, fontSize, shape, node.Info.PrintableName(), cumValue, + label, nodeID, fontSize, shape, escapeForDot(node.Info.PrintableName(), centerJustify), cumValue, dotColor(float64(node.CumValue())/float64(abs64(b.config.Total)), false), dotColor(float64(node.CumValue())/float64(abs64(b.config.Total)), true)) @@ -247,7 +247,7 @@ func (b *builder) addNodelets(node *Node, nodeID int) bool { 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_%d [label = "%s" id="N%d_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", nodeID, i, escapeForDot(t.Name, centerJustify), 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)) @@ -274,7 +274,7 @@ func (b *builder) numericNodelets(nts []*Tag, maxNumNodelets int, flatTags bool, } 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(`N%s_%d [label = "%s" id="N%s_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", source, j, escapeForDot(t.Name, centerJustify), 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) } } @@ -305,7 +305,8 @@ func (b *builder) addEdge(edge *Edge, from, to int, hasNodelets bool) { arrow = "..." } tooltip := fmt.Sprintf(`"%s %s %s (%s)"`, - edge.Src.Info.PrintableName(), arrow, edge.Dest.Info.PrintableName(), w) + escapeForDot(edge.Src.Info.PrintableName(), centerJustify), arrow, + escapeForDot(edge.Dest.Info.PrintableName(), centerJustify), w) attr = fmt.Sprintf(`%s tooltip=%s labeltooltip=%s`, attr, tooltip, tooltip) if edge.Residual { @@ -382,7 +383,7 @@ func dotColor(score float64, isBackground bool) string { func multilinePrintableName(info *NodeInfo) string { infoCopy := *info - infoCopy.Name = ShortenFunctionName(infoCopy.Name) + infoCopy.Name = escapeForDot(ShortenFunctionName(infoCopy.Name), centerJustify) infoCopy.Name = strings.Replace(infoCopy.Name, "::", `\n`, -1) infoCopy.Name = strings.Replace(infoCopy.Name, ".", `\n`, -1) if infoCopy.File != "" { @@ -473,13 +474,35 @@ func min64(a, b int64) int64 { return b } -// escapeForDot escapes double quotes and backslashes, and replaces Graphviz's -// "center" character (\n) with a left-justified character. -// See https://graphviz.org/doc/info/attrs.html#k:escString for more info. -func escapeForDot(in []string) []string { +type dotJustifyType int64 + +const ( + leftJustify dotJustifyType = 0 + centerJustify dotJustifyType = 1 + rightJustify dotJustifyType = 2 +) + +// Applies escapeForDot to all strings in the given slice. +func escapeAllForDot(in []string, justify dotJustifyType) []string { var out = make([]string, len(in)) for i := range in { - out[i] = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(in[i], `\`, `\\`), `"`, `\"`), "\n", `\l`) + out[i] = escapeForDot(in[i], justify) } return out } + +// escapeForDot escapes double quotes and backslashes, and replaces Graphviz's +// "center" character (\n) with a left-justified character. +// See https://graphviz.org/doc/info/attrs.html#k:escString for more info. +func escapeForDot(str string, justify dotJustifyType) string { + var newline string + switch justify { + case leftJustify: + newline = `\l` + case centerJustify: + newline = `\n` + case rightJustify: + newline = `\r` + } + return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(str, `\`, `\\`), `"`, `\"`), "\n", newline) +} diff --git a/internal/graph/dotgraph_test.go b/internal/graph/dotgraph_test.go index 4232efaa5..bd345a6f1 100644 --- a/internal/graph/dotgraph_test.go +++ b/internal/graph/dotgraph_test.go @@ -138,6 +138,30 @@ func TestComposeWithStandardGraphAndURL(t *testing.T) { compareGraphs(t, buf.Bytes(), "compose6.dot") } +func TestComposeWithNamesThatNeedEscaping(t *testing.T) { + g := baseGraph() + a, c := baseAttrsAndConfig() + + // Change node names to have `"` in them, which need to be escaped for dot. + g.Nodes[0].Info = NodeInfo{Name: "var\"src\""} + g.Nodes[1].Info = NodeInfo{Name: "var\"#dest#\""} + + // Add tag to Node 1 with `"` in name. + g.Nodes[0].LabelTags["a"] = &Tag{ + Name: "var\"tag1\"", + Cum: 10, + Flat: 10, + } + + // 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(), "compose7.dot") +} + func baseGraph() *Graph { src := &Node{ Info: NodeInfo{Name: "src"}, @@ -359,8 +383,27 @@ func TestEscapeForDot(t *testing.T) { }, } { t.Run(tc.desc, func(t *testing.T) { - if got := escapeForDot(tc.input); !reflect.DeepEqual(got, tc.want) { - t.Errorf("escapeForDot(%s) = %s, want %s", tc.input, got, tc.want) + if got := escapeAllForDot(tc.input, leftJustify); !reflect.DeepEqual(got, tc.want) { + t.Errorf("escapeAllForDot(%s) = %s, want %s", tc.input, got, tc.want) + } + }) + } + + // Test the different options for justifying text newlines in Dot + for _, justify := range []dotJustifyType{leftJustify, centerJustify, rightJustify} { + t.Run("Dot newline justification", func(t *testing.T) { + input := []string{"Line 1\nLine 2"} + var want []string + switch justify { + case leftJustify: + want = []string{`Line 1\lLine 2`} + case centerJustify: + want = []string{`Line 1\nLine 2`} + case rightJustify: + want = []string{`Line 1\rLine 2`} + } + if got := escapeAllForDot(input, justify); !reflect.DeepEqual(got, want) { + t.Errorf("escapeAllForDot(%s) = %s, want %s", input, got, want) } }) } diff --git a/internal/graph/graph.go b/internal/graph/graph.go index d2397a93d..352830063 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -522,7 +522,7 @@ func joinLabels(s *profile.Sample) string { } } sort.Strings(labels) - return strings.Join(labels, `\n`) + return strings.Join(labels, "\n") // This will be escaped downstream if needed. } // isNegative returns true if the node is considered as "negative" for the diff --git a/internal/graph/testdata/compose7.dot b/internal/graph/testdata/compose7.dot new file mode 100644 index 000000000..f8e007a8d --- /dev/null +++ b/internal/graph/testdata/compose7.dot @@ -0,0 +1,9 @@ +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"] +N1_0 [label = "var\"tag1\"" id="N1_0" fontsize=8 shape=box3d tooltip="10"] +N1 -> N1_0 [label=" 10" weight=100 tooltip="10" labeltooltip="10"] +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)" style="dotted" minlen=2] +}