-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmodel.go
More file actions
188 lines (171 loc) · 6.67 KB
/
Copy pathmodel.go
File metadata and controls
188 lines (171 loc) · 6.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
// Package analysis: neutral profile model and format dispatch.
//
// The analyzer asserts on a small, format-independent representation: a set of
// typed samples, each a folded stack (`a;b;c`), a value, and a canonical label
// set. This is what every assertion in analysis.go consumes.
//
// Rather than convert every wire format into google/pprof and lose whatever
// pprof cannot express, each input format has its own adapter (in its own file)
// that maps its native encoding into this neutral model:
//
// pprof.go FromPprof - google/pprof (labels come from Sample.Label / NumLabel)
// otlp.go FromOTLP - OpenTelemetry profiles (trace/span from the LinkTable,
// other attributes from the per-sample / resource
// attribute tables) - no pprof round-trip
// (future) FromJFR - Java Flight Recorder
//
// The point of the neutral model is that a single semantic expectation in
// expected_profile.json - e.g. label "span id" matches X - is verified
// identically regardless of how each format happens to encode span linkage.
// The Label* constants and canonKey() below are the seam where each format's
// key names are normalized to that shared vocabulary.
package analysis
import (
"path/filepath"
"sort"
"strings"
"github.com/google/pprof/profile"
)
// Canonical label keys. Adapters normalize each format's native key names to
// these so expected_profile.json can assert on one vocabulary across pprof,
// OTLP and (later) JFR. The values intentionally match the keys Datadog pprof
// profilers already emit, so existing scenarios keep working unchanged.
const (
LabelTraceID = "trace id"
LabelSpanID = "span id"
LabelLocalRootSID = "local root span id"
LabelThreadID = "thread id"
LabelThreadName = "thread name"
LabelProcessID = "process_id"
LabelService = "service"
)
// canonKey maps a source format's attribute key to the canonical vocabulary.
// Unknown keys pass through unchanged. This is where JFR/OTLP/pprof key naming
// differences are reconciled.
func canonKey(k string) string {
switch k {
case "thread.id":
return LabelThreadID
case "thread.name":
return LabelThreadName
case "process.pid", "process.id":
return LabelProcessID
case "service.name":
return LabelService
case "trace.id", "trace_id":
return LabelTraceID
case "span.id", "span_id":
return LabelSpanID
case "local_root_span_id", "local.root.span.id":
return LabelLocalRootSID
default:
return k
}
}
// ProfileSet is the neutral, format-independent view of one profile file. A
// single file may contain several profile types (e.g. an OTLP export carrying
// alloc_space + alloc_objects, or a pprof profile with multiple sample types),
// each with its own duration.
type ProfileSet struct {
order []string // sample-type names, in first-seen order
typed map[string][]StackSample
dur map[string]*durAgg
}
// durAgg accumulates, per profile type, the total value and total rate
// (Σ valueᵢ/durationᵢ) across every profile of that type in the file. The
// effective duration is valueSum/rateSum, which makes total/duration equal the
// true aggregate rate even when same-type profiles (e.g. concurrent per-PID
// resources) have different durations. Only profiles with a positive duration
// contribute; snapshots (duration 0) leave the type's duration at 0.
type durAgg struct {
valueSum int64
rateSum float64
}
func newProfileSet() *ProfileSet {
return &ProfileSet{typed: map[string][]StackSample{}, dur: map[string]*durAgg{}}
}
func (ps *ProfileSet) add(profileType string, s StackSample) {
if _, ok := ps.typed[profileType]; !ok {
ps.order = append(ps.order, profileType)
}
ps.typed[profileType] = append(ps.typed[profileType], s)
}
// addProfileDuration folds one profile's (total value, duration) into the
// per-type duration aggregate. See durAgg. secs<=0 (a snapshot) is ignored.
func (ps *ProfileSet) addProfileDuration(profileType string, totalValue int64, secs float64) {
if secs <= 0 {
return
}
a := ps.dur[profileType]
if a == nil {
a = &durAgg{}
ps.dur[profileType] = a
}
a.valueSum += totalValue
a.rateSum += float64(totalValue) / secs
}
// Duration returns the effective duration in seconds for a profile type (0 if
// unknown or a snapshot). Kept per-type because one file can mix, e.g., a 10s
// allocation profile and a 60s CPU profile; for multiple same-type profiles it
// is valueSum/rateSum so total/Duration is the correct aggregate rate.
func (ps *ProfileSet) Duration(profileType string) float64 {
a := ps.dur[profileType]
if a == nil || a.rateSum == 0 {
return 0
}
return float64(a.valueSum) / a.rateSum
}
// SampleTypes returns the profile-type names present, in first-seen order.
func (ps *ProfileSet) SampleTypes() []string { return ps.order }
// Samples returns the samples for a profile type, and whether that type exists.
func (ps *ProfileSet) Samples(profileType string) ([]StackSample, bool) {
s, ok := ps.typed[profileType]
return s, ok
}
// finalize sorts each type's samples by descending value for stable, readable
// capture output (assertions sum, so ordering is cosmetic there).
func (ps *ProfileSet) finalize() *ProfileSet {
for _, s := range ps.typed {
sort.SliceStable(s, func(i, j int) bool { return s[i].Val > s[j].Val })
}
return ps
}
// isJFRName reports whether name should be parsed as a Java Flight Recorder recording.
func isJFRName(name string) bool {
return strings.HasSuffix(strings.ToLower(name), ".jfr")
}
// LoadProfileSet reads a profile file (pprof, OTLP or JFR) and returns the neutral
// ProfileSet. Format is chosen by filename suffix (.jfr -> JFR, .otlp/.otlp.pb -> OTLP
// proto, .otlp.json -> OTLP JSON, else pprof) with an OTLP fallback if pprof
// parsing fails. Ambiguous suffixes such as .pb (used by both pprof and OTLP)
// go through the content-based fallback rather than being forced to a format.
// The per-format parsing lives in the respective adapter file (pprof.go /
// otlp.go / jfr.go).
func LoadProfileSet(path string) (*ProfileSet, error) {
content, err := readAndDecompress(path)
if err != nil {
return nil, err
}
name := filepath.Base(path)
switch {
case isJFRName(name):
return FromJFR(content)
case isOTLPJSONName(name):
return loadOTLP(content, true)
case isOTLPProtoName(name):
return loadOTLP(content, false)
}
// Unknown suffix: try pprof first, then OTLP (proto, then JSON).
if prof, perr := profile.ParseData(content); perr == nil {
return FromPprof(prof), nil
}
if ps, err := loadOTLP(content, false); err == nil {
return ps, nil
}
if ps, err := loadOTLP(content, true); err == nil {
return ps, nil
}
// Report the pprof error, which is the most informative for the common case.
_, perr := profile.ParseData(content)
return nil, perr
}