@@ -1658,7 +1658,10 @@ func addAndResetCounts(hot, cold *histogramCounts) {
16581658type nativeExemplars struct {
16591659 sync.Mutex
16601660
1661- ttl time.Duration
1661+ // Time-to-live for exemplars, it is set to -1 if exemplars are disabled, that is NativeHistogramMaxExemplars is below 0.
1662+ // The ttl is used on insertion to remove an exemplar that is older than ttl, if present.
1663+ ttl time.Duration
1664+
16621665 exemplars []* dto.Exemplar
16631666}
16641667
@@ -1673,6 +1676,7 @@ func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
16731676
16741677 if maxCount < 0 {
16751678 maxCount = 0
1679+ ttl = - 1
16761680 }
16771681
16781682 return nativeExemplars {
@@ -1682,20 +1686,18 @@ func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
16821686}
16831687
16841688func (n * nativeExemplars ) addExemplar (e * dto.Exemplar ) {
1685- if cap ( n . exemplars ) == 0 {
1689+ if n . ttl == - 1 {
16861690 return
16871691 }
16881692
16891693 n .Lock ()
16901694 defer n .Unlock ()
16911695
1692- // The index where to insert the new exemplar.
1693- var nIdx int = - 1
1694-
16951696 // When the number of exemplars has not yet exceeded or
16961697 // is equal to cap(n.exemplars), then
16971698 // insert the new exemplar directly.
16981699 if len (n .exemplars ) < cap (n .exemplars ) {
1700+ var nIdx int
16991701 for nIdx = 0 ; nIdx < len (n .exemplars ); nIdx ++ {
17001702 if * e .Value < * n .exemplars [nIdx ].Value {
17011703 break
@@ -1705,17 +1707,46 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17051707 return
17061708 }
17071709
1710+ if len (n .exemplars ) == 1 {
1711+ // When the number of exemplars is 1, then
1712+ // replace the existing exemplar with the new exemplar.
1713+ n .exemplars [0 ] = e
1714+ return
1715+ }
1716+ // From this point on, the number of exemplars is greater than 1.
1717+
17081718 // When the number of exemplars exceeds the limit, remove one exemplar.
17091719 var (
1710- rIdx int // The index where to remove the old exemplar.
1711-
1712- ot = time .Now () // Oldest timestamp seen.
1713- otIdx = - 1 // Index of the exemplar with the oldest timestamp.
1714-
1715- md = - 1.0 // Logarithm of the delta of the closest pair of exemplars.
1716- mdIdx = - 1 // Index of the older exemplar within the closest pair.
1717- cLog float64 // Logarithm of the current exemplar.
1718- pLog float64 // Logarithm of the previous exemplar.
1720+ ot = time.Time {} // Oldest timestamp seen. Initial value doesn't matter as we replace it due to otIdx == -1 in the loop.
1721+ otIdx = - 1 // Index of the exemplar with the oldest timestamp.
1722+
1723+ md = - 1.0 // Logarithm of the delta of the closest pair of exemplars.
1724+
1725+ // The insertion point of the new exemplar in the exemplars slice after insertion.
1726+ // This is calculated purely based on the order of the exemplars by value.
1727+ // nIdx == len(n.exemplars) means the new exemplar is to be inserted after the end.
1728+ nIdx = - 1
1729+
1730+ // rIdx is ultimately the index for the exemplar that we are replacing with the new exemplar.
1731+ // The aim is to keep a good spread of exemplars by value and not let them bunch up too much.
1732+ // It is calculated in 3 steps:
1733+ // 1. First we set rIdx to the index of the older exemplar within the closest pair by value.
1734+ // That is the following will be true (on log scale):
1735+ // either the exemplar pair on index (rIdx-1, rIdx) or (rIdx, rIdx+1) will have
1736+ // the closest values to each other from all pairs.
1737+ // For example, suppose the values are distributed like this:
1738+ // |-----------x-------------x----------------x----x-----|
1739+ // ^--rIdx as this is older.
1740+ // Or like this:
1741+ // |-----------x-------------x----------------x----x-----|
1742+ // ^--rIdx as this is older.
1743+ // 2. If there is an exemplar that expired, then we simple reset rIdx to that index.
1744+ // 3. We check if by inserting the new exemplar we would create a closer pair at
1745+ // (nIdx-1, nIdx) or (nIdx, nIdx+1) and set rIdx to nIdx-1 or nIdx accordingly to
1746+ // keep the spread of exemplars by value; otherwise we keep rIdx as it is.
1747+ rIdx = - 1
1748+ cLog float64 // Logarithm of the current exemplar.
1749+ pLog float64 // Logarithm of the previous exemplar.
17191750 )
17201751
17211752 for i , exemplar := range n .exemplars {
@@ -1726,7 +1757,7 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17261757 }
17271758
17281759 // Find the index at which to insert new the exemplar.
1729- if * e .Value <= * exemplar .Value && nIdx == - 1 {
1760+ if nIdx == - 1 && * e .Value <= * exemplar .Value {
17301761 nIdx = i
17311762 }
17321763
@@ -1738,11 +1769,13 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17381769 }
17391770 diff := math .Abs (cLog - pLog )
17401771 if md == - 1 || diff < md {
1772+ // The closest exemplar pair is at index: i-1, i.
1773+ // Choose the exemplar with the older timestamp for replacement.
17411774 md = diff
17421775 if n .exemplars [i ].Timestamp .AsTime ().Before (n .exemplars [i - 1 ].Timestamp .AsTime ()) {
1743- mdIdx = i
1776+ rIdx = i
17441777 } else {
1745- mdIdx = i - 1
1778+ rIdx = i - 1
17461779 }
17471780 }
17481781
@@ -1753,8 +1786,12 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17531786 if nIdx == - 1 {
17541787 nIdx = len (n .exemplars )
17551788 }
1789+ // Here, we have the following relationships:
1790+ // n.exemplars[nIdx-1].Value < e.Value (if nIdx > 0)
1791+ // e.Value <= n.exemplars[nIdx].Value (if nIdx < len(n.exemplars))
17561792
17571793 if otIdx != - 1 && e .Timestamp .AsTime ().Sub (ot ) > n .ttl {
1794+ // If the oldest exemplar has expired, then replace it with the new exemplar.
17581795 rIdx = otIdx
17591796 } else {
17601797 // In the previous for loop, when calculating the closest pair of exemplars,
@@ -1764,23 +1801,26 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17641801 if nIdx > 0 {
17651802 diff := math .Abs (elog - math .Log (n .exemplars [nIdx - 1 ].GetValue ()))
17661803 if diff < md {
1804+ // The value we are about to insert is closer to the previous exemplar at the insertion point than what we calculated before in rIdx.
1805+ // v--rIdx
1806+ // |-----------x-n-----------x----------------x----x-----|
1807+ // nIdx-1--^ ^--new exemplar value
1808+ // Do not make the spread worse, replace nIdx-1 and not rIdx.
17671809 md = diff
1768- mdIdx = nIdx
1769- if n .exemplars [nIdx - 1 ].Timestamp .AsTime ().Before (e .Timestamp .AsTime ()) {
1770- mdIdx = nIdx - 1
1771- }
1810+ rIdx = nIdx - 1
17721811 }
17731812 }
17741813 if nIdx < len (n .exemplars ) {
17751814 diff := math .Abs (math .Log (n .exemplars [nIdx ].GetValue ()) - elog )
17761815 if diff < md {
1777- mdIdx = nIdx
1778- if n .exemplars [nIdx ].Timestamp .AsTime ().Before (e .Timestamp .AsTime ()) {
1779- mdIdx = nIdx
1780- }
1816+ // The value we are about to insert is closer to the next exemplar at the insertion point than what we calculated before in rIdx.
1817+ // v--rIdx
1818+ // |-----------x-----------n-x----------------x----x-----|
1819+ // new exemplar value--^ ^--nIdx
1820+ // Do not make the spread worse, replace nIdx-1 and not rIdx.
1821+ rIdx = nIdx
17811822 }
17821823 }
1783- rIdx = mdIdx
17841824 }
17851825
17861826 // Adjust the slice according to rIdx and nIdx.
0 commit comments