p99-ruby 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+
2
+ # ######################################################################## #
3
+ # File: p99/histogram.rb
4
+ #
5
+ # Purpose: Histogram loader for p99.Ruby (pure Ruby; C extension later)
6
+ #
7
+ # Created: 4th August 2026
8
+ # Updated: 4th August 2026
9
+ #
10
+ # Home: http://github.com/synesissoftware/p99.Ruby
11
+ #
12
+ # Author: Matthew Wilson
13
+ #
14
+ # Copyright (c) 2026, Matthew Wilson and Synesis Information Systems
15
+ # All rights reserved.
16
+ #
17
+ # Redistribution and use in source and binary forms, with or without
18
+ # modification, are permitted provided that the following conditions are
19
+ # met:
20
+ #
21
+ # * Redistributions of source code must retain the above copyright
22
+ # notice, this list of conditions and the following disclaimer.
23
+ #
24
+ # * Redistributions in binary form must reproduce the above copyright
25
+ # notice, this list of conditions and the following disclaimer in the
26
+ # documentation and/or other materials provided with the distribution.
27
+ #
28
+ # * Neither the names of the copyright holder nor the names of its
29
+ # contributors may be used to endorse or promote products derived from
30
+ # this software without specific prior written permission.
31
+ #
32
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
33
+ # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
34
+ # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
35
+ # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
36
+ # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
37
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
38
+ # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
39
+ # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
40
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
41
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
42
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43
+ #
44
+ # ######################################################################## #
45
+
46
+
47
+ =begin
48
+ =end
49
+
50
+ module P99
51
+
52
+ # Loads the Histogram implementation.
53
+ #
54
+ # Prefers a future C extension when present, unless +P99_PURE_RUBY+ is set
55
+ # in the environment; otherwise falls back to the pure-Ruby implementation.
56
+ #
57
+ # @return [String] +"c"+ or +"ruby"+
58
+ def self.load_histogram_implementation
59
+
60
+ if ENV['P99_PURE_RUBY']
61
+
62
+ require 'p99/histogram/pure'
63
+
64
+ return 'ruby'
65
+ end
66
+
67
+ begin
68
+
69
+ require 'p99/histogram/ext'
70
+
71
+ 'c'
72
+ rescue LoadError
73
+
74
+ require 'p99/histogram/pure'
75
+
76
+ 'ruby'
77
+ end
78
+ end
79
+
80
+ # Active Histogram backend: +"ruby"+ (current) or +"c"+ (when available).
81
+ IMPLEMENTATION = load_histogram_implementation
82
+ end # module P99
83
+
84
+
85
+ # ############################## end of file ############################# #
data/lib/p99/version.rb CHANGED
@@ -5,7 +5,7 @@
5
5
  # Purpose: Version for p99.Ruby library
6
6
  #
7
7
  # Created: 4th August 2026
8
- # Updated: 28th August 2026
8
+ # Updated: 30th August 2026
9
9
  #
10
10
  # Home: http://github.com/synesissoftware/p99.Ruby
11
11
  #
@@ -50,7 +50,7 @@
50
50
  module P99
51
51
 
52
52
  # Current version of the p99.Ruby library
53
- VERSION = '0.0.1'
53
+ VERSION = '0.1.0'
54
54
 
55
55
  private
56
56
  # @!visibility private
data/lib/p99.rb CHANGED
@@ -54,6 +54,7 @@ end # module P99
54
54
 
55
55
 
56
56
  require 'p99/version'
57
+ require 'p99/histogram'
57
58
 
58
59
 
59
60
  # ############################## end of file ############################# #
@@ -0,0 +1,317 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), '../../lib')
4
+
5
+
6
+ require 'p99'
7
+
8
+ require 'test/unit'
9
+
10
+
11
+ class Test_histogram < Test::Unit::TestCase
12
+
13
+ def setup
14
+
15
+ @h = P99::Histogram.new
16
+ end
17
+
18
+ def test_implementation_is_ruby
19
+
20
+ assert_equal 'ruby', P99::IMPLEMENTATION
21
+ end
22
+
23
+ def test_default_histogram
24
+
25
+ assert_equal 0, @h.event_count
26
+ assert_equal 0, @h.event_time_total
27
+ assert_equal 0, @h.event_time_total_raw
28
+ assert_equal false, @h.has_overflowed?
29
+ assert_nil @h.min_event_time
30
+ assert_nil @h.max_event_time
31
+
32
+ buckets = @h.buckets
33
+
34
+ assert_equal P99::BUCKET_COUNT, buckets.size
35
+
36
+ P99::BUCKET_COUNT.times do |i|
37
+
38
+ assert_equal 0, buckets[i]
39
+ assert_equal 0, @h.bucket_value(i)
40
+ end
41
+
42
+ assert_nil @h.bucket_value(64)
43
+ assert_nil @h.bucket_value(-1)
44
+ end
45
+
46
+ def test_bucket_index
47
+
48
+ cases = [
49
+ [ 0, 0 ],
50
+ [ 1, 0 ],
51
+ [ 2, 1 ],
52
+ [ 3, 1 ],
53
+ [ 4, 2 ],
54
+ [ 7, 2 ],
55
+ [ 8, 3 ],
56
+ [ 15, 3 ],
57
+ [ 1024, 10 ],
58
+ [ 2047, 10 ],
59
+ [ 1 << 63, 63 ],
60
+ [ P99::UINT64_MAX, 63 ],
61
+ ]
62
+
63
+ cases.each do |value, bucket|
64
+
65
+ assert_equal bucket, P99.bucket_index(value), "value=#{value}"
66
+ end
67
+ end
68
+
69
+ def test_bucket_range
70
+
71
+ cases = [
72
+ [ 0, 0, 1 ],
73
+ [ 1, 2, 3 ],
74
+ [ 2, 4, 7 ],
75
+ [ 3, 8, 15 ],
76
+ [ 4, 16, 31 ],
77
+ [ 10, 1024, 2047 ],
78
+ [ 63, 1 << 63, P99::UINT64_MAX ],
79
+ ]
80
+
81
+ cases.each do |index, lower, upper|
82
+
83
+ assert_equal [ lower, upper ], P99.bucket_range(index)
84
+ end
85
+
86
+ assert_nil P99.bucket_range(64)
87
+ assert_nil P99.bucket_range(-1)
88
+ end
89
+
90
+ def test_bucket_placement
91
+
92
+ cases = [
93
+ [ 0, 0 ],
94
+ [ 1, 0 ],
95
+ [ 2, 1 ],
96
+ [ 3, 1 ],
97
+ [ 4, 2 ],
98
+ [ 7, 2 ],
99
+ [ 8, 3 ],
100
+ [ 15, 3 ],
101
+ [ 1024, 10 ],
102
+ [ 2047, 10 ],
103
+ [ 1 << 63, 63 ],
104
+ [ P99::UINT64_MAX, 63 ],
105
+ ]
106
+
107
+ cases.each do |value, bucket|
108
+
109
+ @h.clear
110
+
111
+ assert @h.push_event_time_ns(value)
112
+ assert_equal 1, @h.bucket_value(bucket), "value=#{value}"
113
+ end
114
+ end
115
+
116
+ def test_push_events
117
+
118
+ assert @h.push_event_time_ns(1)
119
+ assert @h.push_event_time_ns(3)
120
+ assert @h.push_event_time_us(10)
121
+ assert @h.push_event_time_ms(5)
122
+ assert @h.push_event_time_s(2)
123
+ assert @h.push_event_time_ns(100)
124
+
125
+ assert_equal 6, @h.event_count
126
+ assert_equal false, @h.has_overflowed?
127
+ assert_equal 1, @h.min_event_time
128
+ assert_equal 2_000_000_000, @h.max_event_time
129
+ assert_equal 2_005_010_104, @h.event_time_total
130
+
131
+ buckets = @h.buckets
132
+
133
+ assert_equal 1, buckets[0]
134
+ assert_equal 1, buckets[1]
135
+ assert_equal 1, buckets[6]
136
+ assert_equal 1, buckets[13]
137
+ assert_equal 1, buckets[22]
138
+ assert_equal 1, buckets[30]
139
+
140
+ @h.clear
141
+
142
+ assert_equal 0, @h.event_count
143
+ assert_equal 0, @h.event_time_total
144
+ end
145
+
146
+ def test_overflow
147
+
148
+ assert @h.push_event_time_ns(P99::UINT64_MAX)
149
+ assert_equal P99::UINT64_MAX, @h.event_time_total
150
+ assert_equal false, @h.has_overflowed?
151
+
152
+ assert_equal false, @h.push_event_time_ns(1)
153
+ assert_equal true, @h.has_overflowed?
154
+ assert_nil @h.event_time_total
155
+ assert_equal P99::UINT64_MAX, @h.event_time_total_raw
156
+ end
157
+
158
+ def test_percentiles_empty
159
+
160
+ assert_nil @h.value_at_percentile(50.0)
161
+ assert_nil @h.value_at_p50
162
+ assert_nil @h.value_at_p99
163
+ assert_nil @h.fixed_percentiles
164
+ assert_nil @h.values_at_percentiles([ 50.0 ])
165
+ end
166
+
167
+ def test_percentiles_single_event
168
+
169
+ assert @h.push_event_time_ns(100)
170
+
171
+ [ 0.0, 50.0, 99.0, 100.0 ].each do |percentile|
172
+
173
+ assert_equal 100, @h.value_at_percentile(percentile)
174
+ end
175
+
176
+ assert_equal 100, @h.value_at_p50
177
+ assert_equal 100, @h.value_at_p90
178
+ assert_equal 100, @h.value_at_p99
179
+ assert_equal 100, @h.value_at_p99_999_9
180
+ end
181
+
182
+ def test_percentiles_interpolation
183
+
184
+ assert @h.push_event_time_ns(100)
185
+ assert @h.push_event_time_ns(200)
186
+
187
+ p50 = @h.value_at_p50
188
+ p99 = @h.value_at_p99
189
+
190
+ assert_operator p50, :>=, 100
191
+ assert_operator p50, :<=, 200
192
+ assert_operator p99, :>=, 100
193
+ assert_operator p99, :<=, 200
194
+ assert_equal 100, @h.value_at_percentile(0.0)
195
+ assert_equal 200, @h.value_at_percentile(100.0)
196
+ end
197
+
198
+ def test_percentiles_wide_range
199
+
200
+ values = [
201
+ 1,
202
+ 10,
203
+ 100,
204
+ 1_000,
205
+ 10_000,
206
+ 100_000,
207
+ 1_000_000,
208
+ 10_000_000,
209
+ 100_000_000,
210
+ 1_000_000_000,
211
+ 10_000_000_000,
212
+ ]
213
+
214
+ values.each do |value|
215
+
216
+ assert @h.push_event_time_ns(value)
217
+ end
218
+
219
+ assert_equal values.size, @h.event_count
220
+ assert_equal 1, @h.min_event_time
221
+ assert_equal 10_000_000_000, @h.max_event_time
222
+
223
+ fixed = @h.fixed_percentiles
224
+
225
+ refute_nil fixed
226
+
227
+ ordered = [
228
+ fixed['p50'],
229
+ fixed['p75'],
230
+ fixed['p90'],
231
+ fixed['p95'],
232
+ fixed['p99'],
233
+ fixed['p99.5'],
234
+ fixed['p99.9'],
235
+ fixed['p99.99'],
236
+ fixed['p99.999'],
237
+ fixed['p99.9999'],
238
+ ]
239
+
240
+ assert_equal ordered.sort, ordered
241
+ assert_operator ordered.first, :>=, 1
242
+ assert_operator ordered.last, :<=, 10_000_000_000
243
+ end
244
+
245
+ def test_percentiles_many_events
246
+
247
+ count = 100_000
248
+
249
+ (1..count).each do |i|
250
+
251
+ assert @h.push_event_time_ns(i)
252
+ end
253
+
254
+ assert_equal count, @h.event_count
255
+ assert_equal 1, @h.min_event_time
256
+ assert_equal count, @h.max_event_time
257
+ assert_equal 50_000, @h.value_at_p50
258
+ assert_equal 100_000, @h.value_at_p90
259
+ assert_equal 100_000, @h.value_at_p99
260
+ assert_equal 100_000, @h.value_at_p99_9
261
+ end
262
+
263
+ def test_compare_float_and_int_percentiles
264
+
265
+ (1..10_000).each do |i|
266
+
267
+ assert @h.push_event_time_ns((i * i) % 1_000_000)
268
+ end
269
+
270
+ pairs = [
271
+ [ 50.0, :value_at_p50 ],
272
+ [ 75.0, :value_at_p75 ],
273
+ [ 90.0, :value_at_p90 ],
274
+ [ 95.0, :value_at_p95 ],
275
+ [ 99.0, :value_at_p99 ],
276
+ [ 99.5, :value_at_p99_5 ],
277
+ [ 99.9, :value_at_p99_9 ],
278
+ [ 99.99, :value_at_p99_99 ],
279
+ [ 99.999, :value_at_p99_999 ],
280
+ [ 99.9999, :value_at_p99_999_9 ],
281
+ ]
282
+
283
+ pairs.each do |level, method_name|
284
+
285
+ float_value = @h.value_at_percentile(level)
286
+ int_value = @h.send(method_name)
287
+
288
+ refute_nil float_value
289
+ refute_nil int_value
290
+
291
+ tolerance = [ float_value.abs * 0.01, 1.0 ].max
292
+
293
+ assert_operator (float_value - int_value).abs, :<=, tolerance
294
+ end
295
+ end
296
+
297
+ def test_values_at_percentiles_empty_levels
298
+
299
+ assert @h.push_event_time_ns(100)
300
+ assert_equal [], @h.values_at_percentiles([])
301
+ end
302
+
303
+ def test_values_at_percentiles
304
+
305
+ assert @h.push_event_time_ns(100)
306
+ assert @h.push_event_time_ns(200)
307
+
308
+ results = @h.values_at_percentiles([ 0.0, 100.0 ])
309
+
310
+ assert_equal [ [ 0.0, 100 ], [ 100.0, 200 ] ], results
311
+ end
312
+
313
+ def test_clear_returns_self
314
+
315
+ assert_same @h, @h.clear
316
+ end
317
+ end
File without changes
data/test/unit/ts_all.rb CHANGED
File without changes
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: p99-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Wilson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-28 00:00:00.000000000 Z
11
+ date: 2026-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: xqsr3
@@ -33,18 +33,39 @@ dependencies:
33
33
  description: |
34
34
  Low-cost generation of performance percentiles (p50, p90, p99, p99.9, etc.), for Ruby.
35
35
 
36
- p99.Ruby is the Ruby implementation of the p99 family of libraries.
36
+ p99.Ruby is the Ruby implementation of the p99 family of libraries. This
37
+ release provides a pure-Ruby Histogram; a C-extension backend is planned.
37
38
  email:
38
39
  - matthew@synesis.com.au
39
40
  executables: []
40
41
  extensions: []
41
42
  extra_rdoc_files: []
42
43
  files:
44
+ - AUTHORS.md
45
+ - CHANGES.md
46
+ - CONTRIBUTING.md
47
+ - EXAMPLES.md
48
+ - FAQ.md
49
+ - INSTALL.md
43
50
  - LICENSE
51
+ - NEWS.md
44
52
  - README.md
45
53
  - Rakefile
54
+ - SECURITY.md
55
+ - TODO.md
56
+ - docs/components/histogram.md
57
+ - docs/guides/README.md
58
+ - docs/guides/recording-and-reading-percentiles.md
59
+ - docs/reference/README.md
60
+ - examples/build_histogram.md
61
+ - examples/build_histogram.rb
62
+ - examples/measure_batch_latency.md
63
+ - examples/measure_batch_latency.rb
46
64
  - lib/p99.rb
65
+ - lib/p99/histogram.rb
66
+ - lib/p99/histogram/pure.rb
47
67
  - lib/p99/version.rb
68
+ - test/unit/tc_histogram.rb
48
69
  - test/unit/tc_version.rb
49
70
  - test/unit/ts_all.rb
50
71
  homepage: https://github.com/synesissoftware/p99.Ruby