// 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 ( ) // 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 DocURL 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 docURLX 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 Column 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 ( io.Reader) (*Profile, error) { , := io.ReadAll() if != nil { return nil, } return ParseData() } // ParseData parses a profile from a buffer and checks for its // validity. func ( []byte) (*Profile, error) { var *Profile var error if len() >= 2 && [0] == 0x1f && [1] == 0x8b { , := gzip.NewReader(bytes.NewBuffer()) if == nil { , = io.ReadAll() } if != nil { return nil, fmt.Errorf("decompressing profile: %v", ) } } if , = ParseUncompressed(); != nil && != errNoData && != errConcatProfile { , = parseLegacy() } if != nil { return nil, fmt.Errorf("parsing profile: %v", ) } if := .CheckValid(); != nil { return nil, fmt.Errorf("malformed profile: %v", ) } return , 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( []byte) (*Profile, error) { := []func([]byte) (*Profile, error){ parseCPU, parseHeap, parseGoCount, // goroutine, threadcreate parseThread, parseContention, parseJavaProfile, } for , := range { , := () if == nil { .addLegacyFrameInfo() return , nil } if != errUnrecognized { return nil, } } return nil, errUnrecognized } // ParseUncompressed parses an uncompressed protobuf into a profile. func ( []byte) (*Profile, error) { if len() == 0 { return nil, errNoData } := &Profile{} if := unmarshal(, ); != nil { return nil, } if := .postDecode(); != nil { return nil, } return , 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 ( *Profile) () { // Merge adjacent regions with matching names, checking that the offsets match if len(.Mapping) > 1 { := []*Mapping{.Mapping[0]} for , := range .Mapping[1:] { := [len()-1] if adjacent(, ) { .Limit = .Limit if .File != "" { .File = .File } if .BuildID != "" { .BuildID = .BuildID } .updateLocationMapping(, ) continue } = append(, ) } .Mapping = } // Use heuristics to identify main binary and move it to the top of the list of mappings for , := range .Mapping { := strings.TrimSpace(strings.Replace(.File, "(deleted)", "", -1)) if len() == 0 { continue } if len(libRx.FindStringSubmatch()) > 0 { continue } if [0] == '[' { continue } // Swap what we guess is main to position 0. .Mapping[0], .Mapping[] = .Mapping[], .Mapping[0] break } // Keep the mapping IDs neatly sorted for , := range .Mapping { .ID = uint64( + 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(, *Mapping) bool { if .File != "" && .File != "" { if .File != .File { return false } } if .BuildID != "" && .BuildID != "" { if .BuildID != .BuildID { return false } } if .Limit != .Start { return false } if .Offset != 0 && .Offset != 0 { := .Offset + (.Limit - .Start) if != .Offset { return false } } return true } func ( *Profile) (, *Mapping) { for , := range .Location { if .Mapping == { .Mapping = } } } func serialize( *Profile) []byte { .encodeMu.Lock() .preEncode() := marshal() .encodeMu.Unlock() return } // Write writes the profile as a gzip-compressed marshaled protobuf. func ( *Profile) ( io.Writer) error { := gzip.NewWriter() defer .Close() , := .Write(serialize()) return } // WriteUncompressed writes the profile as a marshaled protobuf. func ( *Profile) ( io.Writer) error { , := .Write(serialize()) return } // 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 ( *Profile) () error { // Check that sample values are consistent := len(.SampleType) if == 0 && len(.Sample) != 0 { return fmt.Errorf("missing sample type information") } for , := range .Sample { if == nil { return fmt.Errorf("profile has nil sample") } if len(.Value) != { return fmt.Errorf("mismatch: sample has %d values vs. %d types", len(.Value), len(.SampleType)) } for , := range .Location { if == 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 := make(map[uint64]*Mapping, len(.Mapping)) for , := range .Mapping { if == nil { return fmt.Errorf("profile has nil mapping") } if .ID == 0 { return fmt.Errorf("found mapping with reserved ID=0") } if [.ID] != nil { return fmt.Errorf("multiple mappings with same id: %d", .ID) } [.ID] = } := make(map[uint64]*Function, len(.Function)) for , := range .Function { if == nil { return fmt.Errorf("profile has nil function") } if .ID == 0 { return fmt.Errorf("found function with reserved ID=0") } if [.ID] != nil { return fmt.Errorf("multiple functions with same id: %d", .ID) } [.ID] = } := make(map[uint64]*Location, len(.Location)) for , := range .Location { if == nil { return fmt.Errorf("profile has nil location") } if .ID == 0 { return fmt.Errorf("found location with reserved id=0") } if [.ID] != nil { return fmt.Errorf("multiple locations with same id: %d", .ID) } [.ID] = if := .Mapping; != nil { if .ID == 0 || [.ID] != { return fmt.Errorf("inconsistent mapping %p: %d", , .ID) } } for , := range .Line { := .Function if == nil { return fmt.Errorf("location id: %d has a line with nil function", .ID) } if .ID == 0 || [.ID] != { return fmt.Errorf("inconsistent function %p: %d", , .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 ( *Profile) (, , , , , bool) error { for , := range .Mapping { .HasInlineFrames = .HasInlineFrames && .HasFunctions = .HasFunctions && .HasFilenames = .HasFilenames && .HasLineNumbers = .HasLineNumbers && } // Aggregate functions if ! || ! { for , := range .Function { if ! { .Name = "" .SystemName = "" } if ! { .Filename = "" } } } // Aggregate locations if ! || ! || ! || ! { for , := range .Location { if ! && len(.Line) > 1 { .Line = .Line[len(.Line)-1:] } if ! { for := range .Line { .Line[].Line = 0 .Line[].Column = 0 } } if ! { for := range .Line { .Line[].Column = 0 } } if ! { .Address = 0 } } } return .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 ( *Profile) () (map[string]string, map[string][]string) { := map[string]string{} := map[string]map[string]bool{} := map[string]bool{} // Determine units based on numeric tags for each sample. for , := range .Sample { for := range .NumLabel { [] = true for , := range .NumUnit[] { if == "" { continue } if , := []; ! { [] = } else if != { if , := []; { [] = true } else { [] = map[string]bool{: true} } } } } } // Infer units for keys without any units associated with // numeric tag values. for := range { := [] if == "" { switch { case "alignment", "request": [] = "bytes" default: [] = } } } // Copy ignored units into more readable format := make(map[string][]string, len()) for , := range { := make([]string, len()) := 0 for := range { [] = ++ } sort.Strings() [] = } return , } // String dumps a text representation of a profile. Intended mainly // for debugging purposes. func ( *Profile) () string { := make([]string, 0, len(.Comments)+len(.Sample)+len(.Mapping)+len(.Location)) for , := range .Comments { = append(, "Comment: "+) } if := .DocURL; != "" { = append(, fmt.Sprintf("Doc: %s", )) } if := .PeriodType; != nil { = append(, fmt.Sprintf("PeriodType: %s %s", .Type, .Unit)) } = append(, fmt.Sprintf("Period: %d", .Period)) if .TimeNanos != 0 { = append(, fmt.Sprintf("Time: %v", time.Unix(0, .TimeNanos))) } if .DurationNanos != 0 { = append(, fmt.Sprintf("Duration: %.4v", time.Duration(.DurationNanos))) } = append(, "Samples:") var string for , := range .SampleType { := "" if .Type == .DefaultSampleType { = "[dflt]" } = + fmt.Sprintf("%s/%s%s ", .Type, .Unit, ) } = append(, strings.TrimSpace()) for , := range .Sample { = append(, .string()) } = append(, "Locations") for , := range .Location { = append(, .string()) } = append(, "Mappings") for , := range .Mapping { = append(, .string()) } return strings.Join(, "\n") + "\n" } // string dumps a text representation of a mapping. Intended mainly // for debugging purposes. func ( *Mapping) () string { := "" if .HasFunctions { = + "[FN]" } if .HasFilenames { = + "[FL]" } if .HasLineNumbers { = + "[LN]" } if .HasInlineFrames { = + "[IN]" } return fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s", .ID, .Start, .Limit, .Offset, .File, .BuildID, ) } // string dumps a text representation of a location. Intended mainly // for debugging purposes. func ( *Location) () string { := []string{} := fmt.Sprintf("%6d: %#x ", .ID, .Address) if := .Mapping; != nil { = + fmt.Sprintf("M=%d ", .ID) } if .IsFolded { = + "[F] " } if len(.Line) == 0 { = append(, ) } for := range .Line { := "??" if := .Line[].Function; != nil { = fmt.Sprintf("%s %s:%d:%d s=%d", .Name, .Filename, .Line[].Line, .Line[].Column, .StartLine) if .Name != .SystemName { = + "(" + .SystemName + ")" } } = append(, +) // Do not print location details past the first line = " " } return strings.Join(, "\n") } // string dumps a text representation of a sample. Intended mainly // for debugging purposes. func ( *Sample) () string { := []string{} var string for , := range .Value { = fmt.Sprintf("%s %10d", , ) } = + ": " for , := range .Location { = + fmt.Sprintf("%d ", .ID) } = append(, ) const = " " if len(.Label) > 0 { = append(, +labelsToString(.Label)) } if len(.NumLabel) > 0 { = append(, +numLabelsToString(.NumLabel, .NumUnit)) } return strings.Join(, "\n") } // labelsToString returns a string representation of a // map representing labels. func labelsToString( map[string][]string) string { := []string{} for , := range { = append(, fmt.Sprintf("%s:%v", , )) } sort.Strings() return strings.Join(, " ") } // numLabelsToString returns a string representation of a map // representing numeric labels. func numLabelsToString( map[string][]int64, map[string][]string) string { := []string{} for , := range { := [] var string if len() == len() { := make([]string, len()) for , := range { [] = fmt.Sprintf("%d %s", , []) } = fmt.Sprintf("%s:%v", , ) } else { = fmt.Sprintf("%s:%v", , ) } = append(, ) } sort.Strings() return strings.Join(, " ") } // SetLabel sets the specified key to the specified value for all samples in the // profile. func ( *Profile) ( string, []string) { for , := range .Sample { if .Label == nil { .Label = map[string][]string{: } } else { .Label[] = } } } // RemoveLabel removes all labels associated with the specified key for all // samples in the profile. func ( *Profile) ( string) { for , := range .Sample { delete(.Label, ) } } // HasLabel returns true if a sample has a label with indicated key and value. func ( *Sample) (, string) bool { for , := range .Label[] { if == { 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 ( *Profile) ( string, []int64, []string) { for , := range .Sample { if .NumLabel == nil { .NumLabel = map[string][]int64{: } } else { .NumLabel[] = } if .NumUnit == nil { .NumUnit = map[string][]string{: } } else { .NumUnit[] = } } } // RemoveNumLabel removes all numerical labels associated with the specified key for all // samples in the profile. func ( *Profile) ( string) { for , := range .Sample { delete(.NumLabel, ) delete(.NumUnit, ) } } // DiffBaseSample returns true if a sample belongs to the diff base and false // otherwise. func ( *Sample) () bool { return .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 ( *Profile) ( float64) { if == 1 { return } := make([]float64, len(.SampleType)) for := range .SampleType { [] = } .ScaleN() } // 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 ( *Profile) ( []float64) error { if len(.SampleType) != len() { return fmt.Errorf("mismatched scale ratios, got %d, want %d", len(), len(.SampleType)) } := true for , := range { if != 1 { = false break } } if { return nil } := 0 for , := range .Sample { := false for , := range .Value { if [] != 1 { := int64(math.Round(float64() * [])) .Value[] = = || != 0 } } if { .Sample[] = ++ } } .Sample = .Sample[:] return nil } // HasFunctions determines if all locations in this profile have // symbolized function information. func ( *Profile) () bool { for , := range .Location { if .Mapping != nil && !.Mapping.HasFunctions { return false } } return true } // HasFileLines determines if all locations in this profile have // symbolized file and line number information. func ( *Profile) () bool { for , := range .Location { if .Mapping != nil && (!.Mapping.HasFilenames || !.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 ( *Mapping) () bool { := filepath.Base(.File) return strings.HasPrefix(, "[") || strings.HasPrefix(, "linux-vdso") || strings.HasPrefix(.File, "/dev/dri/") || .File == "//anon" } // Copy makes a fully independent copy of a profile. func ( *Profile) () *Profile { := &Profile{} if := unmarshal(serialize(), ); != nil { panic() } if := .postDecode(); != nil { panic() } return }