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