diff --git a/doc/README.md b/doc/README.md index 0eb7b7668..8d02aa314 100644 --- a/doc/README.md +++ b/doc/README.md @@ -171,6 +171,8 @@ pprof text reports show the location hierarchy in text format. * **-peek= _regex_:** Print the location entry with all its predecessors and successors, without trimming any entries. * **-traces:** Prints each sample with a location per line. +* **-folded:** Prints each sample in [Brendan Gregg's Folded Stacks + format](https://queue.acm.org/detail.cfm?id=2927301#:~:text=The%20folded%20stack%2Dtrace%20format,trace%2C%20followed%20by%20a%20semicolon.). ## Graphical reports diff --git a/internal/driver/commands.go b/internal/driver/commands.go index c9edf10bb..07f79b47d 100644 --- a/internal/driver/commands.go +++ b/internal/driver/commands.go @@ -93,6 +93,7 @@ var pprofCommands = commands{ "top": {report.Text, nil, nil, false, "Outputs top entries in text form", reportHelp("top", true, true)}, "traces": {report.Traces, nil, nil, false, "Outputs all profile samples in text form", ""}, "tree": {report.Tree, nil, nil, false, "Outputs a text rendering of call graph", reportHelp("tree", true, true)}, + "folded": {report.Folded, nil, nil, false, "Outputs entries in folded stack form", reportHelp("folded", false, true)}, // Save binary formats to a file "callgrind": {report.Callgrind, nil, awayFromTTY("callgraph.out"), false, "Outputs a graph in callgrind format", reportHelp("callgrind", false, true)}, diff --git a/internal/report/report.go b/internal/report/report.go index e2fb00314..ea2209845 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -48,6 +48,7 @@ const ( Traces Tree WebList + Folded ) // Options are the formatting and filtering options used to generate a @@ -94,6 +95,8 @@ func Generate(w io.Writer, rpt *Report, obj plugin.ObjTool) error { return printDOT(w, rpt) case Tree: return printTree(w, rpt) + case Folded: + return printFolded(w, rpt) case Text: return printText(w, rpt) case Traces: @@ -828,6 +831,55 @@ func printText(w io.Writer, rpt *Report) error { return nil } +// printFolded prints a profile in Brendan Gregg's Folded Stacks format. +func printFolded(w io.Writer, rpt *Report) error { + prof := rpt.prof + o := rpt.options + + _, locations := graph.CreateNodes(prof, &graph.Options{}) + for _, sample := range prof.Sample { + var stack []*graph.NodeInfo + for _, loc := range sample.Location { + nodes := locations[loc.ID] + for _, n := range nodes { + stack = append(stack, &n.Info) + } + } + + if len(stack) == 0 { + continue + } + + var d, v int64 + v = o.SampleValue(sample.Value) + if o.SampleMeanDivisor != nil { + d = o.SampleMeanDivisor(sample.Value) + } + if d != 0 { + v = v / d + } + // Print call stack. + for i := range stack { + // Folded stack convention: start with root frame, end + // with leaves. + s := stack[len(stack)-i-1] + if i > 0 { + fmt.Fprint(w, ";") + } + // TODO: should we print more than just s.Name? + // NodeInfo.PrintableName() has a lot more. + + // Remove semicolons and newlines. + name := strings.ReplaceAll(s.Name, ";", "") + name = strings.ReplaceAll(s.Name, "\n", "") + fmt.Fprint(w, name) + } + // We just want a raw number, so don't use rpt.formatValue(). + fmt.Fprintf(w, " %d\n", v) + } + return nil +} + // printTraces prints all traces from a profile. func printTraces(w io.Writer, rpt *Report) error { fmt.Fprintln(w, strings.Join(ProfileLabels(rpt), "\n")) diff --git a/internal/report/report_test.go b/internal/report/report_test.go index 0c2ee96ea..796670662 100644 --- a/internal/report/report_test.go +++ b/internal/report/report_test.go @@ -36,6 +36,9 @@ type testcase struct { func TestTextReports(t *testing.T) { const path = "testdata/" + sampleValue0 := func(v []int64) int64 { + return v[0] + } sampleValue1 := func(v []int64) int64 { return v[1] } @@ -70,6 +73,43 @@ func TestTextReports(t *testing.T) { ), want: path + "report.dot", }, + { + rpt: New( + testProfile.Copy(), + &Options{ + OutputFormat: Folded, + + SampleValue: sampleValue1, + SampleUnit: testProfile.SampleType[1].Unit, + }, + ), + want: path + "report.folded", + }, + { + rpt: New( + testProfile.Copy(), + &Options{ + OutputFormat: Folded, + + SampleValue: sampleValue0, + SampleUnit: testProfile.SampleType[0].Unit, + }, + ), + want: path + "report0.folded", + }, + { + rpt: New( + testProfile.Copy(), + &Options{ + OutputFormat: Folded, + + SampleValue: sampleValue1, + SampleUnit: testProfile.SampleType[1].Unit, + SampleMeanDivisor: sampleValue0, + }, + ), + want: path + "report_mean.folded", + }, } { var b bytes.Buffer if err := Generate(&b, tc.rpt, &binutils.Binutils{}); err != nil { @@ -259,7 +299,7 @@ var testProfile = &profile.Profile{ }, { Location: []*profile.Location{testL[4], testL[3], testL[0]}, - Value: []int64{1, 10000}, + Value: []int64{2, 10000}, }, }, Location: testL, diff --git a/internal/report/testdata/report.folded b/internal/report/testdata/report.folded new file mode 100644 index 000000000..d72d83467 --- /dev/null +++ b/internal/report/testdata/report.folded @@ -0,0 +1,5 @@ +main 1 +main;foo;bar 10 +main;bar;tee 100 +main;tee 1000 +main;tee;tee 10000 diff --git a/internal/report/testdata/report0.folded b/internal/report/testdata/report0.folded new file mode 100644 index 000000000..6702f0fc0 --- /dev/null +++ b/internal/report/testdata/report0.folded @@ -0,0 +1,5 @@ +main 1 +main;foo;bar 1 +main;bar;tee 1 +main;tee 1 +main;tee;tee 2 diff --git a/internal/report/testdata/report_mean.folded b/internal/report/testdata/report_mean.folded new file mode 100644 index 000000000..3f6b2d69f --- /dev/null +++ b/internal/report/testdata/report_mean.folded @@ -0,0 +1,5 @@ +main 1 +main;foo;bar 10 +main;bar;tee 100 +main;tee 1000 +main;tee;tee 5000