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