Skip to content

Commit d1f7493

Browse files
feat: added raw output type for jfr and pprof (#26)
* feat: added raw output type for jfr and pprof * fix: testing bugs resolved * fix: changed jfr bin file --------- Co-authored-by: Mangglesh Dagar <mangglesh.dagar@nudgebee.com>
1 parent b295b0f commit d1f7493

5 files changed

Lines changed: 146 additions & 39 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: 92 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,111 @@ 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+
// for local testing
90+
// cmd := exec.Command(
91+
// "curl", targetURL, "-o", fileName,
92+
// )
93+
cmd := exec.Command(
94+
"nsenter", "-t", job.PID, "-n", "wget", "-qO", fileName, targetURL,
95+
)
96+
cmd.Stdout = &out
97+
cmd.Stderr = &stderr
98+
err := cmd.Run()
8699
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))
100+
log.ErrorLogLn(out.String())
101+
return errors.Wrapf(err, "failed to nsenter+wget %q error %s", targetURL, stderr.String())
90102
}
91-
92-
profileType := "profile"
93-
if job.OutputType == api.HeapDump {
94-
profileType = "heap"
95-
}
96-
97-
// Build the real HTTP URL
103+
return nil
104+
}
105+
func (p *goPprofManager) cpuProfile(job *job.ProfilingJob, port string, fileName string) error {
106+
var out bytes.Buffer
107+
var stderr bytes.Buffer
98108
targetURL := fmt.Sprintf(
99109
"http://127.0.0.1:%s/debug/pprof/%s?seconds=%d",
100-
port, profileType, int(job.Interval.Seconds()),
110+
port, "profile", int(job.Interval.Seconds()),
101111
)
102-
103-
// Shell out to nsenter + wget
112+
// for local testing
113+
// cmd := exec.Command(
114+
// "curl", targetURL, "-o", fileName,
115+
// )
104116
cmd := exec.Command(
105-
"nsenter", "-t", job.PID, "-n",
106-
"wget", "-qO", "-", targetURL,
117+
"nsenter", "-t", job.PID, "-n", "wget", "-qO", fileName, targetURL,
107118
)
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)
119+
cmd.Stdout = &out
120+
cmd.Stderr = &stderr
121+
err := cmd.Run()
112122
if err != nil {
113-
return errors.Wrap(err, "could not create profile file")
123+
log.ErrorLogLn(out.String())
124+
return errors.Wrapf(err, "failed to nsenter+wget %q error %s", targetURL, stderr.String())
114125
}
115-
defer out.Close()
126+
return nil
127+
}
128+
func (p *goPprofManager) convertPprofToRaw(pprofFilePath string) (string, error) {
129+
// Convert the pprof output to raw format using go tool pprof
130+
var out bytes.Buffer
131+
var stderr bytes.Buffer
132+
cmd := exec.Command("go", "tool", "pprof", "--raw", pprofFilePath)
133+
cmd.Stdout = &out
134+
cmd.Stderr = &stderr
135+
err := cmd.Run()
136+
if err != nil {
137+
log.ErrorLogLn(out.String())
138+
return "", errors.Wrapf(err, "failed to convert pprof output %q to raw format error %s", pprofFilePath, stderr.String())
139+
}
140+
return out.String(), nil
116141

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

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

128189
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 = "/opt/jdk/bin/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

internal/agent/util/publish/publisher.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,17 @@ func (p publisher) Do(compressorType compressor.Type, filePath string, eventType
3939
if err != nil {
4040
return err
4141
}
42-
42+
var compressedFile *os.File
4343
resultFile := filePath + compressor.GetExtensionFileByCompressor[compressorType]
44-
compressedFile, err := os.Create(resultFile)
44+
if compressorType != compressor.None {
45+
compressedFile, err = os.Create(resultFile)
46+
} else {
47+
// if no compressor is specified, we just use the original file as the result file
48+
compressedFile, err = os.Open(resultFile)
49+
if err != nil {
50+
return errors.Wrapf(err, "could not open file %s", resultFile)
51+
}
52+
}
4553
if err != nil {
4654
return errors.Wrapf(err, "could not create result file %s", resultFile)
4755
}
@@ -51,9 +59,11 @@ func (p publisher) Do(compressorType compressor.Type, filePath string, eventType
5159
return err
5260
}
5361

54-
err = comp.Encode(compressedFile, file)
55-
if err != nil {
56-
return errors.Wrapf(err, "could not compress file %s", resultFile)
62+
if compressorType != compressor.None {
63+
err = comp.Encode(compressedFile, file)
64+
if err != nil {
65+
return errors.Wrapf(err, "could not compress file %s", resultFile)
66+
}
5767
}
5868

5969
return log.EventLn(

0 commit comments

Comments
 (0)