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 }