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  }