Skip to content

Commit a8874b7

Browse files
initila commit for json raw for flame graph
1 parent d1f7493 commit a8874b7

2 files changed

Lines changed: 228 additions & 5 deletions

File tree

api/output_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const (
2121
Callgrind OutputType = "callgrind"
2222
Raw OutputType = "raw"
2323
Pprof OutputType = "pprof"
24+
FlameJson OutputType = "flame-json" // Used for AsyncProfiler flamegraph in JSON format
2425
Summary OutputType = "summary"
2526
SummaryByLine OutputType = "summary-by-line"
2627
HeapSnapshot OutputType = "heapsnapshot"

internal/agent/profiler/go_pprof.go

Lines changed: 227 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import (
44
"bufio"
55
"bytes"
66
"context"
7+
"encoding/json"
78
"fmt"
89
"os/exec"
10+
"regexp"
911
"strconv"
1012
"strings"
1113
"time"
@@ -141,24 +143,244 @@ func (p *goPprofManager) convertPprofToRaw(pprofFilePath string) (string, error)
141143

142144
}
143145

146+
type Sample struct {
147+
Count int
148+
CPUCost int
149+
StackIDs []int
150+
StackFuncs []string
151+
}
152+
153+
type Node struct {
154+
Name string `json:"name"`
155+
File string `json:"file,omitempty"`
156+
Value int `json:"value"`
157+
Children []*Node `json:"children"`
158+
childMap map[string]*Node
159+
}
160+
161+
func (m *goPprofManager) convertRawToJson(content string) (string, error) {
162+
lines := strings.Split(content, "\n")
163+
header := make(map[string]interface{})
164+
var rawSamples []Sample
165+
locations := make(map[int][]string)
166+
mappings := make(map[int]string)
167+
168+
mode := ""
169+
var currentLocID int
170+
171+
sampleRegex := regexp.MustCompile(`^\s*(\d+)\s+(\d+):\s+(.+)`)
172+
locLineRegex := regexp.MustCompile(`^\d+:`)
173+
174+
for _, line := range lines {
175+
line = strings.TrimSpace(line)
176+
if mode == "" {
177+
switch {
178+
case strings.HasPrefix(line, "PeriodType:"):
179+
header["PeriodType"] = strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
180+
case strings.HasPrefix(line, "Period:"):
181+
val, _ := strconv.Atoi(strings.TrimSpace(strings.SplitN(line, ":", 2)[1]))
182+
header["Period"] = val
183+
case strings.HasPrefix(line, "Time:"):
184+
header["Time"] = strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
185+
case strings.HasPrefix(line, "Duration:"):
186+
val, _ := strconv.ParseFloat(strings.TrimSpace(strings.SplitN(line, ":", 2)[1]), 64)
187+
header["Duration"] = val
188+
case strings.HasPrefix(line, "samples/count"):
189+
mode = "samples"
190+
}
191+
} else if mode == "samples" {
192+
if line == "" || strings.HasPrefix(line, "Locations") {
193+
mode = "locations"
194+
continue
195+
}
196+
if match := sampleRegex.FindStringSubmatch(line); match != nil {
197+
count, _ := strconv.Atoi(match[1])
198+
cost, _ := strconv.Atoi(match[2])
199+
rawIDs := strings.Fields(match[3])
200+
stackIDs := []int{}
201+
for _, idStr := range rawIDs {
202+
id, _ := strconv.Atoi(idStr)
203+
stackIDs = append(stackIDs, id)
204+
}
205+
rawSamples = append(rawSamples, Sample{Count: count, CPUCost: cost, StackIDs: stackIDs})
206+
}
207+
} else if mode == "locations" {
208+
if line == "" || strings.HasPrefix(line, "Mappings") {
209+
mode = "mappings"
210+
continue
211+
}
212+
if locLineRegex.MatchString(line) {
213+
parts := strings.SplitN(line, ":", 2)
214+
currentLocID, _ = strconv.Atoi(parts[0])
215+
frame := strings.TrimSpace(parts[1])
216+
if frame != "" {
217+
locations[currentLocID] = append(locations[currentLocID], frame)
218+
}
219+
} else {
220+
locations[currentLocID] = append(locations[currentLocID], line)
221+
}
222+
} else if mode == "mappings" {
223+
if locLineRegex.MatchString(line) {
224+
parts := strings.SplitN(line, ":", 2)
225+
id, _ := strconv.Atoi(parts[0])
226+
mappings[id] = strings.TrimSpace(parts[1])
227+
}
228+
}
229+
}
230+
231+
var resolved []Sample
232+
for _, s := range rawSamples {
233+
var funcs []string
234+
for _, id := range s.StackIDs {
235+
frames := locations[id]
236+
for _, f := range frames {
237+
fn := strings.Fields(f)
238+
if len(fn) > 0 {
239+
funcs = append(funcs, fn[0])
240+
}
241+
}
242+
}
243+
s.StackFuncs = funcs
244+
resolved = append(resolved, s)
245+
}
246+
247+
result := map[string]interface{}{
248+
"header": header,
249+
"samples": resolved,
250+
"locations": locations,
251+
"mappings": mappings,
252+
}
253+
samples := result["samples"].([]Sample)
254+
locations = result["locations"].(map[int][]string)
255+
256+
root := m.buildTree(samples, locations)
257+
258+
outBytes, err := json.MarshalIndent(root, "", " ")
259+
if err != nil {
260+
return "", errors.Wrap(err, "failed to marshal JSON")
261+
}
262+
out := string(outBytes)
263+
return out, nil
264+
}
265+
266+
func (m *goPprofManager) buildTree(samples []Sample, locations map[int][]string) *Node {
267+
reverseStrings := func(s []string) []string {
268+
for i := 0; i < len(s)/2; i++ {
269+
s[i], s[len(s)-1-i] = s[len(s)-1-i], s[i]
270+
}
271+
return s
272+
}
273+
getSourceFile := func(fn string, locID int) string {
274+
for _, line := range locations[locID] {
275+
match := regexp.MustCompile(`(.+\.go):(\d+):`).FindStringSubmatch(line)
276+
if match != nil {
277+
return match[1]
278+
}
279+
}
280+
return ""
281+
}
282+
283+
addStack := func(node *Node, stack []string, locIDs []int, value int) {
284+
for i, fn := range stack {
285+
var locID int
286+
if i < len(locIDs) {
287+
locID = locIDs[i]
288+
}
289+
src := getSourceFile(fn, locID)
290+
key := fn
291+
if src != "" {
292+
key += "@" + src
293+
}
294+
295+
child, exists := node.childMap[key]
296+
if !exists {
297+
child = &Node{
298+
Name: fn,
299+
File: src,
300+
Value: 0,
301+
Children: []*Node{},
302+
childMap: make(map[string]*Node),
303+
}
304+
node.Children = append(node.Children, child)
305+
node.childMap[key] = child
306+
}
307+
child.Value += value
308+
node = child
309+
}
310+
}
311+
312+
root := &Node{
313+
Name: "root",
314+
Children: []*Node{},
315+
childMap: make(map[string]*Node),
316+
}
317+
318+
for _, s := range samples {
319+
stack := reverseStrings(s.StackFuncs)
320+
ids := m.reverseInts(s.StackIDs)
321+
addStack(root, stack, ids, s.CPUCost)
322+
root.Value += s.CPUCost
323+
}
324+
325+
m.cleanTree(root)
326+
return root
327+
}
328+
329+
func (m *goPprofManager) cleanTree(n *Node) {
330+
n.childMap = nil
331+
for _, child := range n.Children {
332+
m.cleanTree(child)
333+
}
334+
}
335+
336+
func (m *goPprofManager) reverseStrings(s []string) []string {
337+
for i := 0; i < len(s)/2; i++ {
338+
s[i], s[len(s)-1-i] = s[len(s)-1-i], s[i]
339+
}
340+
return s
341+
}
342+
343+
func (m *goPprofManager) reverseInts(s []int) []int {
344+
for i := 0; i < len(s)/2; i++ {
345+
s[i], s[len(s)-1-i] = s[len(s)-1-i], s[i]
346+
}
347+
return s
348+
}
349+
144350
func (m *goPprofManager) fetchProfileFromPID(job *job.ProfilingJob) error {
145351
port, err := findListeningPortForPID(job.PID)
146352
if err != nil {
147353
log.ErrorLogLn(fmt.Sprintf("failed to find listening port for PID %s: %s", job.PID, err))
148354
port = "8080"
149355
log.DebugLogLn(fmt.Sprintf("using default port %s", port))
150356
}
151-
rawFilePath := common.GetResultFile(common.TmpDir(), job.Tool, job.OutputType, job.PID, job.Iteration)
357+
resultFilePath := common.GetResultFile(common.TmpDir(), job.Tool, job.OutputType, job.PID, job.Iteration)
152358
if job.OutputType == api.HeapDump {
153-
err = m.heapProfile(job, port, rawFilePath)
359+
err = m.heapProfile(job, port, resultFilePath)
154360
if err != nil {
155361
return errors.Wrapf(err, "failed to fetch heap profile for PID %s", job.PID)
156362
}
157363
} else if job.OutputType == api.Pprof {
158-
err = m.cpuProfile(job, port, rawFilePath)
364+
err = m.cpuProfile(job, port, resultFilePath)
159365
if err != nil {
160366
return errors.Wrapf(err, "failed to fetch CPU profile for PID %s", job.PID)
161367
}
368+
} else if job.OutputType == api.FlameJson {
369+
err = m.cpuProfile(job, port, resultFilePath)
370+
profileFilePath := common.GetResultFile(common.TmpDir(), job.Tool, "cpu", job.PID, job.Iteration)
371+
if err != nil {
372+
return errors.Wrapf(err, "failed to fetch CPU profile for PID %s", job.PID)
373+
}
374+
profileRaw, err := m.convertPprofToRaw(profileFilePath)
375+
if err != nil {
376+
return errors.Wrapf(err, "failed to convert CPU profile for PID %s", job.PID)
377+
}
378+
flameJson, err := m.convertRawToJson(profileRaw)
379+
if err != nil {
380+
return errors.Wrapf(err, "failed to convert raw profile to JSON for PID %s", job.PID)
381+
}
382+
file.Write(resultFilePath, flameJson)
383+
162384
} else if job.OutputType == api.Raw {
163385
profileFilePath := common.GetResultFile(common.TmpDir(), job.Tool, "cpu", job.PID, job.Iteration)
164386
err = m.cpuProfile(job, port, profileFilePath)
@@ -178,12 +400,12 @@ func (m *goPprofManager) fetchProfileFromPID(job *job.ProfilingJob) error {
178400
if err != nil {
179401
return errors.Wrapf(err, "failed to convert heap profile for PID %s", job.PID)
180402
}
181-
file.Write(rawFilePath, fmt.Sprintf("heap dump\n %s \n cpu dump\n %s", heapRaw, profileRaw))
403+
file.Write(resultFilePath, fmt.Sprintf("heap dump\n %s \n cpu dump\n %s", heapRaw, profileRaw))
182404
} else {
183405
return errors.New("unsupported output type for Go pprof profiler")
184406
}
185407
// Finally, publish the file as before
186-
return m.publisher.Do(job.Compressor, rawFilePath, job.OutputType)
408+
return m.publisher.Do(job.Compressor, resultFilePath, job.OutputType)
187409
}
188410

189411
func findListeningPortForPID(pid string) (string, error) {

0 commit comments

Comments
 (0)