| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package rate
- import (
- "encoding/json"
- "fmt"
- "math"
- "sync"
- "time"
- "tailscale.com/tstime/mono"
- )
- // Value measures the rate at which events occur,
- // exponentially weighted towards recent activity.
- // It is guaranteed to occupy O(1) memory, operate in O(1) runtime,
- // and is safe for concurrent use.
- // The zero value is safe for immediate use.
- //
- // The algorithm is based on and semantically equivalent to
- // [exponentially weighted moving averages (EWMAs)],
- // but modified to avoid assuming that event samples are gathered
- // at fixed and discrete time-step intervals.
- //
- // In EWMA literature, the average is typically tuned with a λ parameter
- // that determines how much weight to give to recent event samples.
- // A high λ value reacts quickly to new events favoring recent history,
- // while a low λ value reacts more slowly to new events.
- // The EWMA is computed as:
- //
- // zᵢ = λxᵢ + (1-λ)zᵢ₋₁
- //
- // where:
- // - λ is the weight parameter, where 0 ≤ λ ≤ 1
- // - xᵢ is the number of events that has since occurred
- // - zᵢ is the newly computed moving average
- // - zᵢ₋₁ is the previous moving average one time-step ago
- //
- // As mentioned, this implementation does not assume that the average
- // is updated periodically on a fixed time-step interval,
- // but allows the application to indicate that events occurred
- // at any point in time by simply calling Value.Add.
- // Thus, for every time Value.Add is called, it takes into consideration
- // the amount of time elapsed since the last call to Value.Add as
- // opposed to assuming that every call to Value.Add is evenly spaced
- // some fixed time-step interval apart.
- //
- // Since time is critical to this measurement, we tune the metric not
- // with the weight parameter λ (a unit-less constant between 0 and 1),
- // but rather as a half-life period t½. The half-life period is
- // mathematically equivalent but easier for humans to reason about.
- // The parameters λ and t½ and directly related in the following way:
- //
- // t½ = -(ln(2) · ΔT) / ln(1 - λ)
- //
- // λ = 1 - 2^-(ΔT / t½)
- //
- // where:
- // - t½ is the half-life commonly used with exponential decay
- // - λ is the unit-less weight parameter commonly used with EWMAs
- // - ΔT is the discrete time-step interval used with EWMAs
- //
- // The internal algorithm does not use the EWMA formula,
- // but is rather based on [half-life decay].
- // The formula for half-life decay is mathematically related
- // to the formula for computing the EWMA.
- // The calculation of an EWMA is a geometric progression [[1]] and
- // is essentially a discrete version of an exponential function [[2]],
- // for which half-life decay is one particular expression.
- // Given sufficiently small time-steps, the EWMA and half-life
- // algorithms provide equivalent results.
- //
- // The Value type does not take ΔT as a parameter since it relies
- // on a timer with nanosecond resolution. In a way, one could treat
- // this algorithm as operating on a ΔT of 1ns. Practically speaking,
- // the computation operates on non-discrete time intervals.
- //
- // [exponentially weighted moving averages (EWMAs)]: https://en.wikipedia.org/wiki/EWMA_chart
- // [half-life decay]: https://en.wikipedia.org/wiki/Half-life
- // [1]: https://en.wikipedia.org/wiki/Exponential_smoothing#%22Exponential%22_naming
- // [2]: https://en.wikipedia.org/wiki/Exponential_decay
- type Value struct {
- // HalfLife specifies how quickly the rate reacts to rate changes.
- //
- // Specifically, if there is currently a steady-state rate of
- // 0 events per second, and then immediately the rate jumped to
- // N events per second, then it will take HalfLife seconds until
- // the Value represents a rate of N/2 events per second and
- // 2*HalfLife seconds until the Value represents a rate of 3*N/4
- // events per second, and so forth. The rate represented by Value
- // will asymptotically approach N events per second over time.
- //
- // In order for Value to stably represent a steady-state rate,
- // the HalfLife should be larger than the average period between
- // calls to Value.Add.
- //
- // A zero or negative HalfLife is by default 1 second.
- HalfLife time.Duration
- mu sync.Mutex
- updated mono.Time
- value float64 // adjusted count of events
- }
- // halfLife returns the half-life period in seconds.
- func (r *Value) halfLife() float64 {
- if r.HalfLife <= 0 {
- return time.Second.Seconds()
- }
- return time.Duration(r.HalfLife).Seconds()
- }
- // Add records that n number of events just occurred,
- // which must be a finite and non-negative number.
- func (r *Value) Add(n float64) {
- r.mu.Lock()
- defer r.mu.Unlock()
- r.addNow(mono.Now(), n)
- }
- func (r *Value) addNow(now mono.Time, n float64) {
- if n < 0 || math.IsInf(n, 0) || math.IsNaN(n) {
- panic(fmt.Sprintf("invalid count %f; must be a finite, non-negative number", n))
- }
- r.value = r.valueNow(now) + n
- r.updated = now
- }
- // valueNow computes the number of events after some elapsed time.
- // The total count of events decay exponentially so that
- // the computed rate is biased towards recent history.
- func (r *Value) valueNow(now mono.Time) float64 {
- // This uses the half-life formula:
- // N(t) = N₀ · 2^-(t / t½)
- // where:
- // N(t) is the amount remaining after time t,
- // N₀ is the initial quantity, and
- // t½ is the half-life of the decaying quantity.
- //
- // See https://en.wikipedia.org/wiki/Half-life
- age := now.Sub(r.updated).Seconds()
- return r.value * math.Exp2(-age/r.halfLife())
- }
- // Rate computes the rate as events per second.
- func (r *Value) Rate() float64 {
- r.mu.Lock()
- defer r.mu.Unlock()
- return r.rateNow(mono.Now())
- }
- func (r *Value) rateNow(now mono.Time) float64 {
- // The stored value carries the units "events"
- // while we want to compute "events / second".
- //
- // In the trivial case where the events never decay,
- // the average rate can be computed by dividing the total events
- // by the total elapsed time since the start of the Value.
- // This works because the weight distribution is uniform such that
- // the weight of an event in the distant past is equal to
- // the weight of a recent event. This is not the case with
- // exponentially decaying weights, which complicates computation.
- //
- // Since our events are decaying, we can divide the number of events
- // by the total possible accumulated value, which we determine
- // by integrating the half-life formula from t=0 until t=∞,
- // assuming that N₀ is 1:
- // ∫ N(t) dt = t½ / ln(2)
- //
- // Recall that the integral of a curve is the area under a curve,
- // which carries the units of the X-axis multiplied by the Y-axis.
- // In our case this would be the units "events · seconds".
- // By normalizing N₀ to 1, the Y-axis becomes a unit-less quantity,
- // resulting in a integral unit of just "seconds".
- // Dividing the events by the integral quantity correctly produces
- // the units of "events / second".
- return r.valueNow(now) / r.normalizedIntegral()
- }
- // normalizedIntegral computes the quantity t½ / ln(2).
- // It carries the units of "seconds".
- func (r *Value) normalizedIntegral() float64 {
- return r.halfLife() / math.Ln2
- }
- type jsonValue struct {
- // TODO: Use v2 "encoding/json" for native time.Duration formatting.
- HalfLife string `json:"halfLife,omitempty,omitzero"`
- Value float64 `json:"value,omitempty,omitzero"`
- Updated mono.Time `json:"updated,omitempty,omitzero"`
- }
- func (r *Value) MarshalJSON() ([]byte, error) {
- if r == nil {
- return []byte("null"), nil
- }
- r.mu.Lock()
- defer r.mu.Unlock()
- v := jsonValue{Value: r.value, Updated: r.updated}
- if r.HalfLife > 0 {
- v.HalfLife = r.HalfLife.String()
- }
- return json.Marshal(v)
- }
- func (r *Value) UnmarshalJSON(b []byte) error {
- var v jsonValue
- if err := json.Unmarshal(b, &v); err != nil {
- return err
- }
- halfLife, err := time.ParseDuration(v.HalfLife)
- if err != nil && v.HalfLife != "" {
- return fmt.Errorf("invalid halfLife: %w", err)
- }
- r.mu.Lock()
- defer r.mu.Unlock()
- r.HalfLife = halfLife
- r.value = v.Value
- r.updated = v.Updated
- return nil
- }
|