github.com/grafana/pyroscope@v1.18.0/pkg/pprof/pprof.go (about)

     1  package pprof
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/binary"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  	"unsafe"
    16  
    17  	"github.com/cespare/xxhash/v2"
    18  	"github.com/colega/zeropool"
    19  	"github.com/google/pprof/profile"
    20  	"github.com/klauspost/compress/gzip"
    21  	"github.com/pkg/errors"
    22  	"github.com/samber/lo"
    23  
    24  	profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    25  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    26  	"github.com/grafana/pyroscope/pkg/slices"
    27  	"github.com/grafana/pyroscope/pkg/util"
    28  )
    29  
    30  var (
    31  	gzipReaderPool = sync.Pool{
    32  		New: func() any {
    33  			return &gzipReader{
    34  				reader: bytes.NewReader(nil),
    35  			}
    36  		},
    37  	}
    38  	gzipWriterPool = sync.Pool{
    39  		New: func() any {
    40  			return gzip.NewWriter(io.Discard)
    41  		},
    42  	}
    43  	bufPool = sync.Pool{
    44  		New: func() any {
    45  			return bytes.NewBuffer(nil)
    46  		},
    47  	}
    48  )
    49  
    50  type gzipReader struct {
    51  	gzip   *gzip.Reader
    52  	reader *bytes.Reader
    53  }
    54  
    55  // open gzip, create reader if required
    56  func (r *gzipReader) gzipOpen() error {
    57  	var err error
    58  	if r.gzip == nil {
    59  		r.gzip, err = gzip.NewReader(r.reader)
    60  	} else {
    61  		err = r.gzip.Reset(r.reader)
    62  	}
    63  	return err
    64  }
    65  
    66  func (r *gzipReader) openBytes(input []byte) (io.Reader, error) {
    67  	r.reader.Reset(input)
    68  
    69  	// handle if data is not gzipped at all
    70  	if err := r.gzipOpen(); err == gzip.ErrHeader {
    71  		r.reader.Reset(input)
    72  		return r.reader, nil
    73  	} else if err != nil {
    74  		return nil, errors.Wrap(err, "gzip reset")
    75  	}
    76  
    77  	return r.gzip, nil
    78  }
    79  
    80  func NewProfile() *Profile {
    81  	return RawFromProto(new(profilev1.Profile))
    82  }
    83  
    84  func RawFromProto(pbp *profilev1.Profile) *Profile {
    85  	return &Profile{Profile: pbp}
    86  }
    87  
    88  func RawFromBytes(input []byte) (_ *Profile, err error) {
    89  	return RawFromBytesWithLimit(input, 0)
    90  }
    91  
    92  type ErrDecompressedSizeExceedsLimit struct {
    93  	Limit int64
    94  }
    95  
    96  func (e *ErrDecompressedSizeExceedsLimit) Error() string {
    97  	return fmt.Sprintf("decompressed size exceeds maximum allowed size of %d bytes", e.Limit)
    98  }
    99  
   100  // RawFromBytesWithLimit reads a profile from bytes with an optional size limit.
   101  // maxSize limits the decompressed size in bytes. Use 0 for no limit.
   102  // This prevents zip bomb attacks where small compressed data expands to huge sizes.
   103  func RawFromBytesWithLimit(input []byte, maxSize int64) (_ *Profile, err error) {
   104  	gzipReader := gzipReaderPool.Get().(*gzipReader)
   105  	buf := bufPool.Get().(*bytes.Buffer)
   106  	defer func() {
   107  		gzipReaderPool.Put(gzipReader)
   108  		buf.Reset()
   109  		bufPool.Put(buf)
   110  	}()
   111  
   112  	r, err := gzipReader.openBytes(input)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	// Apply size limit if specified (maxSize >= 0)
   118  	// maxSize == 0 means no limit (unlimited decompression)
   119  	if maxSize > 0 {
   120  		r = io.LimitReader(r, maxSize+1) // +1 to detect if limit is exceeded
   121  	}
   122  
   123  	if _, err = io.Copy(buf, r); err != nil {
   124  		return nil, errors.Wrap(err, "copy to buffer")
   125  	}
   126  
   127  	// Check if we hit the size limit
   128  	if maxSize > 0 && int64(buf.Len()) > maxSize {
   129  		return nil, &ErrDecompressedSizeExceedsLimit{Limit: maxSize}
   130  	}
   131  
   132  	rawSize := buf.Len()
   133  	pbp := new(profilev1.Profile)
   134  	if err = pbp.UnmarshalVT(buf.Bytes()); err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	return &Profile{
   139  		Profile: pbp,
   140  		rawSize: rawSize,
   141  	}, nil
   142  }
   143  
   144  func FromBytes(input []byte, fn func(*profilev1.Profile, int) error) error {
   145  	return FromBytesWithLimit(input, 0, fn)
   146  }
   147  
   148  // FromBytesWithLimit reads a profile from bytes with an optional size limit and calls fn with the result.
   149  // maxSize limits the decompressed size in bytes. Use 0 for no limit.
   150  // This prevents zip bomb attacks where small compressed data expands to huge sizes.
   151  func FromBytesWithLimit(input []byte, maxSize int64, fn func(*profilev1.Profile, int) error) error {
   152  	p, err := RawFromBytesWithLimit(input, maxSize)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	return fn(p.Profile, p.rawSize)
   157  }
   158  
   159  func FromProfile(p *profile.Profile) (*profilev1.Profile, error) {
   160  	var r profilev1.Profile
   161  	strings := make(map[string]int)
   162  
   163  	r.Sample = make([]*profilev1.Sample, 0, len(p.Sample))
   164  	r.SampleType = make([]*profilev1.ValueType, 0, len(p.SampleType))
   165  	r.Location = make([]*profilev1.Location, 0, len(p.Location))
   166  	r.Mapping = make([]*profilev1.Mapping, 0, len(p.Mapping))
   167  	r.Function = make([]*profilev1.Function, 0, len(p.Function))
   168  
   169  	addString(strings, "")
   170  	for _, st := range p.SampleType {
   171  		r.SampleType = append(r.SampleType, &profilev1.ValueType{
   172  			Type: addString(strings, st.Type),
   173  			Unit: addString(strings, st.Unit),
   174  		})
   175  	}
   176  	for _, s := range p.Sample {
   177  		sample := &profilev1.Sample{
   178  			LocationId: make([]uint64, len(s.Location)),
   179  			Value:      s.Value,
   180  		}
   181  		for i, loc := range s.Location {
   182  			sample.LocationId[i] = loc.ID
   183  		}
   184  		var keys []string
   185  		for k := range s.Label {
   186  			keys = append(keys, k)
   187  		}
   188  		sort.Strings(keys)
   189  		for _, k := range keys {
   190  			vs := s.Label[k]
   191  			for _, v := range vs {
   192  				sample.Label = append(sample.Label,
   193  					&profilev1.Label{
   194  						Key: addString(strings, k),
   195  						Str: addString(strings, v),
   196  					},
   197  				)
   198  			}
   199  		}
   200  		var numKeys []string
   201  		for k := range s.NumLabel {
   202  			numKeys = append(numKeys, k)
   203  		}
   204  		sort.Strings(numKeys)
   205  		for _, k := range numKeys {
   206  			keyX := addString(strings, k)
   207  			vs := s.NumLabel[k]
   208  			units := s.NumUnit[k]
   209  			for i, v := range vs {
   210  				var unitX int64
   211  				if len(units) != 0 {
   212  					unitX = addString(strings, units[i])
   213  				}
   214  				sample.Label = append(sample.Label,
   215  					&profilev1.Label{
   216  						Key:     keyX,
   217  						Num:     v,
   218  						NumUnit: unitX,
   219  					},
   220  				)
   221  			}
   222  		}
   223  		r.Sample = append(r.Sample, sample)
   224  	}
   225  
   226  	for _, m := range p.Mapping {
   227  		r.Mapping = append(r.Mapping, &profilev1.Mapping{
   228  			Id:              m.ID,
   229  			Filename:        addString(strings, m.File),
   230  			MemoryStart:     m.Start,
   231  			MemoryLimit:     m.Limit,
   232  			FileOffset:      m.Offset,
   233  			BuildId:         addString(strings, m.BuildID),
   234  			HasFunctions:    m.HasFunctions,
   235  			HasFilenames:    m.HasFilenames,
   236  			HasLineNumbers:  m.HasLineNumbers,
   237  			HasInlineFrames: m.HasInlineFrames,
   238  		})
   239  	}
   240  
   241  	for _, l := range p.Location {
   242  		loc := &profilev1.Location{
   243  			Id:       l.ID,
   244  			Line:     make([]*profilev1.Line, len(l.Line)),
   245  			IsFolded: l.IsFolded,
   246  			Address:  l.Address,
   247  		}
   248  		if l.Mapping != nil {
   249  			loc.MappingId = l.Mapping.ID
   250  		}
   251  		for i, ln := range l.Line {
   252  			if ln.Function != nil {
   253  				loc.Line[i] = &profilev1.Line{
   254  					FunctionId: ln.Function.ID,
   255  					Line:       ln.Line,
   256  				}
   257  			} else {
   258  				loc.Line[i] = &profilev1.Line{
   259  					FunctionId: 0,
   260  					Line:       ln.Line,
   261  				}
   262  			}
   263  		}
   264  		r.Location = append(r.Location, loc)
   265  	}
   266  	for _, f := range p.Function {
   267  		r.Function = append(r.Function, &profilev1.Function{
   268  			Id:         f.ID,
   269  			Name:       addString(strings, f.Name),
   270  			SystemName: addString(strings, f.SystemName),
   271  			Filename:   addString(strings, f.Filename),
   272  			StartLine:  f.StartLine,
   273  		})
   274  	}
   275  
   276  	r.DropFrames = addString(strings, p.DropFrames)
   277  	r.KeepFrames = addString(strings, p.KeepFrames)
   278  
   279  	if pt := p.PeriodType; pt != nil {
   280  		r.PeriodType = &profilev1.ValueType{
   281  			Type: addString(strings, pt.Type),
   282  			Unit: addString(strings, pt.Unit),
   283  		}
   284  	}
   285  
   286  	for _, c := range p.Comments {
   287  		r.Comment = append(r.Comment, addString(strings, c))
   288  	}
   289  
   290  	r.DefaultSampleType = addString(strings, p.DefaultSampleType)
   291  	r.DurationNanos = p.DurationNanos
   292  	r.TimeNanos = p.TimeNanos
   293  	r.Period = p.Period
   294  	r.StringTable = make([]string, len(strings))
   295  	for s, i := range strings {
   296  		r.StringTable[i] = s
   297  	}
   298  	return &r, nil
   299  }
   300  
   301  func addString(strings map[string]int, s string) int64 {
   302  	i, ok := strings[s]
   303  	if !ok {
   304  		i = len(strings)
   305  		strings[s] = i
   306  	}
   307  	return int64(i)
   308  }
   309  
   310  func OpenFile(path string) (*Profile, error) {
   311  	data, err := os.ReadFile(path)
   312  	if err != nil {
   313  		return nil, err
   314  	}
   315  
   316  	return RawFromBytes(data)
   317  }
   318  
   319  type Profile struct {
   320  	*profilev1.Profile
   321  	hasher  SampleHasher
   322  	stats   sanitizeStats
   323  	rawSize int
   324  }
   325  
   326  // RawSize of the profile
   327  func (p *Profile) RawSize() int {
   328  	return p.rawSize
   329  }
   330  
   331  // WriteTo writes the profile to the given writer.
   332  func (p *Profile) WriteTo(w io.Writer) (int64, error) {
   333  	buf := bufPool.Get().(*bytes.Buffer)
   334  	defer func() {
   335  		buf.Reset()
   336  		bufPool.Put(buf)
   337  	}()
   338  	buf.Grow(p.SizeVT())
   339  	data := buf.Bytes()
   340  	n, err := p.MarshalToVT(data)
   341  	if err != nil {
   342  		return 0, err
   343  	}
   344  	data = data[:n]
   345  
   346  	gzipWriter := gzipWriterPool.Get().(*gzip.Writer)
   347  	gzipWriter.Reset(w)
   348  	defer func() {
   349  		// reset gzip writer and return to pool
   350  		gzipWriter.Reset(io.Discard)
   351  		gzipWriterPool.Put(gzipWriter)
   352  	}()
   353  
   354  	written, err := gzipWriter.Write(data)
   355  	if err != nil {
   356  		return 0, errors.Wrap(err, "gzip write")
   357  	}
   358  	if err := gzipWriter.Close(); err != nil {
   359  		return 0, errors.Wrap(err, "gzip close")
   360  	}
   361  	return int64(written), nil
   362  }
   363  
   364  type sortedSample struct {
   365  	samples []*profilev1.Sample
   366  	hashes  []uint64
   367  }
   368  
   369  func (s *sortedSample) Len() int {
   370  	return len(s.samples)
   371  }
   372  
   373  func (s *sortedSample) Less(i, j int) bool {
   374  	return s.hashes[i] < s.hashes[j]
   375  }
   376  
   377  func (s *sortedSample) Swap(i, j int) {
   378  	s.samples[i], s.samples[j] = s.samples[j], s.samples[i]
   379  	s.hashes[i], s.hashes[j] = s.hashes[j], s.hashes[i]
   380  }
   381  
   382  var currentTime = time.Now
   383  
   384  // Normalize normalizes the profile by:
   385  //   - Removing all duplicate samples (summing their values).
   386  //   - Removing redundant profile labels (byte => unique of an allocation site)
   387  //     todo: We should reassess if this was a good choice because by merging duplicate stacktrace samples
   388  //     we cannot recompute the allocation per site ("bytes") profile label.
   389  //   - Removing empty samples.
   390  //   - Then remove unused references.
   391  //   - Ensure that the profile has a time_nanos set
   392  //   - Removes addresses from symbolized profiles.
   393  //   - Removes elements with invalid references.
   394  //   - Converts identifiers to indices.
   395  //   - Ensures that string_table[0] is "".
   396  func (p *Profile) Normalize() {
   397  	p.stats.samplesTotal = len(p.Sample)
   398  
   399  	// if the profile has no time, set it to now
   400  	if p.TimeNanos == 0 {
   401  		p.TimeNanos = currentTime().UnixNano()
   402  	}
   403  
   404  	// Non-string labels are not supported.
   405  	for _, sample := range p.Sample {
   406  		sample.Label = slices.RemoveInPlace(sample.Label, func(label *profilev1.Label, i int) bool {
   407  			return label.Str == 0
   408  		})
   409  	}
   410  
   411  	// Remove samples.
   412  	var removedSamples []*profilev1.Sample
   413  	p.Sample = slices.RemoveInPlace(p.Sample, func(s *profilev1.Sample, i int) bool {
   414  		for j := 0; j < len(s.Value); j++ {
   415  			if s.Value[j] < 0 {
   416  				removedSamples = append(removedSamples, s)
   417  				p.stats.sampleValueNegative++
   418  				return true
   419  			}
   420  		}
   421  		for j := 0; j < len(s.Value); j++ {
   422  			if s.Value[j] > 0 {
   423  				return false
   424  			}
   425  		}
   426  		p.stats.sampleValueZero++
   427  		removedSamples = append(removedSamples, s)
   428  		return true
   429  	})
   430  
   431  	// first we sort the samples.
   432  	hashes := p.hasher.Hashes(p.Sample)
   433  	ss := &sortedSample{samples: p.Sample, hashes: hashes}
   434  	sort.Sort(ss)
   435  	p.Sample = ss.samples
   436  	hashes = ss.hashes
   437  
   438  	p.Sample = slices.RemoveInPlace(p.Sample, func(s *profilev1.Sample, i int) bool {
   439  		// if the next sample has the same hash and labels, we can remove this sample but add the value to the next sample.
   440  		if i < len(p.Sample)-1 && hashes[i] == hashes[i+1] {
   441  			// todo handle hashes collisions
   442  			for j := 0; j < len(s.Value); j++ {
   443  				p.Sample[i+1].Value[j] += s.Value[j]
   444  			}
   445  			removedSamples = append(removedSamples, s)
   446  			p.stats.sampleDuplicate++
   447  			return true
   448  		}
   449  		return false
   450  	})
   451  	// Remove references to removed samples.
   452  	p.clearSampleReferences(removedSamples)
   453  	sanitizeProfile(p.Profile, &p.stats)
   454  	p.clearAddresses()
   455  }
   456  
   457  // Removes addresses from symbolized profiles.
   458  func (p *Profile) clearAddresses() {
   459  	for _, m := range p.Mapping {
   460  		if m.HasFunctions {
   461  			m.MemoryLimit = 0
   462  			m.FileOffset = 0
   463  			m.MemoryStart = 0
   464  		}
   465  	}
   466  	for _, l := range p.Location {
   467  		if p.Mapping[l.MappingId-1].HasFunctions {
   468  			l.Address = 0
   469  		}
   470  	}
   471  }
   472  
   473  func (p *Profile) clearSampleReferences(samples []*profilev1.Sample) {
   474  	if len(samples) == 0 {
   475  		return
   476  	}
   477  	// remove all data not used anymore.
   478  	removedLocationIds := map[uint64]struct{}{}
   479  
   480  	for _, s := range samples {
   481  		for _, l := range s.LocationId {
   482  			removedLocationIds[l] = struct{}{}
   483  		}
   484  	}
   485  
   486  	// figure which removed Locations IDs are not used.
   487  	for _, s := range p.Sample {
   488  		for _, l := range s.LocationId {
   489  			delete(removedLocationIds, l)
   490  		}
   491  	}
   492  	if len(removedLocationIds) == 0 {
   493  		return
   494  	}
   495  	removedFunctionIds := map[uint64]struct{}{}
   496  	// remove the locations that are not used anymore.
   497  	p.Location = slices.RemoveInPlace(p.Location, func(loc *profilev1.Location, _ int) bool {
   498  		if _, ok := removedLocationIds[loc.Id]; ok {
   499  			for _, l := range loc.Line {
   500  				removedFunctionIds[l.FunctionId] = struct{}{}
   501  			}
   502  			return true
   503  		}
   504  		return false
   505  	})
   506  
   507  	if len(removedFunctionIds) == 0 {
   508  		return
   509  	}
   510  	// figure which removed Function IDs are not used.
   511  	for _, l := range p.Location {
   512  		for _, f := range l.Line {
   513  			// 	// that ID is used in another location, remove it.
   514  			delete(removedFunctionIds, f.FunctionId)
   515  		}
   516  	}
   517  	removedNamesMap := map[int64]struct{}{}
   518  	// remove the functions that are not used anymore.
   519  	p.Function = slices.RemoveInPlace(p.Function, func(fn *profilev1.Function, _ int) bool {
   520  		if _, ok := removedFunctionIds[fn.Id]; ok {
   521  			removedNamesMap[fn.Name] = struct{}{}
   522  			removedNamesMap[fn.SystemName] = struct{}{}
   523  			removedNamesMap[fn.Filename] = struct{}{}
   524  			return true
   525  		}
   526  		return false
   527  	})
   528  
   529  	if len(removedNamesMap) == 0 {
   530  		return
   531  	}
   532  	// remove names that are still used.
   533  	p.visitAllNameReferences(func(idx *int64) {
   534  		delete(removedNamesMap, *idx)
   535  	})
   536  	if len(removedNamesMap) == 0 {
   537  		return
   538  	}
   539  
   540  	// remove the names that are not used anymore.
   541  	p.StringTable = lo.Reject(p.StringTable, func(_ string, i int) bool {
   542  		_, ok := removedNamesMap[int64(i)]
   543  		return ok
   544  	})
   545  	removedNames := lo.Keys(removedNamesMap)
   546  	// Sort to remove in order.
   547  	sort.Slice(removedNames, func(i, j int) bool { return removedNames[i] < removedNames[j] })
   548  	// Now shift all indices [0,1,2,3,4,5,6]
   549  	// if we removed [1,2,5] then we need to shift [3,4] to [1,2] and [6] to [3]
   550  	// Basically we need to shift all indices that are greater than the removed index by the amount of removed indices.
   551  	p.visitAllNameReferences(func(idx *int64) {
   552  		var shift int64
   553  		for i := 0; i < len(removedNames); i++ {
   554  			if *idx > removedNames[i] {
   555  				shift++
   556  				continue
   557  			}
   558  			break
   559  		}
   560  		*idx -= shift
   561  	})
   562  }
   563  
   564  func (p *Profile) visitAllNameReferences(fn func(*int64)) {
   565  	fn(&p.DropFrames)
   566  	fn(&p.KeepFrames)
   567  	fn(&p.PeriodType.Type)
   568  	fn(&p.PeriodType.Unit)
   569  	for _, st := range p.SampleType {
   570  		fn(&st.Type)
   571  		fn(&st.Unit)
   572  	}
   573  	for _, m := range p.Mapping {
   574  		fn(&m.Filename)
   575  		fn(&m.BuildId)
   576  	}
   577  	for _, s := range p.Sample {
   578  		for _, l := range s.Label {
   579  			fn(&l.Key)
   580  			fn(&l.Num)
   581  			fn(&l.NumUnit)
   582  		}
   583  	}
   584  	for _, f := range p.Function {
   585  		fn(&f.Name)
   586  		fn(&f.SystemName)
   587  		fn(&f.Filename)
   588  	}
   589  	for i := 0; i < len(p.Comment); i++ {
   590  		fn(&p.Comment[i])
   591  	}
   592  }
   593  
   594  type SampleHasher struct {
   595  	hash *xxhash.Digest
   596  	b    [8]byte
   597  }
   598  
   599  func (h SampleHasher) Hashes(samples []*profilev1.Sample) []uint64 {
   600  	if h.hash == nil {
   601  		h.hash = xxhash.New()
   602  	} else {
   603  		h.hash.Reset()
   604  	}
   605  
   606  	hashes := make([]uint64, len(samples))
   607  	for i, sample := range samples {
   608  		if _, err := h.hash.Write(uint64Bytes(sample.LocationId)); err != nil {
   609  			panic("unable to write hash")
   610  		}
   611  		sort.Sort(LabelsByKeyValue(sample.Label))
   612  		for _, l := range sample.Label {
   613  			binary.LittleEndian.PutUint32(h.b[:4], uint32(l.Key))
   614  			binary.LittleEndian.PutUint32(h.b[4:], uint32(l.Str))
   615  			if _, err := h.hash.Write(h.b[:]); err != nil {
   616  				panic("unable to write label hash")
   617  			}
   618  		}
   619  		hashes[i] = h.hash.Sum64()
   620  		h.hash.Reset()
   621  	}
   622  
   623  	return hashes
   624  }
   625  
   626  func uint64Bytes(s []uint64) []byte {
   627  	if len(s) == 0 {
   628  		return nil
   629  	}
   630  	p := (*byte)(unsafe.Pointer(&s[0]))
   631  	return unsafe.Slice(p, len(s)*8)
   632  }
   633  
   634  type SamplesByLabels []*profilev1.Sample
   635  
   636  func (s SamplesByLabels) Len() int {
   637  	return len(s)
   638  }
   639  
   640  func (s SamplesByLabels) Less(i, j int) bool {
   641  	return CompareSampleLabels(s[i].Label, s[j].Label) < 0
   642  }
   643  
   644  func (s SamplesByLabels) Swap(i, j int) {
   645  	s[i], s[j] = s[j], s[i]
   646  }
   647  
   648  type LabelsByKeyValue []*profilev1.Label
   649  
   650  func (l LabelsByKeyValue) Len() int {
   651  	return len(l)
   652  }
   653  
   654  func (l LabelsByKeyValue) Less(i, j int) bool {
   655  	a, b := l[i], l[j]
   656  	if a.Key == b.Key {
   657  		return a.Str < b.Str
   658  	}
   659  	return a.Key < b.Key
   660  }
   661  
   662  func (l LabelsByKeyValue) Swap(i, j int) {
   663  	l[i], l[j] = l[j], l[i]
   664  }
   665  
   666  // SampleGroup refers to a group of samples that share the same
   667  // labels. Note that the Span ID label is handled in a special
   668  // way and is not included in the Labels member but is kept as
   669  // as a sample label.
   670  type SampleGroup struct {
   671  	Labels  []*profilev1.Label
   672  	Samples []*profilev1.Sample
   673  }
   674  
   675  // GroupSamplesByLabels splits samples into groups by labels.
   676  // It's expected that sample labels are sorted.
   677  func GroupSamplesByLabels(p *profilev1.Profile) []SampleGroup {
   678  	if len(p.Sample) < 1 {
   679  		return nil
   680  	}
   681  	var result []SampleGroup
   682  	var start int
   683  	labels := p.Sample[start].Label
   684  	for i := 1; i < len(p.Sample); i++ {
   685  		if CompareSampleLabels(p.Sample[i].Label, labels) != 0 {
   686  			result = append(result, SampleGroup{
   687  				Labels:  labels,
   688  				Samples: p.Sample[start:i],
   689  			})
   690  			start = i
   691  			labels = p.Sample[i].Label
   692  		}
   693  	}
   694  	return append(result, SampleGroup{
   695  		Labels:  labels,
   696  		Samples: p.Sample[start:],
   697  	})
   698  }
   699  
   700  // GroupSamplesWithoutLabels splits samples into groups by labels
   701  // ignoring ones from the list: those are preserved as sample labels.
   702  // It's expected that sample labels are sorted.
   703  func GroupSamplesWithoutLabels(p *profilev1.Profile, labels ...string) []SampleGroup {
   704  	if len(labels) > 0 {
   705  		return GroupSamplesWithoutLabelsByKey(p, LabelKeysByString(p, labels...))
   706  	}
   707  	return GroupSamplesByLabels(p)
   708  }
   709  
   710  func GroupSamplesWithoutLabelsByKey(p *profilev1.Profile, keys []int64) []SampleGroup {
   711  	if len(p.Sample) == 0 {
   712  		return nil
   713  	}
   714  	for _, s := range p.Sample {
   715  		sort.Sort(LabelsByKeyValue(s.Label))
   716  		// We hide labels matching the keys to the end
   717  		// of the slice, after len() boundary.
   718  		s.Label = LabelsWithout(s.Label, keys)
   719  		sort.Sort(LabelsByKeyValue(s.Label)) // TODO: Find a way to avoid this.
   720  	}
   721  	// Sorting and grouping accounts only for labels kept.
   722  	sort.Sort(SamplesByLabels(p.Sample))
   723  	groups := GroupSamplesByLabels(p)
   724  	for _, s := range p.Sample {
   725  		// Replace the labels (that match the group name)
   726  		// with hidden labels matching the keys.
   727  		s.Label = restoreRemovedLabels(s.Label)
   728  	}
   729  	return groups
   730  }
   731  
   732  func restoreRemovedLabels(labels []*profilev1.Label) []*profilev1.Label {
   733  	labels = labels[len(labels):cap(labels)]
   734  	for i, l := range labels {
   735  		if l == nil { // labels had extra capacity in sample labels
   736  			labels = labels[:i]
   737  			break
   738  		}
   739  	}
   740  	return labels
   741  }
   742  
   743  // CompareSampleLabels compares sample label pairs.
   744  // It's expected that sample labels are sorted.
   745  // The result will be 0 if a == b, < 0 if a < b, and > 0 if a > b.
   746  func CompareSampleLabels(a, b []*profilev1.Label) int {
   747  	l := len(a)
   748  	if len(b) < l {
   749  		l = len(b)
   750  	}
   751  	for i := 0; i < l; i++ {
   752  		if a[i].Key != b[i].Key {
   753  			if a[i].Key < b[i].Key {
   754  				return -1
   755  			}
   756  			return 1
   757  		}
   758  		if a[i].Str != b[i].Str {
   759  			if a[i].Str < b[i].Str {
   760  				return -1
   761  			}
   762  			return 1
   763  		}
   764  	}
   765  	return len(a) - len(b)
   766  }
   767  
   768  func LabelsWithout(labels []*profilev1.Label, keys []int64) []*profilev1.Label {
   769  	n := FilterLabelsInPlace(labels, keys)
   770  	slices.Reverse(labels) // TODO: Find a way to avoid this.
   771  	return labels[:len(labels)-n]
   772  }
   773  
   774  func FilterLabelsInPlace(labels []*profilev1.Label, keys []int64) int {
   775  	boundaryIdx := 0
   776  	i := 0 // Pointer to labels
   777  	j := 0 // Pointer to keys
   778  	for i < len(labels) && j < len(keys) {
   779  		if labels[i].Key == keys[j] {
   780  			// If label key matches a key in keys, swap and increment both pointers
   781  			labels[i], labels[boundaryIdx] = labels[boundaryIdx], labels[i]
   782  			boundaryIdx++
   783  			i++
   784  		} else if labels[i].Key < keys[j] {
   785  			i++ // Advance label pointer.
   786  		} else {
   787  			j++ // Advance key pointer.
   788  		}
   789  	}
   790  	return boundaryIdx
   791  }
   792  
   793  func LabelKeysByString(p *profilev1.Profile, keys ...string) []int64 {
   794  	m := LabelKeysMapByString(p, keys...)
   795  	s := make([]int64, len(keys))
   796  	for i, k := range keys {
   797  		s[i] = m[k]
   798  	}
   799  	sort.Slice(s, func(i, j int) bool {
   800  		return s[i] < s[j]
   801  	})
   802  	return s
   803  }
   804  
   805  func LabelKeysMapByString(p *profilev1.Profile, keys ...string) map[string]int64 {
   806  	m := make(map[string]int64, len(keys))
   807  	for _, k := range keys {
   808  		m[k] = 0
   809  	}
   810  	for i, v := range p.StringTable {
   811  		if _, ok := m[v]; ok {
   812  			m[v] = int64(i)
   813  		}
   814  	}
   815  	return m
   816  }
   817  
   818  type SampleExporter struct {
   819  	profile *profilev1.Profile
   820  
   821  	locations lookupTable
   822  	functions lookupTable
   823  	mappings  lookupTable
   824  	strings   lookupTable
   825  }
   826  
   827  type lookupTable struct {
   828  	indices  []int32
   829  	resolved int32
   830  }
   831  
   832  func (t *lookupTable) lookupString(idx int64) int64 {
   833  	if idx != 0 {
   834  		return int64(t.lookup(idx))
   835  	}
   836  	return 0
   837  }
   838  
   839  func (t *lookupTable) lookup(idx int64) int32 {
   840  	x := t.indices[idx]
   841  	if x != 0 {
   842  		return x
   843  	}
   844  	t.resolved++
   845  	t.indices[idx] = t.resolved
   846  	return t.resolved
   847  }
   848  
   849  func (t *lookupTable) reset() {
   850  	t.resolved = 0
   851  	for i := 0; i < len(t.indices); i++ {
   852  		t.indices[i] = 0
   853  	}
   854  }
   855  
   856  func NewSampleExporter(p *profilev1.Profile) *SampleExporter {
   857  	return &SampleExporter{
   858  		profile:   p,
   859  		locations: lookupTable{indices: make([]int32, len(p.Location))},
   860  		functions: lookupTable{indices: make([]int32, len(p.Function))},
   861  		mappings:  lookupTable{indices: make([]int32, len(p.Mapping))},
   862  		strings:   lookupTable{indices: make([]int32, len(p.StringTable))},
   863  	}
   864  }
   865  
   866  // ExportSamples creates a new complete profile with the subset
   867  // of samples provided. It is assumed that those are part of the
   868  // source profile. Provided samples are modified in place.
   869  //
   870  // The same exporter instance can be used to export non-overlapping
   871  // sample sets from a single profile.
   872  func (e *SampleExporter) ExportSamples(dst *profilev1.Profile, samples []*profilev1.Sample) *profilev1.Profile {
   873  	e.reset()
   874  
   875  	dst.Sample = samples
   876  	dst.TimeNanos = e.profile.TimeNanos
   877  	dst.DurationNanos = e.profile.DurationNanos
   878  	dst.Period = e.profile.Period
   879  	dst.DefaultSampleType = e.profile.DefaultSampleType
   880  
   881  	dst.SampleType = slices.GrowLen(dst.SampleType, len(e.profile.SampleType))
   882  	for i, v := range e.profile.SampleType {
   883  		dst.SampleType[i] = &profilev1.ValueType{
   884  			Type: e.strings.lookupString(v.Type),
   885  			Unit: e.strings.lookupString(v.Unit),
   886  		}
   887  	}
   888  	dst.DropFrames = e.strings.lookupString(e.profile.DropFrames)
   889  	dst.KeepFrames = e.strings.lookupString(e.profile.KeepFrames)
   890  	if c := len(e.profile.Comment); c > 0 {
   891  		dst.Comment = slices.GrowLen(dst.Comment, c)
   892  		for i, comment := range e.profile.Comment {
   893  			dst.Comment[i] = e.strings.lookupString(comment)
   894  		}
   895  	}
   896  
   897  	// Rewrite sample stack traces and labels.
   898  	// Note that the provided samples are modified in-place.
   899  	for _, sample := range dst.Sample {
   900  		for i, location := range sample.LocationId {
   901  			sample.LocationId[i] = uint64(e.locations.lookup(int64(location - 1)))
   902  		}
   903  		for _, label := range sample.Label {
   904  			label.Key = e.strings.lookupString(label.Key)
   905  			if label.Str != 0 {
   906  				label.Str = e.strings.lookupString(label.Str)
   907  			} else {
   908  				label.NumUnit = e.strings.lookupString(label.NumUnit)
   909  			}
   910  		}
   911  	}
   912  
   913  	// Copy locations.
   914  	dst.Location = slices.GrowLen(dst.Location, int(e.locations.resolved))
   915  	for i, j := range e.locations.indices {
   916  		// i points to the location in the source profile.
   917  		// j point to the location in the new profile.
   918  		if j == 0 {
   919  			// The location is not referenced by any of the samples.
   920  			continue
   921  		}
   922  		loc := e.profile.Location[i]
   923  		newLoc := &profilev1.Location{
   924  			Id:        uint64(j),
   925  			MappingId: uint64(e.mappings.lookup(int64(loc.MappingId - 1))),
   926  			Address:   loc.Address,
   927  			Line:      make([]*profilev1.Line, len(loc.Line)),
   928  			IsFolded:  loc.IsFolded,
   929  		}
   930  		dst.Location[j-1] = newLoc
   931  		for l, line := range loc.Line {
   932  			newLoc.Line[l] = &profilev1.Line{
   933  				FunctionId: uint64(e.functions.lookup(int64(line.FunctionId - 1))),
   934  				Line:       line.Line,
   935  			}
   936  		}
   937  	}
   938  
   939  	// Copy mappings.
   940  	dst.Mapping = slices.GrowLen(dst.Mapping, int(e.mappings.resolved))
   941  	for i, j := range e.mappings.indices {
   942  		if j == 0 {
   943  			continue
   944  		}
   945  		m := e.profile.Mapping[i]
   946  		dst.Mapping[j-1] = &profilev1.Mapping{
   947  			Id:              uint64(j),
   948  			MemoryStart:     m.MemoryStart,
   949  			MemoryLimit:     m.MemoryLimit,
   950  			FileOffset:      m.FileOffset,
   951  			Filename:        e.strings.lookupString(m.Filename),
   952  			BuildId:         e.strings.lookupString(m.BuildId),
   953  			HasFunctions:    m.HasFunctions,
   954  			HasFilenames:    m.HasFilenames,
   955  			HasLineNumbers:  m.HasLineNumbers,
   956  			HasInlineFrames: m.HasInlineFrames,
   957  		}
   958  	}
   959  
   960  	// Copy functions.
   961  	dst.Function = slices.GrowLen(dst.Function, int(e.functions.resolved))
   962  	for i, j := range e.functions.indices {
   963  		if j == 0 {
   964  			continue
   965  		}
   966  		fn := e.profile.Function[i]
   967  		dst.Function[j-1] = &profilev1.Function{
   968  			Id:         uint64(j),
   969  			Name:       e.strings.lookupString(fn.Name),
   970  			SystemName: e.strings.lookupString(fn.SystemName),
   971  			Filename:   e.strings.lookupString(fn.Filename),
   972  			StartLine:  fn.StartLine,
   973  		}
   974  	}
   975  
   976  	if e.profile.PeriodType != nil {
   977  		dst.PeriodType = &profilev1.ValueType{
   978  			Type: e.strings.lookupString(e.profile.PeriodType.Type),
   979  			Unit: e.strings.lookupString(e.profile.PeriodType.Unit),
   980  		}
   981  	}
   982  
   983  	// Copy strings.
   984  	dst.StringTable = slices.GrowLen(dst.StringTable, int(e.strings.resolved)+1)
   985  	for i, j := range e.strings.indices {
   986  		if j == 0 {
   987  			continue
   988  		}
   989  		dst.StringTable[j] = e.profile.StringTable[i]
   990  	}
   991  
   992  	return dst
   993  }
   994  
   995  func (e *SampleExporter) reset() {
   996  	e.locations.reset()
   997  	e.functions.reset()
   998  	e.mappings.reset()
   999  	e.strings.reset()
  1000  }
  1001  
  1002  var uint32SlicePool zeropool.Pool[[]uint32]
  1003  
  1004  const (
  1005  	ProfileIDLabelName = "profile_id" // For compatibility with the existing clients.
  1006  	SpanIDLabelName    = "span_id"    // Will be supported in the future.
  1007  )
  1008  
  1009  func LabelID(p *profilev1.Profile, name string) int64 {
  1010  	for i, s := range p.StringTable {
  1011  		if s == name {
  1012  			return int64(i)
  1013  		}
  1014  	}
  1015  	return -1
  1016  }
  1017  
  1018  func ProfileSpans(p *profilev1.Profile) []uint64 {
  1019  	if i := LabelID(p, SpanIDLabelName); i > 0 {
  1020  		return Spans(p, i)
  1021  	}
  1022  	return nil
  1023  }
  1024  
  1025  func Spans(p *profilev1.Profile, spanIDLabelIdx int64) []uint64 {
  1026  	tmp := make([]byte, 8)
  1027  	s := make([]uint64, len(p.Sample))
  1028  	for i, sample := range p.Sample {
  1029  		s[i] = spanIDFromLabels(tmp, spanIDLabelIdx, p.StringTable, sample.Label)
  1030  	}
  1031  	return s
  1032  }
  1033  
  1034  func spanIDFromLabels(tmp []byte, labelIdx int64, stringTable []string, labels []*profilev1.Label) uint64 {
  1035  	for _, x := range labels {
  1036  		if x.Key != labelIdx {
  1037  			continue
  1038  		}
  1039  		if s := stringTable[x.Str]; decodeSpanID(tmp, s) {
  1040  			return binary.LittleEndian.Uint64(tmp)
  1041  		}
  1042  	}
  1043  	return 0
  1044  }
  1045  
  1046  func decodeSpanID(tmp []byte, s string) bool {
  1047  	if len(s) != 16 {
  1048  		return false
  1049  	}
  1050  	_, err := hex.Decode(tmp, util.YoloBuf(s))
  1051  	return err == nil
  1052  }
  1053  
  1054  func RenameLabel(p *profilev1.Profile, oldName, newName string) {
  1055  	var oi, ni int64
  1056  	for i, s := range p.StringTable {
  1057  		if s == oldName {
  1058  			oi = int64(i)
  1059  			break
  1060  		}
  1061  	}
  1062  	if oi == 0 {
  1063  		return
  1064  	}
  1065  	for i, s := range p.StringTable {
  1066  		if s == newName {
  1067  			ni = int64(i)
  1068  			break
  1069  		}
  1070  	}
  1071  	if ni == 0 {
  1072  		ni = int64(len(p.StringTable))
  1073  		p.StringTable = append(p.StringTable, newName)
  1074  	}
  1075  	for _, s := range p.Sample {
  1076  		for _, l := range s.Label {
  1077  			if l.Key == oi {
  1078  				l.Key = ni
  1079  			}
  1080  		}
  1081  	}
  1082  }
  1083  
  1084  func ZeroLabelStrings(p *profilev1.Profile) {
  1085  	// TODO: A true bitmap should be used instead.
  1086  	st := slices.GrowLen(uint32SlicePool.Get(), len(p.StringTable))
  1087  	slices.Clear(st)
  1088  	defer uint32SlicePool.Put(st)
  1089  	for _, t := range p.SampleType {
  1090  		st[t.Type] = 1
  1091  		st[t.Unit] = 1
  1092  	}
  1093  	for _, f := range p.Function {
  1094  		st[f.Filename] = 1
  1095  		st[f.SystemName] = 1
  1096  		st[f.Name] = 1
  1097  	}
  1098  	for _, m := range p.Mapping {
  1099  		st[m.Filename] = 1
  1100  		st[m.BuildId] = 1
  1101  	}
  1102  	for _, c := range p.Comment {
  1103  		st[c] = 1
  1104  	}
  1105  	st[p.KeepFrames] = 1
  1106  	st[p.DropFrames] = 1
  1107  	var zeroString string
  1108  	for i, v := range st {
  1109  		if v == 0 {
  1110  			p.StringTable[i] = zeroString
  1111  		}
  1112  	}
  1113  }
  1114  
  1115  var languageMatchers = map[string][]string{
  1116  	"go":     {".go", "/usr/local/go/"},
  1117  	"java":   {"java/", "sun/"},
  1118  	"ruby":   {".rb", "gems/"},
  1119  	"nodejs": {"./node_modules/", ".js"},
  1120  	"dotnet": {"System.", "Microsoft."},
  1121  	"python": {".py"},
  1122  	"rust":   {"main.rs", "core.rs"},
  1123  }
  1124  
  1125  func GetLanguage(profile *Profile) string {
  1126  	for _, symbol := range profile.StringTable {
  1127  		for lang, matcherPatterns := range languageMatchers {
  1128  			for _, pattern := range matcherPatterns {
  1129  				if strings.HasPrefix(symbol, pattern) || strings.HasSuffix(symbol, pattern) {
  1130  					return lang
  1131  				}
  1132  			}
  1133  		}
  1134  	}
  1135  	return "unknown"
  1136  }
  1137  
  1138  // SetProfileMetadata sets the metadata on the profile.
  1139  func SetProfileMetadata(p *profilev1.Profile, ty *typesv1.ProfileType, timeNanos int64, period int64) {
  1140  	m := map[string]int64{
  1141  		ty.SampleUnit: -1,
  1142  		ty.SampleType: -1,
  1143  		ty.PeriodType: -1,
  1144  		ty.PeriodUnit: -1,
  1145  	}
  1146  	for i, s := range p.StringTable {
  1147  		if _, ok := m[s]; ok {
  1148  			m[s] = int64(i)
  1149  		}
  1150  	}
  1151  	for _, k := range []string{
  1152  		ty.SampleUnit,
  1153  		ty.SampleType,
  1154  		ty.PeriodType,
  1155  		ty.PeriodUnit,
  1156  	} {
  1157  		if m[k] == -1 {
  1158  			i := int64(len(p.StringTable))
  1159  			p.StringTable = append(p.StringTable, k)
  1160  			m[k] = i
  1161  		}
  1162  	}
  1163  
  1164  	p.SampleType = []*profilev1.ValueType{{Type: m[ty.SampleType], Unit: m[ty.SampleUnit]}}
  1165  	p.DefaultSampleType = m[ty.SampleType]
  1166  	p.PeriodType = &profilev1.ValueType{Type: m[ty.PeriodType], Unit: m[ty.PeriodUnit]}
  1167  	p.TimeNanos = timeNanos
  1168  
  1169  	if period != 0 {
  1170  		p.Period = period
  1171  	}
  1172  
  1173  	// Try to guess period based on the profile type.
  1174  	// TODO: This should be encoded into the profile type.
  1175  	switch ty.Name {
  1176  	case "process_cpu":
  1177  		p.Period = 1000000000
  1178  	case "memory":
  1179  		p.Period = 512 * 1024
  1180  	default:
  1181  		p.Period = 1
  1182  	}
  1183  }
  1184  
  1185  func Marshal(p *profilev1.Profile, compress bool) ([]byte, error) {
  1186  	b, err := p.MarshalVT()
  1187  	if err != nil {
  1188  		return nil, err
  1189  	}
  1190  	if !compress {
  1191  		return b, nil
  1192  	}
  1193  	var buf bytes.Buffer
  1194  	buf.Grow(len(b) / 2)
  1195  	gw := gzipWriterPool.Get().(*gzip.Writer)
  1196  	gw.Reset(&buf)
  1197  	defer func() {
  1198  		gw.Reset(io.Discard)
  1199  		gzipWriterPool.Put(gw)
  1200  	}()
  1201  	if _, err = gw.Write(b); err != nil {
  1202  		return nil, err
  1203  	}
  1204  	if err = gw.Flush(); err != nil {
  1205  		return nil, err
  1206  	}
  1207  	if err = gw.Close(); err != nil {
  1208  		return nil, err
  1209  	}
  1210  	return buf.Bytes(), nil
  1211  }
  1212  
  1213  func MustMarshal(p *profilev1.Profile, compress bool) []byte {
  1214  	b, err := Marshal(p, compress)
  1215  	if err != nil {
  1216  		panic(err)
  1217  	}
  1218  	return b
  1219  }
  1220  
  1221  func Unmarshal(data []byte, p *profilev1.Profile) error {
  1222  	return UnmarshalWithLimit(data, p, 0)
  1223  }
  1224  
  1225  // UnmarshalWithLimit unmarshals a profile from bytes with an optional size limit.
  1226  // maxSize limits the decompressed size in bytes. Use 0 for no limit.
  1227  // This prevents zip bomb attacks where small compressed data expands to huge sizes.
  1228  func UnmarshalWithLimit(data []byte, p *profilev1.Profile, maxSize int64) error {
  1229  	gr := gzipReaderPool.Get().(*gzipReader)
  1230  	defer gzipReaderPool.Put(gr)
  1231  	r, err := gr.openBytes(data)
  1232  	if err != nil {
  1233  		return err
  1234  	}
  1235  	buf := bufPool.Get().(*bytes.Buffer)
  1236  	defer func() {
  1237  		buf.Reset()
  1238  		bufPool.Put(buf)
  1239  	}()
  1240  	buf.Grow(len(data) * 2)
  1241  
  1242  	// Apply size limit if specified (maxSize >= 0)
  1243  	// maxSize == 0 means no limit (unlimited decompression)
  1244  	if maxSize > 0 {
  1245  		r = io.LimitReader(r, maxSize+1) // +1 to detect if limit is exceeded
  1246  	}
  1247  
  1248  	if _, err = io.Copy(buf, r); err != nil {
  1249  		return err
  1250  	}
  1251  
  1252  	// Check if we hit the size limit
  1253  	if maxSize > 0 && int64(buf.Len()) > maxSize {
  1254  		return &ErrDecompressedSizeExceedsLimit{Limit: maxSize}
  1255  	}
  1256  
  1257  	return p.UnmarshalVT(buf.Bytes())
  1258  }
  1259  
  1260  func sanitizeProfile(p *profilev1.Profile, stats *sanitizeStats) {
  1261  	if p == nil {
  1262  		return
  1263  	}
  1264  	if stats.samplesTotal == 0 {
  1265  		stats.samplesTotal = len(p.Sample)
  1266  	}
  1267  	ms := int64(len(p.StringTable))
  1268  	// Handle the case when "" is not present,
  1269  	// or is not at string_table[0].
  1270  	z := int64(-1)
  1271  	for i, s := range p.StringTable {
  1272  		if s == "" {
  1273  			z = int64(i)
  1274  			break
  1275  		}
  1276  	}
  1277  	if z == -1 {
  1278  		// No empty string found in the table.
  1279  		// Reduce number of invariants by adding one.
  1280  		z = ms
  1281  		p.StringTable = append(p.StringTable, "")
  1282  		ms++
  1283  	}
  1284  	// Swap zero string.
  1285  	p.StringTable[z], p.StringTable[0] = p.StringTable[0], p.StringTable[z]
  1286  	// Now we need to update references to strings:
  1287  	// invalid references (>= len(string_table)) are set to 0.
  1288  	// references to empty string are set to 0.
  1289  	str := func(i int64) int64 {
  1290  		if i == 0 && z > 0 {
  1291  			// z > 0 indicates that "" is not at string_table[0].
  1292  			// This means that element that used to be at 0 has
  1293  			// been moved to z.
  1294  			return z
  1295  		}
  1296  		if i == z || i >= ms || i < 0 {
  1297  			// The reference to empty string, or a string that is
  1298  			// not present in the table.
  1299  			return 0
  1300  		}
  1301  		return i
  1302  	}
  1303  
  1304  	p.SampleType = slices.RemoveInPlace(p.SampleType, func(x *profilev1.ValueType, _ int) bool {
  1305  		if x == nil {
  1306  			stats.sampleTypeNil++
  1307  			return true
  1308  		}
  1309  		x.Type = str(x.Type)
  1310  		x.Unit = str(x.Unit)
  1311  		return false
  1312  	})
  1313  	if p.PeriodType != nil {
  1314  		p.PeriodType.Type = str(p.PeriodType.Type)
  1315  		p.PeriodType.Unit = str(p.PeriodType.Unit)
  1316  	}
  1317  
  1318  	p.DefaultSampleType = str(p.DefaultSampleType)
  1319  	p.DropFrames = str(p.DropFrames)
  1320  	p.KeepFrames = str(p.KeepFrames)
  1321  	for i := range p.Comment {
  1322  		p.Comment[i] = str(p.Comment[i])
  1323  	}
  1324  
  1325  	// Sanitize mappings and references to them.
  1326  	// Locations with invalid references are removed.
  1327  	t := make(map[uint64]uint64, len(p.Location))
  1328  	j := uint64(1)
  1329  	p.Mapping = slices.RemoveInPlace(p.Mapping, func(x *profilev1.Mapping, _ int) bool {
  1330  		if x == nil {
  1331  			stats.mappingNil++
  1332  			return true
  1333  		}
  1334  		x.BuildId = str(x.BuildId)
  1335  		x.Filename = str(x.Filename)
  1336  		x.Id, t[x.Id] = j, j
  1337  		j++
  1338  		return false
  1339  	})
  1340  
  1341  	// Rewrite references to mappings, removing invalid ones.
  1342  	// Locations with mapping ID 0 are allowed: in this case,
  1343  	// a mapping stub is created.
  1344  	var mapping *profilev1.Mapping
  1345  	p.Location = slices.RemoveInPlace(p.Location, func(x *profilev1.Location, _ int) bool {
  1346  		if x == nil {
  1347  			stats.locationNil++
  1348  			return true
  1349  		}
  1350  		if len(x.Line) == 0 && x.Address == 0 {
  1351  			stats.locationEmpty++
  1352  			return true
  1353  		}
  1354  		if x.MappingId == 0 {
  1355  			if mapping == nil {
  1356  				mapping = &profilev1.Mapping{Id: uint64(len(p.Mapping) + 1)}
  1357  				p.Mapping = append(p.Mapping, mapping)
  1358  			}
  1359  			x.MappingId = mapping.Id
  1360  			return false
  1361  		}
  1362  		x.MappingId = t[x.MappingId]
  1363  		if x.MappingId == 0 {
  1364  			stats.locationMappingInvalid++
  1365  			return true
  1366  		}
  1367  		return false
  1368  	})
  1369  
  1370  	// Sanitize functions and references to them.
  1371  	// Locations with invalid references are removed.
  1372  	clear(t)
  1373  	j = 1
  1374  	p.Function = slices.RemoveInPlace(p.Function, func(x *profilev1.Function, _ int) bool {
  1375  		if x == nil {
  1376  			stats.functionNil++
  1377  			return true
  1378  		}
  1379  		x.Name = str(x.Name)
  1380  		x.SystemName = str(x.SystemName)
  1381  		x.Filename = str(x.Filename)
  1382  		x.Id, t[x.Id] = j, j
  1383  		j++
  1384  		return false
  1385  	})
  1386  	// Check locations again, verifying that all functions are valid.
  1387  	p.Location = slices.RemoveInPlace(p.Location, func(x *profilev1.Location, _ int) bool {
  1388  		for _, line := range x.Line {
  1389  			if line.FunctionId = t[line.FunctionId]; line.FunctionId == 0 {
  1390  				stats.locationFunctionInvalid++
  1391  				return true
  1392  			}
  1393  		}
  1394  		return false
  1395  	})
  1396  
  1397  	// Sanitize locations and references to them.
  1398  	// Samples with invalid references are removed.
  1399  	clear(t)
  1400  	j = 1
  1401  	for _, x := range p.Location {
  1402  		x.Id, t[x.Id] = j, j
  1403  		j++
  1404  	}
  1405  
  1406  	vs := len(p.SampleType)
  1407  	p.Sample = slices.RemoveInPlace(p.Sample, func(x *profilev1.Sample, _ int) bool {
  1408  		if x == nil {
  1409  			stats.sampleNil++
  1410  			return true
  1411  		}
  1412  		if len(x.Value) != vs {
  1413  			stats.sampleValueMismatch++
  1414  			return true
  1415  		}
  1416  		for i := range x.LocationId {
  1417  			if x.LocationId[i] = t[x.LocationId[i]]; x.LocationId[i] == 0 {
  1418  				stats.sampleLocationInvalid++
  1419  				return true
  1420  			}
  1421  		}
  1422  		for _, l := range x.Label {
  1423  			if l == nil {
  1424  				stats.sampleLabelNil++
  1425  				return true
  1426  			}
  1427  			l.Key = str(l.Key)
  1428  			l.Str = str(l.Str)
  1429  			l.NumUnit = str(l.NumUnit)
  1430  		}
  1431  		return false
  1432  	})
  1433  }
  1434  
  1435  type sanitizeStats struct {
  1436  	samplesTotal  int
  1437  	sampleTypeNil int
  1438  
  1439  	mappingNil              int
  1440  	functionNil             int
  1441  	locationNil             int
  1442  	locationEmpty           int
  1443  	locationMappingInvalid  int
  1444  	locationFunctionInvalid int
  1445  
  1446  	sampleNil             int
  1447  	sampleLabelNil        int
  1448  	sampleLocationInvalid int
  1449  	sampleValueMismatch   int
  1450  	sampleValueNegative   int
  1451  	sampleValueZero       int
  1452  	sampleDuplicate       int
  1453  }
  1454  
  1455  func (s *sanitizeStats) pretty() string {
  1456  	var b strings.Builder
  1457  	b.WriteString("samples_total=")
  1458  	b.WriteString(strconv.Itoa(s.samplesTotal))
  1459  	put := func(k string, v int) {
  1460  		if v > 0 {
  1461  			b.WriteString(" ")
  1462  			b.WriteString(k)
  1463  			b.WriteString("=")
  1464  			b.WriteString(strconv.Itoa(v))
  1465  		}
  1466  	}
  1467  	put("sample_type_nil", s.sampleTypeNil)
  1468  	put("mapping_nil", s.mappingNil)
  1469  	put("function_nil", s.functionNil)
  1470  	put("location_nil", s.locationNil)
  1471  	put("location_empty", s.locationEmpty)
  1472  	put("location_mapping_invalid", s.locationMappingInvalid)
  1473  	put("location_function_invalid", s.locationFunctionInvalid)
  1474  	put("sample_nil", s.sampleNil)
  1475  	put("sample_label_nil", s.sampleLabelNil)
  1476  	put("sample_location_invalid", s.sampleLocationInvalid)
  1477  	put("sample_value_mismatch", s.sampleValueMismatch)
  1478  	put("sample_value_negative", s.sampleValueNegative)
  1479  	put("sample_value_zero", s.sampleValueZero)
  1480  	put("sample_duplicate", s.sampleDuplicate)
  1481  	return b.String()
  1482  }
  1483  
  1484  func (p *Profile) DebugString() string {
  1485  	bs, _ := p.MarshalVT()
  1486  	gp, _ := profile.ParseData(bs)
  1487  	if gp == nil {
  1488  		return "<nil>"
  1489  	}
  1490  	return gp.String()
  1491  }