uringmachine 0.21.0 → 0.22.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +14 -0
  4. data/TODO.md +144 -0
  5. data/benchmark/README.md +173 -0
  6. data/benchmark/bm_io_pipe.rb +70 -0
  7. data/benchmark/bm_io_socketpair.rb +71 -0
  8. data/benchmark/bm_mutex_cpu.rb +57 -0
  9. data/benchmark/bm_mutex_io.rb +64 -0
  10. data/benchmark/bm_pg_client.rb +109 -0
  11. data/benchmark/bm_queue.rb +76 -0
  12. data/benchmark/chart.png +0 -0
  13. data/benchmark/common.rb +135 -0
  14. data/benchmark/dns_client.rb +47 -0
  15. data/{examples/bm_http_parse.rb → benchmark/http_parse.rb} +1 -1
  16. data/benchmark/run_bm.rb +8 -0
  17. data/benchmark/sqlite.rb +108 -0
  18. data/{examples/bm_write.rb → benchmark/write.rb} +4 -4
  19. data/ext/um/um.c +189 -100
  20. data/ext/um/um.h +36 -10
  21. data/ext/um/um_async_op.c +1 -1
  22. data/ext/um/um_class.c +87 -13
  23. data/ext/um/um_op.c +6 -0
  24. data/ext/um/um_sync.c +2 -2
  25. data/ext/um/um_utils.c +16 -0
  26. data/grant-2025/journal.md +118 -1
  27. data/grant-2025/tasks.md +48 -22
  28. data/lib/uringmachine/actor.rb +8 -0
  29. data/lib/uringmachine/dns_resolver.rb +1 -2
  30. data/lib/uringmachine/fiber_scheduler.rb +127 -81
  31. data/lib/uringmachine/version.rb +1 -1
  32. data/lib/uringmachine.rb +32 -3
  33. data/test/helper.rb +7 -18
  34. data/test/test_actor.rb +12 -3
  35. data/test/test_async_op.rb +10 -10
  36. data/test/test_fiber.rb +84 -1
  37. data/test/test_fiber_scheduler.rb +950 -47
  38. data/test/test_um.rb +297 -120
  39. data/uringmachine.gemspec +2 -1
  40. metadata +38 -16
  41. data/examples/bm_fileno.rb +0 -33
  42. data/examples/bm_queue.rb +0 -111
  43. data/examples/bm_side_running.rb +0 -83
  44. data/examples/bm_sqlite.rb +0 -89
  45. data/examples/dns_client.rb +0 -12
  46. /data/{examples/bm_mutex.rb → benchmark/mutex.rb} +0 -0
  47. /data/{examples/bm_mutex_single.rb → benchmark/mutex_single.rb} +0 -0
  48. /data/{examples/bm_send.rb → benchmark/send.rb} +0 -0
  49. /data/{examples/bm_snooze.rb → benchmark/snooze.rb} +0 -0
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './common'
4
+ require 'securerandom'
5
+
6
+ C = ENV['C']&.to_i ||50
7
+ I = 1000
8
+ puts "C=#{C}"
9
+
10
+ class UMBenchmark
11
+ CONTAINER_NAME = "pg-#{SecureRandom.hex}"
12
+
13
+ def start_pg_server
14
+ `docker run --name #{CONTAINER_NAME} -e POSTGRES_PASSWORD=my_password -d -p 5432:5432 postgres`
15
+ end
16
+
17
+ def stop_pg_server
18
+ `docker stop #{CONTAINER_NAME}`
19
+ end
20
+
21
+ PG_OPTS = {
22
+ host: 'localhost',
23
+ user: 'postgres',
24
+ password: 'my_password',
25
+ dbname: 'postgres'
26
+ }
27
+
28
+ def create_db_conn(retries = 0)
29
+ ::PG.connect(PG_OPTS)
30
+ rescue ::PG::ConnectionBad
31
+ if retries < 3
32
+ sleep 0.5
33
+ create_db_conn(retries + 1)
34
+ else
35
+ raise
36
+ end
37
+ end
38
+
39
+ PREPARE_SQL = <<~SQL
40
+ create table if not exists foo(value int, name text);
41
+ create index if not exists idx_foo_value on foo(value);
42
+ with t1 as (
43
+ select generate_series(1,100000)
44
+ ),
45
+ t2 as (
46
+ select (random()*1000000)::integer as value,
47
+ (md5(random()::text)) as name
48
+ from t1
49
+ )
50
+ insert into foo(value, name) select * from t2;
51
+ SQL
52
+
53
+ def prepare_db
54
+ STDOUT << "Preparing database..."
55
+ t0 = Time.now
56
+ conn = create_db_conn
57
+ conn.exec(PREPARE_SQL)
58
+ puts " elapsed: #{Time.now - t0}"
59
+ end
60
+
61
+ def query_db(conn)
62
+ min = rand(1000000)
63
+ max = min + 2000
64
+ sql = format(
65
+ 'select * from foo where value >= %d and value < %d;', min, max
66
+ )
67
+ conn.query(sql).to_a
68
+ end
69
+
70
+ def with_container
71
+ start_pg_server
72
+ sleep 0.5
73
+ prepare_db
74
+ yield
75
+ rescue Exception => e
76
+ p e
77
+ p e.backtrace
78
+ ensure
79
+ stop_pg_server
80
+ end
81
+
82
+ def benchmark
83
+ with_container {
84
+ Benchmark.bm { run_benchmarks(it) }
85
+ }
86
+ end
87
+
88
+ def do_threads(threads, ios)
89
+ C.times.map do
90
+ threads << Thread.new do
91
+ conn = create_db_conn
92
+ I.times { query_db(conn) }
93
+ ensure
94
+ conn.close
95
+ end
96
+ end
97
+ end
98
+
99
+ def do_scheduler(scheduler, ios)
100
+ C.times do
101
+ Fiber.schedule do
102
+ conn = create_db_conn
103
+ I.times { query_db(conn) }
104
+ ensure
105
+ conn.close
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './common'
4
+
5
+ GROUPS = 40
6
+ PRODUCERS = 5
7
+ CONSUMERS = 10
8
+ ITEMS = 200000
9
+
10
+ class UMBenchmark
11
+ def do_threads(threads, ios)
12
+ GROUPS.times do
13
+ queue = Queue.new
14
+ PRODUCERS.times do
15
+ threads << Thread.new do
16
+ ITEMS.times { queue << rand(1000) }
17
+ CONSUMERS.times { queue << :stop }
18
+ end
19
+ end
20
+ CONSUMERS.times do
21
+ threads << Thread.new do
22
+ loop do
23
+ item = queue.shift
24
+ break if item == :stop
25
+
26
+ item * rand(1000)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def do_scheduler(scheduler, ios)
34
+ GROUPS.times do
35
+ queue = Queue.new
36
+ PRODUCERS.times do
37
+ Fiber.schedule do
38
+ ITEMS.times { queue << rand(1000) }
39
+ CONSUMERS.times { queue << :stop }
40
+ end
41
+ end
42
+ CONSUMERS.times do
43
+ Fiber.schedule do
44
+ loop do
45
+ item = queue.shift
46
+ break if item == :stop
47
+
48
+ item * rand(1000)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def do_um(machine, fibers, fds)
56
+ GROUPS.times do
57
+ queue = UM::Queue.new
58
+ PRODUCERS.times do
59
+ fibers << machine.spin do
60
+ ITEMS.times { machine.push(queue, rand(1000)) }
61
+ CONSUMERS.times { machine.push(queue, :stop) }
62
+ end
63
+ end
64
+ CONSUMERS.times do
65
+ fibers << machine.spin do
66
+ loop do
67
+ item = machine.shift(queue)
68
+ break if item == :stop
69
+
70
+ item * rand(1000)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
Binary file
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ source 'https://rubygems.org'
7
+ gem 'uringmachine', path: '..'
8
+ gem 'benchmark'
9
+ gem 'io-event'
10
+ gem 'async'
11
+ gem 'pg'
12
+ end
13
+
14
+ require 'uringmachine/fiber_scheduler'
15
+
16
+ class WorkerThreadPool
17
+ def initialize(size)
18
+ @size = size
19
+ @queue = Queue.new
20
+ setup_threads
21
+ sleep 0.01 * @size
22
+ end
23
+
24
+ def queue(&block)
25
+ @queue << block
26
+ end
27
+
28
+ def join
29
+ @size.times { @queue << :stop }
30
+ @threads.each(&:join)
31
+ end
32
+
33
+ def setup_threads
34
+ @threads = @size.times.map {
35
+ Thread.new do
36
+ loop do
37
+ job = @queue.shift
38
+ break if job == :stop
39
+
40
+ job.()
41
+ end
42
+ end
43
+ }
44
+ end
45
+ end
46
+
47
+ class UMBenchmark
48
+ def initialize
49
+ @thread_pool = WorkerThreadPool.new(10)
50
+ end
51
+
52
+ def benchmark
53
+ Benchmark.bm { run_benchmarks(it) }
54
+ end
55
+
56
+ @@benchmarks = {
57
+ threads: [:threads, "Threads"],
58
+ thread_pool: [:thread_pool, "ThreadPool"],
59
+ async_uring: [:scheduler, "Async uring"],
60
+ async_epoll: [:scheduler, "Async epoll"],
61
+ um_fs: [:scheduler, "UM FS"],
62
+ um: [:um, "UM"],
63
+ um_sqpoll: [:um, "UM sqpoll"]
64
+ }
65
+
66
+ def run_benchmarks(b)
67
+ @@benchmarks.each do |sym, (doer, name)|
68
+ b.report(name) { send(:"run_#{sym}") } if respond_to?(:"do_#{doer}")
69
+ end
70
+ end
71
+
72
+ def run_threads
73
+ threads = []
74
+ ios = []
75
+ do_threads(threads, ios)
76
+ threads.each(&:join)
77
+ ios.each { it.close rescue nil }
78
+ end
79
+
80
+ def run_thread_pool
81
+ ios = []
82
+ do_thread_pool(@thread_pool, ios)
83
+ @thread_pool.join
84
+ ios.each { it.close rescue nil }
85
+ end
86
+
87
+ def run_async_uring
88
+ selector ||= IO::Event::Selector::URing.new(Fiber.current)
89
+ scheduler = Async::Scheduler.new(selector:)
90
+ Fiber.set_scheduler(scheduler)
91
+ ios = []
92
+ scheduler.run { do_scheduler(scheduler, ios) }
93
+ ios.each { it.close rescue nil }
94
+ end
95
+
96
+ def run_async_epoll
97
+ selector ||= IO::Event::Selector::EPoll.new(Fiber.current)
98
+ scheduler = Async::Scheduler.new(selector:)
99
+ Fiber.set_scheduler(scheduler)
100
+ ios = []
101
+ scheduler.run { do_scheduler(scheduler, ios) }
102
+ ios.each { it.close rescue nil }
103
+ end
104
+
105
+ def run_um_fs
106
+ machine = UM.new
107
+ scheduler = UM::FiberScheduler.new(machine)
108
+ Fiber.set_scheduler(scheduler)
109
+ ios = []
110
+ do_scheduler(scheduler, ios)
111
+ scheduler.join
112
+ ios.each { it.close rescue nil }
113
+ end
114
+
115
+ def run_um
116
+ machine = UM.new(4096)
117
+ fibers = []
118
+ fds = []
119
+ do_um(machine, fibers, fds)
120
+ machine.await_fibers(fibers)
121
+ fds.each { machine.close(it) }
122
+ end
123
+
124
+ def run_um_sqpoll
125
+ machine = UM.new(4096, true)
126
+ fibers = []
127
+ fds = []
128
+ do_um(machine, fibers, fds)
129
+ machine.await_fibers(fibers)
130
+ fds.each { machine.close_async(it) }
131
+ machine.snooze
132
+ end
133
+ end
134
+
135
+ at_exit { UMBenchmark.new.benchmark }
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ source 'https://rubygems.org'
7
+ gem 'uringmachine', path: '..'
8
+ gem 'benchmark'
9
+ gem 'benchmark-ips'
10
+ end
11
+
12
+ require 'uringmachine'
13
+ require 'resolv'
14
+
15
+ def do_addrinfo
16
+ Addrinfo.tcp("status.realiteq.net", 80)
17
+ end
18
+
19
+ def do_resolv
20
+ Resolv.getaddresses('status.realiteq.net')
21
+ end
22
+
23
+ @machine = UM.new
24
+ def do_um
25
+ @machine.resolve('status.realiteq.net')
26
+ end
27
+
28
+ # p do_addrinfo
29
+ # p do_resolv
30
+ # p do_um
31
+ # exit
32
+
33
+ Benchmark.ips do |x|
34
+ x.report("Addrinfo") { do_addrinfo }
35
+ x.report("resolv") { do_resolv }
36
+ x.report("UM.resolve") { do_um }
37
+
38
+ x.compare!(order: :baseline)
39
+ end
40
+
41
+
42
+
43
+ # addrs = machine.resolve('status.realiteq.net')
44
+
45
+ # puts '*' * 40
46
+ # puts addrs.join("\n")
47
+ # puts
@@ -221,4 +221,4 @@ def benchmark
221
221
  end
222
222
 
223
223
  compare_allocs
224
- benchmark
224
+ benchmark
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir["#{__dir__}/bm_*.rb"].each do |fn|
4
+ puts "* #{File.basename(fn)}"
5
+ puts
6
+ puts `ruby -W0 --yjit #{fn}`
7
+ puts
8
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ source 'https://rubygems.org'
7
+ gem 'uringmachine', path: '..'
8
+ gem 'extralite'
9
+ gem 'benchmark'
10
+ gem 'benchmark-ips'
11
+ end
12
+
13
+ require 'uringmachine'
14
+ require 'extralite'
15
+ require 'benchmark/ips'
16
+ require 'securerandom'
17
+ require 'uringmachine/actor'
18
+
19
+ class DBActor2
20
+ def initialize(machine, db)
21
+ @machine = machine
22
+ @db = db
23
+ @mailbox = UM::Queue.new
24
+ end
25
+
26
+ def act
27
+ while (a, k, peer = @machine.shift(@mailbox))
28
+
29
+ begin
30
+ ret = @db.query(*a, **k)
31
+ @machine.schedule(peer, ret)
32
+ rescue => e
33
+ @machine.schedule(peer, e)
34
+ end
35
+ end
36
+ rescue Exception => e
37
+ # handle unhandled exceptions
38
+ ensure
39
+ @machine.fiber_map.delete(self)
40
+ @machine.yield
41
+ end
42
+
43
+ def query(*a, **k)
44
+ @machine.push(@mailbox, [a, k, Fiber.current])
45
+ ret = @machine.yield
46
+ raise(ret) if ret.is_a?(Exception)
47
+ ret
48
+ end
49
+
50
+ def self.spin(machine, db)
51
+ actor = new(machine, db)
52
+ machine.spin { actor.act }
53
+ actor
54
+ end
55
+ end
56
+
57
+ PATH = "/tmp/um_bm_sqlite_#{SecureRandom.hex}"
58
+
59
+ module DBActor
60
+ def setup(db)
61
+ @db = db
62
+ end
63
+
64
+ def query(*, **)
65
+ @db.query(*, **)
66
+ end
67
+ end
68
+
69
+ class Locker
70
+ def initialize(machine, target)
71
+ @machine = machine
72
+ @target = target
73
+ @mutex = UM::Mutex.new
74
+ end
75
+
76
+ def method_missing(sym, *a, **k)
77
+ @machine.synchronize(@mutex) { @target.send(sym, *a, **k) }
78
+ end
79
+ end
80
+
81
+ def prepare_db(machine, path)
82
+ Extralite::Database.new(path).tap {
83
+ it.on_progress(mode: :at_least_once, period: 100, tick: 10) { machine.snooze }
84
+ }
85
+ end
86
+
87
+ machine = UM.new
88
+ mailbox = UM::Queue.new
89
+ raw_db = prepare_db(machine, PATH)
90
+ actor_db = machine.spin_actor(DBActor, prepare_db(machine, PATH))
91
+ actor2_db = DBActor2.spin(machine, prepare_db(machine, PATH))
92
+ locker_db = Locker.new(machine, prepare_db(machine, PATH))
93
+
94
+ p raw_db.query('select 1')
95
+ p actor_db.call(mailbox, :query, 'select 1')
96
+ p actor2_db.query('select 1')
97
+ p locker_db.query('select 1')
98
+
99
+ bm = Benchmark.ips do |x|
100
+ x.config(:time => 5, :warmup => 2)
101
+
102
+ x.report("raw") { raw_db.query('select 1') }
103
+ x.report("actor") { actor_db.call(mailbox, :query, 'select 1') }
104
+ x.report("actor2") { actor2_db.query('select 1') }
105
+ x.report("locker") { locker_db.query('select 1') }
106
+
107
+ x.compare!(order: :baseline)
108
+ end
@@ -11,7 +11,7 @@ end
11
11
  require 'benchmark'
12
12
  require 'uringmachine'
13
13
 
14
- ITERATIONS = 10000
14
+ ITERATIONS = 100000
15
15
  BUF = ('*' * 8192).freeze
16
16
  FN = '/tmp/bm_write'
17
17
 
@@ -50,10 +50,10 @@ end
50
50
  run_io_write(1)
51
51
  run_um_write(1)
52
52
 
53
- Benchmark.bm do |x|
54
- [1, 2, 4, 8].each do |c|
53
+ [1, 2, 4, 8].each do |c|
54
+ Benchmark.bm do |x|
55
55
  x.report("IO (#{c} threads)") { run_io_write(c) }
56
56
  x.report("UM (#{c} fibers) ") { run_um_write(c) }
57
- puts
58
57
  end
58
+ puts
59
59
  end