Skip to content

Commit eb22969

Browse files
feat: added raw output type for jfr and pprof
1 parent b295b0f commit eb22969

4 files changed

Lines changed: 123 additions & 34 deletions

File tree

api/output_types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ const (
3030
// The first one is considered the default
3131
var GetOutputTypesByProfilingTool = map[ProfilingTool][]OutputType{
3232
AsyncProfiler: {FlameGraph, Jfr, Flat, Traces, Collapsed, Tree, Raw},
33-
Jcmd: {Jfr, ThreadDump, HeapDump, HeapHistogram},
33+
Jcmd: {Jfr, ThreadDump, HeapDump, HeapHistogram, Raw},
3434
Pyspy: {FlameGraph, SpeedScope, ThreadDump, Raw},
3535
Bpf: {FlameGraph, Raw},
3636
Perf: {FlameGraph, Raw},
3737
Rbspy: {FlameGraph, SpeedScope, Callgrind, Summary, SummaryByLine},
3838
NodeDummy: {HeapSnapshot, HeapDump},
3939
FakeTool: {FlameGraph},
4040
Austin: {FlameGraph, Raw},
41-
PProf: {Pprof, HeapDump},
41+
PProf: {Pprof, HeapDump, Raw},
4242
}
4343

4444
func AvailableOutputTypesString() string {

internal/agent/profiler/common/common.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func GetResultFile(targetDir string, tool api.ProfilingTool, outputType api.Outp
2424

2525
func GetFileExtension(tool api.ProfilingTool, outputType api.OutputType) string {
2626
switch tool {
27-
case api.Jcmd, api.AsyncProfiler:
27+
case api.AsyncProfiler:
2828
switch outputType {
2929
case api.Jfr:
3030
return ".jfr"
@@ -36,6 +36,18 @@ func GetFileExtension(tool api.ProfilingTool, outputType api.OutputType) string
3636
// api.FlameGraph
3737
return ".html"
3838
}
39+
case api.Jcmd:
40+
switch outputType {
41+
case api.Jfr, api.Raw:
42+
return ".jfr"
43+
case api.ThreadDump, api.HeapHistogram, api.Flat, api.Traces, api.Collapsed, api.SpeedScope:
44+
return ".txt"
45+
case api.HeapDump:
46+
return ".hprof"
47+
default:
48+
// api.FlameGraph
49+
return ".html"
50+
}
3951
case api.Pyspy:
4052
switch outputType {
4153
case api.SpeedScope:

internal/agent/profiler/go_pprof.go

Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"bytes"
66
"context"
77
"fmt"
8-
"os"
98
"os/exec"
109
"strconv"
1110
"strings"
@@ -80,49 +79,103 @@ func (p *GoPprofProfiler) Invoke(job *job.ProfilingJob) (error, time.Duration) {
8079

8180
return err, time.Since(start)
8281
}
83-
84-
func (m *goPprofManager) fetchProfileFromPID(job *job.ProfilingJob) error {
85-
port, err := findListeningPortForPID(job.PID)
82+
func (p *goPprofManager) heapProfile(job *job.ProfilingJob, port string, fileName string) error {
83+
var out bytes.Buffer
84+
var stderr bytes.Buffer
85+
targetURL := fmt.Sprintf(
86+
"http://127.0.0.1:%s/debug/pprof/%s?seconds=%d",
87+
port, "heap", int(job.Interval.Seconds()),
88+
)
89+
cmd := exec.Command(
90+
"curl", targetURL, "-o", fileName,
91+
)
92+
cmd.Stdout = &out
93+
cmd.Stderr = &stderr
94+
err := cmd.Run()
8695
if err != nil {
87-
log.ErrorLogLn(fmt.Sprintf("failed to find listening port for PID %s: %s", job.PID, err))
88-
port = "8080"
89-
log.DebugLogLn(fmt.Sprintf("using default port %s", port))
96+
log.ErrorLogLn(out.String())
97+
return errors.Wrapf(err, "failed to nsenter+wget %q error %s", targetURL, stderr.String())
9098
}
91-
92-
profileType := "profile"
93-
if job.OutputType == api.HeapDump {
94-
profileType = "heap"
95-
}
96-
97-
// Build the real HTTP URL
99+
return nil
100+
}
101+
func (p *goPprofManager) cpuProfile(job *job.ProfilingJob, port string, fileName string) error {
102+
var out bytes.Buffer
103+
var stderr bytes.Buffer
98104
targetURL := fmt.Sprintf(
99105
"http://127.0.0.1:%s/debug/pprof/%s?seconds=%d",
100-
port, profileType, int(job.Interval.Seconds()),
106+
port, "profile", int(job.Interval.Seconds()),
101107
)
102-
103-
// Shell out to nsenter + wget
104108
cmd := exec.Command(
105-
"nsenter", "-t", job.PID, "-n",
106-
"wget", "-qO", "-", targetURL,
109+
"curl", targetURL, "-o", fileName,
107110
)
108-
109-
// Capture stdout of the command directly into a file
110-
rawFile := common.GetResultFile(common.TmpDir(), job.Tool, api.Pprof, job.PID, job.Iteration)
111-
out, err := os.Create(rawFile)
111+
cmd.Stdout = &out
112+
cmd.Stderr = &stderr
113+
err := cmd.Run()
112114
if err != nil {
113-
return errors.Wrap(err, "could not create profile file")
115+
log.ErrorLogLn(out.String())
116+
return errors.Wrapf(err, "failed to nsenter+wget %q error %s", targetURL, stderr.String())
114117
}
115-
defer out.Close()
118+
return nil
119+
}
120+
func (p *goPprofManager) convertPprofToRaw(pprofFilePath string) (string, error) {
121+
// Convert the pprof output to raw format using go tool pprof
122+
var out bytes.Buffer
123+
var stderr bytes.Buffer
124+
cmd := exec.Command("go", "tool", "pprof", "--raw", pprofFilePath)
125+
cmd.Stdout = &out
126+
cmd.Stderr = &stderr
127+
err := cmd.Run()
128+
if err != nil {
129+
log.ErrorLogLn(out.String())
130+
return nil, errors.Wrapf(err, "failed to convert pprof output %q to raw format error %s", pprofFilePath, stderr.String())
131+
}
132+
return out.String(), nil
116133

117-
cmd.Stdout = out
118-
cmd.Stderr = os.Stderr // so you’ll see any wget errors in your logs
134+
}
119135

120-
if err := cmd.Run(); err != nil {
121-
return errors.Wrapf(err, "failed to nsenter+wget %q", targetURL)
136+
func (m *goPprofManager) fetchProfileFromPID(job *job.ProfilingJob) error {
137+
port, err := findListeningPortForPID(job.PID)
138+
if err != nil {
139+
log.ErrorLogLn(fmt.Sprintf("failed to find listening port for PID %s: %s", job.PID, err))
140+
port = "6060"
141+
log.DebugLogLn(fmt.Sprintf("using default port %s", port))
142+
}
143+
rawFilePath := common.GetResultFile(common.TmpDir(), job.Tool, job.OutputType, job.PID, job.Iteration)
144+
if job.OutputType == api.HeapDump {
145+
err = m.heapProfile(job, port, rawFilePath)
146+
if err != nil {
147+
return errors.Wrapf(err, "failed to fetch heap profile for PID %s", job.PID)
148+
}
149+
} else if job.OutputType == api.Pprof {
150+
err = m.cpuProfile(job, port, rawFilePath)
151+
if err != nil {
152+
return errors.Wrapf(err, "failed to fetch CPU profile for PID %s", job.PID)
153+
}
154+
} else if job.OutputType == api.Raw {
155+
profileFilePath := common.GetResultFile(common.TmpDir(), job.Tool, "cpu", job.PID, job.Iteration)
156+
err = m.cpuProfile(job, port, profileFilePath)
157+
if err != nil {
158+
return errors.Wrapf(err, "failed to create CPU profile for PID %s", job.PID)
159+
}
160+
profileRaw, err := m.convertPprofToRaw(profileFilePath)
161+
if err != nil {
162+
return errors.Wrapf(err, "failed to convert CPU profile for PID %s", job.PID)
163+
}
164+
heapFilePath := common.GetResultFile(common.TmpDir(), job.Tool, "heap", job.PID, job.Iteration)
165+
err = m.heapProfile(job, port, heapFilePath)
166+
if err != nil {
167+
return errors.Wrapf(err, "failed to fetch heap profile for PID %s", job.PID)
168+
}
169+
heapRaw, err := m.convertPprofToRaw(heapFilePath)
170+
if err != nil {
171+
return errors.Wrapf(err, "failed to convert heap profile for PID %s", job.PID)
172+
}
173+
file.Write(rawFilePath, fmt.Sprintf("heap dump %s \n cpu dump %s", heapRaw, profileRaw))
174+
} else {
175+
return errors.New("unsupported output type for Go pprof profiler")
122176
}
123-
124177
// Finally, publish the file as before
125-
return m.publisher.Do(job.Compressor, rawFile, job.OutputType)
178+
return m.publisher.Do(job.Compressor, rawFilePath, job.OutputType)
126179
}
127180

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

internal/agent/profiler/jvm/jcmd.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
)
2828

2929
const (
30+
jfr = "jfr"
3031
jcmd = "/opt/jdk/bin/jcmd"
3132
jfrSettingsImageFilePath = "/app/jfr/settings/jfr-profile.jfc"
3233
jfrSettingsTmpFilePath = "/tmp/jfr-profile.jfc"
@@ -202,13 +203,36 @@ func (j *jcmdManager) handleProfilingResult(job *job.ProfilingJob, fileName stri
202203
if err != nil {
203204
return errors.Wrap(err, "could not save dump to file")
204205
}
206+
case api.Raw:
207+
j.handleJcmdRecording(pid, job.Iteration, string(job.OutputType))
208+
rawOutput, err := j.handleJFRFile(pid, fileName, string(job.OutputType))
209+
if err != nil {
210+
return errors.Wrap(err, "error while converting JFR file to raw output")
211+
}
212+
file.Write(fileName, rawOutput)
205213
default:
206214
log.DebugLogLn(out.String())
207215
}
208216

209217
return nil
210218
}
211219

220+
func (j *jcmdManager) handleJFRFile(pid string, fileName string, outputType string) (string, error) {
221+
log.DebugLogLn(fmt.Sprintf("Converting JFr file to json raw for %s", pid))
222+
var out bytes.Buffer
223+
var stderr bytes.Buffer
224+
cmd := silentJcmdCommander.Command(jfr, "summary", fileName)
225+
cmd.Stdout = &out
226+
cmd.Stderr = &stderr
227+
err := cmd.Run()
228+
if err != nil {
229+
log.ErrorLogLn(stderr.String())
230+
return "", err
231+
}
232+
outputTxt := out.String()
233+
return outputTxt, nil
234+
}
235+
212236
func (j *jcmdManager) handleJcmdRecording(pid string, iteration int, outputType string) {
213237
done := make(chan bool)
214238

0 commit comments

Comments
 (0)