Skip to content

Commit 94f3446

Browse files
committed
Generalize JFR pprof conversion
1 parent a5236ff commit 94f3446

4 files changed

Lines changed: 130 additions & 151 deletions

File tree

analysis/jfr.go

Lines changed: 24 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -8,174 +8,53 @@
88
//
99
// 1. At the start of AnalyzeResults, convertJFRFiles walks the output folder
1010
// for any *.jfr files.
11-
// 2. Each JFR file is parsed with github.com/grafana/jfr-parser/parser.
11+
// 2. Each JFR file is parsed with github.com/grafana/jfr-parser/pprof.
1212
// 3. Per-metric pprof profiles are written as <stem>_<metric>.pprof alongside
1313
// the source JFR file (e.g. profile.jfr → profile_cpu.pprof).
1414
// 4. The normal pprof analysis loop then picks those up via the usual
1515
// filename regex.
16-
//
17-
// Supported JFR metrics (= pprof profile-type names):
18-
// - "cpu" : jdk.ExecutionSample (non-sleeping threads)
19-
// - "wall" : jdk.ExecutionSample when event=wall, or
20-
// Datadog WallClockSample
21-
// - "alloc_in_tlab" : jdk.ObjectAllocationInNewTLAB
22-
// - "alloc_outside_tlab" : jdk.ObjectAllocationOutsideTLAB
23-
// - "lock" : jdk.JavaMonitorEnter
2416
package analysis
2517

2618
import (
2719
"bytes"
2820
"fmt"
29-
"io"
3021
"os"
3122
"path/filepath"
3223
"strings"
24+
"time"
3325

3426
"github.com/google/pprof/profile"
35-
"github.com/grafana/jfr-parser/parser"
36-
"github.com/grafana/jfr-parser/parser/types"
27+
jfrpprof "github.com/grafana/jfr-parser/pprof"
3728
)
3829

39-
// parseJFR converts raw JFR bytes into a map of metric name → pprof profile.
30+
// parseJFR converts raw JFR bytes into a map of profile-name → pprof profile.
4031
func parseJFR(data []byte) (map[string]*profile.Profile, error) {
41-
p := parser.NewParser(data, parser.Options{
42-
SymbolProcessor: parser.ProcessSymbols,
43-
})
44-
45-
type builder struct {
46-
prof *profile.Profile
47-
mapping *profile.Mapping
48-
funcByID map[types.MethodRef]*profile.Function
49-
locByID map[types.MethodRef]*profile.Location
50-
}
51-
52-
builders := make(map[string]*builder)
53-
54-
getBuilder := func(metric, sampleType, sampleUnit string) *builder {
55-
b, ok := builders[metric]
56-
if !ok {
57-
m := &profile.Mapping{ID: 1, HasFunctions: true}
58-
b = &builder{
59-
prof: &profile.Profile{
60-
SampleType: []*profile.ValueType{{Type: sampleType, Unit: sampleUnit}},
61-
PeriodType: &profile.ValueType{Type: sampleType, Unit: "nanoseconds"},
62-
Period: 10_000_000, // default 100 Hz
63-
Mapping: []*profile.Mapping{m},
64-
},
65-
mapping: m,
66-
funcByID: make(map[types.MethodRef]*profile.Function),
67-
locByID: make(map[types.MethodRef]*profile.Location),
68-
}
69-
builders[metric] = b
70-
}
71-
return b
72-
}
73-
74-
// resolveFrameName returns "ClassName.methodName" for a JFR method reference.
75-
// JVM internal '/' separators in class names are normalised to '.'.
76-
resolveFrameName := func(methodRef types.MethodRef) string {
77-
m := p.GetMethod(methodRef)
78-
if m == nil {
79-
return ""
80-
}
81-
methodName := p.GetSymbolString(m.Name)
82-
cls := p.GetClass(m.Type)
83-
if cls == nil {
84-
return methodName
85-
}
86-
clsName := strings.ReplaceAll(p.GetSymbolString(cls.Name), "/", ".")
87-
return clsName + "." + methodName
88-
}
89-
90-
// addSample appends one stack-trace observation to the named metric profile.
91-
addSample := func(metric, sampleType, sampleUnit string, stackRef types.StackTraceRef, count int64) {
92-
st := p.GetStacktrace(stackRef)
93-
if st == nil || len(st.Frames) == 0 {
94-
return
95-
}
96-
b := getBuilder(metric, sampleType, sampleUnit)
97-
98-
// JFR frames[0] = leaf (top of stack), which matches the pprof convention
99-
// that sample.Location[0] is the leaf.
100-
locs := make([]*profile.Location, 0, len(st.Frames))
101-
for _, frame := range st.Frames {
102-
loc, ok := b.locByID[frame.Method]
103-
if !ok {
104-
fnName := resolveFrameName(frame.Method)
105-
if fnName == "" {
106-
continue
107-
}
108-
fn, fnOK := b.funcByID[frame.Method]
109-
if !fnOK {
110-
fn = &profile.Function{
111-
ID: uint64(len(b.prof.Function) + 1),
112-
Name: fnName,
113-
}
114-
b.prof.Function = append(b.prof.Function, fn)
115-
b.funcByID[frame.Method] = fn
116-
}
117-
loc = &profile.Location{
118-
ID: uint64(len(b.prof.Location) + 1),
119-
Mapping: b.mapping,
120-
Line: []profile.Line{{Function: fn}},
121-
}
122-
b.prof.Location = append(b.prof.Location, loc)
123-
b.locByID[frame.Method] = loc
124-
}
125-
locs = append(locs, loc)
126-
}
127-
if len(locs) == 0 {
128-
return
129-
}
130-
b.prof.Sample = append(b.prof.Sample, &profile.Sample{
131-
Location: locs,
132-
Value: []int64{count},
133-
})
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)
13442
}
13543

136-
var event string
137-
for {
138-
typ, err := p.ParseEvent()
139-
if err == io.EOF {
140-
break
44+
result := make(map[string]*profile.Profile, len(profiles.Profiles))
45+
for _, parsed := range profiles.Profiles {
46+
data, err := parsed.Profile.MarshalVT()
47+
if err != nil {
48+
return nil, fmt.Errorf("jfr marshal profile %s: %w", parsed.Metric, err)
14149
}
50+
prof, err := profile.ParseData(data)
14251
if err != nil {
143-
// Non-fatal: a truncated JFR file (e.g. from dumponexit=true) may end
144-
// mid-chunk. Return what we have so far plus the error description.
145-
return nil, fmt.Errorf("jfr ParseEvent: %w", err)
52+
return nil, fmt.Errorf("jfr parse pprof profile %s: %w", parsed.Metric, err)
14653
}
147-
148-
switch typ {
149-
case p.TypeMap.T_EXECUTION_SAMPLE:
150-
ts := p.GetThreadState(p.ExecutionSample.State)
151-
if ts != nil && ts.Name != "STATE_SLEEPING" {
152-
addSample("cpu", "cpu", "samples", p.ExecutionSample.StackTrace, 1)
153-
}
154-
if event == "wall" {
155-
addSample("wall", "wall", "samples", p.ExecutionSample.StackTrace, 1)
156-
}
157-
case p.TypeMap.T_WALL_CLOCK_SAMPLE:
158-
addSample("wall", "wall", "samples",
159-
p.WallClockSample.StackTrace, int64(p.WallClockSample.Samples))
160-
case p.TypeMap.T_ALLOC_IN_NEW_TLAB:
161-
addSample("alloc_in_tlab", "alloc_in_new_tlab_objects", "count",
162-
p.ObjectAllocationInNewTLAB.StackTrace, 1)
163-
case p.TypeMap.T_ALLOC_OUTSIDE_TLAB:
164-
addSample("alloc_outside_tlab", "alloc_outside_tlab_objects", "count",
165-
p.ObjectAllocationOutsideTLAB.StackTrace, 1)
166-
case p.TypeMap.T_MONITOR_ENTER:
167-
addSample("lock", "contentions", "count",
168-
p.JavaMonitorEnter.StackTrace, 1)
169-
case p.TypeMap.T_ACTIVE_SETTING:
170-
if p.ActiveSetting.Name == "event" {
171-
event = p.ActiveSetting.Value
172-
}
54+
for _, fn := range prof.Function {
55+
fn.Name = strings.ReplaceAll(fn.Name, "/", ".")
17356
}
174-
}
175-
176-
result := make(map[string]*profile.Profile, len(builders))
177-
for metric, b := range builders {
178-
result[metric] = b.prof
57+
result[jfrProfileName(parsed.Metric, prof)] = prof
17958
}
18059
return result, nil
18160
}

analysis/jfr_mappings.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package analysis
2+
3+
import (
4+
"strings"
5+
"unicode"
6+
7+
"github.com/google/pprof/profile"
8+
)
9+
10+
// jfrProfileMappings keeps the JFR event/profile naming policy in one place.
11+
//
12+
// The raw JFR event IDs are metadata-defined and differ between JDK built-ins
13+
// and Datadog/java-profiler events. github.com/grafana/jfr-parser normalises
14+
// the supported event variants into pprof profiles; this table maps those
15+
// normalised pprof sample-type sets to the file suffix used by prof-correctness
16+
// expectations.
17+
type jfrProfileMapping struct {
18+
metric string
19+
sampleTypes []string
20+
name string
21+
}
22+
23+
var jfrProfileMappings = []jfrProfileMapping{
24+
{metric: "process_cpu", sampleTypes: []string{"cpu"}, name: "cpu"},
25+
{metric: "wall", sampleTypes: []string{"wall"}, name: "wall"},
26+
{metric: "memory", sampleTypes: []string{"alloc_in_new_tlab_objects", "alloc_in_new_tlab_bytes"}, name: "alloc_in_tlab"},
27+
{metric: "memory", sampleTypes: []string{"alloc_outside_tlab_objects", "alloc_outside_tlab_bytes"}, name: "alloc_outside_tlab"},
28+
{metric: "memory", sampleTypes: []string{"alloc_sample_objects", "alloc_sample_bytes"}, name: "alloc_sample"},
29+
{metric: "memory", sampleTypes: []string{"live"}, name: "live"},
30+
{metric: "memory", sampleTypes: []string{"malloc_objects", "malloc_bytes"}, name: "malloc"},
31+
{metric: "mutex", sampleTypes: []string{"contentions", "delay"}, name: "lock"},
32+
{metric: "block", sampleTypes: []string{"contentions", "delay"}, name: "park"},
33+
}
34+
35+
func jfrProfileName(metric string, prof *profile.Profile) string {
36+
sampleTypes := make([]string, 0, len(prof.SampleType))
37+
for _, sampleType := range prof.SampleType {
38+
sampleTypes = append(sampleTypes, sampleType.Type)
39+
}
40+
41+
for _, mapping := range jfrProfileMappings {
42+
if mapping.metric == metric && sameStrings(mapping.sampleTypes, sampleTypes) {
43+
return mapping.name
44+
}
45+
}
46+
47+
parts := append([]string{metric}, sampleTypes...)
48+
return sanitizeJFRProfileName(strings.Join(parts, "_"))
49+
}
50+
51+
func sameStrings(a, b []string) bool {
52+
if len(a) != len(b) {
53+
return false
54+
}
55+
for i := range a {
56+
if a[i] != b[i] {
57+
return false
58+
}
59+
}
60+
return true
61+
}
62+
63+
func sanitizeJFRProfileName(name string) string {
64+
var b strings.Builder
65+
lastUnderscore := false
66+
for _, r := range name {
67+
if unicode.IsLetter(r) || unicode.IsDigit(r) {
68+
b.WriteRune(r)
69+
lastUnderscore = false
70+
continue
71+
}
72+
if !lastUnderscore {
73+
b.WriteByte('_')
74+
lastUnderscore = true
75+
}
76+
}
77+
return strings.Trim(b.String(), "_")
78+
}

go.mod

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ go 1.25.1
44

55
require (
66
github.com/google/pprof v0.0.0-20260507013755-92041b743c96
7+
github.com/grafana/jfr-parser v0.17.1
78
github.com/klauspost/compress v1.18.4
89
github.com/pierrec/lz4/v4 v4.1.25
910
github.com/xeipuuv/gojsonschema v1.2.0
1011
)
1112

1213
require (
13-
github.com/grafana/jfr-parser v0.17.1 // indirect
14+
github.com/google/gnostic v0.7.1 // indirect
15+
github.com/google/gnostic-models v0.7.0 // indirect
16+
github.com/grafana/pyroscope/api v1.5.0 // indirect
17+
github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 // indirect
1418
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
1519
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
20+
go.yaml.in/yaml/v3 v3.0.4 // indirect
1621
golang.org/x/text v0.37.0 // indirect
22+
google.golang.org/protobuf v1.36.11 // indirect
1723
)

go.sum

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,44 @@
1-
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
21
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
32
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
4-
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g=
5-
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
3+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/google/gnostic v0.7.1 h1:t5Kc7j/8kYr8t2u11rykRrPPovlEMG4+xdc/SpekATs=
5+
github.com/google/gnostic v0.7.1/go.mod h1:KSw6sxnxEBFM8jLPfJd46xZP+yQcfE8XkiqfZx5zR28=
6+
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
7+
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
8+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
9+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
610
github.com/google/pprof v0.0.0-20260507013755-92041b743c96 h1:YDDnaZ9afWajDboPMt9Vikqca/yWAX7KAxVzb4lJU1M=
711
github.com/google/pprof v0.0.0-20260507013755-92041b743c96/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
812
github.com/grafana/jfr-parser v0.17.1 h1:dTECCXL+B9V6qu5x4msSKE9/FJPKeK0jrwLa5fkpkCA=
913
github.com/grafana/jfr-parser v0.17.1/go.mod h1:+4zCC+tEWot6oQWjC72bG9TDDBiiiawMprp4EM1BioU=
14+
github.com/grafana/pyroscope/api v1.5.0 h1:5vBeCOJ6uAKPN1k/M51/zLvrXYVxP0WJJT7MGbq68wA=
15+
github.com/grafana/pyroscope/api v1.5.0/go.mod h1:JXy9oodWgLUVkUUlSk9+2Y0D4DWWwTQJo8NLXT36zg4=
1016
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
1117
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
1218
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
1319
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
14-
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
20+
github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 h1:S1hI5JiKP7883xBzZAr1ydcxrKNSVNm7+3+JwjxZEsg=
21+
github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25/go.mod h1:ZQntvDG8TkPgljxtA0R9frDoND4QORU1VXz015N5Ks4=
1522
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1623
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
24+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1725
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
18-
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
1926
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
2027
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
28+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
2129
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
2230
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
2331
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
2432
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
2533
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
2634
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
35+
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
36+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
2737
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
2838
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
39+
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
40+
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
41+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
42+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
43+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
44+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)