diff --git a/internal/driver/commands.go b/internal/driver/commands.go index 80658314..b2c0a9cd 100644 --- a/internal/driver/commands.go +++ b/internal/driver/commands.go @@ -153,6 +153,7 @@ var pprofVariables = variables{ "Using auto will scale each value independently to the most natural unit.")}, "compact_labels": &variable{boolKind, "f", "", "Show minimal headers"}, "source_path": &variable{stringKind, "", "", "Search path for source files"}, + "trim_path": &variable{stringKind, "", "", "Path to trim from source paths before search"}, // Filtering options "nodecount": &variable{intKind, "-1", "", helpText( diff --git a/internal/driver/driver.go b/internal/driver/driver.go index 0a14c67a..986f9a4a 100644 --- a/internal/driver/driver.go +++ b/internal/driver/driver.go @@ -272,6 +272,7 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var OutputUnit: vars["unit"].value, SourcePath: vars["source_path"].stringValue(), + TrimPath: vars["trim_path"].stringValue(), } if len(p.Mapping) > 0 && p.Mapping[0].File != "" { diff --git a/internal/report/report.go b/internal/report/report.go index fa0ee3b9..15cadfb5 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -78,6 +78,7 @@ type Options struct { Symbol *regexp.Regexp // Symbols to include on disassembly report. SourcePath string // Search path for source files. + TrimPath string // Paths to trim from source file paths. } // Generate generates a report as directed by the Report. @@ -238,7 +239,7 @@ func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph { // Clean up file paths using heuristics. prof := rpt.prof for _, f := range prof.Function { - f.Filename = trimPath(f.Filename) + f.Filename = trimPath(f.Filename, o.TrimPath, o.SourcePath) } // Removes all numeric tags except for the bytes tag prior // to making graph. diff --git a/internal/report/report_test.go b/internal/report/report_test.go index c243e20c..49c6e493 100644 --- a/internal/report/report_test.go +++ b/internal/report/report_test.go @@ -46,6 +46,7 @@ func TestSource(t *testing.T) { &Options{ OutputFormat: List, Symbol: regexp.MustCompile(`.`), + TrimPath: "/some/path", SampleValue: sampleValue1, SampleUnit: testProfile.SampleType[1].Unit, @@ -60,6 +61,7 @@ func TestSource(t *testing.T) { OutputFormat: Dot, CallTree: true, Symbol: regexp.MustCompile(`.`), + TrimPath: "/some/path", SampleValue: sampleValue1, SampleUnit: testProfile.SampleType[1].Unit, @@ -119,7 +121,7 @@ var testF = []*profile.Function{ { ID: 4, Name: "tee", - Filename: "testdata/source2", + Filename: "/some/path/testdata/source2", }, } diff --git a/internal/report/source.go b/internal/report/source.go index 52958399..bd376e25 100644 --- a/internal/report/source.go +++ b/internal/report/source.go @@ -63,7 +63,7 @@ func printSource(w io.Writer, rpt *Report) error { } sourcePath = wd } - reader := newSourceReader(sourcePath) + reader := newSourceReader(sourcePath, o.TrimPath) fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total)) for _, fn := range functions { @@ -146,7 +146,7 @@ func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) er } sourcePath = wd } - reader := newSourceReader(sourcePath) + reader := newSourceReader(sourcePath, o.TrimPath) type fileFunction struct { fileName, functionName string @@ -263,7 +263,7 @@ func assemblyPerSourceLine(objSyms []*objSymbol, rs graph.Nodes, src string, obj // // E.g., suppose we are printing source code for F and this // instruction is from H where F called G called H and both - // of those calls were inlined. We want to use the line + // of those calls were inlined. We want to use the line // number from F, not from H (which is what Disasm gives us). // // So find the outer-most linenumber in the source file. @@ -391,8 +391,7 @@ func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyIns continue } curCalls = nil - fname := trimPath(c.file) - fline, ok := reader.line(fname, c.line) + fline, ok := reader.line(c.file, c.line) if !ok { fline = "" } @@ -400,7 +399,7 @@ func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyIns fmt.Fprintf(w, " %8s %10s %10s %8s %s %s:%d\n", "", "", "", "", template.HTMLEscapeString(fmt.Sprintf("%-80s", text)), - template.HTMLEscapeString(filepath.Base(fname)), c.line) + template.HTMLEscapeString(filepath.Base(c.file)), c.line) } curCalls = an.inlineCalls text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction @@ -426,7 +425,6 @@ func printPageClosing(w io.Writer) { // file and annotates it with the samples in fns. Returns the sources // as nodes, using the info.name field to hold the source code. func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) { - file = trimPath(file) lineNodes := make(map[int]graph.Nodes) // Collect source coordinates from profile. @@ -516,20 +514,26 @@ func getMissingFunctionSource(filename string, asm map[int][]assemblyInstruction // sourceReader provides access to source code with caching of file contents. type sourceReader struct { + // searchPath is a filepath.ListSeparator-separated list of directories where + // source files should be searched. searchPath string + // trimPath is a filepath.ListSeparator-separated list of paths to trim. + trimPath string + // files maps from path name to a list of lines. // files[*][0] is unused since line numbering starts at 1. files map[string][]string - // errors collects errors encountered per file. These errors are + // errors collects errors encountered per file. These errors are // consulted before returning out of these module. errors map[string]error } -func newSourceReader(searchPath string) *sourceReader { +func newSourceReader(searchPath, trimPath string) *sourceReader { return &sourceReader{ searchPath, + trimPath, make(map[string][]string), make(map[string]error), } @@ -544,7 +548,7 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) { if !ok { // Read and cache file contents. lines = []string{""} // Skip 0th line - f, err := openSourceFile(path, reader.searchPath) + f, err := openSourceFile(path, reader.searchPath, reader.trimPath) if err != nil { reader.errors[path] = err } else { @@ -565,17 +569,20 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) { return lines[lineno], true } -// openSourceFile opens a source file from a name encoded in a -// profile. File names in a profile after often relative paths, so -// search them in each of the paths in searchPath (or CWD by default), -// and their parents. -func openSourceFile(path, searchPath string) (*os.File, error) { +// openSourceFile opens a source file from a name encoded in a profile. File +// names in a profile after can be relative paths, so search them in each of +// the paths in searchPath and their parents. In case the profile contains +// absolute paths, additional paths may be configured to trim from the source +// paths in the profile. This effectively turns the path into a relative path +// searching it using searchPath as usual). +func openSourceFile(path, searchPath, trim string) (*os.File, error) { + path = trimPath(path, trim, searchPath) + // If file is still absolute, require file to exist. if filepath.IsAbs(path) { f, err := os.Open(path) return f, err } - - // Scan each component of the path + // Scan each component of the path. for _, dir := range filepath.SplitList(searchPath) { // Search up for every parent of each possible path. for { @@ -595,18 +602,32 @@ func openSourceFile(path, searchPath string) (*os.File, error) { } // trimPath cleans up a path by removing prefixes that are commonly -// found on profiles. -func trimPath(path string) string { - basePaths := []string{ - "/proc/self/cwd/./", - "/proc/self/cwd/", +// found on profiles plus configured prefixes. +func trimPath(path, trimPath, searchPath string) string { + // Keep path variable intact as it's used below to form the return value. + sPath, searchPath := filepath.ToSlash(path), filepath.ToSlash(searchPath) + if trimPath == "" { + // If the trim path is not configured, try to guess it heuristically: + // search for basename of each search path in the original path and, if + // found, strip everything up to and including the basename. So, for + // example, given original path "/some/remote/path/my-project/foo/bar.c" + // and search path "/my/local/path/my-project" the heuristic will return + // "/my/local/path/my-project/foo/bar.c". + for _, dir := range filepath.SplitList(searchPath) { + want := "/" + filepath.Base(dir) + "/" + if found := strings.Index(sPath, want); found != -1 { + return path[found+len(want):] + } + } } - - sPath := filepath.ToSlash(path) - - for _, base := range basePaths { - if strings.HasPrefix(sPath, base) { - return filepath.FromSlash(sPath[len(base):]) + // Trim configured trim prefixes. + trimPaths := append(filepath.SplitList(filepath.ToSlash(trimPath)), "/proc/self/cwd/./", "/proc/self/cwd/") + for _, trimPath := range trimPaths { + if !strings.HasSuffix(trimPath, "/") { + trimPath += "/" + } + if strings.HasPrefix(sPath, trimPath) { + return path[len(trimPath):] } } return path diff --git a/internal/report/source_test.go b/internal/report/source_test.go index 682bfe0a..f1dd5c70 100644 --- a/internal/report/source_test.go +++ b/internal/report/source_test.go @@ -48,40 +48,56 @@ func TestOpenSourceFile(t *testing.T) { for _, tc := range []struct { desc string searchPath string + trimPath string fs []string path string wantPath string // If empty, error is wanted. }{ { desc: "exact absolute path is found", - fs: []string{"foo/bar.txt"}, - path: "$dir/foo/bar.txt", - wantPath: "$dir/foo/bar.txt", + fs: []string{"foo/bar.cc"}, + path: "$dir/foo/bar.cc", + wantPath: "$dir/foo/bar.cc", }, { desc: "exact relative path is found", searchPath: "$dir", - fs: []string{"foo/bar.txt"}, - path: "foo/bar.txt", - wantPath: "$dir/foo/bar.txt", + fs: []string{"foo/bar.cc"}, + path: "foo/bar.cc", + wantPath: "$dir/foo/bar.cc", }, { desc: "multiple search path", searchPath: "some/path" + lsep + "$dir", - fs: []string{"foo/bar.txt"}, - path: "foo/bar.txt", - wantPath: "$dir/foo/bar.txt", + fs: []string{"foo/bar.cc"}, + path: "foo/bar.cc", + wantPath: "$dir/foo/bar.cc", }, { desc: "relative path is found in parent dir", searchPath: "$dir/foo/bar", - fs: []string{"bar.txt", "foo/bar/baz.txt"}, - path: "bar.txt", - wantPath: "$dir/bar.txt", + fs: []string{"bar.cc", "foo/bar/baz.cc"}, + path: "bar.cc", + wantPath: "$dir/bar.cc", + }, + { + desc: "trims configured prefix", + searchPath: "$dir", + trimPath: "some-path" + lsep + "/some/remote/path", + fs: []string{"my-project/foo/bar.cc"}, + path: "/some/remote/path/my-project/foo/bar.cc", + wantPath: "$dir/my-project/foo/bar.cc", + }, + { + desc: "trims heuristically", + searchPath: "$dir/my-project", + fs: []string{"my-project/foo/bar.cc"}, + path: "/some/remote/path/my-project/foo/bar.cc", + wantPath: "$dir/my-project/foo/bar.cc", }, { desc: "error when not found", - path: "foo.txt", + path: "foo.cc", }, } { t.Run(tc.desc, func(t *testing.T) { @@ -103,15 +119,15 @@ func TestOpenSourceFile(t *testing.T) { tc.searchPath = filepath.FromSlash(strings.Replace(tc.searchPath, "$dir", tempdir, -1)) tc.path = filepath.FromSlash(strings.Replace(tc.path, "$dir", tempdir, 1)) tc.wantPath = filepath.FromSlash(strings.Replace(tc.wantPath, "$dir", tempdir, 1)) - if file, err := openSourceFile(tc.path, tc.searchPath); err != nil && tc.wantPath != "" { - t.Errorf("openSourceFile(%q, %q) = err %v, want path %q", tc.path, tc.searchPath, err, tc.wantPath) + if file, err := openSourceFile(tc.path, tc.searchPath, tc.trimPath); err != nil && tc.wantPath != "" { + t.Errorf("openSourceFile(%q, %q, %q) = err %v, want path %q", tc.path, tc.searchPath, tc.trimPath, err, tc.wantPath) } else if err == nil { defer file.Close() gotPath := file.Name() if tc.wantPath == "" { - t.Errorf("openSourceFile(%q, %q) = %q, want error", tc.path, tc.searchPath, gotPath) + t.Errorf("openSourceFile(%q, %q, %q) = %q, want error", tc.path, tc.searchPath, tc.trimPath, gotPath) } else if gotPath != tc.wantPath { - t.Errorf("openSourceFile(%q, %q) = %q, want path %q", tc.path, tc.searchPath, gotPath, tc.wantPath) + t.Errorf("openSourceFile(%q, %q, %q) = %q, want path %q", tc.path, tc.searchPath, tc.trimPath, gotPath, tc.wantPath) } } })