Skip to content

Commit d8c7074

Browse files
committed
refract the implementation
Signed-off-by: Ziqi Zhao <[email protected]>
1 parent 494ccce commit d8c7074

File tree

2 files changed

+300
-116
lines changed

2 files changed

+300
-116
lines changed

prometheus/histogram.go

Lines changed: 153 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ type HistogramOpts struct {
440440
// constant (or any negative float value).
441441
NativeHistogramZeroThreshold float64
442442

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
444444
// populated sparse buckets. If NativeHistogramMaxBucketNumber is left
445445
// at zero, the number of buckets is not limited. (Note that this might
446446
// lead to unbounded memory consumption if the values observed by the
@@ -472,8 +472,22 @@ type HistogramOpts struct {
472472
NativeHistogramMaxBucketNumber uint32
473473
NativeHistogramMinResetDuration time.Duration
474474
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
477491

478492
// now is for testing purposes, by default it's time.Now.
479493
now func() time.Time
@@ -534,6 +548,7 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr
534548
if opts.afterFunc == nil {
535549
opts.afterFunc = time.AfterFunc
536550
}
551+
537552
h := &histogram{
538553
desc: desc,
539554
upperBounds: opts.Buckets,
@@ -558,7 +573,7 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr
558573
h.nativeHistogramZeroThreshold = DefNativeHistogramZeroThreshold
559574
} // Leave h.nativeHistogramZeroThreshold at 0 otherwise.
560575
h.nativeHistogramSchema = pickSchema(opts.NativeHistogramBucketFactor)
561-
h.nativeExemplars = newNativeExemplars(opts.NativeHistogramExemplarTTL, opts.NativeHistogramMaxExemplarCount)
576+
h.nativeExemplars = makeNativeExemplars(opts.NativeHistogramExemplarTTL, opts.NativeHistogramMaxExemplars)
562577
}
563578
for i, upperBound := range h.upperBounds {
564579
if i < len(h.upperBounds)-1 {
@@ -728,15 +743,14 @@ type histogram struct {
728743
// resetScheduled is protected by mtx. It is true if a reset is
729744
// scheduled for a later time (when nativeHistogramMinResetDuration has
730745
// passed).
731-
resetScheduled bool
746+
resetScheduled bool
747+
nativeExemplars nativeExemplars
732748

733749
// now is for testing purposes, by default it's time.Now.
734750
now func() time.Time
735751

736752
// afterFunc is for testing purposes, by default it's time.AfterFunc.
737753
afterFunc func(time.Duration, func()) *time.Timer
738-
739-
nativeExemplars nativeExemplars
740754
}
741755

742756
func (h *histogram) Desc() *Desc {
@@ -747,6 +761,8 @@ func (h *histogram) Observe(v float64) {
747761
h.observe(v, h.findBucket(v))
748762
}
749763

764+
// ObserveWithExemplar should not be called in high-frequency settings,
765+
// since it isn't lock-free for native histograms with configured exemplars.
750766
func (h *histogram) ObserveWithExemplar(v float64, e Labels) {
751767
i := h.findBucket(v)
752768
h.observe(v, i)
@@ -827,7 +843,12 @@ func (h *histogram) Write(out *dto.Metric) error {
827843
}}
828844
}
829845

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+
831852
}
832853
addAndResetCounts(hotCounts, coldCounts)
833854
return nil
@@ -1098,8 +1119,10 @@ func (h *histogram) resetCounts(counts *histogramCounts) {
10981119
deleteSyncMap(&counts.nativeHistogramBucketsPositive)
10991120
}
11001121

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.
11031126
func (h *histogram) updateExemplar(v float64, bucket int, l Labels) {
11041127
if l == nil {
11051128
return
@@ -1588,56 +1611,140 @@ func addAndResetCounts(hot, cold *histogramCounts) {
15881611
}
15891612

15901613
type nativeExemplars struct {
1591-
nativeHistogramExemplarTTL time.Duration
1592-
nativeHistogramMaxExemplarCount uint32
1614+
sync.Mutex
15931615

1616+
ttl time.Duration
15941617
exemplars []*dto.Exemplar
1595-
1596-
lock sync.Mutex
15971618
}
15981619

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+
16001633
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),
16051636
}
16061637
}
16071638

16081639
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
16251657
}
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
16311701
}
16321702
}
16331703

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+
}
16381728
}
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
16401739
}
16411740

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+
}
16431750
}

0 commit comments

Comments
 (0)