2023-05-06 00:42:41 +00:00
|
|
|
// 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
|
|
|
|
|
|
|
|
import (
|
2024-10-17 20:09:39 +00:00
|
|
|
"encoding/binary"
|
2023-05-06 00:42:41 +00:00
|
|
|
"fmt"
|
|
|
|
"sort"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Compact performs garbage collection on a profile to remove any
|
|
|
|
// unreferenced fields. This is useful to reduce the size of a profile
|
|
|
|
// after samples or locations have been removed.
|
|
|
|
func (p *Profile) Compact() *Profile {
|
|
|
|
p, _ = Merge([]*Profile{p})
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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. Merges are
|
|
|
|
// associative with the caveat of the first profile having some
|
|
|
|
// specialization in how headers are combined. There may be other
|
|
|
|
// subtleties now or in the future regarding associativity.
|
|
|
|
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
|
2024-10-17 20:09:39 +00:00
|
|
|
pm.locationsByID = makeLocationIDMap(len(src.Location))
|
2023-05-06 00:42:41 +00:00
|
|
|
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.
|
2024-10-17 20:09:39 +00:00
|
|
|
locationsByID locationIDMap
|
2023-05-06 00:42:41 +00:00
|
|
|
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 {
|
2024-10-17 20:09:39 +00:00
|
|
|
// Check memoization table
|
|
|
|
k := pm.sampleKey(src)
|
|
|
|
if ss, ok := pm.samples[k]; ok {
|
|
|
|
for i, v := range src.Value {
|
|
|
|
ss.Value[i] += v
|
|
|
|
}
|
|
|
|
return ss
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make new sample.
|
2023-05-06 00:42:41 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
copy(s.Value, src.Value)
|
|
|
|
pm.samples[k] = s
|
|
|
|
pm.p.Sample = append(pm.p.Sample, s)
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
2024-10-17 20:09:39 +00:00
|
|
|
func (pm *profileMerger) sampleKey(sample *Sample) sampleKey {
|
|
|
|
// Accumulate contents into a string.
|
|
|
|
var buf strings.Builder
|
|
|
|
buf.Grow(64) // Heuristic to avoid extra allocs
|
|
|
|
|
|
|
|
// encode a number
|
|
|
|
putNumber := func(v uint64) {
|
|
|
|
var num [binary.MaxVarintLen64]byte
|
|
|
|
n := binary.PutUvarint(num[:], v)
|
|
|
|
buf.Write(num[:n])
|
|
|
|
}
|
|
|
|
|
|
|
|
// encode a string prefixed with its length.
|
|
|
|
putDelimitedString := func(s string) {
|
|
|
|
putNumber(uint64(len(s)))
|
|
|
|
buf.WriteString(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, l := range sample.Location {
|
|
|
|
// Get the location in the merged profile, which may have a different ID.
|
|
|
|
if loc := pm.mapLocation(l); loc != nil {
|
|
|
|
putNumber(loc.ID)
|
|
|
|
}
|
2023-05-06 00:42:41 +00:00
|
|
|
}
|
2024-10-17 20:09:39 +00:00
|
|
|
putNumber(0) // Delimiter
|
2023-05-06 00:42:41 +00:00
|
|
|
|
2024-10-17 20:09:39 +00:00
|
|
|
for _, l := range sortedKeys1(sample.Label) {
|
|
|
|
putDelimitedString(l)
|
|
|
|
values := sample.Label[l]
|
|
|
|
putNumber(uint64(len(values)))
|
|
|
|
for _, v := range values {
|
|
|
|
putDelimitedString(v)
|
|
|
|
}
|
2023-05-06 00:42:41 +00:00
|
|
|
}
|
|
|
|
|
2024-10-17 20:09:39 +00:00
|
|
|
for _, l := range sortedKeys2(sample.NumLabel) {
|
|
|
|
putDelimitedString(l)
|
|
|
|
values := sample.NumLabel[l]
|
|
|
|
putNumber(uint64(len(values)))
|
|
|
|
for _, v := range values {
|
|
|
|
putNumber(uint64(v))
|
|
|
|
}
|
|
|
|
units := sample.NumUnit[l]
|
|
|
|
putNumber(uint64(len(units)))
|
|
|
|
for _, v := range units {
|
|
|
|
putDelimitedString(v)
|
|
|
|
}
|
2023-05-06 00:42:41 +00:00
|
|
|
}
|
|
|
|
|
2024-10-17 20:09:39 +00:00
|
|
|
return sampleKey(buf.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
type sampleKey string
|
|
|
|
|
|
|
|
// sortedKeys1 returns the sorted keys found in a string->[]string map.
|
|
|
|
//
|
|
|
|
// Note: this is currently non-generic since github pprof runs golint,
|
|
|
|
// which does not support generics. When that issue is fixed, it can
|
|
|
|
// be merged with sortedKeys2 and made into a generic function.
|
|
|
|
func sortedKeys1(m map[string][]string) []string {
|
|
|
|
if len(m) == 0 {
|
|
|
|
return nil
|
2023-05-06 00:42:41 +00:00
|
|
|
}
|
2024-10-17 20:09:39 +00:00
|
|
|
keys := make([]string, 0, len(m))
|
|
|
|
for k := range m {
|
|
|
|
keys = append(keys, k)
|
|
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
return keys
|
2023-05-06 00:42:41 +00:00
|
|
|
}
|
|
|
|
|
2024-10-17 20:09:39 +00:00
|
|
|
// sortedKeys2 returns the sorted keys found in a string->[]int64 map.
|
|
|
|
//
|
|
|
|
// Note: this is currently non-generic since github pprof runs golint,
|
|
|
|
// which does not support generics. When that issue is fixed, it can
|
|
|
|
// be merged with sortedKeys1 and made into a generic function.
|
|
|
|
func sortedKeys2(m map[string][]int64) []string {
|
|
|
|
if len(m) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
keys := make([]string, 0, len(m))
|
|
|
|
for k := range m {
|
|
|
|
keys = append(keys, k)
|
|
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
return keys
|
2023-05-06 00:42:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (pm *profileMerger) mapLocation(src *Location) *Location {
|
|
|
|
if src == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-10-17 20:09:39 +00:00
|
|
|
if l := pm.locationsByID.get(src.ID); l != nil {
|
2023-05-06 00:42:41 +00:00
|
|
|
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 {
|
2024-10-17 20:09:39 +00:00
|
|
|
pm.locationsByID.set(src.ID, ll)
|
2023-05-06 00:42:41 +00:00
|
|
|
return ll
|
|
|
|
}
|
2024-10-17 20:09:39 +00:00
|
|
|
pm.locationsByID.set(src.ID, l)
|
2023-05-06 00:42:41 +00:00
|
|
|
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{
|
2024-10-17 20:09:39 +00:00
|
|
|
ID: uint64(len(pm.p.Mapping) + 1),
|
|
|
|
Start: src.Start,
|
|
|
|
Limit: src.Limit,
|
|
|
|
Offset: src.Offset,
|
|
|
|
File: src.File,
|
|
|
|
KernelRelocationSymbol: src.KernelRelocationSymbol,
|
|
|
|
BuildID: src.BuildID,
|
|
|
|
HasFunctions: src.HasFunctions,
|
|
|
|
HasFilenames: src.HasFilenames,
|
|
|
|
HasLineNumbers: src.HasLineNumbers,
|
|
|
|
HasInlineFrames: src.HasInlineFrames,
|
2023-05-06 00:42:41 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
2024-10-17 20:09:39 +00:00
|
|
|
|
|
|
|
// locationIDMap is like a map[uint64]*Location, but provides efficiency for
|
|
|
|
// ids that are densely numbered, which is often the case.
|
|
|
|
type locationIDMap struct {
|
|
|
|
dense []*Location // indexed by id for id < len(dense)
|
|
|
|
sparse map[uint64]*Location // indexed by id for id >= len(dense)
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeLocationIDMap(n int) locationIDMap {
|
|
|
|
return locationIDMap{
|
|
|
|
dense: make([]*Location, n),
|
|
|
|
sparse: map[uint64]*Location{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lm locationIDMap) get(id uint64) *Location {
|
|
|
|
if id < uint64(len(lm.dense)) {
|
|
|
|
return lm.dense[int(id)]
|
|
|
|
}
|
|
|
|
return lm.sparse[id]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lm locationIDMap) set(id uint64, loc *Location) {
|
|
|
|
if id < uint64(len(lm.dense)) {
|
|
|
|
lm.dense[id] = loc
|
|
|
|
return
|
|
|
|
}
|
|
|
|
lm.sparse[id] = loc
|
|
|
|
}
|
|
|
|
|
|
|
|
// CompatibilizeSampleTypes makes profiles compatible to be compared/merged. It
|
|
|
|
// keeps sample types that appear in all profiles only and drops/reorders the
|
|
|
|
// sample types as necessary.
|
|
|
|
//
|
|
|
|
// In the case of sample types order is not the same for given profiles the
|
|
|
|
// order is derived from the first profile.
|
|
|
|
//
|
|
|
|
// Profiles are modified in-place.
|
|
|
|
//
|
|
|
|
// It returns an error if the sample type's intersection is empty.
|
|
|
|
func CompatibilizeSampleTypes(ps []*Profile) error {
|
|
|
|
sTypes := commonSampleTypes(ps)
|
|
|
|
if len(sTypes) == 0 {
|
|
|
|
return fmt.Errorf("profiles have empty common sample type list")
|
|
|
|
}
|
|
|
|
for _, p := range ps {
|
|
|
|
if err := compatibilizeSampleTypes(p, sTypes); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// commonSampleTypes returns sample types that appear in all profiles in the
|
|
|
|
// order how they ordered in the first profile.
|
|
|
|
func commonSampleTypes(ps []*Profile) []string {
|
|
|
|
if len(ps) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
sTypes := map[string]int{}
|
|
|
|
for _, p := range ps {
|
|
|
|
for _, st := range p.SampleType {
|
|
|
|
sTypes[st.Type]++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var res []string
|
|
|
|
for _, st := range ps[0].SampleType {
|
|
|
|
if sTypes[st.Type] == len(ps) {
|
|
|
|
res = append(res, st.Type)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
// compatibilizeSampleTypes drops sample types that are not present in sTypes
|
|
|
|
// list and reorder them if needed.
|
|
|
|
//
|
|
|
|
// It sets DefaultSampleType to sType[0] if it is not in sType list.
|
|
|
|
//
|
|
|
|
// It assumes that all sample types from the sTypes list are present in the
|
|
|
|
// given profile otherwise it returns an error.
|
|
|
|
func compatibilizeSampleTypes(p *Profile, sTypes []string) error {
|
|
|
|
if len(sTypes) == 0 {
|
|
|
|
return fmt.Errorf("sample type list is empty")
|
|
|
|
}
|
|
|
|
defaultSampleType := sTypes[0]
|
|
|
|
reMap, needToModify := make([]int, len(sTypes)), false
|
|
|
|
for i, st := range sTypes {
|
|
|
|
if st == p.DefaultSampleType {
|
|
|
|
defaultSampleType = p.DefaultSampleType
|
|
|
|
}
|
|
|
|
idx := searchValueType(p.SampleType, st)
|
|
|
|
if idx < 0 {
|
|
|
|
return fmt.Errorf("%q sample type is not found in profile", st)
|
|
|
|
}
|
|
|
|
reMap[i] = idx
|
|
|
|
if idx != i {
|
|
|
|
needToModify = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !needToModify && len(sTypes) == len(p.SampleType) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
p.DefaultSampleType = defaultSampleType
|
|
|
|
oldSampleTypes := p.SampleType
|
|
|
|
p.SampleType = make([]*ValueType, len(sTypes))
|
|
|
|
for i, idx := range reMap {
|
|
|
|
p.SampleType[i] = oldSampleTypes[idx]
|
|
|
|
}
|
|
|
|
values := make([]int64, len(sTypes))
|
|
|
|
for _, s := range p.Sample {
|
|
|
|
for i, idx := range reMap {
|
|
|
|
values[i] = s.Value[idx]
|
|
|
|
}
|
|
|
|
s.Value = s.Value[:len(values)]
|
|
|
|
copy(s.Value, values)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func searchValueType(vts []*ValueType, s string) int {
|
|
|
|
for i, vt := range vts {
|
|
|
|
if vt.Type == s {
|
|
|
|
return i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1
|
|
|
|
}
|