Skip to content

Commit a41b82a

Browse files
authored
Improve handling of Go symbols with type parameters (#717)
* internal/symbolizer: add unit tests for Demangle For the curious: `bar(int (*) [5])` is a pointer to an array of length 5: typedef int IntArray[5]; int bar(IntArray* arr); For #705. * internal/symbolizer: avoid matching generic Go symbols as C++ For #705. * internal/graph: keep Go type parameter ellipsis in dot Go symbols with type parameters are reported in Go pprof profiles as something like `main.Set[...]`. Today, multilinePrintableName mishandles this by replacing each with a newline, resulting in a very strange looking node. Instead, replace ... with a unicode ellipsis so that it isn't replaced with a newline just below. Note that the full symbol name (which is reported by tools like perf) is something like `[go.shape.string_0,go.shape.int_1]`. This case is still not handled very well, but we likely want to strip this out anyways in ShortenFunctionName or demangling as future work. Fixes #705.
1 parent c488b8f commit a41b82a

4 files changed

Lines changed: 157 additions & 23 deletions

File tree

internal/graph/dotgraph.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,9 @@ func multilinePrintableName(info *NodeInfo) string {
385385
infoCopy := *info
386386
infoCopy.Name = escapeForDot(ShortenFunctionName(infoCopy.Name))
387387
infoCopy.Name = strings.Replace(infoCopy.Name, "::", `\n`, -1)
388+
// Go type parameters are reported as "[...]" by Go pprof profiles.
389+
// Keep this ellipsis rather than replacing with newlines below.
390+
infoCopy.Name = strings.Replace(infoCopy.Name, "[...]", "[…]", -1)
388391
infoCopy.Name = strings.Replace(infoCopy.Name, ".", `\n`, -1)
389392
if infoCopy.File != "" {
390393
infoCopy.File = filepath.Base(infoCopy.File)

internal/graph/graph_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,10 @@ func TestShortenFunctionName(t *testing.T) {
445445
"github.com/BlahBlah/foo.Foo",
446446
"foo.Foo",
447447
},
448+
{
449+
"github.com/BlahBlah/foo.Foo[...]",
450+
"foo.Foo[...]",
451+
},
448452
{
449453
"github.com/blah-blah/foo_bar.(*FooBar).Foo",
450454
"foo_bar.(*FooBar).Foo",

internal/symbolizer/symbolizer.go

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -205,49 +205,64 @@ func Demangle(prof *profile.Profile, force bool, demanglerMode string) {
205205
}
206206
}
207207

208-
var options []demangle.Option
208+
options := demanglerModeToOptions(demanglerMode)
209+
for _, fn := range prof.Function {
210+
demangleSingleFunction(fn, options)
211+
}
212+
}
213+
214+
func demanglerModeToOptions(demanglerMode string) []demangle.Option {
209215
switch demanglerMode {
210216
case "": // demangled, simplified: no parameters, no templates, no return type
211-
options = []demangle.Option{demangle.NoParams, demangle.NoTemplateParams}
217+
return []demangle.Option{demangle.NoParams, demangle.NoTemplateParams}
212218
case "templates": // demangled, simplified: no parameters, no return type
213-
options = []demangle.Option{demangle.NoParams}
219+
return []demangle.Option{demangle.NoParams}
214220
case "full":
215-
options = []demangle.Option{demangle.NoClones}
221+
return []demangle.Option{demangle.NoClones}
216222
case "none": // no demangling
217-
return
223+
return []demangle.Option{}
218224
}
219225

226+
panic(fmt.Sprintf("unknown demanglerMode %s", demanglerMode))
227+
}
228+
229+
func demangleSingleFunction(fn *profile.Function, options []demangle.Option) {
230+
if fn.Name != "" && fn.SystemName != fn.Name {
231+
return // Already demangled.
232+
}
220233
// Copy the options because they may be updated by the call.
221234
o := make([]demangle.Option, len(options))
222-
for _, fn := range prof.Function {
223-
if fn.Name != "" && fn.SystemName != fn.Name {
224-
continue // Already demangled.
225-
}
226-
copy(o, options)
227-
if demangled := demangle.Filter(fn.SystemName, o...); demangled != fn.SystemName {
228-
fn.Name = demangled
229-
continue
230-
}
231-
// Could not demangle. Apply heuristics in case the name is
232-
// already demangled.
233-
name := fn.SystemName
234-
if looksLikeDemangledCPlusPlus(name) {
235-
if demanglerMode == "" || demanglerMode == "templates" {
235+
copy(o, options)
236+
if demangled := demangle.Filter(fn.SystemName, o...); demangled != fn.SystemName {
237+
fn.Name = demangled
238+
return
239+
}
240+
// Could not demangle. Apply heuristics in case the name is
241+
// already demangled.
242+
name := fn.SystemName
243+
if looksLikeDemangledCPlusPlus(name) {
244+
for _, o := range options {
245+
switch o {
246+
case demangle.NoParams:
236247
name = removeMatching(name, '(', ')')
237-
}
238-
if demanglerMode == "" {
248+
case demangle.NoTemplateParams:
239249
name = removeMatching(name, '<', '>')
240250
}
241251
}
242-
fn.Name = name
243252
}
253+
fn.Name = name
244254
}
245255

246256
// looksLikeDemangledCPlusPlus is a heuristic to decide if a name is
247257
// the result of demangling C++. If so, further heuristics will be
248258
// applied to simplify the name.
249259
func looksLikeDemangledCPlusPlus(demangled string) bool {
250-
if strings.Contains(demangled, ".<") { // Skip java names of the form "class.<init>"
260+
// Skip java names of the form "class.<init>".
261+
if strings.Contains(demangled, ".<") {
262+
return false
263+
}
264+
// Skip Go names of the form "foo.(*Bar[...]).Method".
265+
if strings.Contains(demangled, "]).") {
251266
return false
252267
}
253268
return strings.ContainsAny(demangled, "<>[]") || strings.Contains(demangled, "::")

internal/symbolizer/symbolizer_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,118 @@ func frame(fname, file string, line int) plugin.Frame {
261261
Line: line}
262262
}
263263

264+
func TestDemangleSingleFunction(t *testing.T) {
265+
// All tests with default mode.
266+
demanglerMode := ""
267+
options := demanglerModeToOptions(demanglerMode)
268+
269+
cases := []struct {
270+
symbol string
271+
want string
272+
}{
273+
{
274+
// Trivial C symbol.
275+
symbol: "printf",
276+
want: "printf",
277+
},
278+
{
279+
// foo::bar(int)
280+
symbol: "_ZN3foo3barEi",
281+
want: "foo::bar",
282+
},
283+
{
284+
// Already demangled.
285+
symbol: "foo::bar(int)",
286+
want: "foo::bar",
287+
},
288+
{
289+
// int foo::baz<double>(double)
290+
symbol: "_ZN3foo3bazIdEEiT",
291+
want: "foo::baz",
292+
},
293+
{
294+
// Already demangled.
295+
//
296+
// TODO: The demangled form of this is actually
297+
// 'int foo::baz<double>(double)', but our heuristic
298+
// can't strip the return type. Should it be able to?
299+
symbol: "foo::baz<double>(double)",
300+
want: "foo::baz",
301+
},
302+
{
303+
// operator delete[](void*)
304+
symbol: "_ZdaPv",
305+
want: "operator delete[]",
306+
},
307+
{
308+
// Already demangled.
309+
symbol: "operator delete[](void*)",
310+
want: "operator delete[]",
311+
},
312+
{
313+
// bar(int (*) [5])
314+
symbol: "_Z3barPA5_i",
315+
want: "bar",
316+
},
317+
{
318+
// Already demangled.
319+
symbol: "bar(int (*) [5])",
320+
want: "bar",
321+
},
322+
// Java symbols, do not demangle.
323+
{
324+
symbol: "java.lang.Float.parseFloat",
325+
want: "java.lang.Float.parseFloat",
326+
},
327+
{
328+
symbol: "java.lang.Float.<init>",
329+
want: "java.lang.Float.<init>",
330+
},
331+
// Go symbols, do not demangle.
332+
{
333+
symbol: "example.com/foo.Bar",
334+
want: "example.com/foo.Bar",
335+
},
336+
{
337+
symbol: "example.com/foo.(*Bar).Bat",
338+
want: "example.com/foo.(*Bar).Bat",
339+
},
340+
{
341+
// Method on type with type parameters, as reported by
342+
// Go pprof profiles (simplified symbol name).
343+
symbol: "example.com/foo.(*Bar[...]).Bat",
344+
want: "example.com/foo.(*Bar[...]).Bat",
345+
},
346+
{
347+
// Method on type with type parameters, as reported by
348+
// perf profiles (actual symbol name).
349+
symbol: "example.com/foo.(*Bar[go.shape.string_0,go.shape.int_1]).Bat",
350+
want: "example.com/foo.(*Bar[go.shape.string_0,go.shape.int_1]).Bat",
351+
},
352+
{
353+
// Function with type parameters, as reported by Go
354+
// pprof profiles (simplified symbol name).
355+
symbol: "example.com/foo.Bar[...]",
356+
want: "example.com/foo.Bar[...]",
357+
},
358+
{
359+
// Function with type parameters, as reported by perf
360+
// profiles (actual symbol name).
361+
symbol: "example.com/foo.Bar[go.shape.string_0,go.shape.int_1]",
362+
want: "example.com/foo.Bar[go.shape.string_0,go.shape.int_1]",
363+
},
364+
}
365+
for _, tc := range cases {
366+
fn := &profile.Function{
367+
SystemName: tc.symbol,
368+
}
369+
demangleSingleFunction(fn, options)
370+
if fn.Name != tc.want {
371+
t.Errorf("demangleSingleFunction(%s) got %s want %s", tc.symbol, fn.Name, tc.want)
372+
}
373+
}
374+
}
375+
264376
type mockObjTool struct{}
265377

266378
func (mockObjTool) Open(file string, start, limit, offset uint64, relocationSymbol string) (plugin.ObjFile, error) {

0 commit comments

Comments
 (0)