polyphony 0.58 → 0.61

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/Gemfile.lock +15 -29
  4. data/examples/core/message_based_supervision.rb +51 -0
  5. data/examples/io/echo_server.rb +16 -7
  6. data/ext/polyphony/backend_common.c +160 -7
  7. data/ext/polyphony/backend_common.h +34 -2
  8. data/ext/polyphony/backend_io_uring.c +119 -40
  9. data/ext/polyphony/backend_io_uring_context.c +10 -1
  10. data/ext/polyphony/backend_io_uring_context.h +5 -3
  11. data/ext/polyphony/backend_libev.c +109 -31
  12. data/ext/polyphony/extconf.rb +2 -2
  13. data/ext/polyphony/fiber.c +1 -34
  14. data/ext/polyphony/polyphony.c +12 -19
  15. data/ext/polyphony/polyphony.h +9 -20
  16. data/ext/polyphony/polyphony_ext.c +0 -4
  17. data/ext/polyphony/queue.c +12 -12
  18. data/ext/polyphony/runqueue.c +21 -98
  19. data/ext/polyphony/runqueue.h +26 -0
  20. data/ext/polyphony/thread.c +6 -113
  21. data/lib/polyphony/core/timer.rb +2 -2
  22. data/lib/polyphony/extensions/fiber.rb +102 -82
  23. data/lib/polyphony/extensions/io.rb +10 -9
  24. data/lib/polyphony/extensions/openssl.rb +14 -4
  25. data/lib/polyphony/extensions/socket.rb +15 -15
  26. data/lib/polyphony/extensions/thread.rb +1 -1
  27. data/lib/polyphony/version.rb +1 -1
  28. data/polyphony.gemspec +0 -7
  29. data/test/test_backend.rb +46 -9
  30. data/test/test_ext.rb +1 -1
  31. data/test/test_fiber.rb +106 -18
  32. data/test/test_global_api.rb +1 -1
  33. data/test/test_io.rb +29 -0
  34. data/test/test_supervise.rb +100 -100
  35. data/test/test_thread.rb +5 -11
  36. data/test/test_thread_pool.rb +1 -1
  37. data/test/test_trace.rb +28 -49
  38. metadata +5 -109
  39. data/ext/polyphony/tracing.c +0 -11
  40. data/lib/polyphony/adapters/trace.rb +0 -138
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8002b8c0e03afa8e915b5ed67d64c1fc288a5fa220dfb2c32d3966a78046eee
4
- data.tar.gz: c3a46eeae1f048f4adee9d3130f5dd3593936e843c1874d928f742d1fa83053f
3
+ metadata.gz: b7056ec954b2264a15f88934d2b1e65f53c76c301453851ceadf60715570e474
4
+ data.tar.gz: 1cd5835b7a5a3d4036e1eabf45ded2e09efdc7cacb207fc0a1a60ac67da02d90
5
5
  SHA512:
6
- metadata.gz: e6681155761daee35b73c9674e69aa505786e576814db0c909028cb576d8609f99bccc79f80b7d06d90c7658747bc2589c9837d3bdb3419782147e573d39f278
7
- data.tar.gz: 90197a8db54405ca37492a54a20adf00c85f142af606cad26ad823161825ccf6bd9a414adcbbf3e27eaf93eea1d94a7cbc50a2fb5982ee4ce48969d4378400cd
6
+ metadata.gz: b5e32edc6bc22ba580e7c6d7d42a73982803080e9a9782326cb7b8796aa62ad6b75bccb1e6607879f0d3e5a5c4db8e5597d8407b76014f08f3945d1d8ad0097a
7
+ data.tar.gz: b8fe0f9419061295f830ed12ae9c8ea089a966686388cff580ddb6e1f7cd8c17a04928f9af41083019a5a11e8cccce25af404959db30a730a2ade2359a7e07ef
data/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ ## 0.61 2021-07-20
2
+
3
+ - Add more statistics, move stats to `Backend#stats`
4
+
5
+ ## 0.60 2021-07-15
6
+
7
+
8
+ - Fix linux version detection (for kernel version > 5.9)
9
+ - Fix op ctx leak in io_uring backend (when polling for I/O readiness)
10
+ - Add support for appending to buffer in `Backend#read`, `Backend#recv` methods
11
+ - Improve anti-event starvation mechanism
12
+ - Redesign fiber monitoring mechanism
13
+ - Implement `Fiber#attach`
14
+ - Add optional maxlen argument to `IO#read_loop`, `Socket#recv_loop` (#60)
15
+ - Implement `Fiber#detach` (#52)
16
+
17
+ ## 0.59.1 2021-06-28
18
+
19
+ - Accept fiber tag in `Polyphony::Timer.new`
20
+
21
+ ## 0.59 2021-06-28
22
+
23
+ - Redesign tracing mechanism and API - now completely separated from Ruby core
24
+ trace API
25
+ - Refactor C code - move run queue into backend
26
+
1
27
  ## 0.58 2021-06-25
2
28
 
3
29
  - Implement `Thread#idle_gc_period`, `#on_idle` (#56)
data/Gemfile.lock CHANGED
@@ -1,27 +1,25 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polyphony (0.58)
4
+ polyphony (0.61)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  ansi (1.5.0)
10
- ast (2.4.0)
10
+ ast (2.4.2)
11
11
  builder (3.2.4)
12
12
  coderay (1.1.3)
13
- docile (1.3.2)
14
- hiredis (0.6.3)
15
- http_parser.rb (0.6.0)
13
+ docile (1.4.0)
16
14
  httparty (0.17.1)
17
15
  mime-types (~> 3.0)
18
16
  multi_xml (>= 0.5.2)
19
- json (2.3.0)
17
+ json (2.5.1)
20
18
  localhost (1.1.8)
21
19
  method_source (1.0.0)
22
20
  mime-types (3.3.1)
23
21
  mime-types-data (~> 3.2015)
24
- mime-types-data (3.2020.0512)
22
+ mime-types-data (3.2021.0704)
25
23
  minitest (5.14.4)
26
24
  minitest-reporters (1.4.2)
27
25
  ansi
@@ -30,22 +28,18 @@ GEM
30
28
  ruby-progressbar
31
29
  msgpack (1.4.2)
32
30
  multi_xml (0.6.0)
33
- mysql2 (0.5.3)
34
- parallel (1.19.1)
35
- parser (2.7.0.2)
36
- ast (~> 2.4.0)
37
- pg (1.1.4)
31
+ parallel (1.20.1)
32
+ parser (3.0.2.0)
33
+ ast (~> 2.4.1)
38
34
  pry (0.13.1)
39
35
  coderay (~> 1.1)
40
36
  method_source (~> 1.0)
41
- rack (2.2.3)
42
37
  rainbow (3.0.0)
43
- rake (13.0.3)
38
+ rake (13.0.6)
44
39
  rake-compiler (1.1.1)
45
40
  rake
46
- redis (4.1.0)
47
- regexp_parser (1.7.1)
48
- rexml (3.2.4)
41
+ regexp_parser (2.1.1)
42
+ rexml (3.2.5)
49
43
  rubocop (0.85.1)
50
44
  parallel (~> 1.10)
51
45
  parser (>= 2.7.0.1)
@@ -55,37 +49,29 @@ GEM
55
49
  rubocop-ast (>= 0.0.3)
56
50
  ruby-progressbar (~> 1.7)
57
51
  unicode-display_width (>= 1.4.0, < 2.0)
58
- rubocop-ast (0.0.3)
59
- parser (>= 2.7.0.1)
60
- ruby-progressbar (1.10.1)
61
- sequel (5.34.0)
52
+ rubocop-ast (1.8.0)
53
+ parser (>= 3.0.1.1)
54
+ ruby-progressbar (1.11.0)
62
55
  simplecov (0.17.1)
63
56
  docile (~> 1.1)
64
57
  json (>= 1.8, < 3)
65
58
  simplecov-html (~> 0.10.0)
66
59
  simplecov-html (0.10.2)
67
- unicode-display_width (1.6.1)
60
+ unicode-display_width (1.7.0)
68
61
 
69
62
  PLATFORMS
70
63
  ruby
71
64
 
72
65
  DEPENDENCIES
73
- hiredis (= 0.6.3)
74
- http_parser.rb (~> 0.6.0)
75
66
  httparty (= 0.17.1)
76
67
  localhost (~> 1.1.4)
77
68
  minitest (= 5.14.4)
78
69
  minitest-reporters (= 1.4.2)
79
70
  msgpack (= 1.4.2)
80
- mysql2 (= 0.5.3)
81
- pg (= 1.1.4)
82
71
  polyphony!
83
72
  pry (= 0.13.1)
84
- rack (>= 2.0.8, < 2.3.0)
85
73
  rake-compiler (= 1.1.1)
86
- redis (= 4.1.0)
87
74
  rubocop (= 0.85.1)
88
- sequel (= 5.34.0)
89
75
  simplecov (= 0.17.1)
90
76
 
91
77
  BUNDLED WITH
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ class Supervisor
7
+ def initialize(*fibers)
8
+ @fiber = spin { do_supervise }
9
+ @fiber.message_on_child_termination = true
10
+ fibers.each { |f| add(f) }
11
+ end
12
+
13
+ def await
14
+ @fiber.await
15
+ end
16
+
17
+ def spin(tag = nil, &block)
18
+ @fiber.spin(tag, &block)
19
+ end
20
+
21
+ def add(fiber)
22
+ fiber.attach(@fiber)
23
+ end
24
+
25
+ def do_supervise
26
+ loop do
27
+ msg = receive
28
+ # puts "Supervisor received #{msg.inspect}"
29
+ f, r = msg
30
+ puts "Fiber #{f.tag} terminated with #{r.inspect}, restarting..."
31
+ f.restart
32
+ end
33
+ end
34
+ end
35
+
36
+ def supervise(*fibers)
37
+ supervisor = Supervisor.new(*fibers)
38
+ supervisor.await
39
+ end
40
+
41
+ def start_worker(id)
42
+ spin_loop(:"worker#{id}") do
43
+ duration = rand(0.5..1.0)
44
+ puts "Worker #{id} sleeping for #{duration} seconds"
45
+ sleep duration
46
+ raise 'foo' if rand > 0.7
47
+ break if rand > 0.6
48
+ end
49
+ end
50
+
51
+ supervise(start_worker(1), start_worker(2))
@@ -3,16 +3,25 @@
3
3
  require 'bundler/setup'
4
4
  require 'polyphony'
5
5
 
6
+ spin_loop(interval: 5) { p Thread.backend.stats }
7
+
6
8
  server = TCPServer.open('127.0.0.1', 1234)
7
9
  puts "Pid: #{Process.pid}"
8
10
  puts 'Echoing on port 1234...'
9
- while (client = server.accept)
10
- spin do
11
- while (data = client.gets)
12
- # client.send("you said: #{data.chomp}!\n", 0)
13
- client.write('you said: ', data.chomp, "!\n")
11
+ begin
12
+ while (client = server.accept)
13
+ spin do
14
+ while (data = client.gets)
15
+ # client.send("you said: #{data.chomp}!\n", 0)
16
+ client.write('you said: ', data.chomp, "!\n")
17
+ end
18
+ rescue Errno::ECONNRESET
19
+ 'Connection reset...'
20
+ ensure
21
+ client.shutdown
22
+ client.close
14
23
  end
15
- rescue Errno::ECONNRESET
16
- 'Connection reset...'
17
24
  end
25
+ ensure
26
+ server.close
18
27
  end
@@ -5,12 +5,127 @@
5
5
  #include "polyphony.h"
6
6
  #include "backend_common.h"
7
7
 
8
- inline void initialize_backend_base(struct Backend_base *base) {
8
+ inline void backend_base_initialize(struct Backend_base *base) {
9
+ runqueue_initialize(&base->runqueue);
9
10
  base->currently_polling = 0;
11
+ base->op_count = 0;
12
+ base->switch_count = 0;
13
+ base->poll_count = 0;
10
14
  base->pending_count = 0;
11
15
  base->idle_gc_period = 0;
12
16
  base->idle_gc_last_time = 0;
13
- base->idle_block = Qnil;
17
+ base->idle_proc = Qnil;
18
+ base->trace_proc = Qnil;
19
+ }
20
+
21
+ inline void backend_base_finalize(struct Backend_base *base) {
22
+ runqueue_finalize(&base->runqueue);
23
+ }
24
+
25
+ inline void backend_base_mark(struct Backend_base *base) {
26
+ if (base->idle_proc != Qnil) rb_gc_mark(base->idle_proc);
27
+ if (base->trace_proc != Qnil) rb_gc_mark(base->trace_proc);
28
+ runqueue_mark(&base->runqueue);
29
+ }
30
+
31
+ const unsigned int ANTI_STARVE_SWITCH_COUNT_THRESHOLD = 64;
32
+
33
+ inline void conditional_nonblocking_poll(VALUE backend, struct Backend_base *base, VALUE current, VALUE next) {
34
+ if ((base->switch_count % ANTI_STARVE_SWITCH_COUNT_THRESHOLD) == 0 || next == current)
35
+ Backend_poll(backend, Qnil);
36
+ }
37
+
38
+ VALUE backend_base_switch_fiber(VALUE backend, struct Backend_base *base) {
39
+ VALUE current_fiber = rb_fiber_current();
40
+ runqueue_entry next;
41
+ unsigned int pending_ops_count = base->pending_count;
42
+ unsigned int backend_was_polled = 0;
43
+ unsigned int idle_tasks_run_count = 0;
44
+
45
+ base->switch_count++;
46
+ COND_TRACE(base, 2, SYM_fiber_switchpoint, current_fiber);
47
+
48
+ while (1) {
49
+ next = runqueue_shift(&base->runqueue);
50
+ if (next.fiber != Qnil) {
51
+ // Polling for I/O op completion is normally done when the run queue is
52
+ // empty, but if the runqueue never empties, we'll never get to process
53
+ // any event completions. In order to prevent this, an anti-starvation
54
+ // mechanism is employed, under the following conditions:
55
+ // - a blocking poll was not yet performed
56
+ // - there are pending blocking operations
57
+ // - the runqueue shift count has reached a fixed threshold (currently 64), or
58
+ // - the next fiber is the same as the current fiber (a single fiber is snoozing)
59
+ if (!backend_was_polled && pending_ops_count)
60
+ conditional_nonblocking_poll(backend, base, current_fiber, next.fiber);
61
+
62
+ break;
63
+ }
64
+
65
+ if (!idle_tasks_run_count) {
66
+ idle_tasks_run_count++;
67
+ backend_run_idle_tasks(base);
68
+ }
69
+ if (pending_ops_count == 0) break;
70
+ Backend_poll(backend, Qtrue);
71
+ backend_was_polled = 1;
72
+ }
73
+
74
+ if (next.fiber == Qnil) return Qnil;
75
+
76
+ // run next fiber
77
+ COND_TRACE(base, 3, SYM_fiber_run, next.fiber, next.value);
78
+
79
+ rb_ivar_set(next.fiber, ID_ivar_runnable, Qnil);
80
+ RB_GC_GUARD(next.fiber);
81
+ RB_GC_GUARD(next.value);
82
+ return (next.fiber == current_fiber) ?
83
+ next.value : FIBER_TRANSFER(next.fiber, next.value);
84
+ }
85
+
86
+ void backend_base_schedule_fiber(VALUE thread, VALUE backend, struct Backend_base *base, VALUE fiber, VALUE value, int prioritize) {
87
+ int already_runnable;
88
+
89
+ if (rb_fiber_alive_p(fiber) != Qtrue) return;
90
+ already_runnable = rb_ivar_get(fiber, ID_ivar_runnable) != Qnil;
91
+
92
+ COND_TRACE(base, 4, SYM_fiber_schedule, fiber, value, prioritize ? Qtrue : Qfalse);
93
+
94
+ (prioritize ? runqueue_unshift : runqueue_push)(&base->runqueue, fiber, value, already_runnable);
95
+ if (!already_runnable) {
96
+ rb_ivar_set(fiber, ID_ivar_runnable, Qtrue);
97
+ if (rb_thread_current() != thread) {
98
+ // If the fiber scheduling is done across threads, we need to make sure the
99
+ // target thread is woken up in case it is in the middle of running its
100
+ // event selector. Otherwise it's gonna be stuck waiting for an event to
101
+ // happen, not knowing that it there's already a fiber ready to run in its
102
+ // run queue.
103
+ Backend_wakeup(backend);
104
+ }
105
+ }
106
+ }
107
+
108
+
109
+ inline void backend_trace(struct Backend_base *base, int argc, VALUE *argv) {
110
+ if (base->trace_proc == Qnil) return;
111
+
112
+ rb_funcallv(base->trace_proc, ID_call, argc, argv);
113
+ }
114
+
115
+ inline struct backend_stats backend_base_stats(struct Backend_base *base) {
116
+ struct backend_stats stats = {
117
+ .runqueue_length = runqueue_len(&base->runqueue),
118
+ .runqueue_max_length = runqueue_max_len(&base->runqueue),
119
+ .op_count = base->op_count,
120
+ .switch_count = base->switch_count,
121
+ .poll_count = base->poll_count,
122
+ .pending_ops = base->pending_count
123
+ };
124
+
125
+ base->op_count = 0;
126
+ base->switch_count = 0;
127
+ base->poll_count = 0;
128
+ return stats;
14
129
  }
15
130
 
16
131
  #ifdef POLYPHONY_USE_PIDFD_OPEN
@@ -80,7 +195,7 @@ inline VALUE io_enc_str(VALUE str, rb_io_t *fptr) {
80
195
  //////////////////////////////////////////////////////////////////////
81
196
  //////////////////////////////////////////////////////////////////////
82
197
 
83
- VALUE backend_await(struct Backend_base *backend) {
198
+ inline VALUE backend_await(struct Backend_base *backend) {
84
199
  VALUE ret;
85
200
  backend->pending_count++;
86
201
  ret = Thread_switch_fiber(rb_thread_current());
@@ -89,9 +204,10 @@ VALUE backend_await(struct Backend_base *backend) {
89
204
  return ret;
90
205
  }
91
206
 
92
- VALUE backend_snooze() {
207
+ inline VALUE backend_snooze() {
208
+ VALUE ret;
93
209
  Fiber_make_runnable(rb_fiber_current(), Qnil);
94
- VALUE ret = Thread_switch_fiber(rb_thread_current());
210
+ ret = Thread_switch_fiber(rb_thread_current());
95
211
  return ret;
96
212
  }
97
213
 
@@ -182,8 +298,8 @@ inline void io_verify_blocking_mode(rb_io_t *fptr, VALUE io, VALUE blocking) {
182
298
  }
183
299
 
184
300
  inline void backend_run_idle_tasks(struct Backend_base *base) {
185
- if (base->idle_block != Qnil)
186
- rb_funcall(base->idle_block, ID_call, 0);
301
+ if (base->idle_proc != Qnil)
302
+ rb_funcall(base->idle_proc, ID_call, 0);
187
303
 
188
304
  if (base->idle_gc_period == 0) return;
189
305
 
@@ -195,3 +311,40 @@ inline void backend_run_idle_tasks(struct Backend_base *base) {
195
311
  rb_gc_start();
196
312
  rb_gc_disable();
197
313
  }
314
+
315
+ VALUE SYM_runqueue_length;
316
+ VALUE SYM_runqueue_max_length;
317
+ VALUE SYM_op_count;
318
+ VALUE SYM_switch_count;
319
+ VALUE SYM_poll_count;
320
+ VALUE SYM_pending_ops;
321
+
322
+ VALUE Backend_stats(VALUE self) {
323
+ struct backend_stats backend_stats = backend_get_stats(self);
324
+
325
+ VALUE stats = rb_hash_new();
326
+ rb_hash_aset(stats, SYM_runqueue_length, INT2NUM(backend_stats.runqueue_length));
327
+ rb_hash_aset(stats, SYM_runqueue_max_length, INT2NUM(backend_stats.runqueue_max_length));
328
+ rb_hash_aset(stats, SYM_op_count, INT2NUM(backend_stats.op_count));
329
+ rb_hash_aset(stats, SYM_switch_count, INT2NUM(backend_stats.switch_count));
330
+ rb_hash_aset(stats, SYM_poll_count, INT2NUM(backend_stats.poll_count));
331
+ rb_hash_aset(stats, SYM_pending_ops, INT2NUM(backend_stats.pending_ops));
332
+ RB_GC_GUARD(stats);
333
+ return stats;
334
+ }
335
+
336
+ void backend_setup_stats_symbols() {
337
+ SYM_runqueue_length = ID2SYM(rb_intern("runqueue_length"));
338
+ SYM_runqueue_max_length = ID2SYM(rb_intern("runqueue_max_length"));
339
+ SYM_op_count = ID2SYM(rb_intern("op_count"));
340
+ SYM_switch_count = ID2SYM(rb_intern("switch_count"));
341
+ SYM_poll_count = ID2SYM(rb_intern("poll_count"));
342
+ SYM_pending_ops = ID2SYM(rb_intern("pending_ops"));
343
+
344
+ rb_global_variable(&SYM_runqueue_length);
345
+ rb_global_variable(&SYM_runqueue_max_length);
346
+ rb_global_variable(&SYM_op_count);
347
+ rb_global_variable(&SYM_switch_count);
348
+ rb_global_variable(&SYM_poll_count);
349
+ rb_global_variable(&SYM_pending_ops);
350
+ }
@@ -3,16 +3,44 @@
3
3
 
4
4
  #include "ruby.h"
5
5
  #include "ruby/io.h"
6
+ #include "runqueue.h"
7
+
8
+ struct backend_stats {
9
+ unsigned int runqueue_length;
10
+ unsigned int runqueue_max_length;
11
+ unsigned int op_count;
12
+ unsigned int switch_count;
13
+ unsigned int poll_count;
14
+ unsigned int pending_ops;
15
+ };
6
16
 
7
17
  struct Backend_base {
18
+ runqueue_t runqueue;
8
19
  unsigned int currently_polling;
20
+ unsigned int op_count;
21
+ unsigned int switch_count;
22
+ unsigned int poll_count;
9
23
  unsigned int pending_count;
10
24
  double idle_gc_period;
11
25
  double idle_gc_last_time;
12
- VALUE idle_block;
26
+ VALUE idle_proc;
27
+ VALUE trace_proc;
13
28
  };
14
29
 
15
- void initialize_backend_base(struct Backend_base *base);
30
+ void backend_base_initialize(struct Backend_base *base);
31
+ void backend_base_finalize(struct Backend_base *base);
32
+ void backend_base_mark(struct Backend_base *base);
33
+ VALUE backend_base_switch_fiber(VALUE backend, struct Backend_base *base);
34
+ void backend_base_schedule_fiber(VALUE thread, VALUE backend, struct Backend_base *base, VALUE fiber, VALUE value, int prioritize);
35
+ void backend_trace(struct Backend_base *base, int argc, VALUE *argv);
36
+ struct backend_stats backend_base_stats(struct Backend_base *base);
37
+
38
+ // tracing
39
+ #define SHOULD_TRACE(base) ((base)->trace_proc != Qnil)
40
+ #define TRACE(base, ...) rb_funcall((base)->trace_proc, ID_call, __VA_ARGS__)
41
+ #define COND_TRACE(base, ...) if (SHOULD_TRACE(base)) { TRACE(base, __VA_ARGS__); }
42
+
43
+
16
44
 
17
45
  #ifdef POLYPHONY_USE_PIDFD_OPEN
18
46
  int pidfd_open(pid_t pid, unsigned int flags);
@@ -39,6 +67,7 @@ VALUE io_enc_str(VALUE str, rb_io_t *fptr);
39
67
  //////////////////////////////////////////////////////////////////////
40
68
  //////////////////////////////////////////////////////////////////////
41
69
 
70
+ struct backend_stats backend_get_stats(VALUE self);
42
71
  VALUE backend_await(struct Backend_base *backend);
43
72
  VALUE backend_snooze();
44
73
 
@@ -71,7 +100,10 @@ VALUE backend_timeout_exception(VALUE exception);
71
100
  VALUE Backend_timeout_ensure_safe(VALUE arg);
72
101
  VALUE Backend_timeout_ensure_safe(VALUE arg);
73
102
  VALUE Backend_sendv(VALUE self, VALUE io, VALUE ary, VALUE flags);
103
+ VALUE Backend_stats(VALUE self);
74
104
  void backend_run_idle_tasks(struct Backend_base *base);
75
105
  void io_verify_blocking_mode(rb_io_t *fptr, VALUE io, VALUE blocking);
76
106
 
107
+ void backend_setup_stats_symbols();
108
+
77
109
  #endif /* BACKEND_COMMON_H */