diff --git a/doc/README.md b/doc/README.md index 0eb7b766..8d02aa31 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 c9edf10b..07f79b47 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 e2fb0031..968ebd32 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(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 99e4bc7d..869deb7f 100644 --- a/internal/report/report_test.go +++ b/internal/report/report_test.go @@ -33,9 +33,12 @@ type testcase struct { want string } -func TestSource(t *testing.T) { +func TestTextReports(t *testing.T) { const path = "testdata/" + sampleValue0 := func(v []int64) int64 { + return v[0] + } sampleValue1 := func(v []int64) int64 { return v[1] } @@ -53,7 +56,7 @@ func TestSource(t *testing.T) { SampleUnit: testProfile.SampleType[1].Unit, }, ), - want: path + "source.rpt", + want: path + "report.source", }, { rpt: New( @@ -68,7 +71,44 @@ func TestSource(t *testing.T) { SampleUnit: testProfile.SampleType[1].Unit, }, ), - want: path + "source.dot", + 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 @@ -169,7 +209,7 @@ var testF = []*profile.Function{ }, { ID: 3, - Name: "bar", + Name: "b;ar", Filename: "testdata/source1", }, { @@ -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/source.dot b/internal/report/testdata/report.dot similarity index 71% rename from internal/report/testdata/source.dot rename to internal/report/testdata/report.dot index e0dadb1b..16f0fc5a 100644 --- a/internal/report/testdata/source.dot +++ b/internal/report/testdata/report.dot @@ -5,13 +5,13 @@ N1 [label="tee\nsource2:8\n10000 (90.00%)" id="node1" fontsize=24 shape=box tool N2 [label="main\nsource1:2\n1 (0.009%)\nof 11111 (100%)" id="node2" fontsize=9 shape=box tooltip="main testdata/source1:2 (11111)" color="#b20000" fillcolor="#edd5d5"] N3 [label="tee\nsource2:2\n1000 (9.00%)\nof 11000 (99.00%)" id="node3" fontsize=14 shape=box tooltip="tee testdata/source2:2 (11000)" color="#b20000" fillcolor="#edd5d5"] N4 [label="tee\nsource2:8\n100 (0.9%)" id="node4" fontsize=10 shape=box tooltip="tee testdata/source2:8 (100)" color="#b2b0aa" fillcolor="#edecec"] -N5 [label="bar\nsource1:10\n10 (0.09%)" id="node5" fontsize=9 shape=box tooltip="bar testdata/source1:10 (10)" color="#b2b2b1" fillcolor="#ededed"] -N6 [label="bar\nsource1:10\n0 of 100 (0.9%)" id="node6" fontsize=8 shape=box tooltip="bar testdata/source1:10 (100)" color="#b2b0aa" fillcolor="#edecec"] +N5 [label="b;ar\nsource1:10\n10 (0.09%)" id="node5" fontsize=9 shape=box tooltip="b;ar testdata/source1:10 (10)" color="#b2b2b1" fillcolor="#ededed"] +N6 [label="b;ar\nsource1:10\n0 of 100 (0.9%)" id="node6" fontsize=8 shape=box tooltip="b;ar testdata/source1:10 (100)" color="#b2b0aa" fillcolor="#edecec"] N7 [label="foo\nsource1:4\n0 of 10 (0.09%)" id="node7" fontsize=8 shape=box tooltip="foo testdata/source1:4 (10)" color="#b2b2b1" fillcolor="#ededed"] N2 -> N3 [label=" 11000" weight=100 penwidth=5 color="#b20000" tooltip="main testdata/source1:2 -> tee testdata/source2:2 (11000)" labeltooltip="main testdata/source1:2 -> tee testdata/source2:2 (11000)"] N3 -> N1 [label=" 10000" weight=91 penwidth=5 color="#b20500" tooltip="tee testdata/source2:2 -> tee testdata/source2:8 (10000)" labeltooltip="tee testdata/source2:2 -> tee testdata/source2:8 (10000)"] -N6 -> N4 [label=" 100" color="#b2b0aa" tooltip="bar testdata/source1:10 -> tee testdata/source2:8 (100)" labeltooltip="bar testdata/source1:10 -> tee testdata/source2:8 (100)"] -N2 -> N6 [label=" 100" color="#b2b0aa" tooltip="main testdata/source1:2 -> bar testdata/source1:10 (100)" labeltooltip="main testdata/source1:2 -> bar testdata/source1:10 (100)"] -N7 -> N5 [label=" 10" color="#b2b2b1" tooltip="foo testdata/source1:4 -> bar testdata/source1:10 (10)" labeltooltip="foo testdata/source1:4 -> bar testdata/source1:10 (10)"] +N6 -> N4 [label=" 100" color="#b2b0aa" tooltip="b;ar testdata/source1:10 -> tee testdata/source2:8 (100)" labeltooltip="b;ar testdata/source1:10 -> tee testdata/source2:8 (100)"] +N2 -> N6 [label=" 100" color="#b2b0aa" tooltip="main testdata/source1:2 -> b;ar testdata/source1:10 (100)" labeltooltip="main testdata/source1:2 -> b;ar testdata/source1:10 (100)"] +N7 -> N5 [label=" 10" color="#b2b2b1" tooltip="foo testdata/source1:4 -> b;ar testdata/source1:10 (10)" labeltooltip="foo testdata/source1:4 -> b;ar testdata/source1:10 (10)"] N2 -> N7 [label=" 10" color="#b2b2b1" tooltip="main testdata/source1:2 -> foo testdata/source1:4 (10)" labeltooltip="main testdata/source1:2 -> foo testdata/source1:4 (10)"] } diff --git a/internal/report/testdata/report.folded b/internal/report/testdata/report.folded new file mode 100644 index 00000000..d72d8346 --- /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/source.rpt b/internal/report/testdata/report.source similarity index 97% rename from internal/report/testdata/source.rpt rename to internal/report/testdata/report.source index 9ec7b3b0..eed7fb9a 100644 --- a/internal/report/testdata/source.rpt +++ b/internal/report/testdata/report.source @@ -1,5 +1,5 @@ Total: 11111 -ROUTINE ======================== bar in testdata/source1 +ROUTINE ======================== b;ar in testdata/source1 10 110 (flat, cum) 0.99% of Total . . 5:source1 line 5; . . 6:source1 line 6; diff --git a/internal/report/testdata/report0.folded b/internal/report/testdata/report0.folded new file mode 100644 index 00000000..6702f0fc --- /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 00000000..3f6b2d69 --- /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