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 }