瀏覽代碼

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

+ 1
- 0
internal/driver/commands.go 查看文件

153
 		"Using auto will scale each value independently to the most natural unit.")},
153
 		"Using auto will scale each value independently to the most natural unit.")},
154
 	"compact_labels": &variable{boolKind, "f", "", "Show minimal headers"},
154
 	"compact_labels": &variable{boolKind, "f", "", "Show minimal headers"},
155
 	"source_path":    &variable{stringKind, "", "", "Search path for source files"},
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
 	// Filtering options
158
 	// Filtering options
158
 	"nodecount": &variable{intKind, "-1", "", helpText(
159
 	"nodecount": &variable{intKind, "-1", "", helpText(

+ 1
- 0
internal/driver/driver.go 查看文件

272
 		OutputUnit: vars["unit"].value,
272
 		OutputUnit: vars["unit"].value,
273
 
273
 
274
 		SourcePath: vars["source_path"].stringValue(),
274
 		SourcePath: vars["source_path"].stringValue(),
275
+		TrimPath:   vars["trim_path"].stringValue(),
275
 	}
276
 	}
276
 
277
 
277
 	if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
278
 	if len(p.Mapping) > 0 && p.Mapping[0].File != "" {

+ 2
- 1
internal/report/report.go 查看文件

78
 
78
 
79
 	Symbol     *regexp.Regexp // Symbols to include on disassembly report.
79
 	Symbol     *regexp.Regexp // Symbols to include on disassembly report.
80
 	SourcePath string         // Search path for source files.
80
 	SourcePath string         // Search path for source files.
81
+	TrimPath   string         // Paths to trim from source file paths.
81
 }
82
 }
82
 
83
 
83
 // Generate generates a report as directed by the Report.
84
 // Generate generates a report as directed by the Report.
238
 	// Clean up file paths using heuristics.
239
 	// Clean up file paths using heuristics.
239
 	prof := rpt.prof
240
 	prof := rpt.prof
240
 	for _, f := range prof.Function {
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
 	// Removes all numeric tags except for the bytes tag prior
244
 	// Removes all numeric tags except for the bytes tag prior
244
 	// to making graph.
245
 	// to making graph.

+ 3
- 1
internal/report/report_test.go 查看文件

46
 				&Options{
46
 				&Options{
47
 					OutputFormat: List,
47
 					OutputFormat: List,
48
 					Symbol:       regexp.MustCompile(`.`),
48
 					Symbol:       regexp.MustCompile(`.`),
49
+					TrimPath:     "/some/path",
49
 
50
 
50
 					SampleValue: sampleValue1,
51
 					SampleValue: sampleValue1,
51
 					SampleUnit:  testProfile.SampleType[1].Unit,
52
 					SampleUnit:  testProfile.SampleType[1].Unit,
60
 					OutputFormat: Dot,
61
 					OutputFormat: Dot,
61
 					CallTree:     true,
62
 					CallTree:     true,
62
 					Symbol:       regexp.MustCompile(`.`),
63
 					Symbol:       regexp.MustCompile(`.`),
64
+					TrimPath:     "/some/path",
63
 
65
 
64
 					SampleValue: sampleValue1,
66
 					SampleValue: sampleValue1,
65
 					SampleUnit:  testProfile.SampleType[1].Unit,
67
 					SampleUnit:  testProfile.SampleType[1].Unit,
119
 	{
121
 	{
120
 		ID:       4,
122
 		ID:       4,
121
 		Name:     "tee",
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
 		}
63
 		}
64
 		sourcePath = wd
64
 		sourcePath = wd
65
 	}
65
 	}
66
-	reader := newSourceReader(sourcePath)
66
+	reader := newSourceReader(sourcePath, o.TrimPath)
67
 
67
 
68
 	fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total))
68
 	fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total))
69
 	for _, fn := range functions {
69
 	for _, fn := range functions {
146
 		}
146
 		}
147
 		sourcePath = wd
147
 		sourcePath = wd
148
 	}
148
 	}
149
-	reader := newSourceReader(sourcePath)
149
+	reader := newSourceReader(sourcePath, o.TrimPath)
150
 
150
 
151
 	type fileFunction struct {
151
 	type fileFunction struct {
152
 		fileName, functionName string
152
 		fileName, functionName string
263
 		//
263
 		//
264
 		// E.g., suppose we are printing source code for F and this
264
 		// E.g., suppose we are printing source code for F and this
265
 		// instruction is from H where F called G called H and both
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
 		// number from F, not from H (which is what Disasm gives us).
267
 		// number from F, not from H (which is what Disasm gives us).
268
 		//
268
 		//
269
 		// So find the outer-most linenumber in the source file.
269
 		// So find the outer-most linenumber in the source file.
391
 				continue
391
 				continue
392
 			}
392
 			}
393
 			curCalls = nil
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
 			if !ok {
395
 			if !ok {
397
 				fline = ""
396
 				fline = ""
398
 			}
397
 			}
400
 			fmt.Fprintf(w, " %8s %10s %10s %8s  <span class=inlinesrc>%s</span> <span class=unimportant>%s:%d</span>\n",
399
 			fmt.Fprintf(w, " %8s %10s %10s %8s  <span class=inlinesrc>%s</span> <span class=unimportant>%s:%d</span>\n",
401
 				"", "", "", "",
400
 				"", "", "", "",
402
 				template.HTMLEscapeString(fmt.Sprintf("%-80s", text)),
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
 		curCalls = an.inlineCalls
404
 		curCalls = an.inlineCalls
406
 		text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction
405
 		text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction
426
 // file and annotates it with the samples in fns. Returns the sources
425
 // file and annotates it with the samples in fns. Returns the sources
427
 // as nodes, using the info.name field to hold the source code.
426
 // as nodes, using the info.name field to hold the source code.
428
 func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) {
427
 func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) {
429
-	file = trimPath(file)
430
 	lineNodes := make(map[int]graph.Nodes)
428
 	lineNodes := make(map[int]graph.Nodes)
431
 
429
 
432
 	// Collect source coordinates from profile.
430
 	// Collect source coordinates from profile.
516
 
514
 
517
 // sourceReader provides access to source code with caching of file contents.
515
 // sourceReader provides access to source code with caching of file contents.
518
 type sourceReader struct {
516
 type sourceReader struct {
517
+	// searchPath is a filepath.ListSeparator-separated list of directories where
518
+	// source files should be searched.
519
 	searchPath string
519
 	searchPath string
520
 
520
 
521
+	// trimPath is a filepath.ListSeparator-separated list of paths to trim.
522
+	trimPath string
523
+
521
 	// files maps from path name to a list of lines.
524
 	// files maps from path name to a list of lines.
522
 	// files[*][0] is unused since line numbering starts at 1.
525
 	// files[*][0] is unused since line numbering starts at 1.
523
 	files map[string][]string
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
 	// consulted before returning out of these module.
529
 	// consulted before returning out of these module.
527
 	errors map[string]error
530
 	errors map[string]error
528
 }
531
 }
529
 
532
 
530
-func newSourceReader(searchPath string) *sourceReader {
533
+func newSourceReader(searchPath, trimPath string) *sourceReader {
531
 	return &sourceReader{
534
 	return &sourceReader{
532
 		searchPath,
535
 		searchPath,
536
+		trimPath,
533
 		make(map[string][]string),
537
 		make(map[string][]string),
534
 		make(map[string]error),
538
 		make(map[string]error),
535
 	}
539
 	}
544
 	if !ok {
548
 	if !ok {
545
 		// Read and cache file contents.
549
 		// Read and cache file contents.
546
 		lines = []string{""} // Skip 0th line
550
 		lines = []string{""} // Skip 0th line
547
-		f, err := openSourceFile(path, reader.searchPath)
551
+		f, err := openSourceFile(path, reader.searchPath, reader.trimPath)
548
 		if err != nil {
552
 		if err != nil {
549
 			reader.errors[path] = err
553
 			reader.errors[path] = err
550
 		} else {
554
 		} else {
565
 	return lines[lineno], true
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
 	if filepath.IsAbs(path) {
581
 	if filepath.IsAbs(path) {
574
 		f, err := os.Open(path)
582
 		f, err := os.Open(path)
575
 		return f, err
583
 		return f, err
576
 	}
584
 	}
577
-
578
-	// Scan each component of the path
585
+	// Scan each component of the path.
579
 	for _, dir := range filepath.SplitList(searchPath) {
586
 	for _, dir := range filepath.SplitList(searchPath) {
580
 		// Search up for every parent of each possible path.
587
 		// Search up for every parent of each possible path.
581
 		for {
588
 		for {
595
 }
602
 }
596
 
603
 
597
 // trimPath cleans up a path by removing prefixes that are commonly
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
 	return path
635
 	return path

+ 33
- 17
internal/report/source_test.go 查看文件

48
 	for _, tc := range []struct {
48
 	for _, tc := range []struct {
49
 		desc       string
49
 		desc       string
50
 		searchPath string
50
 		searchPath string
51
+		trimPath   string
51
 		fs         []string
52
 		fs         []string
52
 		path       string
53
 		path       string
53
 		wantPath   string // If empty, error is wanted.
54
 		wantPath   string // If empty, error is wanted.
54
 	}{
55
 	}{
55
 		{
56
 		{
56
 			desc:     "exact absolute path is found",
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
 			desc:       "exact relative path is found",
63
 			desc:       "exact relative path is found",
63
 			searchPath: "$dir",
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
 			desc:       "multiple search path",
70
 			desc:       "multiple search path",
70
 			searchPath: "some/path" + lsep + "$dir",
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
 			desc:       "relative path is found in parent dir",
77
 			desc:       "relative path is found in parent dir",
77
 			searchPath: "$dir/foo/bar",
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
 			desc: "error when not found",
99
 			desc: "error when not found",
84
-			path: "foo.txt",
100
+			path: "foo.cc",
85
 		},
101
 		},
86
 	} {
102
 	} {
87
 		t.Run(tc.desc, func(t *testing.T) {
103
 		t.Run(tc.desc, func(t *testing.T) {
103
 			tc.searchPath = filepath.FromSlash(strings.Replace(tc.searchPath, "$dir", tempdir, -1))
119
 			tc.searchPath = filepath.FromSlash(strings.Replace(tc.searchPath, "$dir", tempdir, -1))
104
 			tc.path = filepath.FromSlash(strings.Replace(tc.path, "$dir", tempdir, 1))
120
 			tc.path = filepath.FromSlash(strings.Replace(tc.path, "$dir", tempdir, 1))
105
 			tc.wantPath = filepath.FromSlash(strings.Replace(tc.wantPath, "$dir", tempdir, 1))
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
 			} else if err == nil {
124
 			} else if err == nil {
109
 				defer file.Close()
125
 				defer file.Close()
110
 				gotPath := file.Name()
126
 				gotPath := file.Name()
111
 				if tc.wantPath == "" {
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
 				} else if gotPath != tc.wantPath {
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
 		})