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

     1  package model
     2  
     3  import (
     4  	"sort"
     5  
     6  	"github.com/prometheus/common/model"
     7  
     8  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
     9  )
    10  
    11  // ExemplarBuilder builds exemplars for a single time series.
    12  type ExemplarBuilder struct {
    13  	labelSetIndex map[uint64]int
    14  	labelSets     []Labels
    15  	exemplars     []exemplar
    16  }
    17  
    18  type exemplar struct {
    19  	timestamp   int64
    20  	profileID   string
    21  	labelSetRef int
    22  	value       uint64
    23  }
    24  
    25  // NewExemplarBuilder creates a new ExemplarBuilder.
    26  func NewExemplarBuilder() *ExemplarBuilder {
    27  	return &ExemplarBuilder{
    28  		labelSetIndex: make(map[uint64]int),
    29  		labelSets:     make([]Labels, 0),
    30  		exemplars:     make([]exemplar, 0),
    31  	}
    32  }
    33  
    34  // Add adds an exemplar with its full labels.
    35  func (eb *ExemplarBuilder) Add(fp model.Fingerprint, labels Labels, ts int64, profileID string, value uint64) {
    36  	if profileID == "" {
    37  		return
    38  	}
    39  
    40  	labelSetIdx, exists := eb.labelSetIndex[uint64(fp)]
    41  	if !exists {
    42  		eb.labelSets = append(eb.labelSets, labels.Clone())
    43  		labelSetIdx = len(eb.labelSets) - 1
    44  		eb.labelSetIndex[uint64(fp)] = labelSetIdx
    45  	}
    46  
    47  	eb.exemplars = append(eb.exemplars, exemplar{
    48  		timestamp:   ts,
    49  		profileID:   profileID,
    50  		labelSetRef: labelSetIdx,
    51  		value:       value,
    52  	})
    53  }
    54  
    55  // Count returns the number of raw exemplars added.
    56  func (eb *ExemplarBuilder) Count() int {
    57  	return len(eb.exemplars)
    58  }
    59  
    60  // Build returns the final exemplars, sorted and deduplicated.
    61  // Exemplars with the same (profileID, timestamp) are merged by intersecting their labels.
    62  func (eb *ExemplarBuilder) Build() []*typesv1.Exemplar {
    63  	if len(eb.exemplars) == 0 {
    64  		return nil
    65  	}
    66  
    67  	sort.Slice(eb.exemplars, func(i, j int) bool {
    68  		if eb.exemplars[i].timestamp != eb.exemplars[j].timestamp {
    69  			return eb.exemplars[i].timestamp < eb.exemplars[j].timestamp
    70  		}
    71  		return eb.exemplars[i].profileID < eb.exemplars[j].profileID
    72  	})
    73  
    74  	return eb.deduplicateAndIntersect()
    75  }
    76  
    77  // deduplicateAndIntersect merges exemplars with the same profileID, timestamp by intersecting their label sets.
    78  // When multiple exemplars exist for the same (profileID, timestamp), we sum their values.
    79  func (eb *ExemplarBuilder) deduplicateAndIntersect() []*typesv1.Exemplar {
    80  	result := make([]*typesv1.Exemplar, 0, len(eb.exemplars))
    81  
    82  	i := 0
    83  	for i < len(eb.exemplars) {
    84  		curr := eb.exemplars[i]
    85  
    86  		labelSetsToIntersect := []Labels{eb.labelSets[curr.labelSetRef]}
    87  		sumValue := curr.value
    88  		j := i + 1
    89  		for j < len(eb.exemplars) &&
    90  			eb.exemplars[j].profileID == curr.profileID &&
    91  			eb.exemplars[j].timestamp == curr.timestamp {
    92  			labelSetsToIntersect = append(labelSetsToIntersect, eb.labelSets[eb.exemplars[j].labelSetRef])
    93  			sumValue += eb.exemplars[j].value
    94  			j++
    95  		}
    96  
    97  		finalLabels := IntersectAll(labelSetsToIntersect)
    98  
    99  		result = append(result, &typesv1.Exemplar{
   100  			Timestamp: curr.timestamp,
   101  			ProfileId: curr.profileID,
   102  			Value:     sumValue,
   103  			Labels:    finalLabels,
   104  		})
   105  
   106  		i = j
   107  	}
   108  
   109  	return result
   110  }