value.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package rate
  4. import (
  5. "encoding/json"
  6. "fmt"
  7. "math"
  8. "sync"
  9. "time"
  10. "tailscale.com/tstime/mono"
  11. )
  12. // Value measures the rate at which events occur,
  13. // exponentially weighted towards recent activity.
  14. // It is guaranteed to occupy O(1) memory, operate in O(1) runtime,
  15. // and is safe for concurrent use.
  16. // The zero value is safe for immediate use.
  17. //
  18. // The algorithm is based on and semantically equivalent to
  19. // [exponentially weighted moving averages (EWMAs)],
  20. // but modified to avoid assuming that event samples are gathered
  21. // at fixed and discrete time-step intervals.
  22. //
  23. // In EWMA literature, the average is typically tuned with a λ parameter
  24. // that determines how much weight to give to recent event samples.
  25. // A high λ value reacts quickly to new events favoring recent history,
  26. // while a low λ value reacts more slowly to new events.
  27. // The EWMA is computed as:
  28. //
  29. // zᵢ = λxᵢ + (1-λ)zᵢ₋₁
  30. //
  31. // where:
  32. // - λ is the weight parameter, where 0 ≤ λ ≤ 1
  33. // - xᵢ is the number of events that has since occurred
  34. // - zᵢ is the newly computed moving average
  35. // - zᵢ₋₁ is the previous moving average one time-step ago
  36. //
  37. // As mentioned, this implementation does not assume that the average
  38. // is updated periodically on a fixed time-step interval,
  39. // but allows the application to indicate that events occurred
  40. // at any point in time by simply calling Value.Add.
  41. // Thus, for every time Value.Add is called, it takes into consideration
  42. // the amount of time elapsed since the last call to Value.Add as
  43. // opposed to assuming that every call to Value.Add is evenly spaced
  44. // some fixed time-step interval apart.
  45. //
  46. // Since time is critical to this measurement, we tune the metric not
  47. // with the weight parameter λ (a unit-less constant between 0 and 1),
  48. // but rather as a half-life period t½. The half-life period is
  49. // mathematically equivalent but easier for humans to reason about.
  50. // The parameters λ and t½ and directly related in the following way:
  51. //
  52. // t½ = -(ln(2) · ΔT) / ln(1 - λ)
  53. //
  54. // λ = 1 - 2^-(ΔT / t½)
  55. //
  56. // where:
  57. // - t½ is the half-life commonly used with exponential decay
  58. // - λ is the unit-less weight parameter commonly used with EWMAs
  59. // - ΔT is the discrete time-step interval used with EWMAs
  60. //
  61. // The internal algorithm does not use the EWMA formula,
  62. // but is rather based on [half-life decay].
  63. // The formula for half-life decay is mathematically related
  64. // to the formula for computing the EWMA.
  65. // The calculation of an EWMA is a geometric progression [[1]] and
  66. // is essentially a discrete version of an exponential function [[2]],
  67. // for which half-life decay is one particular expression.
  68. // Given sufficiently small time-steps, the EWMA and half-life
  69. // algorithms provide equivalent results.
  70. //
  71. // The Value type does not take ΔT as a parameter since it relies
  72. // on a timer with nanosecond resolution. In a way, one could treat
  73. // this algorithm as operating on a ΔT of 1ns. Practically speaking,
  74. // the computation operates on non-discrete time intervals.
  75. //
  76. // [exponentially weighted moving averages (EWMAs)]: https://en.wikipedia.org/wiki/EWMA_chart
  77. // [half-life decay]: https://en.wikipedia.org/wiki/Half-life
  78. // [1]: https://en.wikipedia.org/wiki/Exponential_smoothing#%22Exponential%22_naming
  79. // [2]: https://en.wikipedia.org/wiki/Exponential_decay
  80. type Value struct {
  81. // HalfLife specifies how quickly the rate reacts to rate changes.
  82. //
  83. // Specifically, if there is currently a steady-state rate of
  84. // 0 events per second, and then immediately the rate jumped to
  85. // N events per second, then it will take HalfLife seconds until
  86. // the Value represents a rate of N/2 events per second and
  87. // 2*HalfLife seconds until the Value represents a rate of 3*N/4
  88. // events per second, and so forth. The rate represented by Value
  89. // will asymptotically approach N events per second over time.
  90. //
  91. // In order for Value to stably represent a steady-state rate,
  92. // the HalfLife should be larger than the average period between
  93. // calls to Value.Add.
  94. //
  95. // A zero or negative HalfLife is by default 1 second.
  96. HalfLife time.Duration
  97. mu sync.Mutex
  98. updated mono.Time
  99. value float64 // adjusted count of events
  100. }
  101. // halfLife returns the half-life period in seconds.
  102. func (r *Value) halfLife() float64 {
  103. if r.HalfLife <= 0 {
  104. return time.Second.Seconds()
  105. }
  106. return time.Duration(r.HalfLife).Seconds()
  107. }
  108. // Add records that n number of events just occurred,
  109. // which must be a finite and non-negative number.
  110. func (r *Value) Add(n float64) {
  111. r.mu.Lock()
  112. defer r.mu.Unlock()
  113. r.addNow(mono.Now(), n)
  114. }
  115. func (r *Value) addNow(now mono.Time, n float64) {
  116. if n < 0 || math.IsInf(n, 0) || math.IsNaN(n) {
  117. panic(fmt.Sprintf("invalid count %f; must be a finite, non-negative number", n))
  118. }
  119. r.value = r.valueNow(now) + n
  120. r.updated = now
  121. }
  122. // valueNow computes the number of events after some elapsed time.
  123. // The total count of events decay exponentially so that
  124. // the computed rate is biased towards recent history.
  125. func (r *Value) valueNow(now mono.Time) float64 {
  126. // This uses the half-life formula:
  127. // N(t) = N₀ · 2^-(t / t½)
  128. // where:
  129. // N(t) is the amount remaining after time t,
  130. // N₀ is the initial quantity, and
  131. // t½ is the half-life of the decaying quantity.
  132. //
  133. // See https://en.wikipedia.org/wiki/Half-life
  134. age := now.Sub(r.updated).Seconds()
  135. return r.value * math.Exp2(-age/r.halfLife())
  136. }
  137. // Rate computes the rate as events per second.
  138. func (r *Value) Rate() float64 {
  139. r.mu.Lock()
  140. defer r.mu.Unlock()
  141. return r.rateNow(mono.Now())
  142. }
  143. func (r *Value) rateNow(now mono.Time) float64 {
  144. // The stored value carries the units "events"
  145. // while we want to compute "events / second".
  146. //
  147. // In the trivial case where the events never decay,
  148. // the average rate can be computed by dividing the total events
  149. // by the total elapsed time since the start of the Value.
  150. // This works because the weight distribution is uniform such that
  151. // the weight of an event in the distant past is equal to
  152. // the weight of a recent event. This is not the case with
  153. // exponentially decaying weights, which complicates computation.
  154. //
  155. // Since our events are decaying, we can divide the number of events
  156. // by the total possible accumulated value, which we determine
  157. // by integrating the half-life formula from t=0 until t=∞,
  158. // assuming that N₀ is 1:
  159. // ∫ N(t) dt = t½ / ln(2)
  160. //
  161. // Recall that the integral of a curve is the area under a curve,
  162. // which carries the units of the X-axis multiplied by the Y-axis.
  163. // In our case this would be the units "events · seconds".
  164. // By normalizing N₀ to 1, the Y-axis becomes a unit-less quantity,
  165. // resulting in a integral unit of just "seconds".
  166. // Dividing the events by the integral quantity correctly produces
  167. // the units of "events / second".
  168. return r.valueNow(now) / r.normalizedIntegral()
  169. }
  170. // normalizedIntegral computes the quantity t½ / ln(2).
  171. // It carries the units of "seconds".
  172. func (r *Value) normalizedIntegral() float64 {
  173. return r.halfLife() / math.Ln2
  174. }
  175. type jsonValue struct {
  176. // TODO: Use v2 "encoding/json" for native time.Duration formatting.
  177. HalfLife string `json:"halfLife,omitempty,omitzero"`
  178. Value float64 `json:"value,omitempty,omitzero"`
  179. Updated mono.Time `json:"updated,omitempty,omitzero"`
  180. }
  181. func (r *Value) MarshalJSON() ([]byte, error) {
  182. if r == nil {
  183. return []byte("null"), nil
  184. }
  185. r.mu.Lock()
  186. defer r.mu.Unlock()
  187. v := jsonValue{Value: r.value, Updated: r.updated}
  188. if r.HalfLife > 0 {
  189. v.HalfLife = r.HalfLife.String()
  190. }
  191. return json.Marshal(v)
  192. }
  193. func (r *Value) UnmarshalJSON(b []byte) error {
  194. var v jsonValue
  195. if err := json.Unmarshal(b, &v); err != nil {
  196. return err
  197. }
  198. halfLife, err := time.ParseDuration(v.HalfLife)
  199. if err != nil && v.HalfLife != "" {
  200. return fmt.Errorf("invalid halfLife: %w", err)
  201. }
  202. r.mu.Lock()
  203. defer r.mu.Unlock()
  204. r.HalfLife = halfLife
  205. r.value = v.Value
  206. r.updated = v.Updated
  207. return nil
  208. }