atomic-ruby 0.15.4 → 0.15.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +14 -12
- data/lib/atomic-ruby/atomic_thread_pool.rb +68 -32
- data/lib/atomic-ruby/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6022f081e7f3e4ce545c40989e5a644beea0697da5d840f645ae3764fbb80e4e
|
|
4
|
+
data.tar.gz: 94b4312e55cfbda05127bde7ac74efd98da8708cb9835e718e7c02641f1d241a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bbcef6942791f3611e55c7c9806bd5daa4b71dd4bae4bedd255c0d1598e16c0ba50a19ae92423768fca04a4baafd80f464877f60960a7e43be0fc08ed2ffb3a3
|
|
7
|
+
data.tar.gz: '0883a4a401963ad0db23713c6ae6917bec1c3bd1cd3bd8ecff069cc63c99b3000a914ee0c9345cd9f2495f88f0d6da6c35a51057b80a56877409ba19ce3c5e4f'
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -122,8 +122,10 @@ p results.sort #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
|
122
122
|
|
|
123
123
|
Pass `max_size` to let the pool temporarily add workers when queued work is
|
|
124
124
|
held up by blocking operations. The pool measures time spent blocked outside
|
|
125
|
-
the GVL and CPU time used
|
|
126
|
-
Ruby execution is the bottleneck.
|
|
125
|
+
the GVL and CPU time used while running Ruby, so it does not add more threads
|
|
126
|
+
when Ruby execution is the bottleneck. Temporary workers remain available
|
|
127
|
+
between blocking bursts, but retire when Ruby execution becomes the bottleneck
|
|
128
|
+
or they remain idle.
|
|
127
129
|
|
|
128
130
|
```ruby
|
|
129
131
|
pool = AtomicThreadPool.new(size: 4, max_size: 16)
|
|
@@ -268,7 +270,7 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
|
|
|
268
270
|
|
|
269
271
|
ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
270
272
|
concurrent-ruby version: 1.3.8
|
|
271
|
-
atomic-ruby version: 0.15.
|
|
273
|
+
atomic-ruby version: 0.15.5
|
|
272
274
|
|
|
273
275
|
Balances:
|
|
274
276
|
Synchronized Bank Account Balance: 975
|
|
@@ -359,7 +361,7 @@ end
|
|
|
359
361
|
|
|
360
362
|
ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
361
363
|
concurrent-ruby version: 1.3.8
|
|
362
|
-
atomic-ruby version: 0.15.
|
|
364
|
+
atomic-ruby version: 0.15.5
|
|
363
365
|
|
|
364
366
|
Warming up --------------------------------------
|
|
365
367
|
Synchronized Boolean Toggle 165.000 i/100ms
|
|
@@ -442,7 +444,7 @@ end
|
|
|
442
444
|
> bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_condition_variable_benchmark.rb
|
|
443
445
|
|
|
444
446
|
ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
445
|
-
atomic-ruby version: 0.15.
|
|
447
|
+
atomic-ruby version: 0.15.5
|
|
446
448
|
|
|
447
449
|
Warming up --------------------------------------
|
|
448
450
|
Synchronized Condition Variable Wait/Signal 4.062k i/100ms
|
|
@@ -517,7 +519,7 @@ end
|
|
|
517
519
|
> bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_queue_benchmark.rb
|
|
518
520
|
|
|
519
521
|
ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
520
|
-
atomic-ruby version: 0.15.
|
|
522
|
+
atomic-ruby version: 0.15.5
|
|
521
523
|
|
|
522
524
|
Warming up --------------------------------------
|
|
523
525
|
Synchronized Queue Push/Pop 184.000 i/100ms
|
|
@@ -620,15 +622,15 @@ puts "Atomic Ruby Adaptive Thread Pool: #{adaptive_results[1].real.round(6)} sec
|
|
|
620
622
|
|
|
621
623
|
ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
622
624
|
concurrent-ruby version: 1.3.8
|
|
623
|
-
atomic-ruby version: 0.15.
|
|
625
|
+
atomic-ruby version: 0.15.5
|
|
624
626
|
|
|
625
627
|
Fixed Pool Results:
|
|
626
|
-
Concurrent Ruby Fixed Thread Pool: 5.
|
|
627
|
-
Atomic Ruby Fixed Thread Pool: 4.
|
|
628
|
+
Concurrent Ruby Fixed Thread Pool: 5.05313 seconds
|
|
629
|
+
Atomic Ruby Fixed Thread Pool: 4.753026 seconds
|
|
628
630
|
|
|
629
631
|
Adaptive Pool Results:
|
|
630
|
-
Concurrent Ruby IO Thread Pool: 0.
|
|
631
|
-
Atomic Ruby Adaptive Thread Pool: 0.
|
|
632
|
+
Concurrent Ruby IO Thread Pool: 0.221072 seconds
|
|
633
|
+
Atomic Ruby Adaptive Thread Pool: 0.233328 seconds
|
|
632
634
|
```
|
|
633
635
|
|
|
634
636
|
</details>
|
|
@@ -682,7 +684,7 @@ puts "Atomic Ruby Atomic Count Down Latch: #{results[1].total.round(6)} CPU seco
|
|
|
682
684
|
|
|
683
685
|
ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
684
686
|
concurrent-ruby version: 1.3.8
|
|
685
|
-
atomic-ruby version: 0.15.
|
|
687
|
+
atomic-ruby version: 0.15.5
|
|
686
688
|
|
|
687
689
|
Benchmark Results:
|
|
688
690
|
Concurrent Ruby Count Down Latch: 0.000511 CPU seconds, 2.001465 elapsed seconds
|
|
@@ -52,9 +52,10 @@ module AtomicRuby
|
|
|
52
52
|
# that cannot be safely shared across ractors.
|
|
53
53
|
class AtomicThreadPool
|
|
54
54
|
AUTOSCALE_IDLE_TIME = 5
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
AUTOSCALE_INITIAL_WINDOW = 0.002
|
|
56
|
+
AUTOSCALE_MAX_WINDOW = 0.05
|
|
57
|
+
AUTOSCALE_SAMPLES_PER_WINDOW = 5
|
|
58
|
+
private_constant :AUTOSCALE_IDLE_TIME, :AUTOSCALE_INITIAL_WINDOW, :AUTOSCALE_MAX_WINDOW, :AUTOSCALE_SAMPLES_PER_WINDOW
|
|
58
59
|
|
|
59
60
|
class Error < StandardError; end
|
|
60
61
|
|
|
@@ -70,8 +71,9 @@ module AtomicRuby
|
|
|
70
71
|
# if work remains queued while its active workers spend most of their time
|
|
71
72
|
# blocked outside the GVL, but not when Ruby execution is using the
|
|
72
73
|
# available CPU.
|
|
73
|
-
# Temporary workers remain available between bursts
|
|
74
|
-
#
|
|
74
|
+
# Temporary workers remain available between blocking bursts, but retire
|
|
75
|
+
# when Ruby execution becomes the bottleneck or they remain idle. Omitting
|
|
76
|
+
# `max_size` creates a fixed-size pool.
|
|
75
77
|
#
|
|
76
78
|
# @param size [Integer] The baseline number of worker threads (must be positive)
|
|
77
79
|
# @param max_size [Integer, Float, nil] Maximum number of worker threads,
|
|
@@ -122,7 +124,8 @@ module AtomicRuby
|
|
|
122
124
|
@next_thread_number = 0
|
|
123
125
|
if adaptive?
|
|
124
126
|
@autoscale_available = AtomicConditionVariable.new
|
|
125
|
-
@
|
|
127
|
+
@idle_trim_requested = AtomicBoolean.new(false)
|
|
128
|
+
@ruby_cpu_trim_requested = AtomicBoolean.new(false)
|
|
126
129
|
@thread_pool_monitor = ThreadPoolMonitor.new
|
|
127
130
|
@thread_pool_monitor.start
|
|
128
131
|
end
|
|
@@ -155,7 +158,7 @@ module AtomicRuby
|
|
|
155
158
|
# @rbs (Proc work) -> void
|
|
156
159
|
def <<(work)
|
|
157
160
|
Thread.handle_interrupt(Exception => :never) do
|
|
158
|
-
@
|
|
161
|
+
@idle_trim_requested&.make_false
|
|
159
162
|
raise EnqueuedWorkAfterShutdownError unless @queue.push(work)
|
|
160
163
|
|
|
161
164
|
@work_available.signal
|
|
@@ -309,7 +312,8 @@ module AtomicRuby
|
|
|
309
312
|
|
|
310
313
|
# Creates a worker thread.
|
|
311
314
|
#
|
|
312
|
-
# Temporary workers
|
|
315
|
+
# Temporary workers retire when Ruby execution becomes the bottleneck or
|
|
316
|
+
# they remain idle.
|
|
313
317
|
#
|
|
314
318
|
# @param temporary [true, false] whether the worker belongs above the baseline
|
|
315
319
|
# @return [Thread]
|
|
@@ -333,10 +337,13 @@ module AtomicRuby
|
|
|
333
337
|
should_exit = false
|
|
334
338
|
|
|
335
339
|
@work_available.wait do
|
|
336
|
-
|
|
337
|
-
unless
|
|
338
|
-
|
|
339
|
-
|
|
340
|
+
should_exit = temporary && @ruby_cpu_trim_requested.true?
|
|
341
|
+
unless should_exit
|
|
342
|
+
work = @queue.pop
|
|
343
|
+
unless work
|
|
344
|
+
should_exit = @shutdown.true? && @queue.empty?
|
|
345
|
+
should_exit ||= temporary && @idle_trim_requested.true? && @queue.empty?
|
|
346
|
+
end
|
|
340
347
|
end
|
|
341
348
|
work || should_exit
|
|
342
349
|
end
|
|
@@ -344,6 +351,10 @@ module AtomicRuby
|
|
|
344
351
|
break if should_exit
|
|
345
352
|
|
|
346
353
|
@active_thread_count.swap { |current_count| current_count + 1 }
|
|
354
|
+
if temporary
|
|
355
|
+
work_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
356
|
+
running_cpu_time_at_start = @thread_pool_monitor.snapshot[6]
|
|
357
|
+
end
|
|
347
358
|
begin
|
|
348
359
|
@thread_pool_monitor&.start_work
|
|
349
360
|
work.call
|
|
@@ -357,6 +368,14 @@ module AtomicRuby
|
|
|
357
368
|
ensure
|
|
358
369
|
@thread_pool_monitor&.stop_work
|
|
359
370
|
@active_thread_count.swap { |current_count| current_count - 1 }
|
|
371
|
+
if temporary && !@queue.empty?
|
|
372
|
+
elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - work_started_at
|
|
373
|
+
running_cpu_time = @thread_pool_monitor.snapshot[6] - running_cpu_time_at_start
|
|
374
|
+
if running_cpu_time * 2 >= elapsed_time * 1_000_000_000
|
|
375
|
+
@ruby_cpu_trim_requested.make_true
|
|
376
|
+
@work_available.broadcast
|
|
377
|
+
end
|
|
378
|
+
end
|
|
360
379
|
end
|
|
361
380
|
end
|
|
362
381
|
ensure
|
|
@@ -369,7 +388,10 @@ module AtomicRuby
|
|
|
369
388
|
thread
|
|
370
389
|
end
|
|
371
390
|
|
|
372
|
-
#
|
|
391
|
+
# Scales temporary workers in response to workload pressure.
|
|
392
|
+
#
|
|
393
|
+
# Sampling starts aggressively so the pool can react to short bursts, then
|
|
394
|
+
# backs off when more workers would not improve throughput.
|
|
373
395
|
#
|
|
374
396
|
# @return [void]
|
|
375
397
|
#
|
|
@@ -379,8 +401,10 @@ module AtomicRuby
|
|
|
379
401
|
pressure_started_at = nil
|
|
380
402
|
idle_started_at = nil
|
|
381
403
|
phase_samples = [0, 0, 0]
|
|
404
|
+
sampling_window = AUTOSCALE_INITIAL_WINDOW
|
|
382
405
|
|
|
383
406
|
loop do
|
|
407
|
+
@ruby_cpu_trim_requested.make_false if @alive_thread_count.value <= @size
|
|
384
408
|
@autoscale_available.wait do
|
|
385
409
|
@shutdown.true? || !@queue.empty? || @alive_thread_count.value > @size
|
|
386
410
|
end
|
|
@@ -394,24 +418,36 @@ module AtomicRuby
|
|
|
394
418
|
previous_snapshot = snapshot
|
|
395
419
|
idle_started_at ||= now
|
|
396
420
|
phase_samples.fill(0)
|
|
421
|
+
sampling_window = AUTOSCALE_INITIAL_WINDOW
|
|
397
422
|
|
|
398
423
|
if now - idle_started_at >= AUTOSCALE_IDLE_TIME
|
|
399
|
-
@
|
|
424
|
+
@idle_trim_requested.make_true
|
|
400
425
|
@work_available.broadcast
|
|
401
426
|
end
|
|
402
427
|
else
|
|
403
|
-
@
|
|
428
|
+
@idle_trim_requested.make_false
|
|
404
429
|
idle_started_at = nil
|
|
405
430
|
|
|
406
431
|
if pressure_started_at.nil?
|
|
407
432
|
previous_snapshot = snapshot
|
|
408
433
|
pressure_started_at = now
|
|
409
434
|
phase_samples.fill(0)
|
|
410
|
-
elsif now - pressure_started_at >=
|
|
411
|
-
|
|
435
|
+
elsif now - pressure_started_at >= sampling_window
|
|
436
|
+
case scaling_direction(previous_snapshot, snapshot, phase_samples, now - pressure_started_at)
|
|
437
|
+
when :up
|
|
438
|
+
@ruby_cpu_trim_requested.make_false
|
|
412
439
|
worker_count = @threads.length
|
|
413
440
|
workers_to_add = [worker_count, @queue.size, @max_size - worker_count].min
|
|
414
441
|
workers_to_add.times { spawn_worker(temporary: true) }
|
|
442
|
+
sampling_window = AUTOSCALE_INITIAL_WINDOW
|
|
443
|
+
when :down
|
|
444
|
+
unless @ruby_cpu_trim_requested.true?
|
|
445
|
+
@ruby_cpu_trim_requested.make_true
|
|
446
|
+
@work_available.broadcast
|
|
447
|
+
end
|
|
448
|
+
sampling_window = [sampling_window * 2, AUTOSCALE_MAX_WINDOW].min
|
|
449
|
+
else
|
|
450
|
+
sampling_window = [sampling_window * 2, AUTOSCALE_MAX_WINDOW].min
|
|
415
451
|
end
|
|
416
452
|
previous_snapshot = snapshot
|
|
417
453
|
pressure_started_at = now
|
|
@@ -421,30 +457,28 @@ module AtomicRuby
|
|
|
421
457
|
end
|
|
422
458
|
end
|
|
423
459
|
|
|
424
|
-
sleep
|
|
460
|
+
sleep((@queue.empty? ? AUTOSCALE_MAX_WINDOW : sampling_window) / AUTOSCALE_SAMPLES_PER_WINDOW)
|
|
425
461
|
end
|
|
426
462
|
end
|
|
427
463
|
|
|
428
|
-
# Returns
|
|
464
|
+
# Returns the direction in which the pool should scale.
|
|
429
465
|
#
|
|
430
466
|
# Requiring workers to spend a majority of their time blocked outside the
|
|
431
|
-
# GVL recognizes blocking operations. The pool
|
|
432
|
-
#
|
|
433
|
-
#
|
|
434
|
-
# GVL contention.
|
|
467
|
+
# GVL recognizes blocking operations. The pool grows while Ruby execution
|
|
468
|
+
# uses less than half of one CPU and shrinks when Ruby execution becomes
|
|
469
|
+
# the bottleneck, without mistaking OS scheduling delays for GVL contention.
|
|
435
470
|
#
|
|
436
471
|
# @param previous_snapshot [Array<Integer>] previous GVL state snapshot
|
|
437
472
|
# @param snapshot [Array<Integer>] current GVL state snapshot
|
|
438
473
|
# @param phase_samples [Array<Integer>] sampled current GVL states
|
|
439
474
|
# @param elapsed_time [Float] seconds covered by the snapshots
|
|
440
|
-
# @return [
|
|
475
|
+
# @return [:up, :down, nil]
|
|
441
476
|
#
|
|
442
|
-
# @rbs (Array[Integer] previous_snapshot, Array[Integer] snapshot, Array[Integer] phase_samples, Float elapsed_time) ->
|
|
443
|
-
def
|
|
477
|
+
# @rbs (Array[Integer] previous_snapshot, Array[Integer] snapshot, Array[Integer] phase_samples, Float elapsed_time) -> (:up | :down)?
|
|
478
|
+
def scaling_direction(previous_snapshot, snapshot, phase_samples, elapsed_time)
|
|
444
479
|
@threads.select!(&:alive?)
|
|
445
480
|
workers = @threads
|
|
446
|
-
return
|
|
447
|
-
return false if @active_thread_count.value < workers.length
|
|
481
|
+
return if @active_thread_count.value < workers.length
|
|
448
482
|
|
|
449
483
|
running_time = snapshot[3] - previous_snapshot[3]
|
|
450
484
|
waiting_time = snapshot[4] - previous_snapshot[4]
|
|
@@ -452,15 +486,17 @@ module AtomicRuby
|
|
|
452
486
|
running_cpu_time = snapshot[6] - previous_snapshot[6]
|
|
453
487
|
total_time = running_time + waiting_time + blocked_time
|
|
454
488
|
|
|
455
|
-
minimum_measured_time = (
|
|
489
|
+
minimum_measured_time = (elapsed_time * 1_000_000_000 * workers.length / 2).to_i
|
|
456
490
|
if total_time < minimum_measured_time
|
|
457
491
|
running_time, waiting_time, blocked_time = phase_samples
|
|
458
492
|
total_time = running_time + waiting_time + blocked_time
|
|
459
493
|
end
|
|
460
494
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
495
|
+
ruby_cpu_bound = running_cpu_time * 2 >= elapsed_time * 1_000_000_000
|
|
496
|
+
blocking = total_time.positive? && blocked_time * 2 > total_time && !ruby_cpu_bound
|
|
497
|
+
|
|
498
|
+
return :up if blocking && workers.length < @max_size
|
|
499
|
+
return :down if ruby_cpu_bound && workers.length > @size
|
|
464
500
|
end
|
|
465
501
|
end
|
|
466
502
|
end
|
data/lib/atomic-ruby/version.rb
CHANGED