...

Source file src/github.com/google/pprof/internal/symbolizer/symbolizer_test.go

Documentation: github.com/google/pprof/internal/symbolizer

     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  
    15  package symbolizer
    16  
    17  import (
    18  	"fmt"
    19  	"regexp"
    20  	"sort"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/google/pprof/internal/plugin"
    25  	"github.com/google/pprof/internal/proftest"
    26  	"github.com/google/pprof/profile"
    27  )
    28  
    29  const filePath = "mapping"
    30  const buildID = "build-id"
    31  
    32  var testM = []*profile.Mapping{
    33  	{
    34  		ID:      1,
    35  		Start:   0x1000,
    36  		Limit:   0x5000,
    37  		File:    filePath,
    38  		BuildID: buildID,
    39  	},
    40  }
    41  
    42  var testL = []*profile.Location{
    43  	{
    44  		ID:      1,
    45  		Mapping: testM[0],
    46  		Address: 1000,
    47  	},
    48  	{
    49  		ID:      2,
    50  		Mapping: testM[0],
    51  		Address: 2000,
    52  	},
    53  	{
    54  		ID:      3,
    55  		Mapping: testM[0],
    56  		Address: 3000,
    57  	},
    58  	{
    59  		ID:      4,
    60  		Mapping: testM[0],
    61  		Address: 4000,
    62  	},
    63  	{
    64  		ID:      5,
    65  		Mapping: testM[0],
    66  		Address: 5000,
    67  	},
    68  }
    69  
    70  var testProfile = profile.Profile{
    71  	DurationNanos: 10e9,
    72  	SampleType: []*profile.ValueType{
    73  		{Type: "cpu", Unit: "cycles"},
    74  	},
    75  	Sample: []*profile.Sample{
    76  		{
    77  			Location: []*profile.Location{testL[0]},
    78  			Value:    []int64{1},
    79  		},
    80  		{
    81  			Location: []*profile.Location{testL[1], testL[0]},
    82  			Value:    []int64{10},
    83  		},
    84  		{
    85  			Location: []*profile.Location{testL[2], testL[0]},
    86  			Value:    []int64{100},
    87  		},
    88  		{
    89  			Location: []*profile.Location{testL[3], testL[0]},
    90  			Value:    []int64{1},
    91  		},
    92  		{
    93  			Location: []*profile.Location{testL[4], testL[3], testL[0]},
    94  			Value:    []int64{10000},
    95  		},
    96  	},
    97  	Location:   testL,
    98  	Mapping:    testM,
    99  	PeriodType: &profile.ValueType{Type: "cpu", Unit: "milliseconds"},
   100  	Period:     10,
   101  }
   102  
   103  func TestSymbolization(t *testing.T) {
   104  	sSym := symbolzSymbolize
   105  	lSym := localSymbolize
   106  	defer func() {
   107  		symbolzSymbolize = sSym
   108  		localSymbolize = lSym
   109  		demangleFunction = Demangle
   110  	}()
   111  	symbolzSymbolize = symbolzMock
   112  	localSymbolize = localMock
   113  	demangleFunction = demangleMock
   114  
   115  	type testcase struct {
   116  		mode        string
   117  		wantComment string
   118  	}
   119  
   120  	s := Symbolizer{
   121  		Obj: mockObjTool{},
   122  		UI:  &proftest.TestUI{T: t},
   123  	}
   124  	for i, tc := range []testcase{
   125  		{
   126  			"local",
   127  			"local=[]",
   128  		},
   129  		{
   130  			"fastlocal",
   131  			"local=[fast]",
   132  		},
   133  		{
   134  			"remote",
   135  			"symbolz=[]",
   136  		},
   137  		{
   138  			"",
   139  			"local=[]:symbolz=[]",
   140  		},
   141  		{
   142  			"demangle=none",
   143  			"demangle=[none]:force:local=[force]:symbolz=[force]",
   144  		},
   145  		{
   146  			"remote:demangle=full",
   147  			"demangle=[full]:force:symbolz=[force]",
   148  		},
   149  		{
   150  			"local:demangle=templates",
   151  			"demangle=[templates]:force:local=[force]",
   152  		},
   153  		{
   154  			"force:remote",
   155  			"force:symbolz=[force]",
   156  		},
   157  	} {
   158  		prof := testProfile.Copy()
   159  		if err := s.Symbolize(tc.mode, nil, prof); err != nil {
   160  			t.Errorf("symbolize #%d: %v", i, err)
   161  			continue
   162  		}
   163  		sort.Strings(prof.Comments)
   164  		if got, want := strings.Join(prof.Comments, ":"), tc.wantComment; got != want {
   165  			t.Errorf("%q: got %s, want %s", tc.mode, got, want)
   166  			continue
   167  		}
   168  	}
   169  }
   170  
   171  func symbolzMock(p *profile.Profile, force bool, sources plugin.MappingSources, syms func(string, string) ([]byte, error), ui plugin.UI) error {
   172  	var args []string
   173  	if force {
   174  		args = append(args, "force")
   175  	}
   176  	p.Comments = append(p.Comments, "symbolz=["+strings.Join(args, ",")+"]")
   177  	return nil
   178  }
   179  
   180  func localMock(p *profile.Profile, fast, force bool, obj plugin.ObjTool, ui plugin.UI) error {
   181  	var args []string
   182  	if fast {
   183  		args = append(args, "fast")
   184  	}
   185  	if force {
   186  		args = append(args, "force")
   187  	}
   188  	p.Comments = append(p.Comments, "local=["+strings.Join(args, ",")+"]")
   189  	return nil
   190  }
   191  
   192  func demangleMock(p *profile.Profile, force bool, mode string) {
   193  	if force {
   194  		p.Comments = append(p.Comments, "force")
   195  	}
   196  	if mode != "" {
   197  		p.Comments = append(p.Comments, "demangle=["+mode+"]")
   198  	}
   199  }
   200  
   201  func TestLocalSymbolization(t *testing.T) {
   202  	prof := testProfile.Copy()
   203  
   204  	if prof.HasFunctions() {
   205  		t.Error("unexpected function names")
   206  	}
   207  	if prof.HasFileLines() {
   208  		t.Error("unexpected filenames or line numbers")
   209  	}
   210  
   211  	b := mockObjTool{}
   212  	if err := localSymbolize(prof, false, false, b, &proftest.TestUI{T: t}); err != nil {
   213  		t.Fatalf("localSymbolize(): %v", err)
   214  	}
   215  
   216  	for _, loc := range prof.Location {
   217  		if err := checkSymbolizedLocation(loc.Address, loc.Line); err != nil {
   218  			t.Errorf("location %d: %v", loc.Address, err)
   219  		}
   220  	}
   221  	if !prof.HasFunctions() {
   222  		t.Error("missing function names")
   223  	}
   224  	if !prof.HasFileLines() {
   225  		t.Error("missing filenames or line numbers")
   226  	}
   227  }
   228  
   229  func TestLocalSymbolizationHandlesSpecialCases(t *testing.T) {
   230  	for _, tc := range []struct {
   231  		desc, file, buildID, allowOutputRx string
   232  		wantNumOutputRegexMatches          int
   233  	}{{
   234  		desc:    "Unsymbolizable files are skipped",
   235  		file:    "[some unsymbolizable file]",
   236  		buildID: "",
   237  	}, {
   238  		desc:    "HTTP URL like paths are skipped",
   239  		file:    "http://original-url-source-of-profile-fetch",
   240  		buildID: "",
   241  	}, {
   242  		desc:                      "Non-existent files are ignored",
   243  		file:                      "/does-not-exist",
   244  		buildID:                   buildID,
   245  		allowOutputRx:             "(?s)unknown or non-existent file|Some binary filenames not available.*Try setting PPROF_BINARY_PATH",
   246  		wantNumOutputRegexMatches: 2,
   247  	}, {
   248  		desc:                      "Missing main binary is detected",
   249  		file:                      "",
   250  		buildID:                   buildID,
   251  		allowOutputRx:             "Main binary filename not available",
   252  		wantNumOutputRegexMatches: 1,
   253  	}, {
   254  		desc:                      "Different build ID is detected",
   255  		file:                      filePath,
   256  		buildID:                   "unexpected-build-id",
   257  		allowOutputRx:             "build ID mismatch",
   258  		wantNumOutputRegexMatches: 1,
   259  	},
   260  	} {
   261  		t.Run(tc.desc, func(t *testing.T) {
   262  			prof := testProfile.Copy()
   263  			prof.Mapping[0].File = tc.file
   264  			prof.Mapping[0].BuildID = tc.buildID
   265  			origProf := prof.Copy()
   266  
   267  			if prof.HasFunctions() {
   268  				t.Error("unexpected function names")
   269  			}
   270  			if prof.HasFileLines() {
   271  				t.Error("unexpected filenames or line numbers")
   272  			}
   273  
   274  			b := mockObjTool{}
   275  			ui := &proftest.TestUI{T: t, AllowRx: tc.allowOutputRx}
   276  			if err := localSymbolize(prof, false, false, b, ui); err != nil {
   277  				t.Fatalf("localSymbolize(): %v", err)
   278  			}
   279  			if ui.NumAllowRxMatches != tc.wantNumOutputRegexMatches {
   280  				t.Errorf("localSymbolize(): got %d matches for %q UI regexp, want %d", ui.NumAllowRxMatches, tc.allowOutputRx, tc.wantNumOutputRegexMatches)
   281  			}
   282  
   283  			if diff, err := proftest.Diff([]byte(origProf.String()), []byte(prof.String())); err != nil {
   284  				t.Fatalf("Failed to get diff: %v", err)
   285  			} else if string(diff) != "" {
   286  				t.Errorf("Profile changed unexpectedly, diff(want->got):\n%s", diff)
   287  			}
   288  		})
   289  	}
   290  }
   291  
   292  func checkSymbolizedLocation(a uint64, got []profile.Line) error {
   293  	want, ok := mockAddresses[a]
   294  	if !ok {
   295  		return fmt.Errorf("unexpected address")
   296  	}
   297  	if len(want) != len(got) {
   298  		return fmt.Errorf("want len %d, got %d", len(want), len(got))
   299  	}
   300  
   301  	for i, w := range want {
   302  		g := got[i]
   303  		if g.Function.Name != w.Func {
   304  			return fmt.Errorf("want function: %q, got %q", w.Func, g.Function.Name)
   305  		}
   306  		if g.Function.Filename != w.File {
   307  			return fmt.Errorf("want filename: %q, got %q", w.File, g.Function.Filename)
   308  		}
   309  		if g.Line != int64(w.Line) {
   310  			return fmt.Errorf("want lineno: %d, got %d", w.Line, g.Line)
   311  		}
   312  		if g.Column != int64(w.Column) {
   313  			return fmt.Errorf("want columnno: %d, got %d", w.Column, g.Column)
   314  		}
   315  	}
   316  	return nil
   317  }
   318  
   319  var mockAddresses = map[uint64][]plugin.Frame{
   320  	1000: {frame("fun11", "file11.src", 10, 1)},
   321  	2000: {frame("fun21", "file21.src", 20, 2), frame("fun22", "file22.src", 20, 2)},
   322  	3000: {frame("fun31", "file31.src", 30, 3), frame("fun32", "file32.src", 30, 3), frame("fun33", "file33.src", 30, 3)},
   323  	4000: {frame("fun41", "file41.src", 40, 4), frame("fun42", "file42.src", 40, 4), frame("fun43", "file43.src", 40, 4), frame("fun44", "file44.src", 40, 4)},
   324  	5000: {frame("fun51", "file51.src", 50, 5), frame("fun52", "file52.src", 50, 5), frame("fun53", "file53.src", 50, 5), frame("fun54", "file54.src", 50, 5), frame("fun55", "file55.src", 50, 5)},
   325  }
   326  
   327  func frame(fname, file string, line int, column int) plugin.Frame {
   328  	return plugin.Frame{
   329  		Func:   fname,
   330  		File:   file,
   331  		Line:   line,
   332  		Column: column}
   333  }
   334  
   335  func TestDemangleSingleFunction(t *testing.T) {
   336  	// All tests with default mode.
   337  	demanglerMode := ""
   338  	options := demanglerModeToOptions(demanglerMode)
   339  
   340  	cases := []struct {
   341  		symbol string
   342  		want   string
   343  	}{
   344  		{
   345  			// Trivial C symbol.
   346  			symbol: "printf",
   347  			want:   "printf",
   348  		},
   349  		{
   350  			// foo::bar(int)
   351  			symbol: "_ZN3foo3barEi",
   352  			want:   "foo::bar",
   353  		},
   354  		{
   355  			// Already demangled.
   356  			symbol: "foo::bar(int)",
   357  			want:   "foo::bar",
   358  		},
   359  		{
   360  			// int foo::baz<double>(double)
   361  			symbol: "_ZN3foo3bazIdEEiT",
   362  			want:   "foo::baz",
   363  		},
   364  		{
   365  			// Already demangled.
   366  			//
   367  			// TODO: The demangled form of this is actually
   368  			// 'int foo::baz<double>(double)', but our heuristic
   369  			// can't strip the return type. Should it be able to?
   370  			symbol: "foo::baz<double>(double)",
   371  			want:   "foo::baz",
   372  		},
   373  		{
   374  			// operator delete[](void*)
   375  			symbol: "_ZdaPv",
   376  			want:   "operator delete[]",
   377  		},
   378  		{
   379  			// Already demangled.
   380  			symbol: "operator delete[](void*)",
   381  			want:   "operator delete[]",
   382  		},
   383  		{
   384  			// bar(int (*) [5])
   385  			symbol: "_Z3barPA5_i",
   386  			want:   "bar",
   387  		},
   388  		{
   389  			// Already demangled.
   390  			symbol: "bar(int (*) [5])",
   391  			want:   "bar",
   392  		},
   393  		// Java symbols, do not demangle.
   394  		{
   395  			symbol: "java.lang.Float.parseFloat",
   396  			want:   "java.lang.Float.parseFloat",
   397  		},
   398  		{
   399  			symbol: "java.lang.Float.<init>",
   400  			want:   "java.lang.Float.<init>",
   401  		},
   402  		// Go symbols, do not demangle.
   403  		{
   404  			symbol: "example.com/foo.Bar",
   405  			want:   "example.com/foo.Bar",
   406  		},
   407  		{
   408  			symbol: "example.com/foo.(*Bar).Bat",
   409  			want:   "example.com/foo.(*Bar).Bat",
   410  		},
   411  		{
   412  			// Method on type with type parameters, as reported by
   413  			// Go pprof profiles (simplified symbol name).
   414  			symbol: "example.com/foo.(*Bar[...]).Bat",
   415  			want:   "example.com/foo.(*Bar[...]).Bat",
   416  		},
   417  		{
   418  			// Method on type with type parameters, as reported by
   419  			// perf profiles (actual symbol name).
   420  			symbol: "example.com/foo.(*Bar[go.shape.string_0,go.shape.int_1]).Bat",
   421  			want:   "example.com/foo.(*Bar[go.shape.string_0,go.shape.int_1]).Bat",
   422  		},
   423  		{
   424  			// Function with type parameters, as reported by Go
   425  			// pprof profiles (simplified symbol name).
   426  			symbol: "example.com/foo.Bar[...]",
   427  			want:   "example.com/foo.Bar[...]",
   428  		},
   429  		{
   430  			// Function with type parameters, as reported by perf
   431  			// profiles (actual symbol name).
   432  			symbol: "example.com/foo.Bar[go.shape.string_0,go.shape.int_1]",
   433  			want:   "example.com/foo.Bar[go.shape.string_0,go.shape.int_1]",
   434  		},
   435  	}
   436  	for _, tc := range cases {
   437  		fn := &profile.Function{
   438  			SystemName: tc.symbol,
   439  		}
   440  		demangleSingleFunction(fn, options)
   441  		if fn.Name != tc.want {
   442  			t.Errorf("demangleSingleFunction(%s) got %s want %s", tc.symbol, fn.Name, tc.want)
   443  		}
   444  	}
   445  }
   446  
   447  type mockObjTool struct{}
   448  
   449  func (mockObjTool) Open(file string, start, limit, offset uint64, relocationSymbol string) (plugin.ObjFile, error) {
   450  	if file != filePath {
   451  		return nil, fmt.Errorf("unknown or non-existent file %q", file)
   452  	}
   453  	return mockObjFile{frames: mockAddresses}, nil
   454  }
   455  
   456  func (mockObjTool) Disasm(file string, start, end uint64, intelSyntax bool) ([]plugin.Inst, error) {
   457  	if file != filePath {
   458  		return nil, fmt.Errorf("unknown or non-existent file %q", file)
   459  	}
   460  	return nil, fmt.Errorf("disassembly not supported")
   461  }
   462  
   463  type mockObjFile struct {
   464  	frames map[uint64][]plugin.Frame
   465  }
   466  
   467  func (mockObjFile) Name() string {
   468  	return filePath
   469  }
   470  
   471  func (mockObjFile) ObjAddr(addr uint64) (uint64, error) {
   472  	return addr, nil
   473  }
   474  
   475  func (mockObjFile) BuildID() string {
   476  	return buildID
   477  }
   478  
   479  func (mf mockObjFile) SourceLine(addr uint64) ([]plugin.Frame, error) {
   480  	return mf.frames[addr], nil
   481  }
   482  
   483  func (mockObjFile) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) {
   484  	return []*plugin.Sym{}, nil
   485  }
   486  
   487  func (mockObjFile) Close() error {
   488  	return nil
   489  }
   490  

View as plain text