profile.mx raw
1 // Copyright 2014 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 // Package profile provides a representation of
6 // github.com/google/pprof/proto/profile.proto and
7 // methods to encode/decode/merge profiles in this format.
8 package profile
9
10 import (
11 "bytes"
12 "compress/gzip"
13 "fmt"
14 "io"
15 "time"
16 )
17
18 // Profile is an in-memory representation of profile.proto.
19 type Profile struct {
20 SampleType []*ValueType
21 DefaultSampleType []byte
22 Sample []*Sample
23 Mapping []*Mapping
24 Location []*Location
25 Function []*Function
26 Comments [][]byte
27
28 DropFrames []byte
29 KeepFrames []byte
30
31 TimeNanos int64
32 DurationNanos int64
33 PeriodType *ValueType
34 Period int64
35
36 commentX []int64
37 dropFramesX int64
38 keepFramesX int64
39 stringTable [][]byte
40 defaultSampleTypeX int64
41 }
42
43 // ValueType corresponds to Profile.ValueType
44 type ValueType struct {
45 Type []byte // cpu, wall, inuse_space, etc
46 Unit []byte // seconds, nanoseconds, bytes, etc
47
48 typeX int64
49 unitX int64
50 }
51
52 // Sample corresponds to Profile.Sample
53 type Sample struct {
54 Location []*Location
55 Value []int64
56 Label map[string][][]byte
57 NumLabel map[string][]int64
58 NumUnit map[string][][]byte
59
60 locationIDX []uint64
61 labelX []Label
62 }
63
64 // Label corresponds to Profile.Label
65 type Label struct {
66 keyX int64
67 // Exactly one of the two following values must be set
68 strX int64
69 numX int64 // Integer value for this label
70 }
71
72 // Mapping corresponds to Profile.Mapping
73 type Mapping struct {
74 ID uint64
75 Start uint64
76 Limit uint64
77 Offset uint64
78 File []byte
79 BuildID []byte
80 HasFunctions bool
81 HasFilenames bool
82 HasLineNumbers bool
83 HasInlineFrames bool
84
85 fileX int64
86 buildIDX int64
87 }
88
89 // Location corresponds to Profile.Location
90 type Location struct {
91 ID uint64
92 Mapping *Mapping
93 Address uint64
94 Line []Line
95 IsFolded bool
96
97 mappingIDX uint64
98 }
99
100 // Line corresponds to Profile.Line
101 type Line struct {
102 Function *Function
103 Line int64
104
105 functionIDX uint64
106 }
107
108 // Function corresponds to Profile.Function
109 type Function struct {
110 ID uint64
111 Name []byte
112 SystemName []byte
113 Filename []byte
114 StartLine int64
115
116 nameX int64
117 systemNameX int64
118 filenameX int64
119 }
120
121 // Parse parses a profile and checks for its validity. The input must be an
122 // encoded pprof protobuf, which may optionally be gzip-compressed.
123 func Parse(r io.Reader) (*Profile, error) {
124 orig, err := io.ReadAll(r)
125 if err != nil {
126 return nil, err
127 }
128
129 if len(orig) >= 2 && orig[0] == 0x1f && orig[1] == 0x8b {
130 gz, err := gzip.NewReader(bytes.NewBuffer(orig))
131 if err != nil {
132 return nil, fmt.Errorf("decompressing profile: %v", err)
133 }
134 data, err := io.ReadAll(gz)
135 if err != nil {
136 return nil, fmt.Errorf("decompressing profile: %v", err)
137 }
138 orig = data
139 }
140
141 p, err := parseUncompressed(orig)
142 if err != nil {
143 return nil, fmt.Errorf("parsing profile: %w", err)
144 }
145
146 if err := p.CheckValid(); err != nil {
147 return nil, fmt.Errorf("malformed profile: %v", err)
148 }
149 return p, nil
150 }
151
152 var errMalformed = fmt.Errorf("malformed profile format")
153 var ErrNoData = fmt.Errorf("empty input file")
154
155 func parseUncompressed(data []byte) (*Profile, error) {
156 if len(data) == 0 {
157 return nil, ErrNoData
158 }
159
160 p := &Profile{}
161 if err := unmarshal(data, p); err != nil {
162 return nil, err
163 }
164
165 if err := p.postDecode(); err != nil {
166 return nil, err
167 }
168
169 return p, nil
170 }
171
172 // Write writes the profile as a gzip-compressed marshaled protobuf.
173 func (p *Profile) Write(w io.Writer) error {
174 p.preEncode()
175 b := marshal(p)
176 zw := gzip.NewWriter(w)
177 defer zw.Close()
178 _, err := zw.Write(b)
179 return err
180 }
181
182 // CheckValid tests whether the profile is valid. Checks include, but are
183 // not limited to:
184 // - len(Profile.Sample[n].value) == len(Profile.value_unit)
185 // - Sample.id has a corresponding Profile.Location
186 func (p *Profile) CheckValid() error {
187 // Check that sample values are consistent
188 sampleLen := len(p.SampleType)
189 if sampleLen == 0 && len(p.Sample) != 0 {
190 return fmt.Errorf("missing sample type information")
191 }
192 for _, s := range p.Sample {
193 if len(s.Value) != sampleLen {
194 return fmt.Errorf("mismatch: sample has: %d values vs. %d types", len(s.Value), len(p.SampleType))
195 }
196 }
197
198 // Check that all mappings/locations/functions are in the tables
199 // Check that there are no duplicate ids
200 mappings := map[uint64]*Mapping{}
201 for _, m := range p.Mapping {
202 if m.ID == 0 {
203 return fmt.Errorf("found mapping with reserved ID=0")
204 }
205 if mappings[m.ID] != nil {
206 return fmt.Errorf("multiple mappings with same id: %d", m.ID)
207 }
208 mappings[m.ID] = m
209 }
210 functions := map[uint64]*Function{}
211 for _, f := range p.Function {
212 if f.ID == 0 {
213 return fmt.Errorf("found function with reserved ID=0")
214 }
215 if functions[f.ID] != nil {
216 return fmt.Errorf("multiple functions with same id: %d", f.ID)
217 }
218 functions[f.ID] = f
219 }
220 locations := map[uint64]*Location{}
221 for _, l := range p.Location {
222 if l.ID == 0 {
223 return fmt.Errorf("found location with reserved id=0")
224 }
225 if locations[l.ID] != nil {
226 return fmt.Errorf("multiple locations with same id: %d", l.ID)
227 }
228 locations[l.ID] = l
229 if m := l.Mapping; m != nil {
230 if m.ID == 0 || mappings[m.ID] != m {
231 return fmt.Errorf("inconsistent mapping %p: %d", m, m.ID)
232 }
233 }
234 for _, ln := range l.Line {
235 if f := ln.Function; f != nil {
236 if f.ID == 0 || functions[f.ID] != f {
237 return fmt.Errorf("inconsistent function %p: %d", f, f.ID)
238 }
239 }
240 }
241 }
242 return nil
243 }
244
245 // Aggregate merges the locations in the profile into equivalence
246 // classes preserving the request attributes. It also updates the
247 // samples to point to the merged locations.
248 func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, address bool) error {
249 for _, m := range p.Mapping {
250 m.HasInlineFrames = m.HasInlineFrames && inlineFrame
251 m.HasFunctions = m.HasFunctions && function
252 m.HasFilenames = m.HasFilenames && filename
253 m.HasLineNumbers = m.HasLineNumbers && linenumber
254 }
255
256 // Aggregate functions
257 if !function || !filename {
258 for _, f := range p.Function {
259 if !function {
260 f.Name = ""
261 f.SystemName = ""
262 }
263 if !filename {
264 f.Filename = ""
265 }
266 }
267 }
268
269 // Aggregate locations
270 if !inlineFrame || !address || !linenumber {
271 for _, l := range p.Location {
272 if !inlineFrame && len(l.Line) > 1 {
273 l.Line = l.Line[len(l.Line)-1:]
274 }
275 if !linenumber {
276 for i := range l.Line {
277 l.Line[i].Line = 0
278 }
279 }
280 if !address {
281 l.Address = 0
282 }
283 }
284 }
285
286 return p.CheckValid()
287 }
288
289 // Print dumps a text representation of a profile. Intended mainly
290 // for debugging purposes.
291 func (p *Profile) String() string {
292
293 ss := [][]byte{:0:len(p.Sample)+len(p.Mapping)+len(p.Location)}
294 if pt := p.PeriodType; pt != nil {
295 ss = append(ss, fmt.Sprintf("PeriodType: %s %s", pt.Type, pt.Unit))
296 }
297 ss = append(ss, fmt.Sprintf("Period: %d", p.Period))
298 if p.TimeNanos != 0 {
299 ss = append(ss, fmt.Sprintf("Time: %v", time.Unix(0, p.TimeNanos)))
300 }
301 if p.DurationNanos != 0 {
302 ss = append(ss, fmt.Sprintf("Duration: %v", time.Duration(p.DurationNanos)))
303 }
304
305 ss = append(ss, "Samples:")
306 var sh1 string
307 for _, s := range p.SampleType {
308 sh1 = sh1 + fmt.Sprintf("%s/%s ", s.Type, s.Unit)
309 }
310 ss = append(ss, bytes.TrimSpace(sh1))
311 for _, s := range p.Sample {
312 var sv string
313 for _, v := range s.Value {
314 sv = fmt.Sprintf("%s %10d", sv, v)
315 }
316 sv = sv + ": "
317 for _, l := range s.Location {
318 sv = sv + fmt.Sprintf("%d ", l.ID)
319 }
320 ss = append(ss, sv)
321 const labelHeader = " "
322 if len(s.Label) > 0 {
323 ls := labelHeader
324 for k, v := range s.Label {
325 ls = ls + fmt.Sprintf("%s:%v ", k, v)
326 }
327 ss = append(ss, ls)
328 }
329 if len(s.NumLabel) > 0 {
330 ls := labelHeader
331 for k, v := range s.NumLabel {
332 ls = ls + fmt.Sprintf("%s:%v ", k, v)
333 }
334 ss = append(ss, ls)
335 }
336 }
337
338 ss = append(ss, "Locations")
339 for _, l := range p.Location {
340 locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address)
341 if m := l.Mapping; m != nil {
342 locStr = locStr + fmt.Sprintf("M=%d ", m.ID)
343 }
344 if len(l.Line) == 0 {
345 ss = append(ss, locStr)
346 }
347 for li := range l.Line {
348 lnStr := "??"
349 if fn := l.Line[li].Function; fn != nil {
350 lnStr = fmt.Sprintf("%s %s:%d s=%d",
351 fn.Name,
352 fn.Filename,
353 l.Line[li].Line,
354 fn.StartLine)
355 if fn.Name != fn.SystemName {
356 lnStr = lnStr + "(" + fn.SystemName + ")"
357 }
358 }
359 ss = append(ss, locStr+lnStr)
360 // Do not print location details past the first line
361 locStr = " "
362 }
363 }
364
365 ss = append(ss, "Mappings")
366 for _, m := range p.Mapping {
367 bits := ""
368 if m.HasFunctions {
369 bits += "[FN]"
370 }
371 if m.HasFilenames {
372 bits += "[FL]"
373 }
374 if m.HasLineNumbers {
375 bits += "[LN]"
376 }
377 if m.HasInlineFrames {
378 bits += "[IN]"
379 }
380 ss = append(ss, fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s",
381 m.ID,
382 m.Start, m.Limit, m.Offset,
383 m.File,
384 m.BuildID,
385 bits))
386 }
387
388 return bytes.Join(ss, "\n") + "\n"
389 }
390
391 // Merge adds profile p adjusted by ratio r into profile p. Profiles
392 // must be compatible (same Type and SampleType).
393 // TODO(rsilvera): consider normalizing the profiles based on the
394 // total samples collected.
395 func (p *Profile) Merge(pb *Profile, r float64) error {
396 if err := p.Compatible(pb); err != nil {
397 return err
398 }
399
400 pb = pb.Copy()
401
402 // Keep the largest of the two periods.
403 if pb.Period > p.Period {
404 p.Period = pb.Period
405 }
406
407 p.DurationNanos += pb.DurationNanos
408
409 p.Mapping = append(p.Mapping, pb.Mapping...)
410 for i, m := range p.Mapping {
411 m.ID = uint64(i + 1)
412 }
413 p.Location = append(p.Location, pb.Location...)
414 for i, l := range p.Location {
415 l.ID = uint64(i + 1)
416 }
417 p.Function = append(p.Function, pb.Function...)
418 for i, f := range p.Function {
419 f.ID = uint64(i + 1)
420 }
421
422 if r != 1.0 {
423 for _, s := range pb.Sample {
424 for i, v := range s.Value {
425 s.Value[i] = int64((float64(v) * r))
426 }
427 }
428 }
429 p.Sample = append(p.Sample, pb.Sample...)
430 return p.CheckValid()
431 }
432
433 // Compatible determines if two profiles can be compared/merged.
434 // returns nil if the profiles are compatible; otherwise an error with
435 // details on the incompatibility.
436 func (p *Profile) Compatible(pb *Profile) error {
437 if !compatibleValueTypes(p.PeriodType, pb.PeriodType) {
438 return fmt.Errorf("incompatible period types %v and %v", p.PeriodType, pb.PeriodType)
439 }
440
441 if len(p.SampleType) != len(pb.SampleType) {
442 return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType)
443 }
444
445 for i := range p.SampleType {
446 if !compatibleValueTypes(p.SampleType[i], pb.SampleType[i]) {
447 return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType)
448 }
449 }
450
451 return nil
452 }
453
454 // HasFunctions determines if all locations in this profile have
455 // symbolized function information.
456 func (p *Profile) HasFunctions() bool {
457 for _, l := range p.Location {
458 if l.Mapping == nil || !l.Mapping.HasFunctions {
459 return false
460 }
461 }
462 return true
463 }
464
465 // HasFileLines determines if all locations in this profile have
466 // symbolized file and line number information.
467 func (p *Profile) HasFileLines() bool {
468 for _, l := range p.Location {
469 if l.Mapping == nil || (!l.Mapping.HasFilenames || !l.Mapping.HasLineNumbers) {
470 return false
471 }
472 }
473 return true
474 }
475
476 func compatibleValueTypes(v1, v2 *ValueType) bool {
477 if v1 == nil || v2 == nil {
478 return true // No grounds to disqualify.
479 }
480 return v1.Type == v2.Type && v1.Unit == v2.Unit
481 }
482
483 // Copy makes a fully independent copy of a profile.
484 func (p *Profile) Copy() *Profile {
485 p.preEncode()
486 b := marshal(p)
487
488 pp := &Profile{}
489 if err := unmarshal(b, pp); err != nil {
490 panic(err)
491 }
492 if err := pp.postDecode(); err != nil {
493 panic(err)
494 }
495
496 return pp
497 }
498
499 // Demangler maps symbol names to a human-readable form. This may
500 // include C++ demangling and additional simplification. Names that
501 // are not demangled may be missing from the resulting map.
502 type Demangler func(name [][]byte) (map[string][]byte, error)
503
504 // Demangle attempts to demangle and optionally simplify any function
505 // names referenced in the profile. It works on a best-effort basis:
506 // it will silently preserve the original names in case of any errors.
507 func (p *Profile) Demangle(d Demangler) error {
508 // Collect names to demangle.
509 var names [][]byte
510 for _, fn := range p.Function {
511 names = append(names, fn.SystemName)
512 }
513
514 // Update profile with demangled names.
515 demangled, err := d(names)
516 if err != nil {
517 return err
518 }
519 for _, fn := range p.Function {
520 if dd, ok := demangled[fn.SystemName]; ok {
521 fn.Name = dd
522 }
523 }
524 return nil
525 }
526
527 // Empty reports whether the profile contains no samples.
528 func (p *Profile) Empty() bool {
529 return len(p.Sample) == 0
530 }
531
532 // Scale multiplies all sample values in a profile by a constant.
533 func (p *Profile) Scale(ratio float64) {
534 if ratio == 1 {
535 return
536 }
537 ratios := []float64{:len(p.SampleType)}
538 for i := range p.SampleType {
539 ratios[i] = ratio
540 }
541 p.ScaleN(ratios)
542 }
543
544 // ScaleN multiplies each sample values in a sample by a different amount.
545 func (p *Profile) ScaleN(ratios []float64) error {
546 if len(p.SampleType) != len(ratios) {
547 return fmt.Errorf("mismatched scale ratios, got %d, want %d", len(ratios), len(p.SampleType))
548 }
549 allOnes := true
550 for _, r := range ratios {
551 if r != 1 {
552 allOnes = false
553 break
554 }
555 }
556 if allOnes {
557 return nil
558 }
559 for _, s := range p.Sample {
560 for i, v := range s.Value {
561 if ratios[i] != 1 {
562 s.Value[i] = int64(float64(v) * ratios[i])
563 }
564 }
565 }
566 return nil
567 }
568