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

     1  package testhelper
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  
     7  	"github.com/google/uuid"
     8  	"github.com/prometheus/common/model"
     9  	"github.com/samber/lo"
    10  
    11  	profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    12  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    13  	phlaremodel "github.com/grafana/pyroscope/pkg/model"
    14  )
    15  
    16  type ProfileBuilder struct {
    17  	*profilev1.Profile
    18  	strings map[string]int
    19  
    20  	uuid.UUID
    21  	Labels      []*typesv1.LabelPair
    22  	Annotations []*typesv1.ProfileAnnotation
    23  
    24  	externalFunctionID2LocationId map[uint32]uint64
    25  	externalSampleID2SampleIndex  map[sampleID]uint32
    26  }
    27  
    28  type sampleID struct {
    29  	locationsID uint64
    30  	labelsID    uint64
    31  }
    32  
    33  // NewProfileBuilder creates a new ProfileBuilder with the given nanoseconds timestamp.
    34  func NewProfileBuilder(ts int64) *ProfileBuilder {
    35  	return NewProfileBuilderWithLabels(ts, []*typesv1.LabelPair{
    36  		{
    37  			Name:  "job",
    38  			Value: "foo",
    39  		},
    40  	})
    41  }
    42  
    43  // NewProfileBuilderWithLabels creates a new ProfileBuilder with the given nanoseconds timestamp and labels.
    44  func NewProfileBuilderWithLabels(ts int64, labels []*typesv1.LabelPair) *ProfileBuilder {
    45  	profile := new(profilev1.Profile)
    46  	profile.TimeNanos = ts
    47  	profile.Mapping = append(profile.Mapping, &profilev1.Mapping{
    48  		Id: 1, HasFunctions: true,
    49  	})
    50  	p := &ProfileBuilder{
    51  		Profile: profile,
    52  		UUID:    uuid.New(),
    53  		Labels:  labels,
    54  		strings: map[string]int{},
    55  
    56  		externalFunctionID2LocationId: map[uint32]uint64{},
    57  	}
    58  	p.addString("")
    59  	return p
    60  }
    61  
    62  func (m *ProfileBuilder) MemoryProfile() *ProfileBuilder {
    63  	m.Profile.PeriodType = &profilev1.ValueType{
    64  		Unit: m.addString("bytes"),
    65  		Type: m.addString("space"),
    66  	}
    67  	m.SampleType = []*profilev1.ValueType{
    68  		{
    69  			Unit: m.addString("count"),
    70  			Type: m.addString("alloc_objects"),
    71  		},
    72  		{
    73  			Unit: m.addString("bytes"),
    74  			Type: m.addString("alloc_space"),
    75  		},
    76  		{
    77  			Unit: m.addString("count"),
    78  			Type: m.addString("inuse_objects"),
    79  		},
    80  		{
    81  			Unit: m.addString("bytes"),
    82  			Type: m.addString("inuse_space"),
    83  		},
    84  	}
    85  	m.DefaultSampleType = m.addString("alloc_space")
    86  
    87  	m.Labels = append(m.Labels, &typesv1.LabelPair{
    88  		Name:  model.MetricNameLabel,
    89  		Value: "memory",
    90  	})
    91  
    92  	return m
    93  }
    94  
    95  func (m *ProfileBuilder) WithLabels(lv ...string) *ProfileBuilder {
    96  Outer:
    97  	for i := 0; i < len(lv); i += 2 {
    98  		for _, lbl := range m.Labels {
    99  			if lbl.Name == lv[i] {
   100  				lbl.Value = lv[i+1]
   101  				continue Outer
   102  			}
   103  		}
   104  		m.Labels = append(m.Labels, &typesv1.LabelPair{
   105  			Name:  lv[i],
   106  			Value: lv[i+1],
   107  		})
   108  	}
   109  	sort.Sort(phlaremodel.Labels(m.Labels))
   110  	return m
   111  }
   112  
   113  func (m *ProfileBuilder) WithAnnotations(annotationValues ...string) *ProfileBuilder {
   114  	for _, a := range annotationValues {
   115  		m.Annotations = append(m.Annotations, &typesv1.ProfileAnnotation{
   116  			Key:   "throttled",
   117  			Value: a,
   118  		})
   119  	}
   120  	return m
   121  }
   122  
   123  func (m *ProfileBuilder) Name() string {
   124  	for _, lbl := range m.Labels {
   125  		if lbl.Name == model.MetricNameLabel {
   126  			return lbl.Value
   127  		}
   128  	}
   129  	return ""
   130  }
   131  
   132  func (m *ProfileBuilder) AddSampleType(typ, unit string) {
   133  	m.SampleType = append(m.SampleType, &profilev1.ValueType{
   134  		Type: m.addString(typ),
   135  		Unit: m.addString(unit),
   136  	})
   137  }
   138  
   139  func (m *ProfileBuilder) MetricName(name string) {
   140  	m.Labels = append(m.Labels, &typesv1.LabelPair{
   141  		Name:  model.MetricNameLabel,
   142  		Value: name,
   143  	})
   144  }
   145  
   146  func (m *ProfileBuilder) PeriodType(periodType string, periodUnit string) {
   147  	m.Profile.PeriodType = &profilev1.ValueType{
   148  		Type: m.addString(periodType),
   149  		Unit: m.addString(periodUnit),
   150  	}
   151  }
   152  
   153  func (m *ProfileBuilder) CustomProfile(name, typ, unit, periodType, periodUnit string) {
   154  	m.AddSampleType(typ, unit)
   155  	m.DefaultSampleType = m.addString(typ)
   156  
   157  	m.PeriodType(periodType, periodUnit)
   158  
   159  	m.MetricName(name)
   160  }
   161  
   162  func (m *ProfileBuilder) CPUProfile() *ProfileBuilder {
   163  	m.CustomProfile("process_cpu", "cpu", "nanoseconds", "cpu", "nanoseconds")
   164  	return m
   165  }
   166  
   167  func (m *ProfileBuilder) ForStacktraceString(stacktraces ...string) *StacktraceBuilder {
   168  	namePositions := lo.Map(stacktraces, func(stacktrace string, i int) int64 {
   169  		return m.addString(stacktrace)
   170  	})
   171  
   172  	// search functions
   173  	functionIds := lo.Map(namePositions, func(namePos int64, i int) uint64 {
   174  		for _, f := range m.Function {
   175  			if f.Name == namePos {
   176  				return f.Id
   177  			}
   178  		}
   179  		f := &profilev1.Function{
   180  			Name: namePos,
   181  			Id:   uint64(len(m.Function)) + 1,
   182  		}
   183  		m.Function = append(m.Function, f)
   184  		return f.Id
   185  	})
   186  	// search locations
   187  	locationIDs := lo.Map(functionIds, func(functionId uint64, i int) uint64 {
   188  		for _, l := range m.Location {
   189  			if l.Line[0].FunctionId == functionId {
   190  				return l.Id
   191  			}
   192  		}
   193  		l := &profilev1.Location{
   194  			MappingId: uint64(1),
   195  			Line: []*profilev1.Line{
   196  				{
   197  					FunctionId: functionId,
   198  				},
   199  			},
   200  			Id: uint64(len(m.Location)) + 1,
   201  		}
   202  		m.Location = append(m.Location, l)
   203  		return l.Id
   204  	})
   205  	return &StacktraceBuilder{
   206  		locationID:     locationIDs,
   207  		ProfileBuilder: m,
   208  	}
   209  }
   210  
   211  func (m *ProfileBuilder) AddString(s string) int64 {
   212  	return m.addString(s)
   213  }
   214  
   215  func (m *ProfileBuilder) addString(s string) int64 {
   216  	i, ok := m.strings[s]
   217  	if !ok {
   218  		i = len(m.strings)
   219  		m.strings[s] = i
   220  		m.StringTable = append(m.StringTable, s)
   221  	}
   222  	return int64(i)
   223  }
   224  
   225  func (m *ProfileBuilder) FindLocationByExternalID(externalID uint32) (uint64, bool) {
   226  	loc, ok := m.externalFunctionID2LocationId[externalID]
   227  	return loc, ok
   228  }
   229  
   230  func (m *ProfileBuilder) AddExternalFunction(frame string, externalFunctionID uint32) uint64 {
   231  	fname := m.addString(frame)
   232  	funcID := uint64(len(m.Function)) + 1
   233  	m.Function = append(m.Function, &profilev1.Function{
   234  		Id:   funcID,
   235  		Name: fname,
   236  	})
   237  	locID := uint64(len(m.Location)) + 1
   238  	m.Location = append(m.Location, &profilev1.Location{
   239  		Id:        locID,
   240  		MappingId: uint64(1),
   241  		Line:      []*profilev1.Line{{FunctionId: funcID}},
   242  	})
   243  	m.externalFunctionID2LocationId[externalFunctionID] = locID
   244  	return locID
   245  }
   246  
   247  func (m *ProfileBuilder) AddExternalSample(locs []uint64, values []int64, externalSampleID uint32) {
   248  	m.AddExternalSampleWithLabels(locs, values, nil, uint64(externalSampleID), 0)
   249  }
   250  
   251  func (m *ProfileBuilder) FindExternalSample(externalSampleID uint32) *profilev1.Sample {
   252  	return m.FindExternalSampleWithLabels(uint64(externalSampleID), 0)
   253  }
   254  
   255  func (m *ProfileBuilder) AddExternalSampleWithLabels(locs []uint64, values []int64, labels phlaremodel.Labels, locationsID, labelsID uint64) {
   256  	sample := &profilev1.Sample{
   257  		LocationId: locs,
   258  		Value:      values,
   259  	}
   260  	if m.externalSampleID2SampleIndex == nil {
   261  		m.externalSampleID2SampleIndex = map[sampleID]uint32{}
   262  	}
   263  	m.externalSampleID2SampleIndex[sampleID{locationsID: locationsID, labelsID: labelsID}] = uint32(len(m.Sample))
   264  	m.Sample = append(m.Sample, sample)
   265  	if len(labels) > 0 {
   266  		sample.Label = make([]*profilev1.Label, 0, len(labels))
   267  		for _, label := range labels {
   268  			sample.Label = append(sample.Label, &profilev1.Label{
   269  				Key: m.addString(label.Name),
   270  				Str: m.addString(label.Value),
   271  			})
   272  		}
   273  	}
   274  }
   275  
   276  func (m *ProfileBuilder) FindExternalSampleWithLabels(locationsID, labelsID uint64) *profilev1.Sample {
   277  	sampleIndex, ok := m.externalSampleID2SampleIndex[sampleID{locationsID: locationsID, labelsID: labelsID}]
   278  	if !ok {
   279  		return nil
   280  	}
   281  	sample := m.Sample[sampleIndex]
   282  	return sample
   283  }
   284  
   285  type StacktraceBuilder struct {
   286  	locationID []uint64
   287  	*ProfileBuilder
   288  }
   289  
   290  func (s *StacktraceBuilder) AddSamples(samples ...int64) *ProfileBuilder {
   291  	if exp, act := len(s.SampleType), len(samples); exp != act {
   292  		panic(fmt.Sprintf("profile expects %d sample(s), there was actually %d sample(s) given.", exp, act))
   293  	}
   294  	s.Sample = append(s.Sample, &profilev1.Sample{
   295  		LocationId: s.locationID,
   296  		Value:      samples,
   297  	})
   298  	return s.ProfileBuilder
   299  }