actuator 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|