| // Copyright 2019 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 |
| |
| import ( |
| "fmt" |
| "sort" |
| "strconv" |
| "strings" |
| ) |
| |
| // Merge merges all the profiles in profs into a single Profile. |
| // Returns a new profile independent of the input profiles. The merged |
| // profile is compacted to eliminate unused samples, locations, |
| // functions and mappings. Profiles must have identical profile sample |
| // and period types or the merge will fail. profile.Period of the |
| // resulting profile will be the maximum of all profiles, and |
| // profile.TimeNanos will be the earliest nonzero one. |
| func Merge(srcs []*Profile) (*Profile, error) { |
| if len(srcs) == 0 { |
| return nil, fmt.Errorf("no profiles to merge") |
| } |
| p, err := combineHeaders(srcs) |
| if err != nil { |
| return nil, err |
| } |
| |
| pm := &profileMerger{ |
| p: p, |
| samples: make(map[sampleKey]*Sample, len(srcs[0].Sample)), |
| locations: make(map[locationKey]*Location, len(srcs[0].Location)), |
| functions: make(map[functionKey]*Function, len(srcs[0].Function)), |
| mappings: make(map[mappingKey]*Mapping, len(srcs[0].Mapping)), |
| } |
| |
| for _, src := range srcs { |
| // Clear the profile-specific hash tables |
| pm.locationsByID = make(map[uint64]*Location, len(src.Location)) |
| pm.functionsByID = make(map[uint64]*Function, len(src.Function)) |
| pm.mappingsByID = make(map[uint64]mapInfo, len(src.Mapping)) |
| |
| if len(pm.mappings) == 0 && len(src.Mapping) > 0 { |
| // The Mapping list has the property that the first mapping |
| // represents the main binary. Take the first Mapping we see, |
| // otherwise the operations below will add mappings in an |
| // arbitrary order. |
| pm.mapMapping(src.Mapping[0]) |
| } |
| |
| for _, s := range src.Sample { |
| if !isZeroSample(s) { |
| pm.mapSample(s) |
| } |
| } |
| } |
| |
| for _, s := range p.Sample { |
| if isZeroSample(s) { |
| // If there are any zero samples, re-merge the profile to GC |
| // them. |
| return Merge([]*Profile{p}) |
| } |
| } |
| |
| return p, nil |
| } |
| |
| // Normalize normalizes the source profile by multiplying each value in profile by the |
| // ratio of the sum of the base profile's values of that sample type to the sum of the |
| // source profile's value of that sample type. |
| func (p *Profile) Normalize(pb *Profile) error { |
| |
| if err := p.compatible(pb); err != nil { |
| return err |
| } |
| |
| baseVals := make([]int64, len(p.SampleType)) |
| for _, s := range pb.Sample { |
| for i, v := range s.Value { |
| baseVals[i] += v |
| } |
| } |
| |
| srcVals := make([]int64, len(p.SampleType)) |
| for _, s := range p.Sample { |
| for i, v := range s.Value { |
| srcVals[i] += v |
| } |
| } |
| |
| normScale := make([]float64, len(baseVals)) |
| for i := range baseVals { |
| if srcVals[i] == 0 { |
| normScale[i] = 0.0 |
| } else { |
| normScale[i] = float64(baseVals[i]) / float64(srcVals[i]) |
| } |
| } |
| p.ScaleN(normScale) |
| return nil |
| } |
| |
| func isZeroSample(s *Sample) bool { |
| for _, v := range s.Value { |
| if v != 0 { |
| return false |
| } |
| } |
| return true |
| } |
| |
| type profileMerger struct { |
| p *Profile |
| |
| // Memoization tables within a profile. |
| locationsByID map[uint64]*Location |
| functionsByID map[uint64]*Function |
| mappingsByID map[uint64]mapInfo |
| |
| // Memoization tables for profile entities. |
| samples map[sampleKey]*Sample |
| locations map[locationKey]*Location |
| functions map[functionKey]*Function |
| mappings map[mappingKey]*Mapping |
| } |
| |
| type mapInfo struct { |
| m *Mapping |
| offset int64 |
| } |
| |
| func (pm *profileMerger) mapSample(src *Sample) *Sample { |
| s := &Sample{ |
| Location: make([]*Location, len(src.Location)), |
| Value: make([]int64, len(src.Value)), |
| Label: make(map[string][]string, len(src.Label)), |
| NumLabel: make(map[string][]int64, len(src.NumLabel)), |
| NumUnit: make(map[string][]string, len(src.NumLabel)), |
| } |
| for i, l := range src.Location { |
| s.Location[i] = pm.mapLocation(l) |
| } |
| for k, v := range src.Label { |
| vv := make([]string, len(v)) |
| copy(vv, v) |
| s.Label[k] = vv |
| } |
| for k, v := range src.NumLabel { |
| u := src.NumUnit[k] |
| vv := make([]int64, len(v)) |
| uu := make([]string, len(u)) |
| copy(vv, v) |
| copy(uu, u) |
| s.NumLabel[k] = vv |
| s.NumUnit[k] = uu |
| } |
| // Check memoization table. Must be done on the remapped location to |
| // account for the remapped mapping. Add current values to the |
| // existing sample. |
| k := s.key() |
| if ss, ok := pm.samples[k]; ok { |
| for i, v := range src.Value { |
| ss.Value[i] += v |
| } |
| return ss |
| } |
| copy(s.Value, src.Value) |
| pm.samples[k] = s |
| pm.p.Sample = append(pm.p.Sample, s) |
| return s |
| } |
| |
| // key generates sampleKey to be used as a key for maps. |
| func (sample *Sample) key() sampleKey { |
| ids := make([]string, len(sample.Location)) |
| for i, l := range sample.Location { |
| ids[i] = strconv.FormatUint(l.ID, 16) |
| } |
| |
| labels := make([]string, 0, len(sample.Label)) |
| for k, v := range sample.Label { |
| labels = append(labels, fmt.Sprintf("%q%q", k, v)) |
| } |
| sort.Strings(labels) |
| |
| numlabels := make([]string, 0, len(sample.NumLabel)) |
| for k, v := range sample.NumLabel { |
| numlabels = append(numlabels, fmt.Sprintf("%q%x%x", k, v, sample.NumUnit[k])) |
| } |
| sort.Strings(numlabels) |
| |
| return sampleKey{ |
| strings.Join(ids, "|"), |
| strings.Join(labels, ""), |
| strings.Join(numlabels, ""), |
| } |
| } |
| |
| type sampleKey struct { |
| locations string |
| labels string |
| numlabels string |
| } |
| |
| func (pm *profileMerger) mapLocation(src *Location) *Location { |
| if src == nil { |
| return nil |
| } |
| |
| if l, ok := pm.locationsByID[src.ID]; ok { |
| pm.locationsByID[src.ID] = l |
| return l |
| } |
| |
| mi := pm.mapMapping(src.Mapping) |
| l := &Location{ |
| ID: uint64(len(pm.p.Location) + 1), |
| Mapping: mi.m, |
| Address: uint64(int64(src.Address) + mi.offset), |
| Line: make([]Line, len(src.Line)), |
| IsFolded: src.IsFolded, |
| } |
| for i, ln := range src.Line { |
| l.Line[i] = pm.mapLine(ln) |
| } |
| // Check memoization table. Must be done on the remapped location to |
| // account for the remapped mapping ID. |
| k := l.key() |
| if ll, ok := pm.locations[k]; ok { |
| pm.locationsByID[src.ID] = ll |
| return ll |
| } |
| pm.locationsByID[src.ID] = l |
| pm.locations[k] = l |
| pm.p.Location = append(pm.p.Location, l) |
| return l |
| } |
| |
| // key generates locationKey to be used as a key for maps. |
| func (l *Location) key() locationKey { |
| key := locationKey{ |
| addr: l.Address, |
| isFolded: l.IsFolded, |
| } |
| if l.Mapping != nil { |
| // Normalizes address to handle address space randomization. |
| key.addr -= l.Mapping.Start |
| key.mappingID = l.Mapping.ID |
| } |
| lines := make([]string, len(l.Line)*2) |
| for i, line := range l.Line { |
| if line.Function != nil { |
| lines[i*2] = strconv.FormatUint(line.Function.ID, 16) |
| } |
| lines[i*2+1] = strconv.FormatInt(line.Line, 16) |
| } |
| key.lines = strings.Join(lines, "|") |
| return key |
| } |
| |
| type locationKey struct { |
| addr, mappingID uint64 |
| lines string |
| isFolded bool |
| } |
| |
| func (pm *profileMerger) mapMapping(src *Mapping) mapInfo { |
| if src == nil { |
| return mapInfo{} |
| } |
| |
| if mi, ok := pm.mappingsByID[src.ID]; ok { |
| return mi |
| } |
| |
| // Check memoization tables. |
| mk := src.key() |
| if m, ok := pm.mappings[mk]; ok { |
| mi := mapInfo{m, int64(m.Start) - int64(src.Start)} |
| pm.mappingsByID[src.ID] = mi |
| return mi |
| } |
| m := &Mapping{ |
| ID: uint64(len(pm.p.Mapping) + 1), |
| Start: src.Start, |
| Limit: src.Limit, |
| Offset: src.Offset, |
| File: src.File, |
| BuildID: src.BuildID, |
| HasFunctions: src.HasFunctions, |
| HasFilenames: src.HasFilenames, |
| HasLineNumbers: src.HasLineNumbers, |
| HasInlineFrames: src.HasInlineFrames, |
| } |
| pm.p.Mapping = append(pm.p.Mapping, m) |
| |
| // Update memoization tables. |
| pm.mappings[mk] = m |
| mi := mapInfo{m, 0} |
| pm.mappingsByID[src.ID] = mi |
| return mi |
| } |
| |
| // key generates encoded strings of Mapping to be used as a key for |
| // maps. |
| func (m *Mapping) key() mappingKey { |
| // Normalize addresses to handle address space randomization. |
| // Round up to next 4K boundary to avoid minor discrepancies. |
| const mapsizeRounding = 0x1000 |
| |
| size := m.Limit - m.Start |
| size = size + mapsizeRounding - 1 |
| size = size - (size % mapsizeRounding) |
| key := mappingKey{ |
| size: size, |
| offset: m.Offset, |
| } |
| |
| switch { |
| case m.BuildID != "": |
| key.buildIDOrFile = m.BuildID |
| case m.File != "": |
| key.buildIDOrFile = m.File |
| default: |
| // A mapping containing neither build ID nor file name is a fake mapping. A |
| // key with empty buildIDOrFile is used for fake mappings so that they are |
| // treated as the same mapping during merging. |
| } |
| return key |
| } |
| |
| type mappingKey struct { |
| size, offset uint64 |
| buildIDOrFile string |
| } |
| |
| func (pm *profileMerger) mapLine(src Line) Line { |
| ln := Line{ |
| Function: pm.mapFunction(src.Function), |
| Line: src.Line, |
| } |
| return ln |
| } |
| |
| func (pm *profileMerger) mapFunction(src *Function) *Function { |
| if src == nil { |
| return nil |
| } |
| if f, ok := pm.functionsByID[src.ID]; ok { |
| return f |
| } |
| k := src.key() |
| if f, ok := pm.functions[k]; ok { |
| pm.functionsByID[src.ID] = f |
| return f |
| } |
| f := &Function{ |
| ID: uint64(len(pm.p.Function) + 1), |
| Name: src.Name, |
| SystemName: src.SystemName, |
| Filename: src.Filename, |
| StartLine: src.StartLine, |
| } |
| pm.functions[k] = f |
| pm.functionsByID[src.ID] = f |
| pm.p.Function = append(pm.p.Function, f) |
| return f |
| } |
| |
| // key generates a struct to be used as a key for maps. |
| func (f *Function) key() functionKey { |
| return functionKey{ |
| f.StartLine, |
| f.Name, |
| f.SystemName, |
| f.Filename, |
| } |
| } |
| |
| type functionKey struct { |
| startLine int64 |
| name, systemName, fileName string |
| } |
| |
| // combineHeaders checks that all profiles can be merged and returns |
| // their combined profile. |
| func combineHeaders(srcs []*Profile) (*Profile, error) { |
| for _, s := range srcs[1:] { |
| if err := srcs[0].compatible(s); err != nil { |
| return nil, err |
| } |
| } |
| |
| var timeNanos, durationNanos, period int64 |
| var comments []string |
| seenComments := map[string]bool{} |
| var defaultSampleType string |
| for _, s := range srcs { |
| if timeNanos == 0 || s.TimeNanos < timeNanos { |
| timeNanos = s.TimeNanos |
| } |
| durationNanos += s.DurationNanos |
| if period == 0 || period < s.Period { |
| period = s.Period |
| } |
| for _, c := range s.Comments { |
| if seen := seenComments[c]; !seen { |
| comments = append(comments, c) |
| seenComments[c] = true |
| } |
| } |
| if defaultSampleType == "" { |
| defaultSampleType = s.DefaultSampleType |
| } |
| } |
| |
| p := &Profile{ |
| SampleType: make([]*ValueType, len(srcs[0].SampleType)), |
| |
| DropFrames: srcs[0].DropFrames, |
| KeepFrames: srcs[0].KeepFrames, |
| |
| TimeNanos: timeNanos, |
| DurationNanos: durationNanos, |
| PeriodType: srcs[0].PeriodType, |
| Period: period, |
| |
| Comments: comments, |
| DefaultSampleType: defaultSampleType, |
| } |
| copy(p.SampleType, srcs[0].SampleType) |
| return p, nil |
| } |
| |
| // 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 (p *Profile) compatible(pb *Profile) error { |
| if !equalValueType(p.PeriodType, pb.PeriodType) { |
| return fmt.Errorf("incompatible period types %v and %v", p.PeriodType, pb.PeriodType) |
| } |
| |
| if len(p.SampleType) != len(pb.SampleType) { |
| return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType) |
| } |
| |
| for i := range p.SampleType { |
| if !equalValueType(p.SampleType[i], pb.SampleType[i]) { |
| return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType) |
| } |
| } |
| return nil |
| } |
| |
| // equalValueType returns true if the two value types are semantically |
| // equal. It ignores the internal fields used during encode/decode. |
| func equalValueType(st1, st2 *ValueType) bool { |
| return st1.Type == st2.Type && st1.Unit == st2.Unit |
| } |