Class: Philiprehberger::RateWindow::Tracker

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/rate_window/tracker.rb

Overview

Thread-safe time-windowed rate tracker with configurable resolution.

Instance Method Summary collapse

Constructor Details

#initialize(window: 60, resolution: 1) ⇒ Tracker

Returns a new instance of Tracker.

Parameters:

  • window (Numeric) (defaults to: 60)

    window duration in seconds

  • resolution (Numeric) (defaults to: 1)

    bucket size in seconds

Raises:



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/philiprehberger/rate_window/tracker.rb', line 9

def initialize(window: 60, resolution: 1)
  raise Error, 'window must be positive' unless window.positive?
  raise Error, 'resolution must be positive' unless resolution.positive?
  raise Error, 'resolution must be <= window' unless resolution <= window

  @window = window.to_f
  @resolution = resolution.to_f
  @bucket_count = (@window / @resolution).ceil
  @mutex = Mutex.new
  @buckets = Array.new(@bucket_count, 0.0)
  @counts = Array.new(@bucket_count, 0)
  @mins = Array.new(@bucket_count, Float::INFINITY)
  @maxs = Array.new(@bucket_count, -Float::INFINITY)
  @last_bucket_index = current_bucket_index
  @last_time = now
end

Instance Method Details

#averageFloat

Average value per recording in the window.

Returns:

  • (Float)

    average, or 0.0 if no recordings



77
78
79
80
81
82
83
84
85
# File 'lib/philiprehberger/rate_window/tracker.rb', line 77

def average
  @mutex.synchronize do
    cleanup
    total_count = @counts.sum
    return 0.0 if total_count.zero?

    @buckets.sum / total_count
  end
end

#countInteger

Number of recordings in the window.

Returns:

  • (Integer)


67
68
69
70
71
72
# File 'lib/philiprehberger/rate_window/tracker.rb', line 67

def count
  @mutex.synchronize do
    cleanup
    @counts.sum
  end
end

#histogram(buckets: 10) ⇒ Array<Hash>

Returns a histogram of value distribution across equal-width buckets.

Parameters:

  • buckets (Integer) (defaults to: 10)

    number of histogram buckets (default: 10)

Returns:

  • (Array<Hash>)

    array of { range:, count: } hashes

Raises:



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/philiprehberger/rate_window/tracker.rb', line 157

def histogram(buckets: 10)
  raise Error, 'buckets must be positive' unless buckets.positive?

  @mutex.synchronize do
    cleanup
    values = collect_values
    return [] if values.empty?

    min_val = values.min
    max_val = values.max

    if min_val == max_val
      return [{ range: (min_val..max_val), count: values.length }]
    end

    width = (max_val - min_val).to_f / buckets
    result = Array.new(buckets) do |i|
      range_start = min_val + (i * width)
      range_end = min_val + ((i + 1) * width)
      { range: (range_start..range_end), count: 0 }
    end

    values.each do |v|
      idx = ((v - min_val) / width).floor
      idx = buckets - 1 if idx >= buckets
      result[idx][:count] += 1
    end

    result
  end
end

#maxFloat

Maximum recorded value in the current window.

Returns:

  • (Float)

    maximum value, or 0.0 if no recordings



142
143
144
145
146
147
148
149
150
151
# File 'lib/philiprehberger/rate_window/tracker.rb', line 142

def max
  @mutex.synchronize do
    cleanup
    result = -Float::INFINITY
    @bucket_count.times do |i|
      result = @maxs[i] if @counts[i].positive? && @maxs[i] > result
    end
    result == -Float::INFINITY ? 0.0 : result
  end
end

#medianFloat

Median value across active buckets (shortcut for percentile(50)).

Returns:

  • (Float)

    the median value



114
115
116
# File 'lib/philiprehberger/rate_window/tracker.rb', line 114

def median
  percentile(50)
end

#minFloat

Minimum recorded value in the current window.

Returns:

  • (Float)

    minimum value, or 0.0 if no recordings



128
129
130
131
132
133
134
135
136
137
# File 'lib/philiprehberger/rate_window/tracker.rb', line 128

def min
  @mutex.synchronize do
    cleanup
    result = Float::INFINITY
    @bucket_count.times do |i|
      result = @mins[i] if @counts[i].positive? && @mins[i] < result
    end
    result == Float::INFINITY ? 0.0 : result
  end
end

#p95Float

95th percentile value across active buckets (shortcut for percentile(95)).

Returns:

  • (Float)

    the 95th percentile value



121
122
123
# File 'lib/philiprehberger/rate_window/tracker.rb', line 121

def p95
  percentile(95)
end

#percentile(p) ⇒ Float

Calculate a percentile of recorded values using linear interpolation.

Parameters:

  • p (Numeric)

    percentile (0-100)

Returns:

  • (Float)

    the percentile value

Raises:



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/philiprehberger/rate_window/tracker.rb', line 91

def percentile(p)
  raise Error, 'percentile must be between 0 and 100' unless p.between?(0, 100)

  @mutex.synchronize do
    cleanup
    values = collect_values
    return 0.0 if values.empty?

    sorted = values.sort
    rank = p / 100.0 * (sorted.length - 1)
    lower = rank.floor
    upper = rank.ceil

    return sorted[lower].to_f if lower == upper

    weight = rank - lower
    (sorted[lower] + (weight * (sorted[upper] - sorted[lower]))).to_f
  end
end

#rateFloat

Calculate the rate per second over the window.

Returns:

  • (Float)

    rate per second



46
47
48
49
50
51
52
# File 'lib/philiprehberger/rate_window/tracker.rb', line 46

def rate
  @mutex.synchronize do
    cleanup
    total = @buckets.sum
    total / @window
  end
end

#record(value = 1) ⇒ self

Record a value in the current time bucket.

Parameters:

  • value (Numeric) (defaults to: 1)

    the value to record (default: 1)

Returns:

  • (self)


30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/philiprehberger/rate_window/tracker.rb', line 30

def record(value = 1)
  @mutex.synchronize do
    cleanup
    idx = current_bucket_index % @bucket_count
    val = value.to_f
    @buckets[idx] += val
    @counts[idx] += 1
    @mins[idx] = val if val < @mins[idx]
    @maxs[idx] = val if val > @maxs[idx]
  end
  self
end

#resetself

Reset all buckets.

Returns:

  • (self)


192
193
194
195
196
197
198
199
200
201
202
# File 'lib/philiprehberger/rate_window/tracker.rb', line 192

def reset
  @mutex.synchronize do
    @buckets.fill(0.0)
    @counts.fill(0)
    @mins.fill(Float::INFINITY)
    @maxs.fill(-Float::INFINITY)
    @last_bucket_index = current_bucket_index
    @last_time = now
  end
  self
end

#sumFloat

Sum of all values in the window.

Returns:

  • (Float)


57
58
59
60
61
62
# File 'lib/philiprehberger/rate_window/tracker.rb', line 57

def sum
  @mutex.synchronize do
    cleanup
    @buckets.sum
  end
end