uringmachine 0.21.0 → 0.22.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +18 -0
  4. data/TODO.md +144 -0
  5. data/benchmark/README.md +139 -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 +139 -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_const.c +1 -1
  24. data/ext/um/um_op.c +6 -0
  25. data/ext/um/um_sync.c +2 -2
  26. data/ext/um/um_utils.c +16 -0
  27. data/grant-2025/journal.md +118 -1
  28. data/grant-2025/tasks.md +49 -23
  29. data/lib/uringmachine/actor.rb +8 -0
  30. data/lib/uringmachine/dns_resolver.rb +1 -2
  31. data/lib/uringmachine/fiber_scheduler.rb +127 -81
  32. data/lib/uringmachine/version.rb +1 -1
  33. data/lib/uringmachine.rb +32 -3
  34. data/test/helper.rb +7 -18
  35. data/test/test_actor.rb +12 -3
  36. data/test/test_async_op.rb +10 -10
  37. data/test/test_fiber.rb +84 -1
  38. data/test/test_fiber_scheduler.rb +950 -47
  39. data/test/test_um.rb +297 -120
  40. data/uringmachine.gemspec +2 -1
  41. metadata +38 -16
  42. data/examples/bm_fileno.rb +0 -33
  43. data/examples/bm_queue.rb +0 -111
  44. data/examples/bm_side_running.rb +0 -83
  45. data/examples/bm_sqlite.rb +0 -89
  46. data/examples/dns_client.rb +0 -12
  47. /data/{examples/bm_mutex.rb → benchmark/mutex.rb} +0 -0
  48. /data/{examples/bm_mutex_single.rb → benchmark/mutex_single.rb} +0 -0
  49. /data/{examples/bm_send.rb → benchmark/send.rb} +0 -0
  50. /data/{examples/bm_snooze.rb → benchmark/snooze.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e14941110d09a575728da68cb038a02334d151f0d384d88ad67a34b21a49d234
4
- data.tar.gz: 158299106b117a973163ae480ea4e9827a0cb88eb76b2323da04fd631e8916ca
3
+ metadata.gz: fbf0486feb49686a85ad162dd569ce125b4f168904e6f364e7c4358be234ca78
4
+ data.tar.gz: 27d0e3533c7d41f87b95376258992e37ebfa0d42c32ac0b46e7595939b3f1afa
5
5
  SHA512:
6
- metadata.gz: f71a5b6b6740fea281df8880511b424a0c759e876e17f79a6b661453416d13c2d9d65e0c53ddee5df066ca92472571ffae8c0e9a4ca4fd745d02705a8645965d
7
- data.tar.gz: baa86b9bc0009c69bfe894aae3489d43ed28d1fca44a24a9ddc7cc1625a54de6c17f65731c1a707af561942174f9584554a183a67f8428d5855e799c274db0b6
6
+ metadata.gz: c4bf87080fe32e145e334944cc1871d109f5b11c5c3cb413c9de3f44cf09b9554ac3fabeb54650c18f67ed96d6381a0290ae8e461ff37b9fef3d27351675ff68
7
+ data.tar.gz: a53addafe3007de3ad77f3e42e4ca3b3f90ae372af24b00beff1b5b486fa4c1f5d6c84de22788192b05328c14c7eeec4e602a77623a00c5c2f8de5bdbd8dca7d
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ AllCops:
2
+ DisabledByDefault: true
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ # 0.22.1 2025-12-11
2
+
3
+ - Comment out SIGCLD constant
4
+
5
+ # 0.22.0 2025-12-10
6
+
7
+ - Fix use of `um_yield` in statx, multishot ops
8
+ - Improve performance of `UM#snooze`
9
+ - Add some profiling info (WIP)
10
+ - Add `UM#metrics` for getting metrics
11
+ - Add `UM#pending_fibers` for detecting leaking fibers in tests
12
+ - More tests and benchmarks
13
+ - Add `UM#await_fibers` for awaiting fibers
14
+ - Add `UM.socketpair` for creating a socket pair
15
+ - Fiber scheduler:
16
+ - Use fiber's mailbox for processing blocking operations
17
+ - Add `#io_close`, `#yield` hooks, remove `#process_fork` hook
18
+
1
19
  # 0.21.0 2025-12-06
2
20
 
3
21
  - Add `UM#submit`
data/TODO.md CHANGED
@@ -1,5 +1,149 @@
1
1
  ## immediate
2
2
 
3
+ ## Measuring CPU time for fibers
4
+
5
+ - use CPU time (CLOCK_THREAD_CPUTIME_ID)
6
+ - measure:
7
+ - time each fiber is waiting
8
+ - time each fiber is running
9
+ - time machine is waiting (for CQEs)
10
+ - time machine is running fibers from the runqueue
11
+ - can be turned on/off at any time
12
+ - no performance impact when off
13
+
14
+ How can this be implemented:
15
+
16
+ - `um_get_time_cpu()` function for reading CPU time (CLOCK_THREAD_CPUTIME_ID) as
17
+ double.
18
+ - add to `struct um`:
19
+
20
+ ```c
21
+ struct um {
22
+ ...
23
+ int profiling_mode;
24
+ double total_time_run;
25
+ double total_time_wait;
26
+ double last_cpu_time;
27
+ }
28
+ ```
29
+
30
+ - `UM#profile=` to turn it on/off.
31
+ - On `machine.profile = true`, reset `total_time_xxx` and `last_cpu_time`
32
+
33
+ ```c
34
+ machine->total_time_run = 0;
35
+ machine->total_time_wait = 0;
36
+ machine->last_cpu_time = um_get_time_cpu();
37
+ ```
38
+
39
+ - when profiling is active:
40
+ - before processing CQEs:
41
+
42
+ ```c
43
+ // before
44
+ double cpu_time0;
45
+ VALUE fiber;
46
+ int profiling_mode = machine->profiling_mode;
47
+ if (profiling_mode) {
48
+ fiber = rb_fiber_current();
49
+ cpu_time0 = um_get_time_cpu();
50
+ double elapsed = cpu_time0 - machine->last_cpu_time;
51
+ um_update_fiber_time_run(fiber, cpu_time0, elapsed);
52
+ machine->total_time_run += elapsed;
53
+ }
54
+ process_cqes(...)
55
+ // after
56
+ if (profiling_mode) {
57
+ double cpu_time1 = um_get_time_cpu();
58
+ double elapsed = cpu_time1 - cpu_time0;
59
+ um_update_fiber_last_time(fiber, cpu_time1);
60
+ machine->total_time_wait += elapsed;
61
+ machine->last_cpu_time = cpu_time1;
62
+ }
63
+ ```
64
+
65
+ - when doing switching, in `um_process_runqueue_op`:
66
+
67
+ ```c
68
+ // before
69
+ double cpu_time;
70
+ VALUE cur_fiber;
71
+ VALUE next_fiber = get_next_fiber(...);
72
+ int profiling_mode = machine->profiling_mode;
73
+ if (profiling_mode) {
74
+ cur_fiber = rb_fiber_current();
75
+ cpu_time = um_get_time_cpu();
76
+ double elapsed = cpu_time - machine->last_cpu_time;
77
+ um_update_fiber_time_run(cur_fiber, cpu_time, elapsed);
78
+ machine->total_time_run += elapsed;
79
+ um_update_fiber_time_wait(next_fiber, cpu_time);
80
+ machine->last_cpu_time = cpu_time;
81
+ }
82
+ do_fiber_transfer(...)
83
+ ```
84
+
85
+ - updating fiber time instance vars:
86
+
87
+ ```c
88
+ inline void um_update_fiber_time_run(VALUE fiber, double stamp, double elapsed) {
89
+ // VALUE fiber_stamp = rb_ivar_get(fiber, ID_time_last_cpu);
90
+ VALUE fiber_total_run = rb_ivar_get(fiber, ID_time_total_run);
91
+ double total = NIL_P(fiber_total_run) ?
92
+ elapsed : NUM2DBL(fiber_total_run) + elapsed;
93
+ rb_ivar_set(fiber, ID_time_total_run, DBL2NUM(total));
94
+ rb_ivar_set(fiber, ID_time_last_cpu, DBL2NUM(stamp));
95
+ }
96
+
97
+ inline void um_update_fiber_time_wait(VALUE fiber, double stamp) {
98
+ VALUE fiber_last_stamp = rb_ivar_get(fiber, ID_time_last_cpu);
99
+ if (likely(!NIL_P(fiber_last_stamp))) {
100
+ double last_stamp = NUM2DBL(fiber_last_stamp);
101
+ double elapsed = stamp - last_stamp;
102
+ VALUE fiber_total_wait = rb_ivar_get(fiber, ID_time_total_wait);
103
+ double total = NIL_P(fiber_total_wait) ?
104
+ elapsed : NUM2DBL(fiber_total_wait) + elapsed;
105
+ rb_ivar_set(fiber, ID_time_total_wait, DBL2NUM(total));
106
+ }
107
+ else
108
+ rb_ivar_set(fiber, ID_time_total_wait, DBL2NUM(0.0));
109
+ rb_ivar_set(fiber, ID_time_last_cpu, DBL2NUM(stamp));
110
+ }
111
+ ```
112
+
113
+ ## Metrics API
114
+
115
+ - machine metrics: `UM#metrics` - returns a hash containing metrics:
116
+
117
+ ```ruby
118
+ {
119
+ size:, # SQ size (entries)
120
+ total_ops:, # total ops submitted
121
+ total_fiber_switches:, # total fiber switches
122
+ total_cqe_waits:, # total number of CQE waits
123
+ ops_pending:, # number of pending ops
124
+ ops_unsubmitted:, # number of unsubmitted
125
+ ops_runqueue:, # number of ops in runqueue
126
+ ops_free:, # number of ops in freelist
127
+ ops_transient:, # number of ops in transient list
128
+ hwm_pending:, # high water mark - pending ops
129
+ hwm_unsubmitted:, # high water mark - unsubmitted ops
130
+ hwm_runqueue:, # high water mark - runqueue depth
131
+ hwm_free:, # high water mark - ops in free list
132
+ hwm_transient:, # high water mark - ops in transient list
133
+ # when profiling is active
134
+ time_total_run:, # total CPU time running
135
+ time_total_wait:, # total CPU time waiting for CQEs
136
+ }
137
+ ```
138
+
139
+ - For this we need to add tracking for:
140
+ - runqueue list size
141
+ - transient list size
142
+ - free list size
143
+ - Those will be done in um_op.c (in linked list management code)
144
+
145
+ - All metrics info in kept in
146
+
3
147
  ## useful concurrency tools
4
148
 
5
149
  - debounce
@@ -0,0 +1,139 @@
1
+ # UringMachine Benchmarks
2
+
3
+ The following benchmarks measure the performance of UringMachine against stock
4
+ Ruby in a variety of scenarios. For each scenario, we compare three different
5
+ implementations:
6
+
7
+ - `Threads`: thread-based concurrency using the stock Ruby I/O and
8
+ synchronization classes.
9
+
10
+ - `ThreadPool`: thread pool consisting of 10 worker threads, receiving jobs
11
+ through a common queue.
12
+
13
+ - `Async epoll`: fiber-based concurrency with
14
+ [Async](https://github.com/socketry/async) fiber scheduler, using an epoll
15
+ selector.
16
+
17
+ - `Async uring`: fiber-based concurrency with Async fiber scheduler, using a
18
+ uring selector.
19
+
20
+ - `UM FS`: fiber-based concurrency with UringMachine fiber scheduler.
21
+
22
+ - `UM`: fiber-based concurrency using the UringMachine low-level API.
23
+
24
+ <img src="./chart.png">
25
+
26
+ ## Observations
27
+
28
+ - We see the stark difference between thread-based and fiber-based concurrency.
29
+ For I/O-bound workloads, there's really no contest - and that's exactly why
30
+ the fiber scheduler interface changes everything.
31
+
32
+ - The UringMachine fiber scheduler is in some cases faster than the Async fiber
33
+ scheduler, but not in all. This might be because the Async FS does scheduling
34
+ of fibers in plain Ruby, while the UMFS implements a runqueue in its
35
+ C-extension.
36
+
37
+ - The UringMachine low-level API is faster to use in most cases, and its
38
+ performance advantage grows with the level of concurrency. Interestingly, when
39
+ performing CPU-bound work, it seems slightly slightly slower. This should be
40
+ investigated.
41
+
42
+ - The [pg](https://github.com/ged/ruby-pg) gem supports the use of fiber
43
+ schedulers, and there too we see a marked performance advantage to using
44
+ fibers instead of threads.
45
+
46
+ According to these benchmarks, for I/O-bound scenarios the different fiber-based
47
+ implementations present a average speedup as follows:
48
+
49
+ |implementation|average factor|
50
+ |--------------|--------------|
51
+ |Async epoll |x2.36 |
52
+ |Async uring |x2.42 |
53
+ |UM FS |x2.85 |
54
+ |UM |x6.20 |
55
+
56
+ ## 1. I/O - Pipe
57
+
58
+ 50 groups, where in each group we create a pipe with a pair of threads/fibers
59
+ writing/reading 1KB of data to the pipe.
60
+
61
+ ```
62
+ C=50x2 user system total real
63
+ Threads 2.105002 2.671980 4.776982 ( 4.272842)
64
+ ThreadPool 4.818014 10.740555 15.558569 ( 7.070236)
65
+ Async epoll 1.118937 0.254803 1.373740 ( 1.374298)
66
+ Async uring 1.363248 0.270063 1.633311 ( 1.633696)
67
+ UM FS 0.746332 0.183006 0.929338 ( 0.929619)
68
+ UM 0.237816 0.328352 0.566168 ( 0.566265)
69
+ ```
70
+
71
+ ## 2. I/O - Socketpair
72
+
73
+ 50 concurrent groups, where in each group we create a unix socketpair with a
74
+ pair of threads/fibers writing/reading 1KB of data to the sockets.
75
+
76
+ ```
77
+ C=50x2 user system total real
78
+ Threads 2.068122 3.247781 5.315903 ( 4.295488)
79
+ ThreadPool 2.283882 3.461607 5.745489 ( 4.650422)
80
+ Async epoll 0.381400 0.846445 1.227845 ( 1.227983)
81
+ Async uring 0.472526 0.821467 1.293993 ( 1.294166)
82
+ UM FS 0.443023 0.734334 1.177357 ( 1.177576)
83
+ UM 0.116995 0.675997 0.792992 ( 0.793183)
84
+ ```
85
+
86
+ ## 3. Mutex - CPU-bound
87
+
88
+ 20 concurrent groups, where in each group we create a mutex and start 10 worker
89
+ threads/fibers locking the mutex and performing a Regexp match.
90
+
91
+ ```
92
+ C=20x10 user system total real
93
+ Threads 5.174998 0.024885 5.199883 ( 5.193211)
94
+ Async epoll 5.309793 0.000949 5.310742 ( 5.311217)
95
+ Async uring 5.341404 0.004860 5.346264 ( 5.346963)
96
+ UM FS 5.363719 0.001976 5.365695 ( 5.366254)
97
+ UM 5.351073 0.005986 5.357059 ( 5.357602)
98
+ ```
99
+
100
+ ## 4. Mutex - I/O-bound
101
+
102
+ N concurrent groups, where in each group we create a mutex, open a file and
103
+ start 10 worker threads/fibers locking the mutex and writing 1KB chunks to the
104
+ file.
105
+
106
+ ```
107
+ C=50x10 user system total real
108
+ Threads 2.042649 3.441547 5.484196 ( 4.328783)
109
+ Async epoll 0.810375 0.744084 1.554459 ( 1.554726)
110
+ Async uring 0.854985 1.129260 1.984245 ( 1.140749)
111
+ UM FS 0.686329 0.872376 1.558705 ( 0.845214)
112
+ UM 0.250370 1.323227 1.573597 ( 0.720928)
113
+ ```
114
+
115
+ ## 5. Postgres client
116
+
117
+ C concurrent threads/fibers, each thread issuing SELECT query to a PG database.
118
+
119
+ ```
120
+ C=50 user system total real
121
+ Threads 4.304292 1.358116 5.662408 ( 4.795725)
122
+ Async epoll 2.890160 0.432836 3.322996 ( 3.334350)
123
+ Async uring 2.818439 0.433896 3.252335 ( 3.252799)
124
+ UM FS 2.819371 0.443182 3.262553 ( 3.264606)
125
+ ```
126
+ ## 6. Queue
127
+
128
+ 20 concurrent groups, where in each group we create a queue, start 5 producer
129
+ threads/fibers that push items to the queue, and 10 consumer threads/fibers that
130
+ pull items from the queue.
131
+
132
+ ```
133
+ C=20x(5+10) user system total real
134
+ Threads 4.880983 0.207451 5.088434 ( 5.071019)
135
+ Async epoll 4.107208 0.006519 4.113727 ( 4.114227)
136
+ Async uring 4.206283 0.028974 4.235257 ( 4.235705)
137
+ UM FS 4.082394 0.001719 4.084113 ( 4.084522)
138
+ UM 4.099893 0.323569 4.423462 ( 4.424089)
139
+ ```
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './common'
4
+
5
+ GROUPS = 50
6
+ ITERATIONS = 10000
7
+
8
+ SIZE = 1024
9
+ DATA = '*' * SIZE
10
+
11
+ class UMBenchmark
12
+ def do_threads(threads, ios)
13
+ GROUPS.times do
14
+ r, w = IO.pipe
15
+ r.sync = true
16
+ w.sync = true
17
+ threads << Thread.new do
18
+ ITERATIONS.times { w.write(DATA) }
19
+ w.close
20
+ end
21
+ threads << Thread.new do
22
+ ITERATIONS.times { r.readpartial(SIZE) }
23
+ r.close
24
+ end
25
+ end
26
+ end
27
+
28
+ def do_thread_pool(thread_pool, ios)
29
+ GROUPS.times do
30
+ r, w = IO.pipe
31
+ r.sync = true
32
+ w.sync = true
33
+ ios << r << w
34
+ ITERATIONS.times {
35
+ thread_pool.queue { w.write(DATA) }
36
+ thread_pool.queue { r.readpartial(SIZE) }
37
+ }
38
+ end
39
+ end
40
+
41
+ def do_scheduler(scheduler, ios)
42
+ GROUPS.times do
43
+ r, w = IO.pipe
44
+ r.sync = true
45
+ w.sync = true
46
+ Fiber.schedule do
47
+ ITERATIONS.times { w.write(DATA) }
48
+ w.close
49
+ end
50
+ Fiber.schedule do
51
+ ITERATIONS.times { r.readpartial(SIZE) }
52
+ r.close
53
+ end
54
+ end
55
+ end
56
+
57
+ def do_um(machine, fibers, fds)
58
+ GROUPS.times do
59
+ r, w = UM.pipe
60
+ fibers << machine.spin do
61
+ ITERATIONS.times { machine.write(w, DATA) }
62
+ machine.close_async(w)
63
+ end
64
+ fibers << machine.spin do
65
+ ITERATIONS.times { machine.read(r, +'', SIZE) }
66
+ machine.close_async(r)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './common'
4
+ require 'socket'
5
+
6
+ GROUPS = 50
7
+ ITERATIONS = 10000
8
+
9
+ SIZE = 1024
10
+ DATA = '*' * SIZE
11
+
12
+ class UMBenchmark
13
+ def do_threads(threads, ios)
14
+ GROUPS.times do
15
+ r, w = Socket.socketpair(:AF_UNIX, :SOCK_STREAM, 0)
16
+ r.sync = true
17
+ w.sync = true
18
+ threads << Thread.new do
19
+ ITERATIONS.times { w.send(DATA, 0) }
20
+ w.close
21
+ end
22
+ threads << Thread.new do
23
+ ITERATIONS.times { r.recv(SIZE) }
24
+ r.close
25
+ end
26
+ end
27
+ end
28
+
29
+ def do_thread_pool(thread_pool, ios)
30
+ GROUPS.times do
31
+ r, w = Socket.socketpair(:AF_UNIX, :SOCK_STREAM, 0)
32
+ r.sync = true
33
+ w.sync = true
34
+ ios << r << w
35
+ ITERATIONS.times {
36
+ thread_pool.queue { w.send(DATA, 0) }
37
+ thread_pool.queue { r.recv(SIZE) }
38
+ }
39
+ end
40
+ end
41
+
42
+ def do_scheduler(scheduler, ios)
43
+ GROUPS.times do
44
+ r, w = Socket.socketpair(:AF_UNIX, :SOCK_STREAM, 0)
45
+ r.sync = true
46
+ w.sync = true
47
+ Fiber.schedule do
48
+ ITERATIONS.times { w.send(DATA, 0) }
49
+ w.close
50
+ end
51
+ Fiber.schedule do
52
+ ITERATIONS.times { r.recv(SIZE) }
53
+ r.close
54
+ end
55
+ end
56
+ end
57
+
58
+ def do_um(machine, fibers, fds)
59
+ GROUPS.times do
60
+ r, w = UM.socketpair(UM::AF_UNIX, UM::SOCK_STREAM, 0)
61
+ fibers << machine.spin do
62
+ ITERATIONS.times { machine.send(w, DATA, SIZE, UM::MSG_WAITALL) }
63
+ machine.close_async(w)
64
+ end
65
+ fibers << machine.spin do
66
+ ITERATIONS.times { machine.recv(r, +'', SIZE, 0) }
67
+ machine.close_async(r)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './common'
4
+
5
+ GROUPS = 20
6
+ WORKERS = 10
7
+ ITERATIONS = 10000
8
+
9
+ STR = "foobar" * 100
10
+ RE = /foo(.+)$/
11
+
12
+ class UMBenchmark
13
+ def do_threads(threads, ios)
14
+ GROUPS.times do
15
+ mutex = Mutex.new
16
+ WORKERS.times do
17
+ threads << Thread.new do
18
+ ITERATIONS.times do
19
+ mutex.synchronize do
20
+ STR.match(RE)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def do_scheduler(scheduler, ios)
29
+ GROUPS.times do
30
+ mutex = Mutex.new
31
+ WORKERS.times do
32
+ Fiber.schedule do
33
+ ITERATIONS.times do
34
+ mutex.synchronize do
35
+ STR.match(RE)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def do_um(machine, fibers, fds)
44
+ GROUPS.times do
45
+ mutex = UM::Mutex.new
46
+ WORKERS.times do
47
+ fibers << machine.spin do
48
+ ITERATIONS.times do
49
+ machine.synchronize(mutex) do
50
+ STR.match(RE)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './common'
4
+ require 'securerandom'
5
+ require 'fileutils'
6
+
7
+ GROUPS = ENV['N']&.to_i || 50
8
+ WORKERS = 10
9
+ ITERATIONS = 1000
10
+
11
+ puts "N=#{GROUPS}"
12
+
13
+ SIZE = 1024
14
+ DATA = "*" * SIZE
15
+
16
+ class UMBenchmark
17
+ def do_threads(threads, ios)
18
+ GROUPS.times do
19
+ mutex = Mutex.new
20
+ ios << (f = File.open("/tmp/mutex_io_threads_#{SecureRandom.hex}", 'w'))
21
+ f.sync = true
22
+ WORKERS.times do
23
+ threads << Thread.new do
24
+ ITERATIONS.times do
25
+ mutex.synchronize do
26
+ f.write(DATA)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def do_scheduler(scheduler, ios)
35
+ GROUPS.times do
36
+ mutex = Mutex.new
37
+ ios << (f = File.open("/tmp/mutex_io_fiber_scheduler_#{SecureRandom.hex}", 'w'))
38
+ f.sync = true
39
+ WORKERS.times do
40
+ Fiber.schedule do
41
+ ITERATIONS.times do
42
+ mutex.synchronize { f.write(DATA) }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def do_um(machine, fibers, fds)
50
+ GROUPS.times do
51
+ mutex = UM::Mutex.new
52
+ fds << (fd = machine.open("/tmp/mutex_io_um_#{SecureRandom.hex}", UM::O_CREAT | UM::O_WRONLY))
53
+ WORKERS.times do
54
+ fibers << machine.spin do
55
+ ITERATIONS.times do
56
+ machine.synchronize(mutex) do
57
+ machine.write(fd, DATA)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -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