소스 검색

Add "trim path" option which can be used to relocate sources. (#366)

Add "trim path" option which can be used to relocate sources.

When pprof is asked to show annotated source for a profile collected on
another machine for a Go program, the profile contains absolute paths
which may not exist on the local machine. In that case pprof currently
fails to locate the source with no option to help it. The new option
adds a way to specify one or several source path prefixes that should be
trimmed from the source paths in the profile before applying the search
using search path option.

For example, taking the example from the issue where the source file
path in the profile is
/home/teamcitycpp/agent09/work/56cbaf9067/_gopath/src/badoo/lakafka/main.go
and the local path is /home/marko/lakafka/main.go. The user may
specify
`-trim-path=/home/teamcitycpp/agent09/work/56cbaf9067/_gopath/src/badoo`
to make pprof find the source. The source path doesn't need to be
specified if the current working dir is anything at or under
`/home/marko/`.

When the trim path is not specified, it is guessed heuristically based
on the basename of configured searched paths. In the example above,
setting `-search-path=/home/marko/lakafka` would be sufficient to
activate the heuristic successfully. Or having the current directory as
`/home/marko/lakafka` since the search path is by default set to the
current working directory. Note that the heuristic currently
does not attempt to walk the configured search paths up like the search
does. This is to keep it simple, use `-trim-path` explicitly in more
complicated cases.

Fixes #262.
Alexey Alexandrov 7 년 전
부모
커밋
630d32d13e
No account linked to committer's email address
6개의 변경된 파일91개의 추가작업 그리고 47개의 파일을 삭제
  1. 1
    0
      internal/driver/commands.go
  2. 1
    0
      internal/driver/driver.go
  3. 2
    1
      internal/report/report.go
  4. 3
    1
      internal/report/report_test.go
  5. 51
    28
      internal/report/source.go
  6. 33
    17
      internal/report/source_test.go

+ 1
- 0
internal/driver/commands.go 파일 보기

@@ -153,6 +153,7 @@ var pprofVariables = variables{
153 153
 		"Using auto will scale each value independently to the most natural unit.")},
154 154
 	"compact_labels": &variable{boolKind, "f", "", "Show minimal headers"},
155 155
 	"source_path":    &variable{stringKind, "", "", "Search path for source files"},
156
+	"trim_path":      &variable{stringKind, "", "", "Path to trim from source paths before search"},
156 157
 
157 158
 	// Filtering options
158 159
 	"nodecount": &variable{intKind, "-1", "", helpText(

+ 1
- 0
internal/driver/driver.go 파일 보기

@@ -272,6 +272,7 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var
272 272
 		OutputUnit: vars["unit"].value,
273 273
 
274 274
 		SourcePath: vars["source_path"].stringValue(),
275
+		TrimPath:   vars["trim_path"].stringValue(),
275 276
 	}
276 277
 
277 278
 	if len(p.Mapping) > 0 && p.Mapping[0].File != "" {

+ 2
- 1
internal/report/report.go 파일 보기

@@ -78,6 +78,7 @@ type Options struct {
78 78
 
79 79
 	Symbol     *regexp.Regexp // Symbols to include on disassembly report.
80 80
 	SourcePath string         // Search path for source files.
81
+	TrimPath   string         // Paths to trim from source file paths.
81 82
 }
82 83
 
83 84
 // Generate generates a report as directed by the Report.
@@ -238,7 +239,7 @@ func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph {
238 239
 	// Clean up file paths using heuristics.
239 240
 	prof := rpt.prof
240 241
 	for _, f := range prof.Function {
241
-		f.Filename = trimPath(f.Filename)
242
+		f.Filename = trimPath(f.Filename, o.TrimPath, o.SourcePath)
242 243
 	}
243 244
 	// Removes all numeric tags except for the bytes tag prior
244 245
 	// to making graph.

+ 3
- 1
internal/report/report_test.go 파일 보기

@@ -46,6 +46,7 @@ func TestSource(t *testing.T) {
46 46
 				&Options{
47 47
 					OutputFormat: List,
48 48
 					Symbol:       regexp.MustCompile(`.`),
49
+					TrimPath:     "/some/path",
49 50
 
50 51
 					SampleValue: sampleValue1,
51 52
 					SampleUnit:  testProfile.SampleType[1].Unit,
@@ -60,6 +61,7 @@ func TestSource(t *testing.T) {
60 61
 					OutputFormat: Dot,
61 62
 					CallTree:     true,
62 63
 					Symbol:       regexp.MustCompile(`.`),
64
+					TrimPath:     "/some/path",
63 65
 
64 66
 					SampleValue: sampleValue1,
65 67
 					SampleUnit:  testProfile.SampleType[1].Unit,
@@ -119,7 +121,7 @@ var testF = []*profile.Function{
119 121
 	{
120 122
 		ID:       4,
121 123
 		Name:     "tee",
122
-		Filename: "testdata/source2",
124
+		Filename: "/some/path/testdata/source2",
123 125
 	},
124 126
 }
125 127
 

+ 51
- 28
internal/report/source.go 파일 보기

@@ -63,7 +63,7 @@ func printSource(w io.Writer, rpt *Report) error {
63 63
 		}
64 64
 		sourcePath = wd
65 65
 	}
66
-	reader := newSourceReader(sourcePath)
66
+	reader := newSourceReader(sourcePath, o.TrimPath)
67 67
 
68 68
 	fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total))
69 69
 	for _, fn := range functions {
@@ -146,7 +146,7 @@ func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) er
146 146
 		}
147 147
 		sourcePath = wd
148 148
 	}
149
-	reader := newSourceReader(sourcePath)
149
+	reader := newSourceReader(sourcePath, o.TrimPath)
150 150
 
151 151
 	type fileFunction struct {
152 152
 		fileName, functionName string
@@ -263,7 +263,7 @@ func assemblyPerSourceLine(objSyms []*objSymbol, rs graph.Nodes, src string, obj
263 263
 		//
264 264
 		// E.g., suppose we are printing source code for F and this
265 265
 		// instruction is from H where F called G called H and both
266
-		// of those calls were inlined.  We want to use the line
266
+		// of those calls were inlined. We want to use the line
267 267
 		// number from F, not from H (which is what Disasm gives us).
268 268
 		//
269 269
 		// So find the outer-most linenumber in the source file.
@@ -391,8 +391,7 @@ func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyIns
391 391
 				continue
392 392
 			}
393 393
 			curCalls = nil
394
-			fname := trimPath(c.file)
395
-			fline, ok := reader.line(fname, c.line)
394
+			fline, ok := reader.line(c.file, c.line)
396 395
 			if !ok {
397 396
 				fline = ""
398 397
 			}
@@ -400,7 +399,7 @@ func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyIns
400 399
 			fmt.Fprintf(w, " %8s %10s %10s %8s  <span class=inlinesrc>%s</span> <span class=unimportant>%s:%d</span>\n",
401 400
 				"", "", "", "",
402 401
 				template.HTMLEscapeString(fmt.Sprintf("%-80s", text)),
403
-				template.HTMLEscapeString(filepath.Base(fname)), c.line)
402
+				template.HTMLEscapeString(filepath.Base(c.file)), c.line)
404 403
 		}
405 404
 		curCalls = an.inlineCalls
406 405
 		text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction
@@ -426,7 +425,6 @@ func printPageClosing(w io.Writer) {
426 425
 // file and annotates it with the samples in fns. Returns the sources
427 426
 // as nodes, using the info.name field to hold the source code.
428 427
 func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) {
429
-	file = trimPath(file)
430 428
 	lineNodes := make(map[int]graph.Nodes)
431 429
 
432 430
 	// Collect source coordinates from profile.
@@ -516,20 +514,26 @@ func getMissingFunctionSource(filename string, asm map[int][]assemblyInstruction
516 514
 
517 515
 // sourceReader provides access to source code with caching of file contents.
518 516
 type sourceReader struct {
517
+	// searchPath is a filepath.ListSeparator-separated list of directories where
518
+	// source files should be searched.
519 519
 	searchPath string
520 520
 
521
+	// trimPath is a filepath.ListSeparator-separated list of paths to trim.
522
+	trimPath string
523
+
521 524
 	// files maps from path name to a list of lines.
522 525
 	// files[*][0] is unused since line numbering starts at 1.
523 526
 	files map[string][]string
524 527
 
525
-	// errors collects errors encountered per file.  These errors are
528
+	// errors collects errors encountered per file. These errors are
526 529
 	// consulted before returning out of these module.
527 530
 	errors map[string]error
528 531
 }
529 532
 
530
-func newSourceReader(searchPath string) *sourceReader {
533
+func newSourceReader(searchPath, trimPath string) *sourceReader {
531 534
 	return &sourceReader{
532 535
 		searchPath,
536
+		trimPath,
533 537
 		make(map[string][]string),
534 538
 		make(map[string]error),
535 539
 	}
@@ -544,7 +548,7 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) {
544 548
 	if !ok {
545 549
 		// Read and cache file contents.
546 550
 		lines = []string{""} // Skip 0th line
547
-		f, err := openSourceFile(path, reader.searchPath)
551
+		f, err := openSourceFile(path, reader.searchPath, reader.trimPath)
548 552
 		if err != nil {
549 553
 			reader.errors[path] = err
550 554
 		} else {
@@ -565,17 +569,20 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) {
565 569
 	return lines[lineno], true
566 570
 }
567 571
 
568
-// openSourceFile opens a source file from a name encoded in a
569
-// profile. File names in a profile after often relative paths, so
570
-// search them in each of the paths in searchPath (or CWD by default),
571
-// and their parents.
572
-func openSourceFile(path, searchPath string) (*os.File, error) {
572
+// openSourceFile opens a source file from a name encoded in a profile. File
573
+// names in a profile after can be relative paths, so search them in each of
574
+// the paths in searchPath and their parents. In case the profile contains
575
+// absolute paths, additional paths may be configured to trim from the source
576
+// paths in the profile. This effectively turns the path into a relative path
577
+// searching it using searchPath as usual).
578
+func openSourceFile(path, searchPath, trim string) (*os.File, error) {
579
+	path = trimPath(path, trim, searchPath)
580
+	// If file is still absolute, require file to exist.
573 581
 	if filepath.IsAbs(path) {
574 582
 		f, err := os.Open(path)
575 583
 		return f, err
576 584
 	}
577
-
578
-	// Scan each component of the path
585
+	// Scan each component of the path.
579 586
 	for _, dir := range filepath.SplitList(searchPath) {
580 587
 		// Search up for every parent of each possible path.
581 588
 		for {
@@ -595,18 +602,34 @@ func openSourceFile(path, searchPath string) (*os.File, error) {
595 602
 }
596 603
 
597 604
 // trimPath cleans up a path by removing prefixes that are commonly
598
-// found on profiles.
599
-func trimPath(path string) string {
600
-	basePaths := []string{
601
-		"/proc/self/cwd/./",
602
-		"/proc/self/cwd/",
605
+// found on profiles plus configured prefixes.
606
+// TODO(aalexand): Consider optimizing out the redundant work done in this
607
+// function if it proves to matter.
608
+func trimPath(path, trimPath, searchPath string) string {
609
+	// Keep path variable intact as it's used below to form the return value.
610
+	sPath, searchPath := filepath.ToSlash(path), filepath.ToSlash(searchPath)
611
+	if trimPath == "" {
612
+		// If the trim path is not configured, try to guess it heuristically:
613
+		// search for basename of each search path in the original path and, if
614
+		// found, strip everything up to and including the basename. So, for
615
+		// example, given original path "/some/remote/path/my-project/foo/bar.c"
616
+		// and search path "/my/local/path/my-project" the heuristic will return
617
+		// "/my/local/path/my-project/foo/bar.c".
618
+		for _, dir := range filepath.SplitList(searchPath) {
619
+			want := "/" + filepath.Base(dir) + "/"
620
+			if found := strings.Index(sPath, want); found != -1 {
621
+				return path[found+len(want):]
622
+			}
623
+		}
603 624
 	}
604
-
605
-	sPath := filepath.ToSlash(path)
606
-
607
-	for _, base := range basePaths {
608
-		if strings.HasPrefix(sPath, base) {
609
-			return filepath.FromSlash(sPath[len(base):])
625
+	// Trim configured trim prefixes.
626
+	trimPaths := append(filepath.SplitList(filepath.ToSlash(trimPath)), "/proc/self/cwd/./", "/proc/self/cwd/")
627
+	for _, trimPath := range trimPaths {
628
+		if !strings.HasSuffix(trimPath, "/") {
629
+			trimPath += "/"
630
+		}
631
+		if strings.HasPrefix(sPath, trimPath) {
632
+			return path[len(trimPath):]
610 633
 		}
611 634
 	}
612 635
 	return path

+ 33
- 17
internal/report/source_test.go 파일 보기

@@ -48,40 +48,56 @@ func TestOpenSourceFile(t *testing.T) {
48 48
 	for _, tc := range []struct {
49 49
 		desc       string
50 50
 		searchPath string
51
+		trimPath   string
51 52
 		fs         []string
52 53
 		path       string
53 54
 		wantPath   string // If empty, error is wanted.
54 55
 	}{
55 56
 		{
56 57
 			desc:     "exact absolute path is found",
57
-			fs:       []string{"foo/bar.txt"},
58
-			path:     "$dir/foo/bar.txt",
59
-			wantPath: "$dir/foo/bar.txt",
58
+			fs:       []string{"foo/bar.cc"},
59
+			path:     "$dir/foo/bar.cc",
60
+			wantPath: "$dir/foo/bar.cc",
60 61
 		},
61 62
 		{
62 63
 			desc:       "exact relative path is found",
63 64
 			searchPath: "$dir",
64
-			fs:         []string{"foo/bar.txt"},
65
-			path:       "foo/bar.txt",
66
-			wantPath:   "$dir/foo/bar.txt",
65
+			fs:         []string{"foo/bar.cc"},
66
+			path:       "foo/bar.cc",
67
+			wantPath:   "$dir/foo/bar.cc",
67 68
 		},
68 69
 		{
69 70
 			desc:       "multiple search path",
70 71
 			searchPath: "some/path" + lsep + "$dir",
71
-			fs:         []string{"foo/bar.txt"},
72
-			path:       "foo/bar.txt",
73
-			wantPath:   "$dir/foo/bar.txt",
72
+			fs:         []string{"foo/bar.cc"},
73
+			path:       "foo/bar.cc",
74
+			wantPath:   "$dir/foo/bar.cc",
74 75
 		},
75 76
 		{
76 77
 			desc:       "relative path is found in parent dir",
77 78
 			searchPath: "$dir/foo/bar",
78
-			fs:         []string{"bar.txt", "foo/bar/baz.txt"},
79
-			path:       "bar.txt",
80
-			wantPath:   "$dir/bar.txt",
79
+			fs:         []string{"bar.cc", "foo/bar/baz.cc"},
80
+			path:       "bar.cc",
81
+			wantPath:   "$dir/bar.cc",
82
+		},
83
+		{
84
+			desc:       "trims configured prefix",
85
+			searchPath: "$dir",
86
+			trimPath:   "some-path" + lsep + "/some/remote/path",
87
+			fs:         []string{"my-project/foo/bar.cc"},
88
+			path:       "/some/remote/path/my-project/foo/bar.cc",
89
+			wantPath:   "$dir/my-project/foo/bar.cc",
90
+		},
91
+		{
92
+			desc:       "trims heuristically",
93
+			searchPath: "$dir/my-project",
94
+			fs:         []string{"my-project/foo/bar.cc"},
95
+			path:       "/some/remote/path/my-project/foo/bar.cc",
96
+			wantPath:   "$dir/my-project/foo/bar.cc",
81 97
 		},
82 98
 		{
83 99
 			desc: "error when not found",
84
-			path: "foo.txt",
100
+			path: "foo.cc",
85 101
 		},
86 102
 	} {
87 103
 		t.Run(tc.desc, func(t *testing.T) {
@@ -103,15 +119,15 @@ func TestOpenSourceFile(t *testing.T) {
103 119
 			tc.searchPath = filepath.FromSlash(strings.Replace(tc.searchPath, "$dir", tempdir, -1))
104 120
 			tc.path = filepath.FromSlash(strings.Replace(tc.path, "$dir", tempdir, 1))
105 121
 			tc.wantPath = filepath.FromSlash(strings.Replace(tc.wantPath, "$dir", tempdir, 1))
106
-			if file, err := openSourceFile(tc.path, tc.searchPath); err != nil && tc.wantPath != "" {
107
-				t.Errorf("openSourceFile(%q, %q) = err %v, want path %q", tc.path, tc.searchPath, err, tc.wantPath)
122
+			if file, err := openSourceFile(tc.path, tc.searchPath, tc.trimPath); err != nil && tc.wantPath != "" {
123
+				t.Errorf("openSourceFile(%q, %q, %q) = err %v, want path %q", tc.path, tc.searchPath, tc.trimPath, err, tc.wantPath)
108 124
 			} else if err == nil {
109 125
 				defer file.Close()
110 126
 				gotPath := file.Name()
111 127
 				if tc.wantPath == "" {
112
-					t.Errorf("openSourceFile(%q, %q) = %q, want error", tc.path, tc.searchPath, gotPath)
128
+					t.Errorf("openSourceFile(%q, %q, %q) = %q, want error", tc.path, tc.searchPath, tc.trimPath, gotPath)
113 129
 				} else if gotPath != tc.wantPath {
114
-					t.Errorf("openSourceFile(%q, %q) = %q, want path %q", tc.path, tc.searchPath, gotPath, tc.wantPath)
130
+					t.Errorf("openSourceFile(%q, %q, %q) = %q, want path %q", tc.path, tc.searchPath, tc.trimPath, gotPath, tc.wantPath)
115 131
 				}
116 132
 			}
117 133
 		})