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