github.com/grafana/pyroscope@v1.18.0/pkg/model/time_series.go (about) 1 package model 2 3 import ( 4 "math" 5 "sort" 6 7 "github.com/prometheus/common/model" 8 "github.com/samber/lo" 9 10 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 11 "github.com/grafana/pyroscope/pkg/iter" 12 ) 13 14 // DefaultMaxExemplarsPerPoint is the default maximum number of exemplars tracked per point. 15 // TODO: make it configurable via tenant limits. 16 const DefaultMaxExemplarsPerPoint = 1 17 18 type TimeSeriesValue struct { 19 Ts int64 20 Lbs []*typesv1.LabelPair 21 LabelsHash uint64 22 Value float64 23 Annotations []*typesv1.ProfileAnnotation 24 Exemplars []*typesv1.Exemplar 25 } 26 27 func (p TimeSeriesValue) Labels() Labels { return p.Lbs } 28 func (p TimeSeriesValue) Timestamp() model.Time { return model.Time(p.Ts) } 29 30 type TimeSeriesIterator struct { 31 point []*typesv1.Point 32 curr TimeSeriesValue 33 } 34 35 func NewSeriesIterator(lbs []*typesv1.LabelPair, points []*typesv1.Point) *TimeSeriesIterator { 36 return &TimeSeriesIterator{ 37 point: points, 38 39 curr: TimeSeriesValue{ 40 Lbs: lbs, 41 LabelsHash: Labels(lbs).Hash(), 42 }, 43 } 44 } 45 46 func (s *TimeSeriesIterator) Next() bool { 47 if len(s.point) == 0 { 48 return false 49 } 50 p := s.point[0] 51 s.point = s.point[1:] 52 s.curr.Ts = p.Timestamp 53 s.curr.Value = p.Value 54 s.curr.Annotations = p.Annotations 55 56 s.curr.Exemplars = p.Exemplars 57 return true 58 } 59 60 func (s *TimeSeriesIterator) At() TimeSeriesValue { return s.curr } 61 func (s *TimeSeriesIterator) Err() error { return nil } 62 func (s *TimeSeriesIterator) Close() error { return nil } 63 64 func NewTimeSeriesMergeIterator(series []*typesv1.Series) iter.Iterator[TimeSeriesValue] { 65 iters := make([]iter.Iterator[TimeSeriesValue], 0, len(series)) 66 for _, s := range series { 67 iters = append(iters, NewSeriesIterator(s.Labels, s.Points)) 68 } 69 return NewMergeIterator(TimeSeriesValue{Ts: math.MaxInt64}, false, iters...) 70 } 71 72 type TimeSeriesAggregator interface { 73 Add(ts int64, point *TimeSeriesValue) 74 GetAndReset() *typesv1.Point 75 IsEmpty() bool 76 GetTimestamp() int64 77 } 78 79 func NewTimeSeriesAggregator(aggregation *typesv1.TimeSeriesAggregationType) TimeSeriesAggregator { 80 return NewTimeSeriesAggregatorWithLimit(aggregation, DefaultMaxExemplarsPerPoint) 81 } 82 83 func NewTimeSeriesAggregatorWithLimit(aggregation *typesv1.TimeSeriesAggregationType, maxExemplarsPerPoint int) TimeSeriesAggregator { 84 if aggregation == nil { 85 return &sumTimeSeriesAggregator{ts: -1, maxExemplarsPerPoint: maxExemplarsPerPoint} 86 } 87 if *aggregation == typesv1.TimeSeriesAggregationType_TIME_SERIES_AGGREGATION_TYPE_AVERAGE { 88 return &avgTimeSeriesAggregator{ts: -1, maxExemplarsPerPoint: maxExemplarsPerPoint} 89 } 90 return &sumTimeSeriesAggregator{ts: -1, maxExemplarsPerPoint: maxExemplarsPerPoint} 91 } 92 93 type sumTimeSeriesAggregator struct { 94 ts int64 95 sum float64 96 annotations []*typesv1.ProfileAnnotation 97 exemplars []*typesv1.Exemplar 98 maxExemplarsPerPoint int 99 } 100 101 func (a *sumTimeSeriesAggregator) Add(ts int64, point *TimeSeriesValue) { 102 a.ts = ts 103 a.sum += point.Value 104 a.annotations = append(a.annotations, point.Annotations...) 105 106 if len(point.Exemplars) > 0 { 107 a.exemplars = mergeExemplars(a.exemplars, point.Exemplars) 108 } 109 } 110 111 func (a *sumTimeSeriesAggregator) GetAndReset() *typesv1.Point { 112 tsCopy := a.ts 113 sumCopy := a.sum 114 annotationsCopy := make([]*typesv1.ProfileAnnotation, len(a.annotations)) 115 copy(annotationsCopy, a.annotations) 116 117 var exemplars []*typesv1.Exemplar 118 if len(a.exemplars) > 0 { 119 exemplars = selectTopNExemplarsProto(a.exemplars, a.maxExemplarsPerPoint) 120 } 121 122 a.ts = -1 123 a.sum = 0 124 a.annotations = a.annotations[:0] 125 a.exemplars = nil 126 127 return &typesv1.Point{ 128 Timestamp: tsCopy, 129 Value: sumCopy, 130 Annotations: annotationsCopy, 131 Exemplars: exemplars, 132 } 133 } 134 135 func (a *sumTimeSeriesAggregator) IsEmpty() bool { return a.ts == -1 } 136 func (a *sumTimeSeriesAggregator) GetTimestamp() int64 { return a.ts } 137 138 type avgTimeSeriesAggregator struct { 139 ts int64 140 sum float64 141 count int64 142 annotations []*typesv1.ProfileAnnotation 143 exemplars []*typesv1.Exemplar 144 maxExemplarsPerPoint int 145 } 146 147 func (a *avgTimeSeriesAggregator) Add(ts int64, point *TimeSeriesValue) { 148 a.ts = ts 149 a.sum += point.Value 150 a.count++ 151 a.annotations = append(a.annotations, point.Annotations...) 152 153 if len(point.Exemplars) > 0 { 154 a.exemplars = mergeExemplars(a.exemplars, point.Exemplars) 155 } 156 } 157 158 func (a *avgTimeSeriesAggregator) GetAndReset() *typesv1.Point { 159 avg := a.sum / float64(a.count) 160 tsCopy := a.ts 161 annotationsCopy := make([]*typesv1.ProfileAnnotation, len(a.annotations)) 162 copy(annotationsCopy, a.annotations) 163 164 var exemplars []*typesv1.Exemplar 165 if len(a.exemplars) > 0 { 166 exemplars = selectTopNExemplarsProto(a.exemplars, a.maxExemplarsPerPoint) 167 } 168 169 a.ts = -1 170 a.sum = 0 171 a.count = 0 172 a.annotations = a.annotations[:0] 173 a.exemplars = nil 174 175 return &typesv1.Point{ 176 Timestamp: tsCopy, 177 Value: avg, 178 Annotations: annotationsCopy, 179 Exemplars: exemplars, 180 } 181 } 182 183 func (a *avgTimeSeriesAggregator) IsEmpty() bool { return a.ts == -1 } 184 func (a *avgTimeSeriesAggregator) GetTimestamp() int64 { return a.ts } 185 186 // RangeSeries aggregates profiles into series. 187 // Series contains points spaced by step from start to end. 188 // Profiles from the same step are aggregated into one point. 189 func RangeSeries(it iter.Iterator[TimeSeriesValue], start, end, step int64, aggregation *typesv1.TimeSeriesAggregationType) []*typesv1.Series { 190 return rangeSeriesWithLimit(it, start, end, step, aggregation, DefaultMaxExemplarsPerPoint) 191 } 192 193 // rangeSeriesWithLimit is an internal function that allows specifying maxExemplarsPerPoint. 194 func rangeSeriesWithLimit(it iter.Iterator[TimeSeriesValue], start, end, step int64, aggregation *typesv1.TimeSeriesAggregationType, maxExemplarsPerPoint int) []*typesv1.Series { 195 defer it.Close() 196 seriesMap := make(map[uint64]*typesv1.Series) 197 aggregators := make(map[uint64]TimeSeriesAggregator) 198 199 if !it.Next() { 200 return nil 201 } 202 203 // advance from the start to the end, adding each step results to the map. 204 Outer: 205 for currentStep := start; currentStep <= end; currentStep += step { 206 for { 207 point := it.At() 208 aggregator, ok := aggregators[point.LabelsHash] 209 if !ok { 210 aggregator = NewTimeSeriesAggregatorWithLimit(aggregation, maxExemplarsPerPoint) 211 aggregators[point.LabelsHash] = aggregator 212 } 213 if point.Ts > currentStep { 214 if !aggregator.IsEmpty() { 215 series := seriesMap[point.LabelsHash] 216 series.Points = append(series.Points, aggregator.GetAndReset()) 217 } 218 break // no more profiles for the currentStep 219 } 220 // find or create series 221 series, ok := seriesMap[point.LabelsHash] 222 if !ok { 223 seriesMap[point.LabelsHash] = &typesv1.Series{ 224 Labels: point.Lbs, 225 Points: []*typesv1.Point{}, 226 } 227 aggregator.Add(currentStep, &point) 228 if !it.Next() { 229 break Outer 230 } 231 continue 232 } 233 // Aggregate point if it is in the current step. 234 if aggregator.GetTimestamp() == currentStep { 235 aggregator.Add(currentStep, &point) 236 if !it.Next() { 237 break Outer 238 } 239 continue 240 } 241 // Next step is missing 242 if !aggregator.IsEmpty() { 243 series.Points = append(series.Points, aggregator.GetAndReset()) 244 } 245 aggregator.Add(currentStep, &point) 246 if !it.Next() { 247 break Outer 248 } 249 } 250 } 251 for lblHash, aggregator := range aggregators { 252 if !aggregator.IsEmpty() { 253 seriesMap[lblHash].Points = append(seriesMap[lblHash].Points, aggregator.GetAndReset()) 254 } 255 } 256 series := lo.Values(seriesMap) 257 sort.Slice(series, func(i, j int) bool { 258 return CompareLabelPairs(series[i].Labels, series[j].Labels) < 0 259 }) 260 return series 261 } 262 263 // selectTopNExemplarsProto selects the top-N exemplars by value. 264 func selectTopNExemplarsProto(exemplars []*typesv1.Exemplar, maxExemplars int) []*typesv1.Exemplar { 265 if len(exemplars) <= maxExemplars { 266 return exemplars 267 } 268 269 sort.Slice(exemplars, func(i, j int) bool { 270 return exemplars[i].Value > exemplars[j].Value 271 }) 272 return exemplars[:maxExemplars] 273 }