// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package profile provides a representation of // github.com/google/pprof/proto/profile.proto and // methods to encode/decode/merge 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 DropFrames string KeepFrames string TimeNanos int64 DurationNanos int64 PeriodType *ValueType Period int64 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 map[string][]string NumLabel map[string][]int64 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 } // 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 } // 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 must be an // encoded pprof protobuf, which may optionally be gzip-compressed. func ( io.Reader) (*Profile, error) { , := io.ReadAll() if != nil { return nil, } if len() >= 2 && [0] == 0x1f && [1] == 0x8b { , := gzip.NewReader(bytes.NewBuffer()) if != nil { return nil, fmt.Errorf("decompressing profile: %v", ) } , := io.ReadAll() if != nil { return nil, fmt.Errorf("decompressing profile: %v", ) } = } , := parseUncompressed() if != nil { return nil, fmt.Errorf("parsing profile: %w", ) } if := .CheckValid(); != nil { return nil, fmt.Errorf("malformed profile: %v", ) } return , nil } var errMalformed = fmt.Errorf("malformed profile format") var ErrNoData = fmt.Errorf("empty input file") func parseUncompressed( []byte) (*Profile, error) { if len() == 0 { return nil, ErrNoData } := &Profile{} if := unmarshal(, ); != nil { return nil, } if := .postDecode(); != nil { return nil, } return , nil } // Write writes the profile as a gzip-compressed marshaled protobuf. func ( *Profile) ( io.Writer) error { .preEncode() := marshal() := gzip.NewWriter() defer .Close() , := .Write() 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 len(.Value) != { return fmt.Errorf("mismatch: sample has: %d values vs. %d types", len(.Value), len(.SampleType)) } } // 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 .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 .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 .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 { if := .Function; != nil { 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 } } if ! { .Address = 0 } } } return .CheckValid() } // Print dumps a text representation of a profile. Intended mainly // for debugging purposes. func ( *Profile) () string { := make([]string, 0, len(.Sample)+len(.Mapping)+len(.Location)) 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: %v", time.Duration(.DurationNanos))) } = append(, "Samples:") var string for , := range .SampleType { = + fmt.Sprintf("%s/%s ", .Type, .Unit) } = append(, strings.TrimSpace()) for , := range .Sample { var string for , := range .Value { = fmt.Sprintf("%s %10d", , ) } = + ": " for , := range .Location { = + fmt.Sprintf("%d ", .ID) } = append(, ) const = " " if len(.Label) > 0 { := for , := range .Label { = + fmt.Sprintf("%s:%v ", , ) } = append(, ) } if len(.NumLabel) > 0 { := for , := range .NumLabel { = + fmt.Sprintf("%s:%v ", , ) } = append(, ) } } = append(, "Locations") for , := range .Location { := fmt.Sprintf("%6d: %#x ", .ID, .Address) if := .Mapping; != nil { = + fmt.Sprintf("M=%d ", .ID) } if len(.Line) == 0 { = append(, ) } for := range .Line { := "??" if := .Line[].Function; != nil { = fmt.Sprintf("%s %s:%d s=%d", .Name, .Filename, .Line[].Line, .StartLine) if .Name != .SystemName { = + "(" + .SystemName + ")" } } = append(, +) // Do not print location details past the first line = " " } } = append(, "Mappings") for , := range .Mapping { := "" if .HasFunctions { += "[FN]" } if .HasFilenames { += "[FL]" } if .HasLineNumbers { += "[LN]" } if .HasInlineFrames { += "[IN]" } = append(, fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s", .ID, .Start, .Limit, .Offset, .File, .BuildID, )) } return strings.Join(, "\n") + "\n" } // Merge adds profile p adjusted by ratio r into profile p. Profiles // must be compatible (same Type and SampleType). // TODO(rsilvera): consider normalizing the profiles based on the // total samples collected. func ( *Profile) ( *Profile, float64) error { if := .Compatible(); != nil { return } = .Copy() // Keep the largest of the two periods. if .Period > .Period { .Period = .Period } .DurationNanos += .DurationNanos .Mapping = append(.Mapping, .Mapping...) for , := range .Mapping { .ID = uint64( + 1) } .Location = append(.Location, .Location...) for , := range .Location { .ID = uint64( + 1) } .Function = append(.Function, .Function...) for , := range .Function { .ID = uint64( + 1) } if != 1.0 { for , := range .Sample { for , := range .Value { .Value[] = int64((float64() * )) } } } .Sample = append(.Sample, .Sample...) return .CheckValid() } // Compatible determines if two profiles can be compared/merged. // returns nil if the profiles are compatible; otherwise an error with // details on the incompatibility. func ( *Profile) ( *Profile) error { if !compatibleValueTypes(.PeriodType, .PeriodType) { return fmt.Errorf("incompatible period types %v and %v", .PeriodType, .PeriodType) } if len(.SampleType) != len(.SampleType) { return fmt.Errorf("incompatible sample types %v and %v", .SampleType, .SampleType) } for := range .SampleType { if !compatibleValueTypes(.SampleType[], .SampleType[]) { return fmt.Errorf("incompatible sample types %v and %v", .SampleType, .SampleType) } } 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 } func compatibleValueTypes(, *ValueType) bool { if == nil || == nil { return true // No grounds to disqualify. } return .Type == .Type && .Unit == .Unit } // Copy makes a fully independent copy of a profile. func ( *Profile) () *Profile { .preEncode() := marshal() := &Profile{} if := unmarshal(, ); != nil { panic() } if := .postDecode(); != nil { panic() } return } // Demangler maps symbol names to a human-readable form. This may // include C++ demangling and additional simplification. Names that // are not demangled may be missing from the resulting map. type Demangler func(name []string) (map[string]string, error) // Demangle attempts to demangle and optionally simplify any function // names referenced in the profile. It works on a best-effort basis: // it will silently preserve the original names in case of any errors. func ( *Profile) ( Demangler) error { // Collect names to demangle. var []string for , := range .Function { = append(, .SystemName) } // Update profile with demangled names. , := () if != nil { return } for , := range .Function { if , := [.SystemName]; { .Name = } } return nil } // Empty reports whether the profile contains no samples. func ( *Profile) () bool { return len(.Sample) == 0 } // Scale multiplies all sample values in a profile by a constant. 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. 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 } for , := range .Sample { for , := range .Value { if [] != 1 { .Value[] = int64(float64() * []) } } } return nil }