@@ -440,7 +440,7 @@ type HistogramOpts struct {
440
440
// constant (or any negative float value).
441
441
NativeHistogramZeroThreshold float64
442
442
443
- // The remaining fields define a strategy to limit the number of
443
+ // The next three fields define a strategy to limit the number of
444
444
// populated sparse buckets. If NativeHistogramMaxBucketNumber is left
445
445
// at zero, the number of buckets is not limited. (Note that this might
446
446
// lead to unbounded memory consumption if the values observed by the
@@ -472,8 +472,22 @@ type HistogramOpts struct {
472
472
NativeHistogramMaxBucketNumber uint32
473
473
NativeHistogramMinResetDuration time.Duration
474
474
NativeHistogramMaxZeroThreshold float64
475
- NativeHistogramMaxExemplarCount uint32
476
- NativeHistogramExemplarTTL time.Duration
475
+
476
+ // NativeHistogramMaxExemplars limits the number of exemplars
477
+ // that are kept in memory for each native histogram. If you leave it at
478
+ // zero, a default value of 10 is used. If no exemplars should be kept specifically
479
+ // for native histograms, set it to a negative value. (Scrapers can
480
+ // still use the exemplars exposed for classic buckets, which are managed
481
+ // independently.)
482
+ NativeHistogramMaxExemplars int
483
+ // NativeHistogramExemplarTTL is only checked once
484
+ // NativeHistogramMaxExemplars is exceeded. In that case, the
485
+ // oldest exemplar is removed if it is older than NativeHistogramExemplarTTL.
486
+ // Otherwise, the older exemplar in the pair of exemplars that are closest
487
+ // together (on an exponential scale) is removed.
488
+ // If NativeHistogramExemplarTTL is left at its zero value, a default value of
489
+ // 5m is used. To always delete the oldest exemplar, set it to a negative value.
490
+ NativeHistogramExemplarTTL time.Duration
477
491
478
492
// now is for testing purposes, by default it's time.Now.
479
493
now func () time.Time
@@ -534,6 +548,7 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr
534
548
if opts .afterFunc == nil {
535
549
opts .afterFunc = time .AfterFunc
536
550
}
551
+
537
552
h := & histogram {
538
553
desc : desc ,
539
554
upperBounds : opts .Buckets ,
@@ -558,7 +573,7 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr
558
573
h .nativeHistogramZeroThreshold = DefNativeHistogramZeroThreshold
559
574
} // Leave h.nativeHistogramZeroThreshold at 0 otherwise.
560
575
h .nativeHistogramSchema = pickSchema (opts .NativeHistogramBucketFactor )
561
- h .nativeExemplars = newNativeExemplars (opts .NativeHistogramExemplarTTL , opts .NativeHistogramMaxExemplarCount )
576
+ h .nativeExemplars = makeNativeExemplars (opts .NativeHistogramExemplarTTL , opts .NativeHistogramMaxExemplars )
562
577
}
563
578
for i , upperBound := range h .upperBounds {
564
579
if i < len (h .upperBounds )- 1 {
@@ -728,15 +743,14 @@ type histogram struct {
728
743
// resetScheduled is protected by mtx. It is true if a reset is
729
744
// scheduled for a later time (when nativeHistogramMinResetDuration has
730
745
// passed).
731
- resetScheduled bool
746
+ resetScheduled bool
747
+ nativeExemplars nativeExemplars
732
748
733
749
// now is for testing purposes, by default it's time.Now.
734
750
now func () time.Time
735
751
736
752
// afterFunc is for testing purposes, by default it's time.AfterFunc.
737
753
afterFunc func (time.Duration , func ()) * time.Timer
738
-
739
- nativeExemplars nativeExemplars
740
754
}
741
755
742
756
func (h * histogram ) Desc () * Desc {
@@ -747,6 +761,8 @@ func (h *histogram) Observe(v float64) {
747
761
h .observe (v , h .findBucket (v ))
748
762
}
749
763
764
+ // ObserveWithExemplar should not be called in high-frequency settings,
765
+ // since it isn't lock-free for native histograms with configured exemplars.
750
766
func (h * histogram ) ObserveWithExemplar (v float64 , e Labels ) {
751
767
i := h .findBucket (v )
752
768
h .observe (v , i )
@@ -827,7 +843,12 @@ func (h *histogram) Write(out *dto.Metric) error {
827
843
}}
828
844
}
829
845
830
- his .Exemplars = append (his .Exemplars , h .nativeExemplars .exemplars ... )
846
+ if cap (h .nativeExemplars .exemplars ) > 0 {
847
+ h .nativeExemplars .Lock ()
848
+ his .Exemplars = append (his .Exemplars , h .nativeExemplars .exemplars ... )
849
+ h .nativeExemplars .Unlock ()
850
+ }
851
+
831
852
}
832
853
addAndResetCounts (hotCounts , coldCounts )
833
854
return nil
@@ -1098,8 +1119,10 @@ func (h *histogram) resetCounts(counts *histogramCounts) {
1098
1119
deleteSyncMap (& counts .nativeHistogramBucketsPositive )
1099
1120
}
1100
1121
1101
- // updateExemplar replaces the exemplar for the provided bucket. With empty
1102
- // labels, it's a no-op. It panics if any of the labels is invalid.
1122
+ // updateExemplar replaces the exemplar for the provided classic bucket.
1123
+ // With empty labels, it's a no-op. It panics if any of the labels is invalid.
1124
+ // If histogram is native, the exemplar will be cached into nativeExemplars,
1125
+ // which has a limit, and will remove one exemplar when limit is reached.
1103
1126
func (h * histogram ) updateExemplar (v float64 , bucket int , l Labels ) {
1104
1127
if l == nil {
1105
1128
return
@@ -1588,56 +1611,140 @@ func addAndResetCounts(hot, cold *histogramCounts) {
1588
1611
}
1589
1612
1590
1613
type nativeExemplars struct {
1591
- nativeHistogramExemplarTTL time.Duration
1592
- nativeHistogramMaxExemplarCount uint32
1614
+ sync.Mutex
1593
1615
1616
+ ttl time.Duration
1594
1617
exemplars []* dto.Exemplar
1595
-
1596
- lock sync.Mutex
1597
1618
}
1598
1619
1599
- func newNativeExemplars (ttl time.Duration , count uint32 ) nativeExemplars {
1620
+ func makeNativeExemplars (ttl time.Duration , maxCount int ) nativeExemplars {
1621
+ if ttl == 0 {
1622
+ ttl = 5 * time .Minute
1623
+ }
1624
+
1625
+ if maxCount == 0 {
1626
+ maxCount = 10
1627
+ }
1628
+
1629
+ if maxCount < 0 {
1630
+ maxCount = 0
1631
+ }
1632
+
1600
1633
return nativeExemplars {
1601
- nativeHistogramExemplarTTL : ttl ,
1602
- nativeHistogramMaxExemplarCount : count ,
1603
- exemplars : make ([]* dto.Exemplar , 0 ),
1604
- lock : sync.Mutex {},
1634
+ ttl : ttl ,
1635
+ exemplars : make ([]* dto.Exemplar , 0 , maxCount ),
1605
1636
}
1606
1637
}
1607
1638
1608
1639
func (n * nativeExemplars ) addExemplar (e * dto.Exemplar ) {
1609
- n .lock .Lock ()
1610
- defer n .lock .Unlock ()
1611
-
1612
- elogarithm := math .Log (e .GetValue ())
1613
- if len (n .exemplars ) == int (n .nativeHistogramMaxExemplarCount ) {
1614
- // check if oldestIndex is beyond TTL,
1615
- // if so, find the oldest exemplar, and nearest exemplar
1616
- oldestTimestamp := time .Now ()
1617
- oldestIndex := - 1
1618
- nearestValue := - 1.0
1619
- nearestIndex := - 1
1620
-
1621
- for i , exemplar := range n .exemplars {
1622
- if exemplar .Timestamp .AsTime ().Before (oldestTimestamp ) {
1623
- oldestTimestamp = exemplar .Timestamp .AsTime ()
1624
- oldestIndex = i
1640
+ if cap (n .exemplars ) == 0 {
1641
+ return
1642
+ }
1643
+
1644
+ n .Lock ()
1645
+ defer n .Unlock ()
1646
+
1647
+ // The index where to insert the new exemplar.
1648
+ var nIdx int = - 1
1649
+
1650
+ // When the number of exemplars has not yet exceeded or
1651
+ // is equal to cap(n.exemplars), then
1652
+ // insert the new exemplar directly.
1653
+ if len (n .exemplars ) < cap (n .exemplars ) {
1654
+ for nIdx = 0 ; nIdx < len (n .exemplars ); nIdx ++ {
1655
+ if * e .Value < * n .exemplars [nIdx ].Value {
1656
+ break
1625
1657
}
1626
- logarithm := math .Log (exemplar .GetValue ())
1627
- if nearestValue == - 1 || math .Abs (elogarithm - logarithm ) < nearestValue {
1628
- fmt .Printf ("gap: %f" , math .Abs (elogarithm - logarithm ))
1629
- nearestValue = math .Abs (elogarithm - logarithm )
1630
- nearestIndex = i
1658
+ }
1659
+ n .exemplars = append (n .exemplars [:nIdx ], append ([]* dto.Exemplar {e }, n .exemplars [nIdx :]... )... )
1660
+ return
1661
+ }
1662
+
1663
+ // When the number of exemplars exceeds the limit, remove one exemplar.
1664
+ var (
1665
+ rIdx int // The index where to remove the old exemplar.
1666
+
1667
+ ot = time .Now () // Oldest timestamp seen.
1668
+ otIdx = - 1 // Index of the exemplar with the oldest timestamp.
1669
+
1670
+ md = - 1.0 // Logarithm of the delta of the closest pair of exemplars.
1671
+ mdIdx = - 1 // Index of the older exemplar within the closest pair.
1672
+ cLog float64 // Logarithm of the current exemplar.
1673
+ pLog float64 // Logarithm of the previous exemplar.
1674
+ )
1675
+
1676
+ for i , exemplar := range n .exemplars {
1677
+ // Find the exemplar with the oldest timestamp.
1678
+ if otIdx == - 1 || exemplar .Timestamp .AsTime ().Before (ot ) {
1679
+ ot = exemplar .Timestamp .AsTime ()
1680
+ otIdx = i
1681
+ }
1682
+
1683
+ // Find the index at which to insert new the exemplar.
1684
+ if * e .Value <= * exemplar .Value && nIdx == - 1 {
1685
+ nIdx = i
1686
+ }
1687
+
1688
+ // Find the two closest exemplars and pick the one the with older timestamp.
1689
+ pLog = cLog
1690
+ cLog = math .Log (exemplar .GetValue ())
1691
+ if i == 0 {
1692
+ continue
1693
+ }
1694
+ diff := math .Abs (cLog - pLog )
1695
+ if md == - 1 || diff < md {
1696
+ md = diff
1697
+ if n .exemplars [i ].Timestamp .AsTime ().Before (n .exemplars [i - 1 ].Timestamp .AsTime ()) {
1698
+ mdIdx = i
1699
+ } else {
1700
+ mdIdx = i - 1
1631
1701
}
1632
1702
}
1633
1703
1634
- if oldestIndex != - 1 && time .Since (oldestTimestamp ) > n .nativeHistogramExemplarTTL {
1635
- n .exemplars [oldestIndex ] = e
1636
- } else {
1637
- n .exemplars [nearestIndex ] = e
1704
+ }
1705
+
1706
+ // If all existing exemplar are smaller than new exemplar,
1707
+ // then the exemplar should be inserted at the end.
1708
+ if nIdx == - 1 {
1709
+ nIdx = len (n .exemplars )
1710
+ }
1711
+
1712
+ if otIdx != - 1 && time .Since (ot ) > n .ttl {
1713
+ rIdx = otIdx
1714
+ } else {
1715
+ // In the previous for loop, when calculating the closest pair of exemplars,
1716
+ // we did not take into account the newly inserted exemplar.
1717
+ // So we need to calculate with the newly inserted exemplar again.
1718
+ elog := math .Log (e .GetValue ())
1719
+ if nIdx > 0 {
1720
+ diff := math .Abs (elog - math .Log (n .exemplars [nIdx - 1 ].GetValue ()))
1721
+ if diff < md {
1722
+ md = diff
1723
+ mdIdx = nIdx
1724
+ if n .exemplars [nIdx - 1 ].Timestamp .AsTime ().Before (e .Timestamp .AsTime ()) {
1725
+ mdIdx = nIdx - 1
1726
+ }
1727
+ }
1638
1728
}
1639
- return
1729
+ if nIdx < len (n .exemplars ) {
1730
+ diff := math .Abs (math .Log (n .exemplars [nIdx ].GetValue ()) - elog )
1731
+ if diff < md {
1732
+ mdIdx = nIdx
1733
+ if n .exemplars [nIdx ].Timestamp .AsTime ().Before (e .Timestamp .AsTime ()) {
1734
+ mdIdx = nIdx
1735
+ }
1736
+ }
1737
+ }
1738
+ rIdx = mdIdx
1640
1739
}
1641
1740
1642
- n .exemplars = append (n .exemplars , e )
1741
+ // Adjust the slice according to rIdx and nIdx.
1742
+ switch {
1743
+ case rIdx == nIdx :
1744
+ n .exemplars [nIdx ] = e
1745
+ case rIdx < nIdx :
1746
+ n .exemplars = append (n .exemplars [:rIdx ], append (n .exemplars [rIdx + 1 :nIdx ], append ([]* dto.Exemplar {e }, n .exemplars [nIdx :]... )... )... )
1747
+ case rIdx > nIdx :
1748
+ n .exemplars = append (n .exemplars [:nIdx ], append ([]* dto.Exemplar {e }, append (n .exemplars [nIdx :rIdx ], n .exemplars [rIdx + 1 :]... )... )... )
1749
+ }
1643
1750
}
0 commit comments