|
1 | 1 | // Package analysis — JFR support. |
2 | 2 | // |
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. |
16 | 8 | package analysis |
17 | 9 |
|
18 | 10 | import ( |
19 | | - "bytes" |
20 | 11 | "fmt" |
21 | | - "os" |
22 | | - "path/filepath" |
| 12 | + "io" |
23 | 13 | "strings" |
24 | | - "time" |
25 | 14 |
|
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" |
28 | 18 | ) |
29 | 19 |
|
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 | + } |
47 | 38 | 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 |
49 | 43 | } |
50 | | - prof, err := profile.ParseData(data) |
| 44 | + |
| 45 | + fields, err := decodeJFREventFields(event.Type, event.Data, &p.TypeMap) |
51 | 46 | 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) |
53 | 48 | } |
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) |
56 | 53 | } |
57 | | - result[jfrProfileName(parsed.Metric, prof)] = prof |
58 | 54 | } |
59 | | - return result, nil |
| 55 | + |
| 56 | + return ps.finalize(), nil |
60 | 57 | } |
61 | 58 |
|
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 == "" { |
70 | 73 | return |
71 | 74 | } |
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) |
75 | 109 | } |
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 | +} |
78 | 127 |
|
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) |
80 | 133 | 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 |
83 | 152 | } |
| 153 | + count = int(n) |
| 154 | + } |
84 | 155 |
|
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) |
86 | 160 | if err != nil { |
87 | | - r.Logf("convertJFRFiles: parsing %s: %v", jfrPath, err) |
88 | | - continue |
| 161 | + return nil, pos, err |
89 | 162 | } |
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 |
93 | 205 | } |
| 206 | + pos = next |
| 207 | + } |
| 208 | + return jfrValue{set: true}, pos, nil |
| 209 | +} |
94 | 210 |
|
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 |
101 | 262 | } |
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 |
105 | 277 | } |
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) |
108 | 280 | } |
| 281 | + return string(runes), pos, nil |
| 282 | + default: |
| 283 | + return "", pos, fmt.Errorf("unknown string type %d", kind) |
109 | 284 | } |
110 | 285 | } |
0 commit comments