 9bdcbe0447
			
		
	
	9bdcbe0447
	
	
	
		
			
			Major integrations and fixes: - Added BACKBEAT SDK integration for P2P operation timing - Implemented beat-aware status tracking for distributed operations - Added Docker secrets support for secure license management - Resolved KACHING license validation via HTTPS/TLS - Updated docker-compose configuration for clean stack deployment - Disabled rollback policies to prevent deployment failures - Added license credential storage (CHORUS-DEV-MULTI-001) Technical improvements: - BACKBEAT P2P operation tracking with phase management - Enhanced configuration system with file-based secrets - Improved error handling for license validation - Clean separation of KACHING and CHORUS deployment stacks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			857 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			857 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2014 Google Inc. All Rights Reserved.
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| // Package profile provides a representation of profile.proto and
 | |
| // methods to encode/decode profiles in this format.
 | |
| package profile
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"compress/gzip"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"math"
 | |
| 	"path/filepath"
 | |
| 	"regexp"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| // Profile is an in-memory representation of profile.proto.
 | |
| type Profile struct {
 | |
| 	SampleType        []*ValueType
 | |
| 	DefaultSampleType string
 | |
| 	Sample            []*Sample
 | |
| 	Mapping           []*Mapping
 | |
| 	Location          []*Location
 | |
| 	Function          []*Function
 | |
| 	Comments          []string
 | |
| 
 | |
| 	DropFrames string
 | |
| 	KeepFrames string
 | |
| 
 | |
| 	TimeNanos     int64
 | |
| 	DurationNanos int64
 | |
| 	PeriodType    *ValueType
 | |
| 	Period        int64
 | |
| 
 | |
| 	// The following fields are modified during encoding and copying,
 | |
| 	// so are protected by a Mutex.
 | |
| 	encodeMu sync.Mutex
 | |
| 
 | |
| 	commentX           []int64
 | |
| 	dropFramesX        int64
 | |
| 	keepFramesX        int64
 | |
| 	stringTable        []string
 | |
| 	defaultSampleTypeX int64
 | |
| }
 | |
| 
 | |
| // ValueType corresponds to Profile.ValueType
 | |
| type ValueType struct {
 | |
| 	Type string // cpu, wall, inuse_space, etc
 | |
| 	Unit string // seconds, nanoseconds, bytes, etc
 | |
| 
 | |
| 	typeX int64
 | |
| 	unitX int64
 | |
| }
 | |
| 
 | |
| // Sample corresponds to Profile.Sample
 | |
| type Sample struct {
 | |
| 	Location []*Location
 | |
| 	Value    []int64
 | |
| 	// Label is a per-label-key map to values for string labels.
 | |
| 	//
 | |
| 	// In general, having multiple values for the given label key is strongly
 | |
| 	// discouraged - see docs for the sample label field in profile.proto.  The
 | |
| 	// main reason this unlikely state is tracked here is to make the
 | |
| 	// decoding->encoding roundtrip not lossy. But we expect that the value
 | |
| 	// slices present in this map are always of length 1.
 | |
| 	Label map[string][]string
 | |
| 	// NumLabel is a per-label-key map to values for numeric labels. See a note
 | |
| 	// above on handling multiple values for a label.
 | |
| 	NumLabel map[string][]int64
 | |
| 	// NumUnit is a per-label-key map to the unit names of corresponding numeric
 | |
| 	// label values. The unit info may be missing even if the label is in
 | |
| 	// NumLabel, see the docs in profile.proto for details. When the value is
 | |
| 	// slice is present and not nil, its length must be equal to the length of
 | |
| 	// the corresponding value slice in NumLabel.
 | |
| 	NumUnit map[string][]string
 | |
| 
 | |
| 	locationIDX []uint64
 | |
| 	labelX      []label
 | |
| }
 | |
| 
 | |
| // label corresponds to Profile.Label
 | |
| type label struct {
 | |
| 	keyX int64
 | |
| 	// Exactly one of the two following values must be set
 | |
| 	strX int64
 | |
| 	numX int64 // Integer value for this label
 | |
| 	// can be set if numX has value
 | |
| 	unitX int64
 | |
| }
 | |
| 
 | |
| // Mapping corresponds to Profile.Mapping
 | |
| type Mapping struct {
 | |
| 	ID              uint64
 | |
| 	Start           uint64
 | |
| 	Limit           uint64
 | |
| 	Offset          uint64
 | |
| 	File            string
 | |
| 	BuildID         string
 | |
| 	HasFunctions    bool
 | |
| 	HasFilenames    bool
 | |
| 	HasLineNumbers  bool
 | |
| 	HasInlineFrames bool
 | |
| 
 | |
| 	fileX    int64
 | |
| 	buildIDX int64
 | |
| 
 | |
| 	// Name of the kernel relocation symbol ("_text" or "_stext"), extracted from File.
 | |
| 	// For linux kernel mappings generated by some tools, correct symbolization depends
 | |
| 	// on knowing which of the two possible relocation symbols was used for `Start`.
 | |
| 	// This is given to us as a suffix in `File` (e.g. "[kernel.kallsyms]_stext").
 | |
| 	//
 | |
| 	// Note, this public field is not persisted in the proto. For the purposes of
 | |
| 	// copying / merging / hashing profiles, it is considered subsumed by `File`.
 | |
| 	KernelRelocationSymbol string
 | |
| }
 | |
| 
 | |
| // Location corresponds to Profile.Location
 | |
| type Location struct {
 | |
| 	ID       uint64
 | |
| 	Mapping  *Mapping
 | |
| 	Address  uint64
 | |
| 	Line     []Line
 | |
| 	IsFolded bool
 | |
| 
 | |
| 	mappingIDX uint64
 | |
| }
 | |
| 
 | |
| // Line corresponds to Profile.Line
 | |
| type Line struct {
 | |
| 	Function *Function
 | |
| 	Line     int64
 | |
| 
 | |
| 	functionIDX uint64
 | |
| }
 | |
| 
 | |
| // Function corresponds to Profile.Function
 | |
| type Function struct {
 | |
| 	ID         uint64
 | |
| 	Name       string
 | |
| 	SystemName string
 | |
| 	Filename   string
 | |
| 	StartLine  int64
 | |
| 
 | |
| 	nameX       int64
 | |
| 	systemNameX int64
 | |
| 	filenameX   int64
 | |
| }
 | |
| 
 | |
| // Parse parses a profile and checks for its validity. The input
 | |
| // may be a gzip-compressed encoded protobuf or one of many legacy
 | |
| // profile formats which may be unsupported in the future.
 | |
| func Parse(r io.Reader) (*Profile, error) {
 | |
| 	data, err := io.ReadAll(r)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return ParseData(data)
 | |
| }
 | |
| 
 | |
| // ParseData parses a profile from a buffer and checks for its
 | |
| // validity.
 | |
| func ParseData(data []byte) (*Profile, error) {
 | |
| 	var p *Profile
 | |
| 	var err error
 | |
| 	if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
 | |
| 		gz, err := gzip.NewReader(bytes.NewBuffer(data))
 | |
| 		if err == nil {
 | |
| 			data, err = io.ReadAll(gz)
 | |
| 		}
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("decompressing profile: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if p, err = ParseUncompressed(data); err != nil && err != errNoData && err != errConcatProfile {
 | |
| 		p, err = parseLegacy(data)
 | |
| 	}
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("parsing profile: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	if err := p.CheckValid(); err != nil {
 | |
| 		return nil, fmt.Errorf("malformed profile: %v", err)
 | |
| 	}
 | |
| 	return p, nil
 | |
| }
 | |
| 
 | |
| var errUnrecognized = fmt.Errorf("unrecognized profile format")
 | |
| var errMalformed = fmt.Errorf("malformed profile format")
 | |
| var errNoData = fmt.Errorf("empty input file")
 | |
| var errConcatProfile = fmt.Errorf("concatenated profiles detected")
 | |
| 
 | |
| func parseLegacy(data []byte) (*Profile, error) {
 | |
| 	parsers := []func([]byte) (*Profile, error){
 | |
| 		parseCPU,
 | |
| 		parseHeap,
 | |
| 		parseGoCount, // goroutine, threadcreate
 | |
| 		parseThread,
 | |
| 		parseContention,
 | |
| 		parseJavaProfile,
 | |
| 	}
 | |
| 
 | |
| 	for _, parser := range parsers {
 | |
| 		p, err := parser(data)
 | |
| 		if err == nil {
 | |
| 			p.addLegacyFrameInfo()
 | |
| 			return p, nil
 | |
| 		}
 | |
| 		if err != errUnrecognized {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 	return nil, errUnrecognized
 | |
| }
 | |
| 
 | |
| // ParseUncompressed parses an uncompressed protobuf into a profile.
 | |
| func ParseUncompressed(data []byte) (*Profile, error) {
 | |
| 	if len(data) == 0 {
 | |
| 		return nil, errNoData
 | |
| 	}
 | |
| 	p := &Profile{}
 | |
| 	if err := unmarshal(data, p); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if err := p.postDecode(); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return p, nil
 | |
| }
 | |
| 
 | |
| var libRx = regexp.MustCompile(`([.]so$|[.]so[._][0-9]+)`)
 | |
| 
 | |
| // massageMappings applies heuristic-based changes to the profile
 | |
| // mappings to account for quirks of some environments.
 | |
| func (p *Profile) massageMappings() {
 | |
| 	// Merge adjacent regions with matching names, checking that the offsets match
 | |
| 	if len(p.Mapping) > 1 {
 | |
| 		mappings := []*Mapping{p.Mapping[0]}
 | |
| 		for _, m := range p.Mapping[1:] {
 | |
| 			lm := mappings[len(mappings)-1]
 | |
| 			if adjacent(lm, m) {
 | |
| 				lm.Limit = m.Limit
 | |
| 				if m.File != "" {
 | |
| 					lm.File = m.File
 | |
| 				}
 | |
| 				if m.BuildID != "" {
 | |
| 					lm.BuildID = m.BuildID
 | |
| 				}
 | |
| 				p.updateLocationMapping(m, lm)
 | |
| 				continue
 | |
| 			}
 | |
| 			mappings = append(mappings, m)
 | |
| 		}
 | |
| 		p.Mapping = mappings
 | |
| 	}
 | |
| 
 | |
| 	// Use heuristics to identify main binary and move it to the top of the list of mappings
 | |
| 	for i, m := range p.Mapping {
 | |
| 		file := strings.TrimSpace(strings.Replace(m.File, "(deleted)", "", -1))
 | |
| 		if len(file) == 0 {
 | |
| 			continue
 | |
| 		}
 | |
| 		if len(libRx.FindStringSubmatch(file)) > 0 {
 | |
| 			continue
 | |
| 		}
 | |
| 		if file[0] == '[' {
 | |
| 			continue
 | |
| 		}
 | |
| 		// Swap what we guess is main to position 0.
 | |
| 		p.Mapping[0], p.Mapping[i] = p.Mapping[i], p.Mapping[0]
 | |
| 		break
 | |
| 	}
 | |
| 
 | |
| 	// Keep the mapping IDs neatly sorted
 | |
| 	for i, m := range p.Mapping {
 | |
| 		m.ID = uint64(i + 1)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // adjacent returns whether two mapping entries represent the same
 | |
| // mapping that has been split into two. Check that their addresses are adjacent,
 | |
| // and if the offsets match, if they are available.
 | |
| func adjacent(m1, m2 *Mapping) bool {
 | |
| 	if m1.File != "" && m2.File != "" {
 | |
| 		if m1.File != m2.File {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	if m1.BuildID != "" && m2.BuildID != "" {
 | |
| 		if m1.BuildID != m2.BuildID {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	if m1.Limit != m2.Start {
 | |
| 		return false
 | |
| 	}
 | |
| 	if m1.Offset != 0 && m2.Offset != 0 {
 | |
| 		offset := m1.Offset + (m1.Limit - m1.Start)
 | |
| 		if offset != m2.Offset {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| func (p *Profile) updateLocationMapping(from, to *Mapping) {
 | |
| 	for _, l := range p.Location {
 | |
| 		if l.Mapping == from {
 | |
| 			l.Mapping = to
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func serialize(p *Profile) []byte {
 | |
| 	p.encodeMu.Lock()
 | |
| 	p.preEncode()
 | |
| 	b := marshal(p)
 | |
| 	p.encodeMu.Unlock()
 | |
| 	return b
 | |
| }
 | |
| 
 | |
| // Write writes the profile as a gzip-compressed marshaled protobuf.
 | |
| func (p *Profile) Write(w io.Writer) error {
 | |
| 	zw := gzip.NewWriter(w)
 | |
| 	defer zw.Close()
 | |
| 	_, err := zw.Write(serialize(p))
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // WriteUncompressed writes the profile as a marshaled protobuf.
 | |
| func (p *Profile) WriteUncompressed(w io.Writer) error {
 | |
| 	_, err := w.Write(serialize(p))
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // CheckValid tests whether the profile is valid. Checks include, but are
 | |
| // not limited to:
 | |
| //   - len(Profile.Sample[n].value) == len(Profile.value_unit)
 | |
| //   - Sample.id has a corresponding Profile.Location
 | |
| func (p *Profile) CheckValid() error {
 | |
| 	// Check that sample values are consistent
 | |
| 	sampleLen := len(p.SampleType)
 | |
| 	if sampleLen == 0 && len(p.Sample) != 0 {
 | |
| 		return fmt.Errorf("missing sample type information")
 | |
| 	}
 | |
| 	for _, s := range p.Sample {
 | |
| 		if s == nil {
 | |
| 			return fmt.Errorf("profile has nil sample")
 | |
| 		}
 | |
| 		if len(s.Value) != sampleLen {
 | |
| 			return fmt.Errorf("mismatch: sample has %d values vs. %d types", len(s.Value), len(p.SampleType))
 | |
| 		}
 | |
| 		for _, l := range s.Location {
 | |
| 			if l == nil {
 | |
| 				return fmt.Errorf("sample has nil location")
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Check that all mappings/locations/functions are in the tables
 | |
| 	// Check that there are no duplicate ids
 | |
| 	mappings := make(map[uint64]*Mapping, len(p.Mapping))
 | |
| 	for _, m := range p.Mapping {
 | |
| 		if m == nil {
 | |
| 			return fmt.Errorf("profile has nil mapping")
 | |
| 		}
 | |
| 		if m.ID == 0 {
 | |
| 			return fmt.Errorf("found mapping with reserved ID=0")
 | |
| 		}
 | |
| 		if mappings[m.ID] != nil {
 | |
| 			return fmt.Errorf("multiple mappings with same id: %d", m.ID)
 | |
| 		}
 | |
| 		mappings[m.ID] = m
 | |
| 	}
 | |
| 	functions := make(map[uint64]*Function, len(p.Function))
 | |
| 	for _, f := range p.Function {
 | |
| 		if f == nil {
 | |
| 			return fmt.Errorf("profile has nil function")
 | |
| 		}
 | |
| 		if f.ID == 0 {
 | |
| 			return fmt.Errorf("found function with reserved ID=0")
 | |
| 		}
 | |
| 		if functions[f.ID] != nil {
 | |
| 			return fmt.Errorf("multiple functions with same id: %d", f.ID)
 | |
| 		}
 | |
| 		functions[f.ID] = f
 | |
| 	}
 | |
| 	locations := make(map[uint64]*Location, len(p.Location))
 | |
| 	for _, l := range p.Location {
 | |
| 		if l == nil {
 | |
| 			return fmt.Errorf("profile has nil location")
 | |
| 		}
 | |
| 		if l.ID == 0 {
 | |
| 			return fmt.Errorf("found location with reserved id=0")
 | |
| 		}
 | |
| 		if locations[l.ID] != nil {
 | |
| 			return fmt.Errorf("multiple locations with same id: %d", l.ID)
 | |
| 		}
 | |
| 		locations[l.ID] = l
 | |
| 		if m := l.Mapping; m != nil {
 | |
| 			if m.ID == 0 || mappings[m.ID] != m {
 | |
| 				return fmt.Errorf("inconsistent mapping %p: %d", m, m.ID)
 | |
| 			}
 | |
| 		}
 | |
| 		for _, ln := range l.Line {
 | |
| 			f := ln.Function
 | |
| 			if f == nil {
 | |
| 				return fmt.Errorf("location id: %d has a line with nil function", l.ID)
 | |
| 			}
 | |
| 			if f.ID == 0 || functions[f.ID] != f {
 | |
| 				return fmt.Errorf("inconsistent function %p: %d", f, f.ID)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Aggregate merges the locations in the profile into equivalence
 | |
| // classes preserving the request attributes. It also updates the
 | |
| // samples to point to the merged locations.
 | |
| func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, address bool) error {
 | |
| 	for _, m := range p.Mapping {
 | |
| 		m.HasInlineFrames = m.HasInlineFrames && inlineFrame
 | |
| 		m.HasFunctions = m.HasFunctions && function
 | |
| 		m.HasFilenames = m.HasFilenames && filename
 | |
| 		m.HasLineNumbers = m.HasLineNumbers && linenumber
 | |
| 	}
 | |
| 
 | |
| 	// Aggregate functions
 | |
| 	if !function || !filename {
 | |
| 		for _, f := range p.Function {
 | |
| 			if !function {
 | |
| 				f.Name = ""
 | |
| 				f.SystemName = ""
 | |
| 			}
 | |
| 			if !filename {
 | |
| 				f.Filename = ""
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Aggregate locations
 | |
| 	if !inlineFrame || !address || !linenumber {
 | |
| 		for _, l := range p.Location {
 | |
| 			if !inlineFrame && len(l.Line) > 1 {
 | |
| 				l.Line = l.Line[len(l.Line)-1:]
 | |
| 			}
 | |
| 			if !linenumber {
 | |
| 				for i := range l.Line {
 | |
| 					l.Line[i].Line = 0
 | |
| 				}
 | |
| 			}
 | |
| 			if !address {
 | |
| 				l.Address = 0
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return p.CheckValid()
 | |
| }
 | |
| 
 | |
| // NumLabelUnits returns a map of numeric label keys to the units
 | |
| // associated with those keys and a map of those keys to any units
 | |
| // that were encountered but not used.
 | |
| // Unit for a given key is the first encountered unit for that key. If multiple
 | |
| // units are encountered for values paired with a particular key, then the first
 | |
| // unit encountered is used and all other units are returned in sorted order
 | |
| // in map of ignored units.
 | |
| // If no units are encountered for a particular key, the unit is then inferred
 | |
| // based on the key.
 | |
| func (p *Profile) NumLabelUnits() (map[string]string, map[string][]string) {
 | |
| 	numLabelUnits := map[string]string{}
 | |
| 	ignoredUnits := map[string]map[string]bool{}
 | |
| 	encounteredKeys := map[string]bool{}
 | |
| 
 | |
| 	// Determine units based on numeric tags for each sample.
 | |
| 	for _, s := range p.Sample {
 | |
| 		for k := range s.NumLabel {
 | |
| 			encounteredKeys[k] = true
 | |
| 			for _, unit := range s.NumUnit[k] {
 | |
| 				if unit == "" {
 | |
| 					continue
 | |
| 				}
 | |
| 				if wantUnit, ok := numLabelUnits[k]; !ok {
 | |
| 					numLabelUnits[k] = unit
 | |
| 				} else if wantUnit != unit {
 | |
| 					if v, ok := ignoredUnits[k]; ok {
 | |
| 						v[unit] = true
 | |
| 					} else {
 | |
| 						ignoredUnits[k] = map[string]bool{unit: true}
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	// Infer units for keys without any units associated with
 | |
| 	// numeric tag values.
 | |
| 	for key := range encounteredKeys {
 | |
| 		unit := numLabelUnits[key]
 | |
| 		if unit == "" {
 | |
| 			switch key {
 | |
| 			case "alignment", "request":
 | |
| 				numLabelUnits[key] = "bytes"
 | |
| 			default:
 | |
| 				numLabelUnits[key] = key
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Copy ignored units into more readable format
 | |
| 	unitsIgnored := make(map[string][]string, len(ignoredUnits))
 | |
| 	for key, values := range ignoredUnits {
 | |
| 		units := make([]string, len(values))
 | |
| 		i := 0
 | |
| 		for unit := range values {
 | |
| 			units[i] = unit
 | |
| 			i++
 | |
| 		}
 | |
| 		sort.Strings(units)
 | |
| 		unitsIgnored[key] = units
 | |
| 	}
 | |
| 
 | |
| 	return numLabelUnits, unitsIgnored
 | |
| }
 | |
| 
 | |
| // String dumps a text representation of a profile. Intended mainly
 | |
| // for debugging purposes.
 | |
| func (p *Profile) String() string {
 | |
| 	ss := make([]string, 0, len(p.Comments)+len(p.Sample)+len(p.Mapping)+len(p.Location))
 | |
| 	for _, c := range p.Comments {
 | |
| 		ss = append(ss, "Comment: "+c)
 | |
| 	}
 | |
| 	if pt := p.PeriodType; pt != nil {
 | |
| 		ss = append(ss, fmt.Sprintf("PeriodType: %s %s", pt.Type, pt.Unit))
 | |
| 	}
 | |
| 	ss = append(ss, fmt.Sprintf("Period: %d", p.Period))
 | |
| 	if p.TimeNanos != 0 {
 | |
| 		ss = append(ss, fmt.Sprintf("Time: %v", time.Unix(0, p.TimeNanos)))
 | |
| 	}
 | |
| 	if p.DurationNanos != 0 {
 | |
| 		ss = append(ss, fmt.Sprintf("Duration: %.4v", time.Duration(p.DurationNanos)))
 | |
| 	}
 | |
| 
 | |
| 	ss = append(ss, "Samples:")
 | |
| 	var sh1 string
 | |
| 	for _, s := range p.SampleType {
 | |
| 		dflt := ""
 | |
| 		if s.Type == p.DefaultSampleType {
 | |
| 			dflt = "[dflt]"
 | |
| 		}
 | |
| 		sh1 = sh1 + fmt.Sprintf("%s/%s%s ", s.Type, s.Unit, dflt)
 | |
| 	}
 | |
| 	ss = append(ss, strings.TrimSpace(sh1))
 | |
| 	for _, s := range p.Sample {
 | |
| 		ss = append(ss, s.string())
 | |
| 	}
 | |
| 
 | |
| 	ss = append(ss, "Locations")
 | |
| 	for _, l := range p.Location {
 | |
| 		ss = append(ss, l.string())
 | |
| 	}
 | |
| 
 | |
| 	ss = append(ss, "Mappings")
 | |
| 	for _, m := range p.Mapping {
 | |
| 		ss = append(ss, m.string())
 | |
| 	}
 | |
| 
 | |
| 	return strings.Join(ss, "\n") + "\n"
 | |
| }
 | |
| 
 | |
| // string dumps a text representation of a mapping. Intended mainly
 | |
| // for debugging purposes.
 | |
| func (m *Mapping) string() string {
 | |
| 	bits := ""
 | |
| 	if m.HasFunctions {
 | |
| 		bits = bits + "[FN]"
 | |
| 	}
 | |
| 	if m.HasFilenames {
 | |
| 		bits = bits + "[FL]"
 | |
| 	}
 | |
| 	if m.HasLineNumbers {
 | |
| 		bits = bits + "[LN]"
 | |
| 	}
 | |
| 	if m.HasInlineFrames {
 | |
| 		bits = bits + "[IN]"
 | |
| 	}
 | |
| 	return fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s",
 | |
| 		m.ID,
 | |
| 		m.Start, m.Limit, m.Offset,
 | |
| 		m.File,
 | |
| 		m.BuildID,
 | |
| 		bits)
 | |
| }
 | |
| 
 | |
| // string dumps a text representation of a location. Intended mainly
 | |
| // for debugging purposes.
 | |
| func (l *Location) string() string {
 | |
| 	ss := []string{}
 | |
| 	locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address)
 | |
| 	if m := l.Mapping; m != nil {
 | |
| 		locStr = locStr + fmt.Sprintf("M=%d ", m.ID)
 | |
| 	}
 | |
| 	if l.IsFolded {
 | |
| 		locStr = locStr + "[F] "
 | |
| 	}
 | |
| 	if len(l.Line) == 0 {
 | |
| 		ss = append(ss, locStr)
 | |
| 	}
 | |
| 	for li := range l.Line {
 | |
| 		lnStr := "??"
 | |
| 		if fn := l.Line[li].Function; fn != nil {
 | |
| 			lnStr = fmt.Sprintf("%s %s:%d s=%d",
 | |
| 				fn.Name,
 | |
| 				fn.Filename,
 | |
| 				l.Line[li].Line,
 | |
| 				fn.StartLine)
 | |
| 			if fn.Name != fn.SystemName {
 | |
| 				lnStr = lnStr + "(" + fn.SystemName + ")"
 | |
| 			}
 | |
| 		}
 | |
| 		ss = append(ss, locStr+lnStr)
 | |
| 		// Do not print location details past the first line
 | |
| 		locStr = "             "
 | |
| 	}
 | |
| 	return strings.Join(ss, "\n")
 | |
| }
 | |
| 
 | |
| // string dumps a text representation of a sample. Intended mainly
 | |
| // for debugging purposes.
 | |
| func (s *Sample) string() string {
 | |
| 	ss := []string{}
 | |
| 	var sv string
 | |
| 	for _, v := range s.Value {
 | |
| 		sv = fmt.Sprintf("%s %10d", sv, v)
 | |
| 	}
 | |
| 	sv = sv + ": "
 | |
| 	for _, l := range s.Location {
 | |
| 		sv = sv + fmt.Sprintf("%d ", l.ID)
 | |
| 	}
 | |
| 	ss = append(ss, sv)
 | |
| 	const labelHeader = "                "
 | |
| 	if len(s.Label) > 0 {
 | |
| 		ss = append(ss, labelHeader+labelsToString(s.Label))
 | |
| 	}
 | |
| 	if len(s.NumLabel) > 0 {
 | |
| 		ss = append(ss, labelHeader+numLabelsToString(s.NumLabel, s.NumUnit))
 | |
| 	}
 | |
| 	return strings.Join(ss, "\n")
 | |
| }
 | |
| 
 | |
| // labelsToString returns a string representation of a
 | |
| // map representing labels.
 | |
| func labelsToString(labels map[string][]string) string {
 | |
| 	ls := []string{}
 | |
| 	for k, v := range labels {
 | |
| 		ls = append(ls, fmt.Sprintf("%s:%v", k, v))
 | |
| 	}
 | |
| 	sort.Strings(ls)
 | |
| 	return strings.Join(ls, " ")
 | |
| }
 | |
| 
 | |
| // numLabelsToString returns a string representation of a map
 | |
| // representing numeric labels.
 | |
| func numLabelsToString(numLabels map[string][]int64, numUnits map[string][]string) string {
 | |
| 	ls := []string{}
 | |
| 	for k, v := range numLabels {
 | |
| 		units := numUnits[k]
 | |
| 		var labelString string
 | |
| 		if len(units) == len(v) {
 | |
| 			values := make([]string, len(v))
 | |
| 			for i, vv := range v {
 | |
| 				values[i] = fmt.Sprintf("%d %s", vv, units[i])
 | |
| 			}
 | |
| 			labelString = fmt.Sprintf("%s:%v", k, values)
 | |
| 		} else {
 | |
| 			labelString = fmt.Sprintf("%s:%v", k, v)
 | |
| 		}
 | |
| 		ls = append(ls, labelString)
 | |
| 	}
 | |
| 	sort.Strings(ls)
 | |
| 	return strings.Join(ls, " ")
 | |
| }
 | |
| 
 | |
| // SetLabel sets the specified key to the specified value for all samples in the
 | |
| // profile.
 | |
| func (p *Profile) SetLabel(key string, value []string) {
 | |
| 	for _, sample := range p.Sample {
 | |
| 		if sample.Label == nil {
 | |
| 			sample.Label = map[string][]string{key: value}
 | |
| 		} else {
 | |
| 			sample.Label[key] = value
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // RemoveLabel removes all labels associated with the specified key for all
 | |
| // samples in the profile.
 | |
| func (p *Profile) RemoveLabel(key string) {
 | |
| 	for _, sample := range p.Sample {
 | |
| 		delete(sample.Label, key)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // HasLabel returns true if a sample has a label with indicated key and value.
 | |
| func (s *Sample) HasLabel(key, value string) bool {
 | |
| 	for _, v := range s.Label[key] {
 | |
| 		if v == value {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // SetNumLabel sets the specified key to the specified value for all samples in the
 | |
| // profile. "unit" is a slice that describes the units that each corresponding member
 | |
| // of "values" is measured in (e.g. bytes or seconds).  If there is no relevant
 | |
| // unit for a given value, that member of "unit" should be the empty string.
 | |
| // "unit" must either have the same length as "value", or be nil.
 | |
| func (p *Profile) SetNumLabel(key string, value []int64, unit []string) {
 | |
| 	for _, sample := range p.Sample {
 | |
| 		if sample.NumLabel == nil {
 | |
| 			sample.NumLabel = map[string][]int64{key: value}
 | |
| 		} else {
 | |
| 			sample.NumLabel[key] = value
 | |
| 		}
 | |
| 		if sample.NumUnit == nil {
 | |
| 			sample.NumUnit = map[string][]string{key: unit}
 | |
| 		} else {
 | |
| 			sample.NumUnit[key] = unit
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // RemoveNumLabel removes all numerical labels associated with the specified key for all
 | |
| // samples in the profile.
 | |
| func (p *Profile) RemoveNumLabel(key string) {
 | |
| 	for _, sample := range p.Sample {
 | |
| 		delete(sample.NumLabel, key)
 | |
| 		delete(sample.NumUnit, key)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // DiffBaseSample returns true if a sample belongs to the diff base and false
 | |
| // otherwise.
 | |
| func (s *Sample) DiffBaseSample() bool {
 | |
| 	return s.HasLabel("pprof::base", "true")
 | |
| }
 | |
| 
 | |
| // Scale multiplies all sample values in a profile by a constant and keeps
 | |
| // only samples that have at least one non-zero value.
 | |
| func (p *Profile) Scale(ratio float64) {
 | |
| 	if ratio == 1 {
 | |
| 		return
 | |
| 	}
 | |
| 	ratios := make([]float64, len(p.SampleType))
 | |
| 	for i := range p.SampleType {
 | |
| 		ratios[i] = ratio
 | |
| 	}
 | |
| 	p.ScaleN(ratios)
 | |
| }
 | |
| 
 | |
| // ScaleN multiplies each sample values in a sample by a different amount
 | |
| // and keeps only samples that have at least one non-zero value.
 | |
| func (p *Profile) ScaleN(ratios []float64) error {
 | |
| 	if len(p.SampleType) != len(ratios) {
 | |
| 		return fmt.Errorf("mismatched scale ratios, got %d, want %d", len(ratios), len(p.SampleType))
 | |
| 	}
 | |
| 	allOnes := true
 | |
| 	for _, r := range ratios {
 | |
| 		if r != 1 {
 | |
| 			allOnes = false
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 	if allOnes {
 | |
| 		return nil
 | |
| 	}
 | |
| 	fillIdx := 0
 | |
| 	for _, s := range p.Sample {
 | |
| 		keepSample := false
 | |
| 		for i, v := range s.Value {
 | |
| 			if ratios[i] != 1 {
 | |
| 				val := int64(math.Round(float64(v) * ratios[i]))
 | |
| 				s.Value[i] = val
 | |
| 				keepSample = keepSample || val != 0
 | |
| 			}
 | |
| 		}
 | |
| 		if keepSample {
 | |
| 			p.Sample[fillIdx] = s
 | |
| 			fillIdx++
 | |
| 		}
 | |
| 	}
 | |
| 	p.Sample = p.Sample[:fillIdx]
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // HasFunctions determines if all locations in this profile have
 | |
| // symbolized function information.
 | |
| func (p *Profile) HasFunctions() bool {
 | |
| 	for _, l := range p.Location {
 | |
| 		if l.Mapping != nil && !l.Mapping.HasFunctions {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // HasFileLines determines if all locations in this profile have
 | |
| // symbolized file and line number information.
 | |
| func (p *Profile) HasFileLines() bool {
 | |
| 	for _, l := range p.Location {
 | |
| 		if l.Mapping != nil && (!l.Mapping.HasFilenames || !l.Mapping.HasLineNumbers) {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // Unsymbolizable returns true if a mapping points to a binary for which
 | |
| // locations can't be symbolized in principle, at least now. Examples are
 | |
| // "[vdso]", [vsyscall]" and some others, see the code.
 | |
| func (m *Mapping) Unsymbolizable() bool {
 | |
| 	name := filepath.Base(m.File)
 | |
| 	return strings.HasPrefix(name, "[") || strings.HasPrefix(name, "linux-vdso") || strings.HasPrefix(m.File, "/dev/dri/")
 | |
| }
 | |
| 
 | |
| // Copy makes a fully independent copy of a profile.
 | |
| func (p *Profile) Copy() *Profile {
 | |
| 	pp := &Profile{}
 | |
| 	if err := unmarshal(serialize(p), pp); err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 	if err := pp.postDecode(); err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 
 | |
| 	return pp
 | |
| }
 |