| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | package profile |
| |
|
| | import ( |
| | "bytes" |
| | "compress/gzip" |
| | "fmt" |
| | "io" |
| | "math" |
| | "path/filepath" |
| | "regexp" |
| | "slices" |
| | "sort" |
| | "strings" |
| | "sync" |
| | "time" |
| | ) |
| |
|
| | |
| | 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 |
| |
|
| | |
| | |
| | encodeMu sync.Mutex |
| |
|
| | commentX []int64 |
| | docURLX int64 |
| | dropFramesX int64 |
| | keepFramesX int64 |
| | stringTable []string |
| | defaultSampleTypeX int64 |
| | } |
| |
|
| | |
| | type ValueType struct { |
| | Type string |
| | Unit string |
| |
|
| | typeX int64 |
| | unitX int64 |
| | } |
| |
|
| | |
| | type Sample struct { |
| | Location []*Location |
| | Value []int64 |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | Label map[string][]string |
| | |
| | |
| | NumLabel map[string][]int64 |
| | |
| | |
| | |
| | |
| | |
| | NumUnit map[string][]string |
| |
|
| | locationIDX []uint64 |
| | labelX []label |
| | } |
| |
|
| | |
| | type label struct { |
| | keyX int64 |
| | |
| | strX int64 |
| | numX int64 |
| | |
| | unitX int64 |
| | } |
| |
|
| | |
| | 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 |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | KernelRelocationSymbol string |
| | } |
| |
|
| | |
| | type Location struct { |
| | ID uint64 |
| | Mapping *Mapping |
| | Address uint64 |
| | Line []Line |
| | IsFolded bool |
| |
|
| | mappingIDX uint64 |
| | } |
| |
|
| | |
| | type Line struct { |
| | Function *Function |
| | Line int64 |
| | Column int64 |
| |
|
| | functionIDX uint64 |
| | } |
| |
|
| | |
| | type Function struct { |
| | ID uint64 |
| | Name string |
| | SystemName string |
| | Filename string |
| | StartLine int64 |
| |
|
| | nameX int64 |
| | systemNameX int64 |
| | filenameX int64 |
| | } |
| |
|
| | |
| | |
| | |
| | func Parse(r io.Reader) (*Profile, error) { |
| | data, err := io.ReadAll(r) |
| | if err != nil { |
| | return nil, err |
| | } |
| | return ParseData(data) |
| | } |
| |
|
| | |
| | |
| | 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, |
| | 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 |
| | } |
| |
|
| | |
| | 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]+)`) |
| |
|
| | |
| | |
| | func (p *Profile) massageMappings() { |
| | |
| | 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 |
| | } |
| |
|
| | |
| | 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 |
| | } |
| | |
| | p.Mapping[0], p.Mapping[i] = p.Mapping[i], p.Mapping[0] |
| | break |
| | } |
| |
|
| | |
| | for i, m := range p.Mapping { |
| | m.ID = uint64(i + 1) |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | 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 |
| | } |
| |
|
| | |
| | func (p *Profile) Write(w io.Writer) error { |
| | zw := gzip.NewWriter(w) |
| | defer zw.Close() |
| | _, err := zw.Write(serialize(p)) |
| | return err |
| | } |
| |
|
| | |
| | func (p *Profile) WriteUncompressed(w io.Writer) error { |
| | _, err := w.Write(serialize(p)) |
| | return err |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func (p *Profile) CheckValid() error { |
| | |
| | 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") |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | 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 |
| | } |
| |
|
| | |
| | |
| | |
| | func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, columnnumber, 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 |
| | } |
| |
|
| | |
| | if !function || !filename { |
| | for _, f := range p.Function { |
| | if !function { |
| | f.Name = "" |
| | f.SystemName = "" |
| | } |
| | if !filename { |
| | f.Filename = "" |
| | } |
| | } |
| | } |
| |
|
| | |
| | if !inlineFrame || !address || !linenumber || !columnnumber { |
| | 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 |
| | l.Line[i].Column = 0 |
| | } |
| | } |
| | if !columnnumber { |
| | for i := range l.Line { |
| | l.Line[i].Column = 0 |
| | } |
| | } |
| | if !address { |
| | l.Address = 0 |
| | } |
| | } |
| | } |
| |
|
| | return p.CheckValid() |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | func (p *Profile) NumLabelUnits() (map[string]string, map[string][]string) { |
| | numLabelUnits := map[string]string{} |
| | ignoredUnits := map[string]map[string]bool{} |
| | encounteredKeys := map[string]bool{} |
| |
|
| | |
| | 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} |
| | } |
| | } |
| | } |
| | } |
| | } |
| | |
| | |
| | for key := range encounteredKeys { |
| | unit := numLabelUnits[key] |
| | if unit == "" { |
| | switch key { |
| | case "alignment", "request": |
| | numLabelUnits[key] = "bytes" |
| | default: |
| | numLabelUnits[key] = key |
| | } |
| | } |
| | } |
| |
|
| | |
| | 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 |
| | } |
| |
|
| | |
| | |
| | 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 url := p.DocURL; url != "" { |
| | ss = append(ss, fmt.Sprintf("Doc: %s", url)) |
| | } |
| | 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" |
| | } |
| |
|
| | |
| | |
| | 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) |
| | } |
| |
|
| | |
| | |
| | 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:%d s=%d", |
| | fn.Name, |
| | fn.Filename, |
| | l.Line[li].Line, |
| | l.Line[li].Column, |
| | fn.StartLine) |
| | if fn.Name != fn.SystemName { |
| | lnStr = lnStr + "(" + fn.SystemName + ")" |
| | } |
| | } |
| | ss = append(ss, locStr+lnStr) |
| | |
| | locStr = " " |
| | } |
| | return strings.Join(ss, "\n") |
| | } |
| |
|
| | |
| | |
| | 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") |
| | } |
| |
|
| | |
| | |
| | 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, " ") |
| | } |
| |
|
| | |
| | |
| | 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, " ") |
| | } |
| |
|
| | |
| | |
| | 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 |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | func (p *Profile) RemoveLabel(key string) { |
| | for _, sample := range p.Sample { |
| | delete(sample.Label, key) |
| | } |
| | } |
| |
|
| | |
| | func (s *Sample) HasLabel(key, value string) bool { |
| | return slices.Contains(s.Label[key], value) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | 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 |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | func (p *Profile) RemoveNumLabel(key string) { |
| | for _, sample := range p.Sample { |
| | delete(sample.NumLabel, key) |
| | delete(sample.NumUnit, key) |
| | } |
| | } |
| |
|
| | |
| | |
| | func (s *Sample) DiffBaseSample() bool { |
| | return s.HasLabel("pprof::base", "true") |
| | } |
| |
|
| | |
| | |
| | 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) |
| | } |
| |
|
| | |
| | |
| | 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 |
| | } |
| |
|
| | |
| | |
| | func (p *Profile) HasFunctions() bool { |
| | for _, l := range p.Location { |
| | if l.Mapping != nil && !l.Mapping.HasFunctions { |
| | return false |
| | } |
| | } |
| | return true |
| | } |
| |
|
| | |
| | |
| | func (p *Profile) HasFileLines() bool { |
| | for _, l := range p.Location { |
| | if l.Mapping != nil && (!l.Mapping.HasFilenames || !l.Mapping.HasLineNumbers) { |
| | return false |
| | } |
| | } |
| | return true |
| | } |
| |
|
| | |
| | |
| | |
| | func (m *Mapping) Unsymbolizable() bool { |
| | name := filepath.Base(m.File) |
| | switch { |
| | case strings.HasPrefix(name, "["): |
| | case strings.HasPrefix(name, "linux-vdso"): |
| | case strings.HasPrefix(m.File, "/dev/dri/"): |
| | case m.File == "//anon": |
| | case m.File == "": |
| | case strings.HasPrefix(m.File, "/memfd:"): |
| | default: |
| | return false |
| | } |
| | return true |
| | } |
| |
|
| | |
| | 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 |
| | } |
| |
|