Skip to content

Commit 321758f

Browse files
committed
Parse JFR directly into profile model
1 parent 8a8ba18 commit 321758f

7 files changed

Lines changed: 267 additions & 193 deletions

File tree

analysis/analysis.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -661,13 +661,8 @@ func AnalyzePprofFile(r Reporter, pprofFile string, typedStacks TypedStacks, tes
661661
}
662662

663663
// AnalyzeResults loads the expected_profile.json at jsonFilePath and asserts
664-
// every pprof file under pprofFolder matches it. Failures are reported via r.
665-
// Any *.jfr files in pprofFolder are converted to per-metric pprof files
666-
// before the analysis loop runs.
664+
// every profile file under pprofFolder matches it. Failures are reported via r.
667665
func AnalyzeResults(r Reporter, jsonFilePath string, pprofFolder string) {
668-
// Convert any JFR files to pprof before the analysis loop.
669-
convertJFRFiles(r, pprofFolder)
670-
671666
stackTestData, err := ReadJSONFile(jsonFilePath)
672667
if err != nil {
673668
r.Fatalf("Error opening file %s: %v", jsonFilePath, err)

analysis/jfr.go

Lines changed: 251 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,285 @@
11
// Package analysis — JFR support.
22
//
3-
// This file adds the ability to read JFR (Java Flight Recorder) files and
4-
// convert them to pprof profiles that the rest of the analysis pipeline can
5-
// consume.
6-
//
7-
// Flow:
8-
//
9-
// 1. At the start of AnalyzeResults, convertJFRFiles walks the output folder
10-
// for any *.jfr files.
11-
// 2. Each JFR file is parsed with github.com/grafana/jfr-parser/pprof.
12-
// 3. Per-metric pprof profiles are written as <stem>_<metric>.pprof alongside
13-
// the source JFR file (e.g. profile.jfr → profile_cpu.pprof).
14-
// 4. The normal pprof analysis loop then picks those up via the usual
15-
// filename regex.
3+
// This file adds the ability to read JFR (Java Flight Recorder) files and map
4+
// JFR events directly into the analyzer's neutral ProfileSet. The mapping from
5+
// event names to profile semantics intentionally lives here: prof-correctness is
6+
// the consumer that knows which JDK / Datadog profiler events should satisfy a
7+
// given expected_profile.json assertion.
168
package analysis
179

1810
import (
19-
"bytes"
2011
"fmt"
21-
"os"
22-
"path/filepath"
12+
"io"
2313
"strings"
24-
"time"
2514

26-
"github.com/google/pprof/profile"
27-
jfrpprof "github.com/grafana/jfr-parser/pprof"
15+
"github.com/grafana/jfr-parser/parser"
16+
"github.com/grafana/jfr-parser/parser/types"
17+
"github.com/grafana/jfr-parser/parser/types/def"
2818
)
2919

30-
// parseJFR converts raw JFR bytes into a map of profile-name → pprof profile.
31-
func parseJFR(data []byte) (map[string]*profile.Profile, error) {
32-
profiles, err := jfrpprof.ParseJFR(data, &jfrpprof.ParseInput{
33-
StartTime: time.Unix(0, 0),
34-
EndTime: time.Unix(0, 0),
35-
// Keep prof-correctness values sample-like (1 per CPU/wall event) while
36-
// reusing jfr-parser's pprof conversion, which otherwise scales CPU/wall
37-
// samples by 1e9/SampleRate.
38-
SampleRate: 1_000_000_000,
39-
}, nil)
40-
if err != nil {
41-
return nil, fmt.Errorf("jfr ParseJFR: %w", err)
42-
}
43-
44-
result := make(map[string]*profile.Profile, len(profiles.Profiles))
45-
for _, parsed := range profiles.Profiles {
46-
data, err := parsed.Profile.MarshalVT()
20+
type jfrValue struct {
21+
u64 uint64
22+
str string
23+
set bool
24+
}
25+
26+
// FromJFR builds a ProfileSet directly from Java Flight Recorder events.
27+
func FromJFR(data []byte) (*ProfileSet, error) {
28+
p := parser.NewParser(data, parser.Options{
29+
SymbolProcessor: parser.ProcessSymbols,
30+
})
31+
ps := newProfileSet()
32+
33+
for {
34+
event, err := p.ParseRawEvent()
35+
if err == io.EOF {
36+
break
37+
}
4738
if err != nil {
48-
return nil, fmt.Errorf("jfr marshal profile %s: %w", parsed.Metric, err)
39+
return nil, fmt.Errorf("jfr ParseRawEvent: %w", err)
40+
}
41+
if event.Type == nil {
42+
continue
4943
}
50-
prof, err := profile.ParseData(data)
44+
45+
fields, err := decodeJFREventFields(event.Type, event.Data, &p.TypeMap)
5146
if err != nil {
52-
return nil, fmt.Errorf("jfr parse pprof profile %s: %w", parsed.Metric, err)
47+
return nil, fmt.Errorf("jfr decode %s: %w", event.Type.Name, err)
5348
}
54-
for _, fn := range prof.Function {
55-
fn.Name = strings.ReplaceAll(fn.Name, "/", ".")
49+
50+
switch event.Type.Name {
51+
case "jdk.ExecutionSample", "datadog.ExecutionSample":
52+
addJFRCPU(ps, p, fields)
5653
}
57-
result[jfrProfileName(parsed.Metric, prof)] = prof
5854
}
59-
return result, nil
55+
56+
return ps.finalize(), nil
6057
}
6158

62-
// convertJFRFiles walks dir for *.jfr files and converts each to a set of
63-
// per-metric pprof files written into the same directory.
64-
// Output files are named <stem>_<metric>.pprof (e.g. profile_cpu.pprof).
65-
// Errors are non-fatal: they are logged through r and the function continues.
66-
func convertJFRFiles(r Reporter, dir string) {
67-
entries, err := os.ReadDir(dir)
68-
if err != nil {
69-
r.Logf("convertJFRFiles: reading dir %s: %v", dir, err)
59+
func addJFRCPU(ps *ProfileSet, p *parser.Parser, fields map[string]jfrValue) {
60+
stack := types.StackTraceRef(fields["stackTrace"].u64)
61+
state := types.ThreadStateRef(fields["state"].u64)
62+
63+
// Match jfr-parser/pprof's CPU semantics: execution samples from sleeping
64+
// threads do not count as CPU samples. If the state is absent, keep the
65+
// sample rather than silently dropping producer-specific events.
66+
if fields["state"].set {
67+
if ts := p.GetThreadState(state); ts != nil && ts.Name == "STATE_SLEEPING" {
68+
return
69+
}
70+
}
71+
folded := foldJFRStack(p, stack)
72+
if folded == "" {
7073
return
7174
}
72-
for _, entry := range entries {
73-
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".jfr") {
74-
continue
75+
76+
val := int64(1)
77+
if weight := fields["weight"]; weight.set && weight.u64 > 0 {
78+
val = int64(weight.u64)
79+
}
80+
81+
ps.add("cpu", StackSample{Stack: folded, Val: val, Labels: executionSampleLabels(fields)})
82+
}
83+
84+
func executionSampleLabels(fields map[string]jfrValue) map[string][]string {
85+
labels := map[string][]string{}
86+
if spanID := fields["spanId"]; spanID.set && spanID.u64 != 0 {
87+
labels[LabelSpanID] = []string{fmt.Sprintf("%d", spanID.u64)}
88+
}
89+
traceHi := fields["traceIdHi"]
90+
traceLo := fields["traceIdLo"]
91+
if (traceHi.set || traceLo.set) && (traceHi.u64 != 0 || traceLo.u64 != 0) {
92+
labels[LabelTraceID] = []string{fmt.Sprintf("%016x%016x", traceHi.u64, traceLo.u64)}
93+
}
94+
return labels
95+
}
96+
97+
func foldJFRStack(p *parser.Parser, stackRef types.StackTraceRef) string {
98+
st := p.GetStacktrace(stackRef)
99+
if st == nil || len(st.Frames) == 0 {
100+
return ""
101+
}
102+
// JFR frames are leaf-first. The analyzer uses root-first folded stacks,
103+
// matching FromPprof and the historical expected_profile.json captures.
104+
frames := make([]string, 0, len(st.Frames))
105+
for i := len(st.Frames) - 1; i >= 0; i-- {
106+
name := jfrFrameName(p, st.Frames[i].Method)
107+
if name != "" {
108+
frames = append(frames, name)
75109
}
76-
jfrPath := filepath.Join(dir, entry.Name())
77-
stem := strings.TrimSuffix(entry.Name(), ".jfr")
110+
}
111+
return strings.Join(frames, ";")
112+
}
113+
114+
func jfrFrameName(p *parser.Parser, methodRef types.MethodRef) string {
115+
m := p.GetMethod(methodRef)
116+
if m == nil {
117+
return ""
118+
}
119+
methodName := p.GetSymbolString(m.Name)
120+
cls := p.GetClass(m.Type)
121+
if cls == nil {
122+
return methodName
123+
}
124+
clsName := strings.ReplaceAll(p.GetSymbolString(cls.Name), "/", ".")
125+
return clsName + "." + methodName
126+
}
78127

79-
data, err := os.ReadFile(jfrPath)
128+
func decodeJFREventFields(class *def.Class, data []byte, typeMap *def.TypeMap) (map[string]jfrValue, error) {
129+
pos := 0
130+
out := map[string]jfrValue{}
131+
for _, field := range class.Fields {
132+
vals, next, err := decodeJFRField(data, pos, field, typeMap)
80133
if err != nil {
81-
r.Logf("convertJFRFiles: reading %s: %v", jfrPath, err)
82-
continue
134+
return nil, fmt.Errorf("field %s: %w", field.Name, err)
135+
}
136+
pos = next
137+
if len(vals) > 0 {
138+
out[field.Name] = vals[0]
139+
}
140+
}
141+
return out, nil
142+
}
143+
144+
func decodeJFRField(data []byte, pos int, field def.Field, typeMap *def.TypeMap) ([]jfrValue, int, error) {
145+
count := 1
146+
var err error
147+
if field.Array {
148+
var n uint64
149+
n, pos, err = readJFRVar(data, pos, 32)
150+
if err != nil {
151+
return nil, pos, err
83152
}
153+
count = int(n)
154+
}
84155

85-
profiles, err := parseJFR(data)
156+
vals := make([]jfrValue, 0, count)
157+
for i := 0; i < count; i++ {
158+
var v jfrValue
159+
v, pos, err = decodeJFRScalar(data, pos, field.Type, field.ConstantPool, typeMap)
86160
if err != nil {
87-
r.Logf("convertJFRFiles: parsing %s: %v", jfrPath, err)
88-
continue
161+
return nil, pos, err
89162
}
90-
if len(profiles) == 0 {
91-
r.Logf("convertJFRFiles: no profiles found in %s", jfrPath)
92-
continue
163+
vals = append(vals, v)
164+
}
165+
return vals, pos, nil
166+
}
167+
168+
func decodeJFRScalar(data []byte, pos int, typeID def.TypeID, constantPool bool, typeMap *def.TypeMap) (jfrValue, int, error) {
169+
if constantPool {
170+
v, next, err := readJFRVar(data, pos, 64)
171+
return jfrValue{u64: v, set: true}, next, err
172+
}
173+
174+
switch typeID {
175+
case typeMap.T_STRING:
176+
s, next, err := readJFRString(data, pos, typeMap)
177+
return jfrValue{str: s, set: true}, next, err
178+
case typeMap.T_INT:
179+
v, next, err := readJFRVar(data, pos, 32)
180+
return jfrValue{u64: v, set: true}, next, err
181+
case typeMap.T_LONG:
182+
v, next, err := readJFRVar(data, pos, 64)
183+
return jfrValue{u64: v, set: true}, next, err
184+
case typeMap.T_SHORT:
185+
v, next, err := readJFRVar(data, pos, 16)
186+
return jfrValue{u64: v, set: true}, next, err
187+
case typeMap.T_BOOLEAN:
188+
if pos >= len(data) {
189+
return jfrValue{}, pos, io.ErrUnexpectedEOF
190+
}
191+
return jfrValue{u64: uint64(data[pos]), set: true}, pos + 1, nil
192+
case typeMap.T_FLOAT:
193+
v, next, err := readJFRVar(data, pos, 32)
194+
return jfrValue{u64: v, set: true}, next, err
195+
}
196+
197+
cls := typeMap.IDMap[typeID]
198+
if cls == nil || len(cls.Fields) == 0 {
199+
return jfrValue{}, pos, fmt.Errorf("unknown type %d", typeID)
200+
}
201+
for _, nested := range cls.Fields {
202+
_, next, err := decodeJFRField(data, pos, nested, typeMap)
203+
if err != nil {
204+
return jfrValue{}, pos, err
93205
}
206+
pos = next
207+
}
208+
return jfrValue{set: true}, pos, nil
209+
}
94210

95-
for metric, prof := range profiles {
96-
outPath := filepath.Join(dir, stem+"_"+metric+".pprof")
97-
var buf bytes.Buffer
98-
if err := prof.Write(&buf); err != nil {
99-
r.Logf("convertJFRFiles: serialising %s metric %s: %v", entry.Name(), metric, err)
100-
continue
211+
func readJFRVar(data []byte, pos int, bits uint) (uint64, int, error) {
212+
v := uint64(0)
213+
maxShift := bits
214+
if bits == 64 {
215+
maxShift = 56
216+
}
217+
for shift := uint(0); ; shift += 7 {
218+
if (bits < 64 && shift >= maxShift) || (bits == 64 && shift > maxShift) {
219+
return 0, pos, def.ErrIntOverflow
220+
}
221+
if pos >= len(data) {
222+
return 0, pos, io.ErrUnexpectedEOF
223+
}
224+
b := data[pos]
225+
pos++
226+
if bits == 64 && shift == 56 {
227+
v |= uint64(b&0xff) << shift
228+
break
229+
}
230+
v |= uint64(b&0x7f) << shift
231+
if b < 0x80 {
232+
break
233+
}
234+
}
235+
return v, pos, nil
236+
}
237+
238+
func readJFRString(data []byte, pos int, typeMap *def.TypeMap) (string, int, error) {
239+
if pos >= len(data) {
240+
return "", pos, io.ErrUnexpectedEOF
241+
}
242+
kind := data[pos]
243+
pos++
244+
switch kind {
245+
case 0, 1:
246+
return "", pos, nil
247+
case 3, 5:
248+
n, next, err := readJFRVar(data, pos, 32)
249+
if err != nil {
250+
return "", next, err
251+
}
252+
pos = next
253+
end := pos + int(n)
254+
if end < pos || end > len(data) {
255+
return "", pos, io.ErrUnexpectedEOF
256+
}
257+
bs := data[pos:end]
258+
if kind == 5 {
259+
decoded, err := typeMap.ISO8859_1Decoder.Bytes(bs)
260+
if err != nil {
261+
return "", pos, err
101262
}
102-
if err := os.WriteFile(outPath, buf.Bytes(), 0644); err != nil {
103-
r.Logf("convertJFRFiles: writing %s: %v", outPath, err)
104-
continue
263+
bs = decoded
264+
}
265+
return string(bs), end, nil
266+
case 4:
267+
n, next, err := readJFRVar(data, pos, 32)
268+
if err != nil {
269+
return "", next, err
270+
}
271+
pos = next
272+
runes := make([]rune, int(n))
273+
for i := range runes {
274+
c, next, err := readJFRVar(data, pos, 32)
275+
if err != nil {
276+
return "", next, err
105277
}
106-
r.Logf("Converted JFR %s → %s (%d samples)",
107-
entry.Name(), filepath.Base(outPath), len(prof.Sample))
278+
pos = next
279+
runes[i] = rune(c)
108280
}
281+
return string(runes), pos, nil
282+
default:
283+
return "", pos, fmt.Errorf("unknown string type %d", kind)
109284
}
110285
}

0 commit comments

Comments
 (0)