podman
1// Copyright 2014 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package profile provides a representation of profile.proto and
16// methods to encode/decode profiles in this format.
17package profile18
19import (20"bytes"21"compress/gzip"22"fmt"23"io"24"io/ioutil"25"math"26"path/filepath"27"regexp"28"sort"29"strings"30"sync"31"time"32)
33
34// Profile is an in-memory representation of profile.proto.
35type Profile struct {36SampleType []*ValueType37DefaultSampleType string38Sample []*Sample39Mapping []*Mapping40Location []*Location41Function []*Function42Comments []string43
44DropFrames string45KeepFrames string46
47TimeNanos int6448DurationNanos int6449PeriodType *ValueType50Period int6451
52// The following fields are modified during encoding and copying,53// so are protected by a Mutex.54encodeMu sync.Mutex55
56commentX []int6457dropFramesX int6458keepFramesX int6459stringTable []string60defaultSampleTypeX int6461}
62
63// ValueType corresponds to Profile.ValueType
64type ValueType struct {65Type string // cpu, wall, inuse_space, etc66Unit string // seconds, nanoseconds, bytes, etc67
68typeX int6469unitX int6470}
71
72// Sample corresponds to Profile.Sample
73type Sample struct {74Location []*Location75Value []int6476Label map[string][]string77NumLabel map[string][]int6478NumUnit map[string][]string79
80locationIDX []uint6481labelX []label82}
83
84// label corresponds to Profile.Label
85type label struct {86keyX int6487// Exactly one of the two following values must be set88strX int6489numX int64 // Integer value for this label90// can be set if numX has value91unitX int6492}
93
94// Mapping corresponds to Profile.Mapping
95type Mapping struct {96ID uint6497Start uint6498Limit uint6499Offset uint64100File string101BuildID string102HasFunctions bool103HasFilenames bool104HasLineNumbers bool105HasInlineFrames bool106
107fileX int64108buildIDX int64109}
110
111// Location corresponds to Profile.Location
112type Location struct {113ID uint64114Mapping *Mapping115Address uint64116Line []Line117IsFolded bool118
119mappingIDX uint64120}
121
122// Line corresponds to Profile.Line
123type Line struct {124Function *Function125Line int64126
127functionIDX uint64128}
129
130// Function corresponds to Profile.Function
131type Function struct {132ID uint64133Name string134SystemName string135Filename string136StartLine int64137
138nameX int64139systemNameX int64140filenameX int64141}
142
143// Parse parses a profile and checks for its validity. The input
144// may be a gzip-compressed encoded protobuf or one of many legacy
145// profile formats which may be unsupported in the future.
146func Parse(r io.Reader) (*Profile, error) {147data, err := ioutil.ReadAll(r)148if err != nil {149return nil, err150}151return ParseData(data)152}
153
154// ParseData parses a profile from a buffer and checks for its
155// validity.
156func ParseData(data []byte) (*Profile, error) {157var p *Profile158var err error159if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {160gz, err := gzip.NewReader(bytes.NewBuffer(data))161if err == nil {162data, err = ioutil.ReadAll(gz)163}164if err != nil {165return nil, fmt.Errorf("decompressing profile: %v", err)166}167}168if p, err = ParseUncompressed(data); err != nil && err != errNoData && err != errConcatProfile {169p, err = parseLegacy(data)170}171
172if err != nil {173return nil, fmt.Errorf("parsing profile: %v", err)174}175
176if err := p.CheckValid(); err != nil {177return nil, fmt.Errorf("malformed profile: %v", err)178}179return p, nil180}
181
182var errUnrecognized = fmt.Errorf("unrecognized profile format")183var errMalformed = fmt.Errorf("malformed profile format")184var errNoData = fmt.Errorf("empty input file")185var errConcatProfile = fmt.Errorf("concatenated profiles detected")186
187func parseLegacy(data []byte) (*Profile, error) {188parsers := []func([]byte) (*Profile, error){189parseCPU,190parseHeap,191parseGoCount, // goroutine, threadcreate192parseThread,193parseContention,194parseJavaProfile,195}196
197for _, parser := range parsers {198p, err := parser(data)199if err == nil {200p.addLegacyFrameInfo()201return p, nil202}203if err != errUnrecognized {204return nil, err205}206}207return nil, errUnrecognized208}
209
210// ParseUncompressed parses an uncompressed protobuf into a profile.
211func ParseUncompressed(data []byte) (*Profile, error) {212if len(data) == 0 {213return nil, errNoData214}215p := &Profile{}216if err := unmarshal(data, p); err != nil {217return nil, err218}219
220if err := p.postDecode(); err != nil {221return nil, err222}223
224return p, nil225}
226
227var libRx = regexp.MustCompile(`([.]so$|[.]so[._][0-9]+)`)228
229// massageMappings applies heuristic-based changes to the profile
230// mappings to account for quirks of some environments.
231func (p *Profile) massageMappings() {232// Merge adjacent regions with matching names, checking that the offsets match233if len(p.Mapping) > 1 {234mappings := []*Mapping{p.Mapping[0]}235for _, m := range p.Mapping[1:] {236lm := mappings[len(mappings)-1]237if adjacent(lm, m) {238lm.Limit = m.Limit239if m.File != "" {240lm.File = m.File241}242if m.BuildID != "" {243lm.BuildID = m.BuildID244}245p.updateLocationMapping(m, lm)246continue247}248mappings = append(mappings, m)249}250p.Mapping = mappings251}252
253// Use heuristics to identify main binary and move it to the top of the list of mappings254for i, m := range p.Mapping {255file := strings.TrimSpace(strings.Replace(m.File, "(deleted)", "", -1))256if len(file) == 0 {257continue258}259if len(libRx.FindStringSubmatch(file)) > 0 {260continue261}262if file[0] == '[' {263continue264}265// Swap what we guess is main to position 0.266p.Mapping[0], p.Mapping[i] = p.Mapping[i], p.Mapping[0]267break268}269
270// Keep the mapping IDs neatly sorted271for i, m := range p.Mapping {272m.ID = uint64(i + 1)273}274}
275
276// adjacent returns whether two mapping entries represent the same
277// mapping that has been split into two. Check that their addresses are adjacent,
278// and if the offsets match, if they are available.
279func adjacent(m1, m2 *Mapping) bool {280if m1.File != "" && m2.File != "" {281if m1.File != m2.File {282return false283}284}285if m1.BuildID != "" && m2.BuildID != "" {286if m1.BuildID != m2.BuildID {287return false288}289}290if m1.Limit != m2.Start {291return false292}293if m1.Offset != 0 && m2.Offset != 0 {294offset := m1.Offset + (m1.Limit - m1.Start)295if offset != m2.Offset {296return false297}298}299return true300}
301
302func (p *Profile) updateLocationMapping(from, to *Mapping) {303for _, l := range p.Location {304if l.Mapping == from {305l.Mapping = to306}307}308}
309
310func serialize(p *Profile) []byte {311p.encodeMu.Lock()312p.preEncode()313b := marshal(p)314p.encodeMu.Unlock()315return b316}
317
318// Write writes the profile as a gzip-compressed marshaled protobuf.
319func (p *Profile) Write(w io.Writer) error {320zw := gzip.NewWriter(w)321defer zw.Close()322_, err := zw.Write(serialize(p))323return err324}
325
326// WriteUncompressed writes the profile as a marshaled protobuf.
327func (p *Profile) WriteUncompressed(w io.Writer) error {328_, err := w.Write(serialize(p))329return err330}
331
332// CheckValid tests whether the profile is valid. Checks include, but are
333// not limited to:
334// - len(Profile.Sample[n].value) == len(Profile.value_unit)
335// - Sample.id has a corresponding Profile.Location
336func (p *Profile) CheckValid() error {337// Check that sample values are consistent338sampleLen := len(p.SampleType)339if sampleLen == 0 && len(p.Sample) != 0 {340return fmt.Errorf("missing sample type information")341}342for _, s := range p.Sample {343if s == nil {344return fmt.Errorf("profile has nil sample")345}346if len(s.Value) != sampleLen {347return fmt.Errorf("mismatch: sample has %d values vs. %d types", len(s.Value), len(p.SampleType))348}349for _, l := range s.Location {350if l == nil {351return fmt.Errorf("sample has nil location")352}353}354}355
356// Check that all mappings/locations/functions are in the tables357// Check that there are no duplicate ids358mappings := make(map[uint64]*Mapping, len(p.Mapping))359for _, m := range p.Mapping {360if m == nil {361return fmt.Errorf("profile has nil mapping")362}363if m.ID == 0 {364return fmt.Errorf("found mapping with reserved ID=0")365}366if mappings[m.ID] != nil {367return fmt.Errorf("multiple mappings with same id: %d", m.ID)368}369mappings[m.ID] = m370}371functions := make(map[uint64]*Function, len(p.Function))372for _, f := range p.Function {373if f == nil {374return fmt.Errorf("profile has nil function")375}376if f.ID == 0 {377return fmt.Errorf("found function with reserved ID=0")378}379if functions[f.ID] != nil {380return fmt.Errorf("multiple functions with same id: %d", f.ID)381}382functions[f.ID] = f383}384locations := make(map[uint64]*Location, len(p.Location))385for _, l := range p.Location {386if l == nil {387return fmt.Errorf("profile has nil location")388}389if l.ID == 0 {390return fmt.Errorf("found location with reserved id=0")391}392if locations[l.ID] != nil {393return fmt.Errorf("multiple locations with same id: %d", l.ID)394}395locations[l.ID] = l396if m := l.Mapping; m != nil {397if m.ID == 0 || mappings[m.ID] != m {398return fmt.Errorf("inconsistent mapping %p: %d", m, m.ID)399}400}401for _, ln := range l.Line {402f := ln.Function403if f == nil {404return fmt.Errorf("location id: %d has a line with nil function", l.ID)405}406if f.ID == 0 || functions[f.ID] != f {407return fmt.Errorf("inconsistent function %p: %d", f, f.ID)408}409}410}411return nil412}
413
414// Aggregate merges the locations in the profile into equivalence
415// classes preserving the request attributes. It also updates the
416// samples to point to the merged locations.
417func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, address bool) error {418for _, m := range p.Mapping {419m.HasInlineFrames = m.HasInlineFrames && inlineFrame420m.HasFunctions = m.HasFunctions && function421m.HasFilenames = m.HasFilenames && filename422m.HasLineNumbers = m.HasLineNumbers && linenumber423}424
425// Aggregate functions426if !function || !filename {427for _, f := range p.Function {428if !function {429f.Name = ""430f.SystemName = ""431}432if !filename {433f.Filename = ""434}435}436}437
438// Aggregate locations439if !inlineFrame || !address || !linenumber {440for _, l := range p.Location {441if !inlineFrame && len(l.Line) > 1 {442l.Line = l.Line[len(l.Line)-1:]443}444if !linenumber {445for i := range l.Line {446l.Line[i].Line = 0447}448}449if !address {450l.Address = 0451}452}453}454
455return p.CheckValid()456}
457
458// NumLabelUnits returns a map of numeric label keys to the units
459// associated with those keys and a map of those keys to any units
460// that were encountered but not used.
461// Unit for a given key is the first encountered unit for that key. If multiple
462// units are encountered for values paired with a particular key, then the first
463// unit encountered is used and all other units are returned in sorted order
464// in map of ignored units.
465// If no units are encountered for a particular key, the unit is then inferred
466// based on the key.
467func (p *Profile) NumLabelUnits() (map[string]string, map[string][]string) {468numLabelUnits := map[string]string{}469ignoredUnits := map[string]map[string]bool{}470encounteredKeys := map[string]bool{}471
472// Determine units based on numeric tags for each sample.473for _, s := range p.Sample {474for k := range s.NumLabel {475encounteredKeys[k] = true476for _, unit := range s.NumUnit[k] {477if unit == "" {478continue479}480if wantUnit, ok := numLabelUnits[k]; !ok {481numLabelUnits[k] = unit482} else if wantUnit != unit {483if v, ok := ignoredUnits[k]; ok {484v[unit] = true485} else {486ignoredUnits[k] = map[string]bool{unit: true}487}488}489}490}491}492// Infer units for keys without any units associated with493// numeric tag values.494for key := range encounteredKeys {495unit := numLabelUnits[key]496if unit == "" {497switch key {498case "alignment", "request":499numLabelUnits[key] = "bytes"500default:501numLabelUnits[key] = key502}503}504}505
506// Copy ignored units into more readable format507unitsIgnored := make(map[string][]string, len(ignoredUnits))508for key, values := range ignoredUnits {509units := make([]string, len(values))510i := 0511for unit := range values {512units[i] = unit513i++514}515sort.Strings(units)516unitsIgnored[key] = units517}518
519return numLabelUnits, unitsIgnored520}
521
522// String dumps a text representation of a profile. Intended mainly
523// for debugging purposes.
524func (p *Profile) String() string {525ss := make([]string, 0, len(p.Comments)+len(p.Sample)+len(p.Mapping)+len(p.Location))526for _, c := range p.Comments {527ss = append(ss, "Comment: "+c)528}529if pt := p.PeriodType; pt != nil {530ss = append(ss, fmt.Sprintf("PeriodType: %s %s", pt.Type, pt.Unit))531}532ss = append(ss, fmt.Sprintf("Period: %d", p.Period))533if p.TimeNanos != 0 {534ss = append(ss, fmt.Sprintf("Time: %v", time.Unix(0, p.TimeNanos)))535}536if p.DurationNanos != 0 {537ss = append(ss, fmt.Sprintf("Duration: %.4v", time.Duration(p.DurationNanos)))538}539
540ss = append(ss, "Samples:")541var sh1 string542for _, s := range p.SampleType {543dflt := ""544if s.Type == p.DefaultSampleType {545dflt = "[dflt]"546}547sh1 = sh1 + fmt.Sprintf("%s/%s%s ", s.Type, s.Unit, dflt)548}549ss = append(ss, strings.TrimSpace(sh1))550for _, s := range p.Sample {551ss = append(ss, s.string())552}553
554ss = append(ss, "Locations")555for _, l := range p.Location {556ss = append(ss, l.string())557}558
559ss = append(ss, "Mappings")560for _, m := range p.Mapping {561ss = append(ss, m.string())562}563
564return strings.Join(ss, "\n") + "\n"565}
566
567// string dumps a text representation of a mapping. Intended mainly
568// for debugging purposes.
569func (m *Mapping) string() string {570bits := ""571if m.HasFunctions {572bits = bits + "[FN]"573}574if m.HasFilenames {575bits = bits + "[FL]"576}577if m.HasLineNumbers {578bits = bits + "[LN]"579}580if m.HasInlineFrames {581bits = bits + "[IN]"582}583return fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s",584m.ID,585m.Start, m.Limit, m.Offset,586m.File,587m.BuildID,588bits)589}
590
591// string dumps a text representation of a location. Intended mainly
592// for debugging purposes.
593func (l *Location) string() string {594ss := []string{}595locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address)596if m := l.Mapping; m != nil {597locStr = locStr + fmt.Sprintf("M=%d ", m.ID)598}599if l.IsFolded {600locStr = locStr + "[F] "601}602if len(l.Line) == 0 {603ss = append(ss, locStr)604}605for li := range l.Line {606lnStr := "??"607if fn := l.Line[li].Function; fn != nil {608lnStr = fmt.Sprintf("%s %s:%d s=%d",609fn.Name,610fn.Filename,611l.Line[li].Line,612fn.StartLine)613if fn.Name != fn.SystemName {614lnStr = lnStr + "(" + fn.SystemName + ")"615}616}617ss = append(ss, locStr+lnStr)618// Do not print location details past the first line619locStr = " "620}621return strings.Join(ss, "\n")622}
623
624// string dumps a text representation of a sample. Intended mainly
625// for debugging purposes.
626func (s *Sample) string() string {627ss := []string{}628var sv string629for _, v := range s.Value {630sv = fmt.Sprintf("%s %10d", sv, v)631}632sv = sv + ": "633for _, l := range s.Location {634sv = sv + fmt.Sprintf("%d ", l.ID)635}636ss = append(ss, sv)637const labelHeader = " "638if len(s.Label) > 0 {639ss = append(ss, labelHeader+labelsToString(s.Label))640}641if len(s.NumLabel) > 0 {642ss = append(ss, labelHeader+numLabelsToString(s.NumLabel, s.NumUnit))643}644return strings.Join(ss, "\n")645}
646
647// labelsToString returns a string representation of a
648// map representing labels.
649func labelsToString(labels map[string][]string) string {650ls := []string{}651for k, v := range labels {652ls = append(ls, fmt.Sprintf("%s:%v", k, v))653}654sort.Strings(ls)655return strings.Join(ls, " ")656}
657
658// numLabelsToString returns a string representation of a map
659// representing numeric labels.
660func numLabelsToString(numLabels map[string][]int64, numUnits map[string][]string) string {661ls := []string{}662for k, v := range numLabels {663units := numUnits[k]664var labelString string665if len(units) == len(v) {666values := make([]string, len(v))667for i, vv := range v {668values[i] = fmt.Sprintf("%d %s", vv, units[i])669}670labelString = fmt.Sprintf("%s:%v", k, values)671} else {672labelString = fmt.Sprintf("%s:%v", k, v)673}674ls = append(ls, labelString)675}676sort.Strings(ls)677return strings.Join(ls, " ")678}
679
680// SetLabel sets the specified key to the specified value for all samples in the
681// profile.
682func (p *Profile) SetLabel(key string, value []string) {683for _, sample := range p.Sample {684if sample.Label == nil {685sample.Label = map[string][]string{key: value}686} else {687sample.Label[key] = value688}689}690}
691
692// RemoveLabel removes all labels associated with the specified key for all
693// samples in the profile.
694func (p *Profile) RemoveLabel(key string) {695for _, sample := range p.Sample {696delete(sample.Label, key)697}698}
699
700// HasLabel returns true if a sample has a label with indicated key and value.
701func (s *Sample) HasLabel(key, value string) bool {702for _, v := range s.Label[key] {703if v == value {704return true705}706}707return false708}
709
710// DiffBaseSample returns true if a sample belongs to the diff base and false
711// otherwise.
712func (s *Sample) DiffBaseSample() bool {713return s.HasLabel("pprof::base", "true")714}
715
716// Scale multiplies all sample values in a profile by a constant and keeps
717// only samples that have at least one non-zero value.
718func (p *Profile) Scale(ratio float64) {719if ratio == 1 {720return721}722ratios := make([]float64, len(p.SampleType))723for i := range p.SampleType {724ratios[i] = ratio725}726p.ScaleN(ratios)727}
728
729// ScaleN multiplies each sample values in a sample by a different amount
730// and keeps only samples that have at least one non-zero value.
731func (p *Profile) ScaleN(ratios []float64) error {732if len(p.SampleType) != len(ratios) {733return fmt.Errorf("mismatched scale ratios, got %d, want %d", len(ratios), len(p.SampleType))734}735allOnes := true736for _, r := range ratios {737if r != 1 {738allOnes = false739break740}741}742if allOnes {743return nil744}745fillIdx := 0746for _, s := range p.Sample {747keepSample := false748for i, v := range s.Value {749if ratios[i] != 1 {750val := int64(math.Round(float64(v) * ratios[i]))751s.Value[i] = val752keepSample = keepSample || val != 0753}754}755if keepSample {756p.Sample[fillIdx] = s757fillIdx++758}759}760p.Sample = p.Sample[:fillIdx]761return nil762}
763
764// HasFunctions determines if all locations in this profile have
765// symbolized function information.
766func (p *Profile) HasFunctions() bool {767for _, l := range p.Location {768if l.Mapping != nil && !l.Mapping.HasFunctions {769return false770}771}772return true773}
774
775// HasFileLines determines if all locations in this profile have
776// symbolized file and line number information.
777func (p *Profile) HasFileLines() bool {778for _, l := range p.Location {779if l.Mapping != nil && (!l.Mapping.HasFilenames || !l.Mapping.HasLineNumbers) {780return false781}782}783return true784}
785
786// Unsymbolizable returns true if a mapping points to a binary for which
787// locations can't be symbolized in principle, at least now. Examples are
788// "[vdso]", [vsyscall]" and some others, see the code.
789func (m *Mapping) Unsymbolizable() bool {790name := filepath.Base(m.File)791return strings.HasPrefix(name, "[") || strings.HasPrefix(name, "linux-vdso") || strings.HasPrefix(m.File, "/dev/dri/")792}
793
794// Copy makes a fully independent copy of a profile.
795func (p *Profile) Copy() *Profile {796pp := &Profile{}797if err := unmarshal(serialize(p), pp); err != nil {798panic(err)799}800if err := pp.postDecode(); err != nil {801panic(err)802}803
804return pp805}
806