Skip to content

Commit 843a7e0

Browse files
Merge pull request #1 from codeperfio/flamegraph
Added Flame Graph exporting support
2 parents 1d37944 + 2fa472a commit 843a7e0

File tree

5 files changed

+194
-8
lines changed

5 files changed

+194
-8
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ build:
2525
$(GOCMD) build .
2626

2727
fmt:
28-
$(GOFMT) ./...
28+
$(GOFMT) ./*.go
29+
$(GOFMT) ./cmd/*.go
2930

3031
lint:
3132
$(GOGET) github.com/golangci/golangci-lint/cmd/golangci-lint

cmd/export.go

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"github.com/google/pprof/driver"
9+
"github.com/google/pprof/profile"
910
"github.com/spf13/cobra"
1011
"io"
1112
"io/ioutil"
@@ -38,12 +39,21 @@ func exportLogic() func(cmd *cobra.Command, args []string) {
3839
log.Fatalf("Exactly one profile file is required")
3940
}
4041
if local {
42+
err, finalTree := generateFlameGraph(args[0])
43+
if err != nil {
44+
log.Fatalf("An Error Occured %v", err)
45+
}
46+
postBody, err := json.Marshal(finalTree)
47+
if err != nil {
48+
log.Fatalf("An Error Occured %v", err)
49+
}
50+
fmt.Println(string(postBody))
51+
4152
for _, granularity := range granularityOptions {
4253
err, report := generateTextReports(granularity, args[0])
4354
if err == nil {
4455
var w io.Writer
4556
// open output file
46-
fmt.Println()
4757
localExportLogic(w, report)
4858
} else {
4959
log.Fatal(err)
@@ -53,16 +63,45 @@ func exportLogic() func(cmd *cobra.Command, args []string) {
5363
for _, granularity := range granularityOptions {
5464
err, report := generateTextReports(granularity, args[0])
5565
if err == nil {
56-
fmt.Println()
5766
remoteExportLogic(report, granularity)
5867
} else {
5968
log.Fatal(err)
6069
}
6170
}
62-
log.Printf("Successfully published profile data. Check it at: %s/gh/%s/%s/commit/%s/bench/%s/cpu", codeperfUrl, gitOrg, gitRepo, gitCommit, bench)
71+
err, finalTree := generateFlameGraph(args[0])
72+
if err != nil {
73+
log.Fatalf("An Error Occured %v", err)
74+
}
75+
remoteFlameGraphExport(finalTree)
76+
log.Printf("Successfully published profile data")
77+
log.Printf("Check it at: %s/gh/%s/%s/commit/%s/bench/%s/cpu", codeperfUrl, gitOrg, gitRepo, gitCommit, bench)
6378
}
79+
}
80+
}
81+
82+
func remoteFlameGraphExport(tree treeNodeSlice) {
83+
postBody, err := json.Marshal(tree)
84+
if err != nil {
85+
log.Fatalf("An Error Occured %v", err)
86+
}
87+
responseBody := bytes.NewBuffer(postBody)
88+
endPoint := fmt.Sprintf("%s/v1/gh/%s/%s/commit/%s/bench/%s/cpu/flamegraph", codeperfApiUrl, gitOrg, gitRepo, gitCommit, bench)
89+
resp, err := http.Post(endPoint, "application/json", responseBody)
90+
//Handle Error
91+
if err != nil {
92+
log.Fatalf("An Error Occured %v", err)
93+
}
94+
defer resp.Body.Close()
6495

96+
//Read the response body
97+
reply, err := ioutil.ReadAll(resp.Body)
98+
if err != nil {
99+
log.Fatalln(err)
65100
}
101+
if resp.StatusCode != 200 {
102+
log.Fatalf("An error ocurred while phusing data to remote %s. Status code %d. Reply: %s", codeperfApiUrl, resp.StatusCode, string(reply))
103+
}
104+
66105
}
67106

68107
func remoteExportLogic(report TextReport, granularity string) {
@@ -111,6 +150,48 @@ func localExportLogic(w io.Writer, report TextReport) {
111150
}
112151
}
113152

153+
func generateFlameGraph(input string) (err error, tree treeNodeSlice) {
154+
f := baseFlags()
155+
156+
// Read the profile from the encoded protobuf
157+
outputTempFile, err := ioutil.TempFile("", "profile_output")
158+
if err != nil {
159+
log.Fatalf("cannot create tempfile: %v", err)
160+
}
161+
log.Printf("Generating temp file %s", outputTempFile.Name())
162+
//defer os.Remove(outputTempFile.Name())
163+
defer outputTempFile.Close()
164+
f.strings["output"] = outputTempFile.Name()
165+
f.bools["proto"] = true
166+
f.bools["text"] = false
167+
f.args = []string{
168+
input,
169+
}
170+
reader := bufio.NewReader(os.Stdin)
171+
options := &driver.Options{
172+
Flagset: f,
173+
UI: &UI{r: reader},
174+
}
175+
176+
if err = driver.PProf(options); err != nil {
177+
log.Fatalf("cannot read pprof profile from %s. Error: %v", input, err)
178+
return
179+
}
180+
181+
file, err := os.Open(outputTempFile.Name())
182+
if err != nil {
183+
log.Fatal(err)
184+
}
185+
defer file.Close()
186+
r := bufio.NewReader(file)
187+
profile, err := profile.Parse(r)
188+
if err != nil {
189+
log.Fatal(err)
190+
}
191+
tree = profileToFolded(profile)
192+
return
193+
}
194+
114195
func generateTextReports(granularity string, input string) (err error, report TextReport) {
115196
f := baseFlags()
116197

cmd/flagset.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package cmd
22

33
import (
44
"bufio"
5-
"fmt"
5+
"log"
66
"os"
77
)
88

@@ -88,11 +88,11 @@ func (ui *UI) ReadLine(prompt string) (string, error) {
8888
}
8989

9090
func (ui *UI) Print(args ...interface{}) {
91-
fmt.Fprint(os.Stderr, args...)
91+
log.Print(args...)
9292
}
9393

9494
func (ui *UI) PrintErr(args ...interface{}) {
95-
fmt.Fprint(os.Stderr, args...)
95+
log.Print(args...)
9696
}
9797

9898
func (ui *UI) IsTerminal() bool {

cmd/folded.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package cmd
2+
3+
import (
4+
"github.com/google/pprof/profile"
5+
"log"
6+
"regexp"
7+
"sort"
8+
"strings"
9+
)
10+
11+
type treeNode struct {
12+
Name string `json:"n"`
13+
FullName string `json:"f"`
14+
Cum int64 `json:"v"`
15+
Children map[string]*treeNode `json:"c"`
16+
}
17+
18+
type treeNodeSlice struct {
19+
Name string `json:"n"`
20+
FullName string `json:"f"`
21+
Cum int64 `json:"v"`
22+
Children []treeNodeSlice `json:"c"`
23+
}
24+
25+
var (
26+
// Removes package name and method arguments for Java method names.
27+
// See tests for examples.
28+
javaRegExp = regexp.MustCompile(`^(?:[a-z]\w*\.)*([A-Z][\w\$]*\.(?:<init>|[a-z][\w\$]*(?:\$\d+)?))(?:(?:\()|$)`)
29+
// Removes package name and method arguments for Go function names.
30+
// See tests for examples.
31+
goRegExp = regexp.MustCompile(`^(?:[\w\-\.]+\/)+(.+)`)
32+
// Removes potential module versions in a package path.
33+
goVerRegExp = regexp.MustCompile(`^(.*?)/v(?:[2-9]|[1-9][0-9]+)([./].*)$`)
34+
// Strips C++ namespace prefix from a C++ function / method name.
35+
// NOTE: Make sure to keep the template parameters in the name. Normally,
36+
// template parameters are stripped from the C++ names but when
37+
// -symbolize=demangle=templates flag is used, they will not be.
38+
// See tests for examples.
39+
cppRegExp = regexp.MustCompile(`^(?:[_a-zA-Z]\w*::)+(_*[A-Z]\w*::~?[_a-zA-Z]\w*(?:<.*>)?)`)
40+
cppAnonymousPrefixRegExp = regexp.MustCompile(`^\(anonymous namespace\)::`)
41+
)
42+
43+
// ShortenFunctionName returns a shortened version of a function's name.
44+
func ShortenFunctionName(f string) string {
45+
f = cppAnonymousPrefixRegExp.ReplaceAllString(f, "")
46+
f = goVerRegExp.ReplaceAllString(f, `${1}${2}`)
47+
for _, re := range []*regexp.Regexp{goRegExp, javaRegExp, cppRegExp} {
48+
if matches := re.FindStringSubmatch(f); len(matches) >= 2 {
49+
return strings.Join(matches[1:], "")
50+
}
51+
}
52+
return f
53+
}
54+
55+
// Convert marshals the given protobuf profile into folded text format.
56+
func profileToFolded(protobuf *profile.Profile) treeNodeSlice {
57+
rootNode := treeNode{"root", "root", 0, make(map[string]*treeNode, 0)}
58+
if err := protobuf.Aggregate(true, true, false, false, false); err != nil {
59+
log.Fatal(err)
60+
}
61+
protobuf = protobuf.Compact()
62+
sort.Slice(protobuf.Sample, func(i, j int) bool {
63+
return protobuf.Sample[i].Value[0] > protobuf.Sample[j].Value[0]
64+
})
65+
66+
for _, sample := range protobuf.Sample {
67+
var cum int64 = 0
68+
for _, val := range sample.Value {
69+
cum = cum + val
70+
break
71+
}
72+
var frames []string
73+
var currentNode *treeNode
74+
var currentMap map[string]*treeNode = rootNode.Children
75+
for i := range sample.Location {
76+
var ok bool
77+
loc := sample.Location[len(sample.Location)-i-1]
78+
for j := range loc.Line {
79+
line := loc.Line[len(loc.Line)-j-1]
80+
fname := ShortenFunctionName(line.Function.Name)
81+
shortName := fname[strings.LastIndex(fname, ".")+1:]
82+
currentNode, ok = currentMap[shortName]
83+
if !ok {
84+
currentNode = &treeNode{shortName, fname, 0, make(map[string]*treeNode, 0)}
85+
currentMap[shortName] = currentNode
86+
}
87+
currentNode.Cum += cum
88+
currentMap = currentNode.Children
89+
frames = append(frames, shortName)
90+
}
91+
}
92+
}
93+
finalTree := treeNodeSlice{rootNode.Name, rootNode.FullName, rootNode.Cum, collapse(rootNode.Children)}
94+
return finalTree
95+
}
96+
97+
func collapse(children map[string]*treeNode) (tree []treeNodeSlice) {
98+
tree = make([]treeNodeSlice, 0)
99+
for _, k := range children {
100+
nS := treeNodeSlice{k.Name, k.FullName, k.Cum, collapse(k.Children)}
101+
tree = append(tree, nS)
102+
}
103+
return
104+
}

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func init() {
7878
repoStartPos := strings.LastIndex(remoteUsed, "/") + 1
7979
defaultGitRepo = remoteUsed[repoStartPos : len(remoteUsed)-4]
8080
defaultGitCommit = refHash
81-
fmt.Printf("Detected the following git vars org=%s repo=%s hash=%s", defaultGitOrg, defaultGitRepo, defaultGitCommit)
81+
log.Printf("Detected the following git vars org=%s repo=%s hash=%s\n", defaultGitOrg, defaultGitRepo, defaultGitCommit)
8282
}
8383

8484
// Here you will define your flags and configuration settings.

0 commit comments

Comments
 (0)