|
8 | 8 | // |
9 | 9 | // 1. At the start of AnalyzeResults, convertJFRFiles walks the output folder |
10 | 10 | // 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. |
12 | 12 | // 3. Per-metric pprof profiles are written as <stem>_<metric>.pprof alongside |
13 | 13 | // the source JFR file (e.g. profile.jfr → profile_cpu.pprof). |
14 | 14 | // 4. The normal pprof analysis loop then picks those up via the usual |
15 | 15 | // 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 |
24 | 16 | package analysis |
25 | 17 |
|
26 | 18 | import ( |
27 | 19 | "bytes" |
28 | 20 | "fmt" |
29 | | - "io" |
30 | 21 | "os" |
31 | 22 | "path/filepath" |
32 | 23 | "strings" |
| 24 | + "time" |
33 | 25 |
|
34 | 26 | "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" |
37 | 28 | ) |
38 | 29 |
|
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. |
40 | 31 | 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) |
134 | 42 | } |
135 | 43 |
|
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) |
141 | 49 | } |
| 50 | + prof, err := profile.ParseData(data) |
142 | 51 | 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) |
146 | 53 | } |
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, "/", ".") |
173 | 56 | } |
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 |
179 | 58 | } |
180 | 59 | return result, nil |
181 | 60 | } |
|
0 commit comments