atomic-ruby 0.15.0 → 0.15.1
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 +9 -9
- data/ext/atomic_ruby/atomic_ruby.c +22 -3
- data/lib/atomic-ruby/atomic_thread_pool.rb +16 -12
- 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: af7a35affcaa2790e6944ee01eb6af8f4d92b5c2479bb1995128742a562fd395
|
|
4
|
+
data.tar.gz: d3908277e6ef09a70f7d46a057a5db7f60c179885d980b4fbbf407bdade6b073
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5a3525b194636ddb439c0c8e9597e3ed1870a8c0165bc1582cb120a45c49b30a8cf6bd60f393a9e3e0c848abf760b181dce1b9b3026199ff8ffb2ba833dcc1a
|
|
7
|
+
data.tar.gz: e89a1f593774fc1722bfc8a9229385682e33a10df705ba78da3c67f904ab566c770b7aebdebc32069ed7f4dfc2781ad52165f08f10cf3fd4e4d723c41687e02f
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -122,8 +122,8 @@ 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
|
|
126
|
-
is the bottleneck. It returns to `size` when the queue drains.
|
|
125
|
+
the GVL and CPU time used by its workers, so it does not add more threads when
|
|
126
|
+
Ruby execution is the bottleneck. It returns to `size` when the queue drains.
|
|
127
127
|
|
|
128
128
|
```ruby
|
|
129
129
|
pool = AtomicThreadPool.new(size: 4, max_size: 16)
|
|
@@ -268,7 +268,7 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
|
|
|
268
268
|
|
|
269
269
|
ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
270
270
|
concurrent-ruby version: 1.3.6
|
|
271
|
-
atomic-ruby version: 0.15.
|
|
271
|
+
atomic-ruby version: 0.15.1
|
|
272
272
|
|
|
273
273
|
Balances:
|
|
274
274
|
Synchronized Bank Account Balance: 975
|
|
@@ -359,7 +359,7 @@ end
|
|
|
359
359
|
|
|
360
360
|
ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
361
361
|
concurrent-ruby version: 1.3.6
|
|
362
|
-
atomic-ruby version: 0.15.
|
|
362
|
+
atomic-ruby version: 0.15.1
|
|
363
363
|
|
|
364
364
|
Warming up --------------------------------------
|
|
365
365
|
Synchronized Boolean Toggle 157.000 i/100ms
|
|
@@ -442,7 +442,7 @@ end
|
|
|
442
442
|
> bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_condition_variable_benchmark.rb
|
|
443
443
|
|
|
444
444
|
ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
445
|
-
atomic-ruby version: 0.15.
|
|
445
|
+
atomic-ruby version: 0.15.1
|
|
446
446
|
|
|
447
447
|
Warming up --------------------------------------
|
|
448
448
|
Synchronized Condition Variable Wait/Signal 3.977k i/100ms
|
|
@@ -517,7 +517,7 @@ end
|
|
|
517
517
|
> bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_queue_benchmark.rb
|
|
518
518
|
|
|
519
519
|
ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
520
|
-
atomic-ruby version: 0.15.
|
|
520
|
+
atomic-ruby version: 0.15.1
|
|
521
521
|
|
|
522
522
|
Warming up --------------------------------------
|
|
523
523
|
Synchronized Queue Push/Pop 181.000 i/100ms
|
|
@@ -587,11 +587,11 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
|
|
|
587
587
|
|
|
588
588
|
ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
589
589
|
concurrent-ruby version: 1.3.8
|
|
590
|
-
atomic-ruby version: 0.15.
|
|
590
|
+
atomic-ruby version: 0.15.1
|
|
591
591
|
|
|
592
592
|
Benchmark Results:
|
|
593
|
-
Concurrent Ruby Thread Pool: 5.
|
|
594
|
-
Atomic Ruby Atomic Thread Pool: 4.
|
|
593
|
+
Concurrent Ruby Thread Pool: 5.079561 seconds
|
|
594
|
+
Atomic Ruby Atomic Thread Pool: 4.99168 seconds
|
|
595
595
|
```
|
|
596
596
|
|
|
597
597
|
</details>
|
|
@@ -535,12 +535,14 @@ typedef struct {
|
|
|
535
535
|
_Atomic unsigned long long running_time;
|
|
536
536
|
_Atomic unsigned long long waiting_time;
|
|
537
537
|
_Atomic unsigned long long blocked_time;
|
|
538
|
+
_Atomic unsigned long long running_cpu_time;
|
|
538
539
|
} atomic_ruby_thread_pool_monitor_t;
|
|
539
540
|
|
|
540
541
|
typedef struct {
|
|
541
542
|
atomic_ruby_thread_pool_monitor_t *monitor;
|
|
542
543
|
atomic_ruby_thread_pool_worker_phase_t phase;
|
|
543
544
|
unsigned long long phase_started_at;
|
|
545
|
+
unsigned long long running_cpu_started_at;
|
|
544
546
|
} atomic_ruby_thread_pool_worker_state_t;
|
|
545
547
|
|
|
546
548
|
static rb_internal_thread_specific_key_t atomic_ruby_thread_pool_worker_key;
|
|
@@ -569,14 +571,27 @@ static unsigned long long atomic_ruby_monotonic_time(void) {
|
|
|
569
571
|
return (unsigned long long)time.tv_sec * 1000000000ULL + (unsigned long long)time.tv_nsec;
|
|
570
572
|
}
|
|
571
573
|
|
|
574
|
+
static unsigned long long atomic_ruby_thread_cpu_time(void) {
|
|
575
|
+
struct timespec time;
|
|
576
|
+
if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time) == -1) return 0;
|
|
577
|
+
|
|
578
|
+
return (unsigned long long)time.tv_sec * 1000000000ULL + (unsigned long long)time.tv_nsec;
|
|
579
|
+
}
|
|
580
|
+
|
|
572
581
|
static void atomic_ruby_thread_pool_worker_leave_phase(atomic_ruby_thread_pool_worker_state_t *state, unsigned long long now) {
|
|
573
582
|
unsigned long long elapsed = now != 0 && state->phase_started_at != 0 && now >= state->phase_started_at ? now - state->phase_started_at : 0;
|
|
574
583
|
|
|
575
584
|
switch (state->phase) {
|
|
576
|
-
case ATOMIC_RUBY_THREAD_POOL_WORKER_RUNNING:
|
|
585
|
+
case ATOMIC_RUBY_THREAD_POOL_WORKER_RUNNING: {
|
|
586
|
+
unsigned long long cpu_now = atomic_ruby_thread_cpu_time();
|
|
587
|
+
unsigned long long cpu_elapsed = cpu_now != 0 && state->running_cpu_started_at != 0 && cpu_now >= state->running_cpu_started_at
|
|
588
|
+
? cpu_now - state->running_cpu_started_at
|
|
589
|
+
: 0;
|
|
577
590
|
atomic_fetch_sub_explicit(&state->monitor->running_count, 1, memory_order_relaxed);
|
|
578
591
|
atomic_fetch_add_explicit(&state->monitor->running_time, elapsed, memory_order_relaxed);
|
|
592
|
+
atomic_fetch_add_explicit(&state->monitor->running_cpu_time, cpu_elapsed, memory_order_relaxed);
|
|
579
593
|
break;
|
|
594
|
+
}
|
|
580
595
|
case ATOMIC_RUBY_THREAD_POOL_WORKER_WAITING:
|
|
581
596
|
atomic_fetch_sub_explicit(&state->monitor->waiting_count, 1, memory_order_relaxed);
|
|
582
597
|
atomic_fetch_add_explicit(&state->monitor->waiting_time, elapsed, memory_order_relaxed);
|
|
@@ -597,6 +612,7 @@ static void atomic_ruby_thread_pool_worker_enter_phase(atomic_ruby_thread_pool_w
|
|
|
597
612
|
switch (phase) {
|
|
598
613
|
case ATOMIC_RUBY_THREAD_POOL_WORKER_RUNNING:
|
|
599
614
|
atomic_fetch_add_explicit(&state->monitor->running_count, 1, memory_order_relaxed);
|
|
615
|
+
state->running_cpu_started_at = atomic_ruby_thread_cpu_time();
|
|
600
616
|
break;
|
|
601
617
|
case ATOMIC_RUBY_THREAD_POOL_WORKER_WAITING:
|
|
602
618
|
atomic_fetch_add_explicit(&state->monitor->waiting_count, 1, memory_order_relaxed);
|
|
@@ -638,6 +654,7 @@ static VALUE rb_cThreadPoolMonitor_allocate(VALUE klass) {
|
|
|
638
654
|
atomic_init(&monitor->running_time, 0);
|
|
639
655
|
atomic_init(&monitor->waiting_time, 0);
|
|
640
656
|
atomic_init(&monitor->blocked_time, 0);
|
|
657
|
+
atomic_init(&monitor->running_cpu_time, 0);
|
|
641
658
|
return obj;
|
|
642
659
|
}
|
|
643
660
|
|
|
@@ -650,6 +667,7 @@ static VALUE rb_cThreadPoolMonitor_register_worker(VALUE self) {
|
|
|
650
667
|
state->monitor = monitor;
|
|
651
668
|
state->phase = ATOMIC_RUBY_THREAD_POOL_WORKER_INACTIVE;
|
|
652
669
|
state->phase_started_at = 0;
|
|
670
|
+
state->running_cpu_started_at = 0;
|
|
653
671
|
rb_internal_thread_specific_set(thread, atomic_ruby_thread_pool_worker_key, state);
|
|
654
672
|
return Qnil;
|
|
655
673
|
}
|
|
@@ -683,13 +701,14 @@ static VALUE rb_cThreadPoolMonitor_snapshot(VALUE self) {
|
|
|
683
701
|
TypedData_Get_Struct(self, atomic_ruby_thread_pool_monitor_t, &atomic_ruby_thread_pool_monitor_type, monitor);
|
|
684
702
|
|
|
685
703
|
return rb_ary_new_from_args(
|
|
686
|
-
|
|
704
|
+
7,
|
|
687
705
|
UINT2NUM(atomic_load_explicit(&monitor->running_count, memory_order_relaxed)),
|
|
688
706
|
UINT2NUM(atomic_load_explicit(&monitor->waiting_count, memory_order_relaxed)),
|
|
689
707
|
UINT2NUM(atomic_load_explicit(&monitor->blocked_count, memory_order_relaxed)),
|
|
690
708
|
ULL2NUM(atomic_load_explicit(&monitor->running_time, memory_order_relaxed)),
|
|
691
709
|
ULL2NUM(atomic_load_explicit(&monitor->waiting_time, memory_order_relaxed)),
|
|
692
|
-
ULL2NUM(atomic_load_explicit(&monitor->blocked_time, memory_order_relaxed))
|
|
710
|
+
ULL2NUM(atomic_load_explicit(&monitor->blocked_time, memory_order_relaxed)),
|
|
711
|
+
ULL2NUM(atomic_load_explicit(&monitor->running_cpu_time, memory_order_relaxed))
|
|
693
712
|
);
|
|
694
713
|
}
|
|
695
714
|
|
|
@@ -69,7 +69,8 @@ module AtomicRuby
|
|
|
69
69
|
#
|
|
70
70
|
# When `max_size` is greater than `size`, the pool adds temporary workers
|
|
71
71
|
# if work remains queued while its active workers spend most of their time
|
|
72
|
-
# blocked outside the GVL, but not when
|
|
72
|
+
# blocked outside the GVL, but not when Ruby execution is using the
|
|
73
|
+
# available CPU.
|
|
73
74
|
# Temporary workers leave after the queue remains empty, returning the pool
|
|
74
75
|
# to its baseline size. Omitting `max_size` creates a fixed-size pool.
|
|
75
76
|
#
|
|
@@ -403,7 +404,7 @@ module AtomicRuby
|
|
|
403
404
|
pressure_started_at = now
|
|
404
405
|
phase_samples.fill(0)
|
|
405
406
|
elsif now - pressure_started_at >= AUTOSCALE_WINDOW
|
|
406
|
-
if should_grow?(previous_snapshot, snapshot, phase_samples)
|
|
407
|
+
if should_grow?(previous_snapshot, snapshot, phase_samples, now - pressure_started_at)
|
|
407
408
|
growth_samples += 1
|
|
408
409
|
if growth_samples >= AUTOSCALE_GROWTH_SAMPLES
|
|
409
410
|
spawn_worker(temporary: true)
|
|
@@ -427,25 +428,28 @@ module AtomicRuby
|
|
|
427
428
|
# Returns whether another worker is likely to improve throughput.
|
|
428
429
|
#
|
|
429
430
|
# Requiring workers to spend a majority of their time blocked outside the
|
|
430
|
-
# GVL recognizes blocking operations. The pool only grows
|
|
431
|
-
#
|
|
431
|
+
# GVL recognizes blocking operations. The pool only grows while its workers
|
|
432
|
+
# use less than half of one CPU, avoiding extra threads when Ruby
|
|
433
|
+
# execution is the bottleneck without mistaking OS scheduling delays for
|
|
434
|
+
# GVL contention.
|
|
432
435
|
#
|
|
433
436
|
# @param previous_snapshot [Array<Integer>] previous GVL state snapshot
|
|
434
437
|
# @param snapshot [Array<Integer>] current GVL state snapshot
|
|
435
438
|
# @param phase_samples [Array<Integer>] sampled current GVL states
|
|
439
|
+
# @param elapsed_time [Float] seconds covered by the snapshots
|
|
436
440
|
# @return [true, false]
|
|
437
441
|
#
|
|
438
|
-
# @rbs (Array[Integer] previous_snapshot, Array[Integer] snapshot, Array[Integer] phase_samples) -> bool
|
|
439
|
-
def should_grow?(previous_snapshot, snapshot, phase_samples)
|
|
442
|
+
# @rbs (Array[Integer] previous_snapshot, Array[Integer] snapshot, Array[Integer] phase_samples, Float elapsed_time) -> bool
|
|
443
|
+
def should_grow?(previous_snapshot, snapshot, phase_samples, elapsed_time)
|
|
440
444
|
@threads.select!(&:alive?)
|
|
441
445
|
workers = @threads
|
|
442
446
|
return false if workers.length >= @max_size
|
|
443
447
|
return false if @active_thread_count.value < workers.length
|
|
444
448
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
+
running_time = snapshot[3] - previous_snapshot[3]
|
|
450
|
+
waiting_time = snapshot[4] - previous_snapshot[4]
|
|
451
|
+
blocked_time = snapshot[5] - previous_snapshot[5]
|
|
452
|
+
running_cpu_time = snapshot[6] - previous_snapshot[6]
|
|
449
453
|
total_time = running_time + waiting_time + blocked_time
|
|
450
454
|
|
|
451
455
|
minimum_measured_time = (AUTOSCALE_WINDOW * 1_000_000_000 * workers.length / 2).to_i
|
|
@@ -455,8 +459,8 @@ module AtomicRuby
|
|
|
455
459
|
end
|
|
456
460
|
|
|
457
461
|
total_time.positive? &&
|
|
458
|
-
|
|
459
|
-
|
|
462
|
+
blocked_time * 2 > total_time &&
|
|
463
|
+
running_cpu_time * 2 < elapsed_time * 1_000_000_000
|
|
460
464
|
end
|
|
461
465
|
end
|
|
462
466
|
end
|
data/lib/atomic-ruby/version.rb
CHANGED