actuator 0.0.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 +7 -0
- data/.autotest +13 -0
- data/History.txt +3 -0
- data/Manifest.txt +28 -0
- data/README.md +105 -0
- data/Rakefile +23 -0
- data/ext/actuator/actuator.c +2 -0
- data/ext/actuator/actuator.h +16 -0
- data/ext/actuator/clock.c +81 -0
- data/ext/actuator/clock.h +15 -0
- data/ext/actuator/debug.c +4 -0
- data/ext/actuator/debug.h +56 -0
- data/ext/actuator/extconf.rb +5 -0
- data/ext/actuator/log.cpp +134 -0
- data/ext/actuator/log.h +18 -0
- data/ext/actuator/reactor.cpp +212 -0
- data/ext/actuator/reactor.h +34 -0
- data/ext/actuator/ruby_helpers.c +17 -0
- data/ext/actuator/ruby_helpers.h +17 -0
- data/ext/actuator/timer.cpp +450 -0
- data/ext/actuator/timer.h +45 -0
- data/lib/actuator.rb +22 -0
- data/lib/actuator/fiber.rb +14 -0
- data/lib/actuator/fiber_pool.rb +82 -0
- data/lib/actuator/job.rb +256 -0
- data/lib/actuator/mutex.rb +116 -0
- data/lib/actuator/mutex/replace.rb +8 -0
- data/test/setup_test.rb +63 -0
- data/test/test_actuator.rb +90 -0
- metadata +134 -0
data/ext/actuator/log.h
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#ifndef ACTUATOR_LOG_H
|
2
|
+
#define ACTUATOR_LOG_H
|
3
|
+
|
4
|
+
#include "actuator.h"
|
5
|
+
|
6
|
+
class Log {
|
7
|
+
public:
|
8
|
+
static void Setup();
|
9
|
+
static void Debug(const char *format, ...);
|
10
|
+
static void Info(const char *format, ...);
|
11
|
+
static void Warn(const char *format, ...);
|
12
|
+
static void Error(const char *format, ...);
|
13
|
+
|
14
|
+
static FILE *debug_file;
|
15
|
+
static FILE *log_file;
|
16
|
+
};
|
17
|
+
|
18
|
+
#endif
|
@@ -0,0 +1,212 @@
|
|
1
|
+
#include "reactor.h"
|
2
|
+
|
3
|
+
Actuator *actuator = 0;
|
4
|
+
|
5
|
+
Actuator::Actuator()
|
6
|
+
{
|
7
|
+
}
|
8
|
+
|
9
|
+
Actuator::~Actuator()
|
10
|
+
{
|
11
|
+
}
|
12
|
+
|
13
|
+
void Actuator::Start()
|
14
|
+
{
|
15
|
+
if (is_running) {
|
16
|
+
Log::Warn("[Actuator] Start called while already running");
|
17
|
+
return;
|
18
|
+
}
|
19
|
+
|
20
|
+
thread = rb_thread_current();
|
21
|
+
is_running = true;
|
22
|
+
|
23
|
+
if (rb_block_given_p()) rb_yield(Qundef);
|
24
|
+
|
25
|
+
long total_ticks = 0;
|
26
|
+
|
27
|
+
double now = clock_time();
|
28
|
+
|
29
|
+
while (is_running)
|
30
|
+
{
|
31
|
+
total_ticks++;
|
32
|
+
|
33
|
+
Timer::Update(now);
|
34
|
+
|
35
|
+
int remaining_dequeues = next_tick_queue.size();
|
36
|
+
while (remaining_dequeues--)
|
37
|
+
{
|
38
|
+
VALUE proc = next_tick_queue.front();
|
39
|
+
next_tick_queue.pop();
|
40
|
+
rb_proc_call_fast(proc);
|
41
|
+
}
|
42
|
+
|
43
|
+
if (!is_running) break;
|
44
|
+
|
45
|
+
now = clock_time();
|
46
|
+
|
47
|
+
timeval delay_duration;
|
48
|
+
double next_timer_at = Timer::GetNextEventTime();
|
49
|
+
if (next_timer_at) {
|
50
|
+
double delay = next_timer_at - now;
|
51
|
+
if (delay > 0) {
|
52
|
+
if (delay > max_delta) {
|
53
|
+
// Never sleep for longer than max_delta
|
54
|
+
delay_duration.tv_sec = 0;
|
55
|
+
delay_duration.tv_usec = max_delta * 1000000;
|
56
|
+
} else {
|
57
|
+
// Extra delay avoids most wake ups that would happen before any timers expire due to bad sleep precision
|
58
|
+
delay_duration.tv_sec = (long)delay;
|
59
|
+
delay_duration.tv_usec = (((long)(delay * 1000000)) % 1000000) + 1;
|
60
|
+
}
|
61
|
+
} else {
|
62
|
+
// Sleep for as few microseconds as possible so that we give other ruby threads
|
63
|
+
// GVL time while also waking up to process expired timers as soon as possible
|
64
|
+
delay_duration.tv_sec = delay_duration.tv_usec = 0;
|
65
|
+
}
|
66
|
+
} else {
|
67
|
+
delay_duration.tv_sec = 0;
|
68
|
+
delay_duration.tv_usec = max_delta * 1000000;
|
69
|
+
}
|
70
|
+
|
71
|
+
is_sleeping = true;
|
72
|
+
|
73
|
+
// rb_thread_wait_for has far better precision on Windows builds than using undocumented kernel system calls
|
74
|
+
rb_thread_wait_for(delay_duration);
|
75
|
+
|
76
|
+
is_waking = false;
|
77
|
+
is_sleeping = false;
|
78
|
+
|
79
|
+
/*double after = clock_time();
|
80
|
+
if (next_timer_at && after < next_timer_at)
|
81
|
+
{
|
82
|
+
//double delay = (next_timer_at - after) * 1000000;
|
83
|
+
//Log::Warn("Woke up %.2f us early - Slept for %.2f / %d us", delay, (after - now) * 1000000.0, delay_duration.tv_usec);
|
84
|
+
} else {
|
85
|
+
double late = (after - now) * 1000000 - delay_duration.tv_usec;
|
86
|
+
if (late > 25) Log::Warn("Woke up %.2f us late - Slept for %.2f / %d us - Timer delay: %.2f", late, (after - now) * 1000000.0, delay_duration.tv_usec, (next_timer_at - now) * 1000000.0);
|
87
|
+
}*/
|
88
|
+
|
89
|
+
/*double sleep_delta = after - now;
|
90
|
+
double late = (sleep_delta * 1000000) - (delay_duration.tv_sec * 1000000 + delay_duration.tv_usec);
|
91
|
+
if (late > 2) Log::Warn("%.10f - Woke up after %.10f - %.2f us late", after, sleep_delta, late);*/
|
92
|
+
|
93
|
+
now = clock_time();
|
94
|
+
}
|
95
|
+
|
96
|
+
thread = 0;
|
97
|
+
}
|
98
|
+
|
99
|
+
void Actuator::Stop()
|
100
|
+
{
|
101
|
+
if (!is_running) {
|
102
|
+
Log::Warn("[Actuator] Actuator.stop called while not running");
|
103
|
+
return;
|
104
|
+
}
|
105
|
+
is_running = false;
|
106
|
+
Timer::Clear();
|
107
|
+
if (is_sleeping) Wake();
|
108
|
+
}
|
109
|
+
|
110
|
+
void Actuator::Wake()
|
111
|
+
{
|
112
|
+
if (!is_sleeping || is_waking) return;
|
113
|
+
is_waking = true;
|
114
|
+
rb_thread_wakeup(thread);
|
115
|
+
}
|
116
|
+
|
117
|
+
static VALUE Actuator_now(VALUE klass)
|
118
|
+
{
|
119
|
+
return DBL2NUM(clock_time());
|
120
|
+
}
|
121
|
+
|
122
|
+
static VALUE Actuator_is_running(VALUE klass)
|
123
|
+
{
|
124
|
+
return actuator->is_running ? Qtrue : Qfalse;
|
125
|
+
}
|
126
|
+
|
127
|
+
static VALUE Actuator_start(VALUE klass)
|
128
|
+
{
|
129
|
+
if (actuator->is_running) {
|
130
|
+
if (rb_block_given_p()) rb_yield(Qundef);
|
131
|
+
} else {
|
132
|
+
actuator->Start();
|
133
|
+
}
|
134
|
+
return Qnil;
|
135
|
+
}
|
136
|
+
|
137
|
+
static VALUE Actuator_stop(VALUE klass)
|
138
|
+
{
|
139
|
+
actuator->Stop();
|
140
|
+
return Qnil;
|
141
|
+
}
|
142
|
+
|
143
|
+
static VALUE Actuator_wake(VALUE klass)
|
144
|
+
{
|
145
|
+
actuator->Wake();
|
146
|
+
return Qnil;
|
147
|
+
}
|
148
|
+
|
149
|
+
static VALUE Actuator_next_tick(VALUE self)
|
150
|
+
{
|
151
|
+
//TODO: Prevent proc from being GC'd while scheduled
|
152
|
+
rb_need_block();
|
153
|
+
actuator->next_tick_queue.push(rb_block_proc());
|
154
|
+
//rb_gc_register_address(&next_tick_queue.back());
|
155
|
+
}
|
156
|
+
|
157
|
+
static VALUE deferred_fiber(VALUE curr, VALUE block)
|
158
|
+
{
|
159
|
+
rb_proc_call_fast(block);
|
160
|
+
}
|
161
|
+
|
162
|
+
static VALUE Actuator_defer(VALUE self)
|
163
|
+
{
|
164
|
+
//TODO: Pool fibers and prevent them from being GC'd while scheduled
|
165
|
+
rb_need_block();
|
166
|
+
VALUE fiber = rb_fiber_new(RUBY_METHOD_FUNC(deferred_fiber), rb_block_proc());
|
167
|
+
if (rb_thread_current() == actuator->thread)
|
168
|
+
{
|
169
|
+
rb_fiber_resume(fiber, 0, 0);
|
170
|
+
}
|
171
|
+
else
|
172
|
+
{
|
173
|
+
Timer *timer = new Timer();
|
174
|
+
timer->SetFiber(fiber);
|
175
|
+
timer->SetDelay(0);
|
176
|
+
timer->Schedule();
|
177
|
+
}
|
178
|
+
return fiber;
|
179
|
+
}
|
180
|
+
|
181
|
+
static VALUE Actuator_sleep(VALUE self, VALUE delay_value)
|
182
|
+
{
|
183
|
+
//TODO: Prevent fibers from being GC'd while scheduled
|
184
|
+
Timer *timer = new Timer();
|
185
|
+
timer->SetFiber(rb_fiber_current());
|
186
|
+
timer->SetDelay(NUM2DBL(delay_value));
|
187
|
+
timer->Schedule();
|
188
|
+
VALUE nil = Qnil;
|
189
|
+
return rb_fiber_yield(1, &nil);
|
190
|
+
}
|
191
|
+
|
192
|
+
extern "C"
|
193
|
+
void Init_actuator()
|
194
|
+
{
|
195
|
+
init_ruby_helpers();
|
196
|
+
clock_init();
|
197
|
+
|
198
|
+
actuator = new Actuator();
|
199
|
+
|
200
|
+
Log::Setup();
|
201
|
+
Timer::Setup();
|
202
|
+
|
203
|
+
VALUE ActuatorClass = rb_define_module("Actuator");
|
204
|
+
rb_define_singleton_method(ActuatorClass, "now", RUBY_METHOD_FUNC(Actuator_now), 0);
|
205
|
+
rb_define_singleton_method(ActuatorClass, "running?", RUBY_METHOD_FUNC(Actuator_is_running), 0);
|
206
|
+
rb_define_singleton_method(ActuatorClass, "start", RUBY_METHOD_FUNC(Actuator_start), 0);
|
207
|
+
rb_define_singleton_method(ActuatorClass, "stop", RUBY_METHOD_FUNC(Actuator_stop), 0);
|
208
|
+
rb_define_singleton_method(ActuatorClass, "wake", RUBY_METHOD_FUNC(Actuator_wake), 0);
|
209
|
+
//rb_define_singleton_method(ActuatorClass, "next_tick", RUBY_METHOD_FUNC(Actuator_next_tick), 0);
|
210
|
+
//rb_define_singleton_method(ActuatorClass, "defer", RUBY_METHOD_FUNC(Actuator_defer), 0);
|
211
|
+
//rb_define_singleton_method(FiberClass, "sleep", RUBY_METHOD_FUNC(Actuator_sleep), 1);
|
212
|
+
}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#include <iostream>
|
2
|
+
#include <map>
|
3
|
+
#include <queue>
|
4
|
+
#include <ruby.h>
|
5
|
+
#include "actuator.h"
|
6
|
+
#include "timer.h"
|
7
|
+
#include "log.h"
|
8
|
+
#include "ruby_helpers.h"
|
9
|
+
|
10
|
+
class Actuator
|
11
|
+
{
|
12
|
+
public:
|
13
|
+
bool is_running = false;
|
14
|
+
bool is_sleeping = false;
|
15
|
+
bool is_waking = false;
|
16
|
+
VALUE thread = 0;
|
17
|
+
|
18
|
+
const double max_delta = 0.05;
|
19
|
+
|
20
|
+
double sleep_ended_at = 0;
|
21
|
+
double total_late = 0;
|
22
|
+
|
23
|
+
std::queue<VALUE> next_tick_queue;
|
24
|
+
|
25
|
+
Actuator();
|
26
|
+
~Actuator();
|
27
|
+
|
28
|
+
void Start();
|
29
|
+
void Stop();
|
30
|
+
void Wake();
|
31
|
+
timeval GetNextEventDelay(double now);
|
32
|
+
};
|
33
|
+
|
34
|
+
extern Actuator *actuator;
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
#include <ruby/intern.h>
|
3
|
+
#include "actuator.h"
|
4
|
+
#include "ruby_helpers.h"
|
5
|
+
|
6
|
+
VALUE empty_array_value;
|
7
|
+
|
8
|
+
VALUE rb_proc_call_fast(VALUE block)
|
9
|
+
{
|
10
|
+
return rb_proc_call(block, empty_array_value);
|
11
|
+
}
|
12
|
+
|
13
|
+
void init_ruby_helpers()
|
14
|
+
{
|
15
|
+
empty_array_value = rb_ary_new();
|
16
|
+
rb_gc_register_address(&empty_array_value);
|
17
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#ifndef ACTUATOR_RUBY_HELPERS_H
|
2
|
+
#define ACTUATOR_RUBY_HELPERS_H
|
3
|
+
|
4
|
+
#ifdef __cplusplus
|
5
|
+
extern "C" {
|
6
|
+
#endif
|
7
|
+
|
8
|
+
extern VALUE empty_array_value;
|
9
|
+
|
10
|
+
VALUE rb_proc_call_fast(VALUE block);
|
11
|
+
void init_ruby_helpers();
|
12
|
+
|
13
|
+
#ifdef __cplusplus
|
14
|
+
}
|
15
|
+
#endif
|
16
|
+
|
17
|
+
#endif
|
@@ -0,0 +1,450 @@
|
|
1
|
+
#include "reactor.h"
|
2
|
+
|
3
|
+
const unsigned int MaxOutstandingTimers = 1000000;
|
4
|
+
|
5
|
+
static int late_warning_us;
|
6
|
+
|
7
|
+
static int total_count = 0;
|
8
|
+
static int current_timer_count = 0;
|
9
|
+
static int current_object_count = 0;
|
10
|
+
static int current_gc_registered_count = 0;
|
11
|
+
static int fired_current_second_count = 0;
|
12
|
+
static int fired_last_second_count = 0;
|
13
|
+
static int current_second_frame_count = 0;
|
14
|
+
static int last_second_frame_count = 0;
|
15
|
+
static int current_second_empty_frames = 0;
|
16
|
+
static int last_second_empty_frames = 0;
|
17
|
+
static int current_second_earliest_fire = INT_MAX;
|
18
|
+
static int last_second_earliest_fire = INT_MAX;
|
19
|
+
static int current_second_latest_fire = 0;
|
20
|
+
static int last_second_latest_fire = 0;
|
21
|
+
static int current_second_started_at = 0;
|
22
|
+
|
23
|
+
//TODO: Consider replacing multimap with a high precision timer wheel implementation like the one I built in C#
|
24
|
+
static std::multimap<double, Timer*> all;
|
25
|
+
static std::deque<Timer*> expired_queue;
|
26
|
+
static std::deque<Timer*> interval_queue;
|
27
|
+
|
28
|
+
static VALUE TimerClass;
|
29
|
+
static VALUE proc_call_args[2];
|
30
|
+
|
31
|
+
static void Timer_free(Timer *timer)
|
32
|
+
{
|
33
|
+
current_object_count--;
|
34
|
+
timer->Destroy();
|
35
|
+
delete timer;
|
36
|
+
}
|
37
|
+
|
38
|
+
static void Timer_mark(Timer *timer)
|
39
|
+
{
|
40
|
+
if (timer->callback_block)
|
41
|
+
rb_gc_mark(timer->callback_block);
|
42
|
+
else if (timer->fiber)
|
43
|
+
rb_gc_mark(timer->fiber);
|
44
|
+
}
|
45
|
+
|
46
|
+
static VALUE Timer_alloc(VALUE klass)
|
47
|
+
{
|
48
|
+
Timer *timer = new Timer();
|
49
|
+
current_object_count++;
|
50
|
+
return timer->instance = Data_Wrap_Struct(klass, Timer_mark, Timer_free, timer);
|
51
|
+
}
|
52
|
+
|
53
|
+
static VALUE Timer_initialize(VALUE self)
|
54
|
+
{
|
55
|
+
Timer *timer = Timer::Get(self);
|
56
|
+
if (rb_block_given_p()) timer->SetCallback(rb_block_proc());
|
57
|
+
return self;
|
58
|
+
}
|
59
|
+
|
60
|
+
static VALUE Timer_destroy(VALUE self)
|
61
|
+
{
|
62
|
+
Timer *timer = Timer::Get(self);
|
63
|
+
if (timer)
|
64
|
+
timer->Destroy();
|
65
|
+
else
|
66
|
+
rb_raise(rb_eRuntimeError, "Timer instance has no allocated timer struct");
|
67
|
+
return Qnil;
|
68
|
+
}
|
69
|
+
|
70
|
+
static VALUE Timer_expires_at(VALUE self)
|
71
|
+
{
|
72
|
+
return DBL2NUM(Timer::Get(self)->at);
|
73
|
+
}
|
74
|
+
|
75
|
+
static VALUE Timer_is_destroyed(VALUE self)
|
76
|
+
{
|
77
|
+
return (Timer::Get(self))->is_destroyed ? Qtrue : Qfalse;
|
78
|
+
}
|
79
|
+
|
80
|
+
static VALUE Timer_fire_bang(VALUE self)
|
81
|
+
{
|
82
|
+
Timer *timer = Timer::Get(self);
|
83
|
+
if (timer->is_destroyed || !timer->is_scheduled) return Qnil;
|
84
|
+
timer->ExpireImmediately();
|
85
|
+
return Qtrue;
|
86
|
+
}
|
87
|
+
|
88
|
+
static VALUE Timer_in(VALUE self, VALUE delay_value)
|
89
|
+
{
|
90
|
+
rb_need_block();
|
91
|
+
Log::Debug("Timer.in");
|
92
|
+
Timer *timer = new Timer(NUM2DBL(delay_value));
|
93
|
+
timer->SetCallback(rb_block_proc());
|
94
|
+
timer->Schedule();
|
95
|
+
current_object_count++;
|
96
|
+
// We skip calling initialize on the timer to reduce overhead
|
97
|
+
return timer->instance = Data_Wrap_Struct(TimerClass, Timer_mark, Timer_free, timer);
|
98
|
+
}
|
99
|
+
|
100
|
+
static VALUE Timer_every(VALUE self, VALUE delay_value)
|
101
|
+
{
|
102
|
+
rb_need_block();
|
103
|
+
Log::Debug("Timer.every");
|
104
|
+
double delay = NUM2DBL(delay_value);
|
105
|
+
Timer *timer = new Timer(delay);
|
106
|
+
timer->interval = delay;
|
107
|
+
timer->SetCallback(rb_block_proc());
|
108
|
+
timer->Schedule();
|
109
|
+
current_object_count++;
|
110
|
+
// We skip calling initialize on the timer to reduce overhead
|
111
|
+
return timer->instance = Data_Wrap_Struct(TimerClass, Timer_mark, Timer_free, timer);
|
112
|
+
}
|
113
|
+
|
114
|
+
static VALUE Timer_late_warning_us(VALUE self)
|
115
|
+
{
|
116
|
+
return INT2NUM(late_warning_us);
|
117
|
+
}
|
118
|
+
|
119
|
+
static VALUE Timer_late_warning_us_set(VALUE self, VALUE value)
|
120
|
+
{
|
121
|
+
return INT2NUM(late_warning_us = NUM2INT(value));
|
122
|
+
}
|
123
|
+
|
124
|
+
static VALUE Timer_stats(VALUE self)
|
125
|
+
{
|
126
|
+
return rb_sprintf("Frames: %d, Empty: %d, Fires: %d, Early: %d, Late: %d, Current: %d, Objects: %d, Scheduled: %d, GC: %d, Total: %d", last_second_frame_count, last_second_empty_frames, fired_last_second_count, last_second_earliest_fire < INT_MAX ? last_second_earliest_fire : -1, last_second_latest_fire, current_timer_count, current_object_count, all.size(), current_gc_registered_count, total_count);
|
127
|
+
}
|
128
|
+
|
129
|
+
void Timer::Setup()
|
130
|
+
{
|
131
|
+
proc_call_args[0] = Qnil;
|
132
|
+
proc_call_args[1] = empty_array_value;
|
133
|
+
|
134
|
+
TimerClass = rb_define_class("Timer", rb_cObject);
|
135
|
+
rb_define_singleton_method(TimerClass, "in", RUBY_METHOD_FUNC(Timer_in), 1);
|
136
|
+
rb_define_singleton_method(TimerClass, "every", RUBY_METHOD_FUNC(Timer_every), 1);
|
137
|
+
rb_define_singleton_method(TimerClass, "stats", RUBY_METHOD_FUNC(Timer_stats), 0);
|
138
|
+
rb_define_singleton_method(TimerClass, "late_warning_us", RUBY_METHOD_FUNC(Timer_late_warning_us), 0);
|
139
|
+
rb_define_singleton_method(TimerClass, "late_warning_us=", RUBY_METHOD_FUNC(Timer_late_warning_us_set), 1);
|
140
|
+
rb_define_alloc_func(TimerClass, Timer_alloc);
|
141
|
+
rb_define_method(TimerClass, "initialize", RUBY_METHOD_FUNC(Timer_initialize), 0);
|
142
|
+
rb_define_method(TimerClass, "expires_at", RUBY_METHOD_FUNC(Timer_expires_at), 0);
|
143
|
+
rb_define_method(TimerClass, "destroy", RUBY_METHOD_FUNC(Timer_destroy), 0);
|
144
|
+
rb_define_method(TimerClass, "destroyed?", RUBY_METHOD_FUNC(Timer_is_destroyed), 0);
|
145
|
+
rb_define_method(TimerClass, "fire!", RUBY_METHOD_FUNC(Timer_fire_bang), 0);
|
146
|
+
|
147
|
+
late_warning_us = 0;
|
148
|
+
current_second_started_at = clock_time();
|
149
|
+
}
|
150
|
+
|
151
|
+
Timer* Timer::Get(VALUE instance)
|
152
|
+
{
|
153
|
+
Timer *timer;
|
154
|
+
Data_Get_Struct(instance, Timer, timer);
|
155
|
+
return timer;
|
156
|
+
}
|
157
|
+
|
158
|
+
void Timer::Update(double now)
|
159
|
+
{
|
160
|
+
std::multimap<double, Timer*>::iterator it = all.begin();
|
161
|
+
for (; it != all.end(); ++it) {
|
162
|
+
if (it->first > now) break;
|
163
|
+
//if (now - it->first > 0.000005) {
|
164
|
+
//long late = (now - it->first) * 1000000;
|
165
|
+
//puts("%.10f - Timer expired: %.10f, Late: %d us", now, it->first, late);
|
166
|
+
//}
|
167
|
+
Timer *timer = it->second;
|
168
|
+
if (!timer->is_scheduled) Log::Error("Expired timer %d has is_scheduled set to false!", timer->id);
|
169
|
+
expired_queue.push_back(timer);
|
170
|
+
}
|
171
|
+
|
172
|
+
int active_count = all.size();
|
173
|
+
int expired_count = expired_queue.size();
|
174
|
+
|
175
|
+
current_second_frame_count++;
|
176
|
+
if (expired_count < 1) current_second_empty_frames++;
|
177
|
+
fired_current_second_count += expired_count;
|
178
|
+
if (now >= current_second_started_at + 5.0)
|
179
|
+
{
|
180
|
+
current_second_started_at = now;
|
181
|
+
last_second_frame_count = current_second_frame_count;
|
182
|
+
current_second_frame_count = 0;
|
183
|
+
last_second_empty_frames = current_second_empty_frames;
|
184
|
+
current_second_empty_frames = 0;
|
185
|
+
fired_last_second_count = fired_current_second_count;
|
186
|
+
fired_current_second_count = 0;
|
187
|
+
last_second_earliest_fire = current_second_earliest_fire;
|
188
|
+
current_second_earliest_fire = INT_MAX;
|
189
|
+
last_second_latest_fire = current_second_latest_fire;
|
190
|
+
current_second_latest_fire = 0;
|
191
|
+
}
|
192
|
+
|
193
|
+
if (expired_count < 1) return;
|
194
|
+
|
195
|
+
Log::Debug("Update - %d / %d timers expiring", expired_count, active_count);
|
196
|
+
|
197
|
+
if (expired_count < active_count)
|
198
|
+
all.erase(all.begin(), it);
|
199
|
+
else
|
200
|
+
all.clear();
|
201
|
+
|
202
|
+
std::deque<Timer*>::iterator deq = expired_queue.begin();
|
203
|
+
while (deq != expired_queue.end()) {
|
204
|
+
Timer *timer = (Timer*)*deq++;
|
205
|
+
timer->is_scheduled = false;
|
206
|
+
Log::Debug("Update - Expired");
|
207
|
+
if (timer->interval) {
|
208
|
+
//Log::Debug("Update - Firing: %s", RSTRING_PTR(rb_inspect(timer->callback_block)));
|
209
|
+
timer->Fire();
|
210
|
+
if (timer->is_destroyed)
|
211
|
+
{
|
212
|
+
Log::Debug("Update - Interval destroyed from it's own callback");
|
213
|
+
timer->StoppedBeingScheduled();
|
214
|
+
}
|
215
|
+
else
|
216
|
+
{
|
217
|
+
Log::Debug("Update - Adding to interval queue");
|
218
|
+
interval_queue.push_back(timer);
|
219
|
+
}
|
220
|
+
} else {
|
221
|
+
timer->is_destroyed = true;
|
222
|
+
timer->StoppedBeingScheduled();
|
223
|
+
timer->Fire();
|
224
|
+
}
|
225
|
+
if (!actuator->is_running) break;
|
226
|
+
}
|
227
|
+
expired_queue.clear();
|
228
|
+
|
229
|
+
if (!actuator->is_running) return;
|
230
|
+
|
231
|
+
now = clock_time();
|
232
|
+
deq = interval_queue.begin();
|
233
|
+
while (deq != interval_queue.end()) {
|
234
|
+
Timer *timer = (Timer*)*deq++;
|
235
|
+
if (timer->is_destroyed) {
|
236
|
+
Log::Debug("Update - Interval destroyed from another timers callback");
|
237
|
+
timer->StoppedBeingScheduled();
|
238
|
+
continue;
|
239
|
+
}
|
240
|
+
Log::Debug("Update - Rescheduling interval");
|
241
|
+
timer->at = now + timer->interval;
|
242
|
+
timer->InsertIntoSchedule();
|
243
|
+
}
|
244
|
+
interval_queue.clear();
|
245
|
+
|
246
|
+
Log::Debug("Update - Done");
|
247
|
+
}
|
248
|
+
|
249
|
+
double Timer::GetNextEventTime()
|
250
|
+
{
|
251
|
+
return all.empty() ? 0 : all.begin()->first;
|
252
|
+
}
|
253
|
+
|
254
|
+
Timer::Timer()
|
255
|
+
{
|
256
|
+
id = ++total_count;
|
257
|
+
delay = 0;
|
258
|
+
at = 0;
|
259
|
+
interval = 0;
|
260
|
+
callback_block = 0;
|
261
|
+
fiber = 0;
|
262
|
+
is_scheduled = false;
|
263
|
+
is_destroyed = false;
|
264
|
+
instance = 0;
|
265
|
+
inspected = 0;
|
266
|
+
current_timer_count++;
|
267
|
+
}
|
268
|
+
|
269
|
+
Timer::Timer(double initial_delay) : Timer()
|
270
|
+
{
|
271
|
+
delay = initial_delay;
|
272
|
+
at = clock_time() + initial_delay;
|
273
|
+
}
|
274
|
+
|
275
|
+
Timer::~Timer()
|
276
|
+
{
|
277
|
+
if (callback_block) {
|
278
|
+
//TODO: Profile overhead of unregister when loaded (walks the entire global object list)
|
279
|
+
rb_gc_unregister_address(&callback_block);
|
280
|
+
callback_block = 0;
|
281
|
+
if (inspected) {
|
282
|
+
free(inspected);
|
283
|
+
inspected = 0;
|
284
|
+
}
|
285
|
+
}
|
286
|
+
if (is_scheduled) Log::Error("Timer freed while still scheduled");
|
287
|
+
if (!is_destroyed) Log::Error("Timer freed before being destroyed");
|
288
|
+
current_timer_count--;
|
289
|
+
}
|
290
|
+
|
291
|
+
void Timer::SetDelay(double initial_delay)
|
292
|
+
{
|
293
|
+
Log::Debug("SetDelay");
|
294
|
+
delay = initial_delay;
|
295
|
+
at = clock_time() + initial_delay;
|
296
|
+
}
|
297
|
+
|
298
|
+
void Timer::Destroy()
|
299
|
+
{
|
300
|
+
if (is_destroyed) return;
|
301
|
+
Log::Debug("Destroy");
|
302
|
+
is_destroyed = true;
|
303
|
+
Remove();
|
304
|
+
}
|
305
|
+
|
306
|
+
void Timer::Schedule()
|
307
|
+
{
|
308
|
+
if (!at) {
|
309
|
+
Log::Warn("Timer::Schedule() called before delay was set");
|
310
|
+
return;
|
311
|
+
}
|
312
|
+
if (all.size() > MaxOutstandingTimers) {
|
313
|
+
Log::Warn("Error: There are %d / %d active timers!", all.size(), MaxOutstandingTimers);
|
314
|
+
return;
|
315
|
+
}
|
316
|
+
InsertIntoSchedule();
|
317
|
+
StartedBeingScheduled();
|
318
|
+
}
|
319
|
+
|
320
|
+
void Timer::Remove()
|
321
|
+
{
|
322
|
+
Log::Debug("Remove");
|
323
|
+
if (RemoveFromSchedule())
|
324
|
+
StoppedBeingScheduled();
|
325
|
+
}
|
326
|
+
|
327
|
+
void Timer::InsertIntoSchedule()
|
328
|
+
{
|
329
|
+
if (is_scheduled) return;
|
330
|
+
Log::Debug("InsertIntoSchedule");
|
331
|
+
is_scheduled = true;
|
332
|
+
iterator = all.insert(std::make_pair(at, this));
|
333
|
+
}
|
334
|
+
|
335
|
+
bool Timer::RemoveFromSchedule()
|
336
|
+
{
|
337
|
+
if (!is_scheduled) return false;
|
338
|
+
Log::Debug("RemoveFromSchedule");
|
339
|
+
is_scheduled = false;
|
340
|
+
all.erase(iterator);
|
341
|
+
return true;
|
342
|
+
}
|
343
|
+
|
344
|
+
void Timer::StartedBeingScheduled()
|
345
|
+
{
|
346
|
+
Log::Debug("StartedBeingScheduled");
|
347
|
+
rb_gc_register_address(&instance);
|
348
|
+
current_gc_registered_count++;
|
349
|
+
}
|
350
|
+
|
351
|
+
void Timer::StoppedBeingScheduled()
|
352
|
+
{
|
353
|
+
Log::Debug("StoppedBeingScheduled");
|
354
|
+
rb_gc_unregister_address(&instance);
|
355
|
+
current_gc_registered_count--;
|
356
|
+
Log::Debug("GC pointer count: %d", current_gc_registered_count);
|
357
|
+
}
|
358
|
+
|
359
|
+
void Timer::SetCallback(VALUE block)
|
360
|
+
{
|
361
|
+
Log::Debug("SetCallback");
|
362
|
+
if (callback_block) {
|
363
|
+
rb_gc_unregister_address(&callback_block);
|
364
|
+
free(inspected);
|
365
|
+
inspected = 0;
|
366
|
+
}
|
367
|
+
callback_block = block;
|
368
|
+
if (block) {
|
369
|
+
rb_gc_register_address(&callback_block);
|
370
|
+
//VALUE obj = rb_inspect(block);
|
371
|
+
//inspected = (char*)malloc(RSTRING_LEN(obj)+1);
|
372
|
+
//strcpy(inspected, RSTRING_PTR(obj));
|
373
|
+
}
|
374
|
+
}
|
375
|
+
|
376
|
+
void Timer::SetFiber(VALUE current_fiber)
|
377
|
+
{
|
378
|
+
fiber = current_fiber;
|
379
|
+
}
|
380
|
+
|
381
|
+
void Timer::ExpireImmediately()
|
382
|
+
{
|
383
|
+
if (is_destroyed || !is_scheduled) return;
|
384
|
+
Log::Debug("ExpireImmediately");
|
385
|
+
if (interval) {
|
386
|
+
RemoveFromSchedule();
|
387
|
+
Fire();
|
388
|
+
if (is_destroyed) {
|
389
|
+
StoppedBeingScheduled();
|
390
|
+
} else {
|
391
|
+
at = clock_time() + interval;
|
392
|
+
InsertIntoSchedule();
|
393
|
+
}
|
394
|
+
} else {
|
395
|
+
is_destroyed = true;
|
396
|
+
Remove();
|
397
|
+
Fire();
|
398
|
+
}
|
399
|
+
}
|
400
|
+
|
401
|
+
static VALUE fire_rescue(VALUE _, VALUE errinfo)
|
402
|
+
{
|
403
|
+
Log::Debug("Uncaught exception, stopping reactor");
|
404
|
+
actuator->Stop();
|
405
|
+
Log::Debug("Raising uncaught exception");
|
406
|
+
rb_exc_raise(errinfo);
|
407
|
+
Log::Debug("Uncaught exception has been raised");
|
408
|
+
return Qnil;
|
409
|
+
}
|
410
|
+
|
411
|
+
void Timer::Fire()
|
412
|
+
{
|
413
|
+
double before_call = clock_time();
|
414
|
+
|
415
|
+
double before_resume;
|
416
|
+
if (callback_block)
|
417
|
+
{
|
418
|
+
double late_us = (double)((before_call - at) * 1000000);
|
419
|
+
if ((int)late_us < current_second_earliest_fire) current_second_earliest_fire = (int)late_us;
|
420
|
+
if ((int)late_us > current_second_latest_fire) current_second_latest_fire = (int)late_us;
|
421
|
+
if (late_warning_us && late_us > late_warning_us) {
|
422
|
+
Log::Warn("Firing %.2f us late - %d active timers, %d fired last second", late_us, all.size(), fired_last_second_count);
|
423
|
+
}
|
424
|
+
int rescue_state;
|
425
|
+
proc_call_args[0] = callback_block;
|
426
|
+
rb_rescue(RUBY_METHOD_FUNC(rb_proc_call_fast), callback_block, RUBY_METHOD_FUNC(fire_rescue), Qnil);
|
427
|
+
}
|
428
|
+
else if (fiber)
|
429
|
+
{
|
430
|
+
Log::Warn("[Fire] Resuming fiber %.2f us late", (double)((before_call - at) * 1000000));
|
431
|
+
if (!rb_fiber_alive_p(fiber))
|
432
|
+
{
|
433
|
+
Log::Error("[Fire] Unable to resume fiber (not alive)");
|
434
|
+
return;
|
435
|
+
}
|
436
|
+
before_resume = clock_time();
|
437
|
+
VALUE ret = DBL2NUM(before_resume);
|
438
|
+
rb_fiber_resume(fiber, 1, &ret);
|
439
|
+
delete this; //TODO: Shouldn't this happen before calling rb_fiber_resume
|
440
|
+
}
|
441
|
+
// Assume at least 0.3us overhead for ruby to call into C
|
442
|
+
//double after_call = clock_time();
|
443
|
+
//puts("[FireTimer] call took %.2f us, resume: %.2f us, resume_total: %.2f us, late: %.2f us", (double)((after_call - before_call) * 1000000), (double)((sleep_ended_at - before_resume) * 1000000) - 0.3, (double)((after_call - before_resume) * 1000000) - 0.3, (double)((sleep_ended_at - at) * 1000000) - 0.3);
|
444
|
+
}
|
445
|
+
|
446
|
+
void Timer::Clear()
|
447
|
+
{
|
448
|
+
//TODO: Actually clean up and free all timers and next_tick callbacks
|
449
|
+
all.clear();
|
450
|
+
}
|