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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 459b67293c89bd2854e96adbaf484ec950774fc2
|
4
|
+
data.tar.gz: cdea3e412329800ae7d1553fae14dd509a0c03cc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0b25766398cac34c1ec62b3ac959381850517afcae1cbbd4b59ef7cd8a67b1c0e3de2bcae698007023a47cec6711a1e523c8b1824a0fc000147c42efb6a981b8
|
7
|
+
data.tar.gz: c9972bc77b4e58b94ca2c74367aa1cf116629cf5b916c7a41de5623320ffb2c76dabcff0f856f01296410e87dd609839c143a1e4b6b144e9db7f1ff418a6ed75
|
data/.autotest
ADDED
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
.autotest
|
2
|
+
README.md
|
3
|
+
History.txt
|
4
|
+
Manifest.txt
|
5
|
+
Rakefile
|
6
|
+
lib/actuator.rb
|
7
|
+
lib/actuator/fiber.rb
|
8
|
+
lib/actuator/fiber_pool.rb
|
9
|
+
lib/actuator/job.rb
|
10
|
+
lib/actuator/mutex.rb
|
11
|
+
lib/actuator/mutex/replace.rb
|
12
|
+
ext/actuator/extconf.rb
|
13
|
+
ext/actuator/actuator.h
|
14
|
+
ext/actuator/actuator.c
|
15
|
+
ext/actuator/clock.h
|
16
|
+
ext/actuator/clock.c
|
17
|
+
ext/actuator/debug.h
|
18
|
+
ext/actuator/debug.c
|
19
|
+
ext/actuator/ruby_helpers.h
|
20
|
+
ext/actuator/ruby_helpers.c
|
21
|
+
ext/actuator/reactor.h
|
22
|
+
ext/actuator/reactor.cpp
|
23
|
+
ext/actuator/timer.h
|
24
|
+
ext/actuator/timer.cpp
|
25
|
+
ext/actuator/log.h
|
26
|
+
ext/actuator/log.cpp
|
27
|
+
test/setup_test.rb
|
28
|
+
test/test_actuator.rb
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
## Welcome to Actuator
|
2
|
+
|
3
|
+
Code: https://github.com/bawNg/actuator \
|
4
|
+
Bugs: https://github.com/bawNg/actuator/issues
|
5
|
+
|
6
|
+
Actuator provides high precision scheduling of light weight timers for async and fibered real-time Ruby applications.
|
7
|
+
Even on Windows where kernel precision is lower, average timer accuracy is ~2 us on modern high end hardware (subject to system load).
|
8
|
+
|
9
|
+
This C++ Ruby extension allows time-sensitive applications to replace native threads with jobs that run in pooled fibers.
|
10
|
+
|
11
|
+
This is an alpha release, not recommended for production use. While fairly well tested on Windows and Linux, minimal safety
|
12
|
+
and error handling has been implemented in order to minimize overhead. Using the API wrong may result in a segfault.
|
13
|
+
|
14
|
+
#### Features
|
15
|
+
|
16
|
+
* Provides a high precision float representing the current reactor time
|
17
|
+
* High precision single threaded timer callback scheduling
|
18
|
+
* Light weight jobs can be used to replace threads with pooled fibers
|
19
|
+
* Job-based implementation of sleep, join, kill, Mutex and ConditionVariable
|
20
|
+
* Job-aware sample-based CPU profiling API and execution time warnings
|
21
|
+
* Warnings for timers that fire later than the configured threshold
|
22
|
+
* Low overhead timestamped logging API which is thread-safe
|
23
|
+
|
24
|
+
#### Supported platforms
|
25
|
+
|
26
|
+
* MRI Ruby 2.x (1.9 is probably compatible but has not been tested).
|
27
|
+
* High precision timer support is implemented for Windows, Linux and OSX.
|
28
|
+
|
29
|
+
#### Getting started
|
30
|
+
|
31
|
+
Install the gem with `gem install actuator` or by adding it to your bundle.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require 'actuator'
|
35
|
+
|
36
|
+
Actuator.run do
|
37
|
+
# Schedule a once off timer which fires after 500 us delay
|
38
|
+
Timer.in 0.0005 do
|
39
|
+
Log.puts "Reactor time: #{Actuator.now}"
|
40
|
+
end
|
41
|
+
# Schedule a repeating timer which fires every 50ms
|
42
|
+
Timer.every 0.05 do
|
43
|
+
Log.puts "50ms have passed"
|
44
|
+
end
|
45
|
+
# Schedule a timer which we will cancel before it expires
|
46
|
+
timer = Timer.in 0.005 do
|
47
|
+
Log.warn "This should never be printed"
|
48
|
+
end
|
49
|
+
# Create a new job which executes inside a pooled fiber
|
50
|
+
job1 = Actuator.defer do
|
51
|
+
begin
|
52
|
+
Log.puts "Job has started"
|
53
|
+
# Yield from the job fiber for 200ms
|
54
|
+
Job.sleep 0.2
|
55
|
+
Log.warn "Job 1 finished sleeping even though it should have been killed"
|
56
|
+
ensure
|
57
|
+
# The stack is unwound when a job is killed so ensure blocks are executed
|
58
|
+
Log.puts "Job 1 is ending"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
# Create another new job
|
62
|
+
job2 = Actuator.defer do
|
63
|
+
# Yield from job fiber until job1 ends
|
64
|
+
job1.join
|
65
|
+
Log.puts "Job 2 finished waiting for Job 1"
|
66
|
+
end
|
67
|
+
Job.sleep 0.001
|
68
|
+
# Cancel the 5ms timer that we scheduled above
|
69
|
+
timer.destroy
|
70
|
+
Job.sleep 0.1
|
71
|
+
# Kill job1 before it finishes sleeping
|
72
|
+
job1.kill
|
73
|
+
# Wait for job2 to end naturally
|
74
|
+
job2.join
|
75
|
+
Log.puts "Job 2 has ended"
|
76
|
+
# Shutdown the reactor
|
77
|
+
Actuator.stop
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
#### Known issues
|
82
|
+
- Memory for active timers will not be freed when calling Actuator.stop
|
83
|
+
- The profiling API will include time spent yielded from the job.
|
84
|
+
The job-aware implementation has been commented out to reduce overhead until the profiling has been rewritten in C++.
|
85
|
+
- Minimal safety checks and error handling has been implemented in order to minimize overhead. Using the API wrong may result in a segfault.
|
86
|
+
Feel free to open a bug report for any such cases that you may come across.
|
87
|
+
|
88
|
+
|
89
|
+
#### Contributing
|
90
|
+
|
91
|
+
Actuator is an open source project and any contributions which improve the project are encouraged.
|
92
|
+
Feel free to make a pull request for any contributions that you would like to see merged into the project.
|
93
|
+
|
94
|
+
After cloning the source from this repo, run `rake test` to build the C++ extension and run the reactor and precision tests.
|
95
|
+
|
96
|
+
Some ways that you can contribute include:
|
97
|
+
- Create new [bug reports](https://github.com/bawNg/actuator/issues/new)
|
98
|
+
- Reviewing and providing detailed feedback on existing [issues](https://github.com/bawNg/actuator/issues/new)
|
99
|
+
- Writing and improving documentation
|
100
|
+
- Writing additional tests
|
101
|
+
- Implementing new features
|
102
|
+
|
103
|
+
#### License
|
104
|
+
|
105
|
+
Actuator is released under the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "hoe"
|
5
|
+
require 'rake/extensiontask'
|
6
|
+
|
7
|
+
Hoe.plugin :clean
|
8
|
+
|
9
|
+
Hoe.spec "actuator" do
|
10
|
+
developer 'bawNg', 'bawng@intoxicated.co.za'
|
11
|
+
license 'MIT'
|
12
|
+
self.urls = 'https://github.com/bawNg/actuator'
|
13
|
+
|
14
|
+
self.extra_dev_deps << ['rake-compiler', '>= 0']
|
15
|
+
self.extra_dev_deps << ['minitest', '>= 0']
|
16
|
+
self.spec_extras = { extensions: ['ext/actuator/extconf.rb'] }
|
17
|
+
|
18
|
+
Rake::ExtensionTask.new('actuator', spec) do |ext|
|
19
|
+
ext.lib_dir = File.join('lib', 'actuator')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Rake::Task[:test].prerequisites << :clean << :compile
|
@@ -0,0 +1,81 @@
|
|
1
|
+
#include <stdint.h>
|
2
|
+
#include "clock.h"
|
3
|
+
#include "debug.h"
|
4
|
+
|
5
|
+
#if !defined(__APPLE__) && !defined(_WIN32)
|
6
|
+
#define HAVE_POSIX_TIMER
|
7
|
+
#include <time.h>
|
8
|
+
#ifdef CLOCK_MONOTONIC
|
9
|
+
#define CLOCKID CLOCK_MONOTONIC
|
10
|
+
#else
|
11
|
+
#define CLOCKID CLOCK_REALTIME
|
12
|
+
#endif
|
13
|
+
#endif
|
14
|
+
|
15
|
+
#ifdef __APPLE__
|
16
|
+
#define HAVE_MACH_TIMER
|
17
|
+
#include <mach/mach_time.h>
|
18
|
+
#endif
|
19
|
+
|
20
|
+
#if defined(_WIN32) || defined(_MSC_VER)
|
21
|
+
#define HAVE_WIN32_TIMER
|
22
|
+
#include <windows.h>
|
23
|
+
|
24
|
+
#ifdef _MSC_VER
|
25
|
+
#include <Strsafe.h>
|
26
|
+
#endif
|
27
|
+
#endif
|
28
|
+
|
29
|
+
static uint64_t last_count;
|
30
|
+
static uint64_t frequency;
|
31
|
+
|
32
|
+
void clock_init()
|
33
|
+
{
|
34
|
+
#ifdef HAVE_POSIX_TIMER
|
35
|
+
struct timespec freq, time;
|
36
|
+
clock_getres(CLOCKID, &freq);
|
37
|
+
clock_gettime(CLOCKID, &time);
|
38
|
+
frequency = 1000000000LL * freq.tv_nsec;
|
39
|
+
last_count = time.tv_sec * 1000000000LL + time.tv_nsec;
|
40
|
+
#endif
|
41
|
+
#ifdef HAVE_MACH_TIMER
|
42
|
+
mach_timebase_info_data_t frequency_nsec;
|
43
|
+
mach_timebase_info(&frequency_nsec);
|
44
|
+
frequency = 1000000000LL * frequency_nsec.numer / frequency_nsec.denom;
|
45
|
+
last_count = mach_absolute_time();
|
46
|
+
#endif
|
47
|
+
#ifdef HAVE_WIN32_TIMER
|
48
|
+
{
|
49
|
+
LARGE_INTEGER tmp;
|
50
|
+
QueryPerformanceFrequency(&tmp);
|
51
|
+
frequency = tmp.QuadPart;
|
52
|
+
QueryPerformanceCounter(&tmp);
|
53
|
+
last_count = tmp.QuadPart;
|
54
|
+
}
|
55
|
+
#endif
|
56
|
+
//puts("Timer resolution: %g ns", 1e9 / (double)frequency);
|
57
|
+
}
|
58
|
+
|
59
|
+
double clock_time()
|
60
|
+
{
|
61
|
+
uint64_t now;
|
62
|
+
double delta;
|
63
|
+
#ifdef HAVE_POSIX_TIMER
|
64
|
+
struct timespec time;
|
65
|
+
clock_gettime(CLOCKID, &time);
|
66
|
+
now = time.tv_sec * 1000000000LL + time.tv_nsec;
|
67
|
+
delta = (now - last_count) / (double)frequency;
|
68
|
+
#endif
|
69
|
+
#ifdef HAVE_MACH_TIMER
|
70
|
+
now = mach_absolute_time();
|
71
|
+
#endif
|
72
|
+
#ifdef HAVE_WIN32_TIMER
|
73
|
+
{
|
74
|
+
LARGE_INTEGER tmp;
|
75
|
+
QueryPerformanceCounter(&tmp);
|
76
|
+
now = tmp.QuadPart;
|
77
|
+
delta = (now - last_count) / (double)frequency;
|
78
|
+
}
|
79
|
+
#endif
|
80
|
+
return delta;
|
81
|
+
}
|
@@ -0,0 +1,56 @@
|
|
1
|
+
#ifndef ACTUATOR_DEBUG_H
|
2
|
+
#define ACTUATOR_DEBUG_H
|
3
|
+
|
4
|
+
#include <stdio.h>
|
5
|
+
|
6
|
+
#ifdef __cplusplus
|
7
|
+
extern "C" {
|
8
|
+
#endif
|
9
|
+
|
10
|
+
#define DEBUG 0
|
11
|
+
|
12
|
+
#ifdef DEBUG
|
13
|
+
#define puts(...) do { \
|
14
|
+
fprintf(stdout, __VA_ARGS__); \
|
15
|
+
fprintf(stdout, "\n"); \
|
16
|
+
fflush(stdout); \
|
17
|
+
} while (0)
|
18
|
+
#else
|
19
|
+
#define puts(...)
|
20
|
+
#endif
|
21
|
+
|
22
|
+
#ifdef DEBUG
|
23
|
+
#define debug(...) do { \
|
24
|
+
if (!debug_file) debug_file = fopen("debug.txt", "w"); \
|
25
|
+
fprintf(debug_file, __VA_ARGS__); \
|
26
|
+
fflush(debug_file); \
|
27
|
+
} while (0)
|
28
|
+
#define DEBUG_LOG(...) do { \
|
29
|
+
if (!debug_file) debug_file = fopen("debug.txt", "w"); \
|
30
|
+
fprintf(debug_file, __VA_ARGS__); \
|
31
|
+
fflush(debug_file); \
|
32
|
+
} while (0)
|
33
|
+
#else
|
34
|
+
#define debug(...)
|
35
|
+
#define DEBUG_LOG(...)
|
36
|
+
#endif
|
37
|
+
|
38
|
+
#define INFO(...) do { \
|
39
|
+
fprintf(log_file, "%010.3f INFO ", clock_time() * 1000); \
|
40
|
+
fprintf(log_file, __VA_ARGS__); \
|
41
|
+
fprintf(log_file, "\n"); \
|
42
|
+
fflush(log_file); \
|
43
|
+
} while (0)
|
44
|
+
|
45
|
+
#define WARN(...) do { \
|
46
|
+
fprintf(log_file, "%010.3f WARN ", clock_time() * 1000); \
|
47
|
+
fprintf(log_file, __VA_ARGS__); \
|
48
|
+
fprintf(log_file, "\n"); \
|
49
|
+
fflush(log_file); \
|
50
|
+
} while (0)
|
51
|
+
|
52
|
+
#ifdef __cplusplus
|
53
|
+
}
|
54
|
+
#endif
|
55
|
+
|
56
|
+
#endif
|
@@ -0,0 +1,134 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
#include "reactor.h"
|
3
|
+
|
4
|
+
enum class LogLevel
|
5
|
+
{
|
6
|
+
None,
|
7
|
+
Error,
|
8
|
+
Warn,
|
9
|
+
Info,
|
10
|
+
Debug
|
11
|
+
};
|
12
|
+
|
13
|
+
FILE *Log::debug_file = 0;
|
14
|
+
FILE *Log::log_file = 0;
|
15
|
+
|
16
|
+
static VALUE LogClass;
|
17
|
+
static LogLevel Level = LogLevel::Info;
|
18
|
+
|
19
|
+
static void Print(const char *tag, const char *format, va_list args)
|
20
|
+
{
|
21
|
+
if (!Log::log_file) return;
|
22
|
+
fprintf(Log::log_file, "%010.3f %s ", clock_time() * 1000, tag);
|
23
|
+
vfprintf(Log::log_file, format, args);
|
24
|
+
fprintf(Log::log_file, "\n");
|
25
|
+
fflush(Log::log_file);
|
26
|
+
}
|
27
|
+
|
28
|
+
static VALUE Log_Debug(VALUE self, VALUE message)
|
29
|
+
{
|
30
|
+
Log::Debug("%s", StringValueCStr(message));
|
31
|
+
}
|
32
|
+
|
33
|
+
static VALUE Log_Info(VALUE self, VALUE message)
|
34
|
+
{
|
35
|
+
Log::Info("%s", StringValueCStr(message));
|
36
|
+
}
|
37
|
+
|
38
|
+
static VALUE Log_Warn(VALUE self, VALUE message)
|
39
|
+
{
|
40
|
+
Log::Warn("%s", StringValueCStr(message));
|
41
|
+
}
|
42
|
+
|
43
|
+
static VALUE Log_Error(VALUE self, VALUE message)
|
44
|
+
{
|
45
|
+
Log::Error("%s", StringValueCStr(message));
|
46
|
+
}
|
47
|
+
|
48
|
+
static VALUE Log_SetFilePath(VALUE self, VALUE path)
|
49
|
+
{
|
50
|
+
if (NIL_P(path)) {
|
51
|
+
Log::log_file = 0;
|
52
|
+
} else if (SYMBOL_P(path) && SYM2ID(path) == rb_intern("stdout")) {
|
53
|
+
Log::log_file = stdout;
|
54
|
+
} else if (RB_TYPE_P(path, T_STRING) && CLASS_OF(path) == rb_cString) {
|
55
|
+
Log::log_file = fopen(rb_id2name(SYM2ID(path)), "w");
|
56
|
+
} else {
|
57
|
+
rb_raise(rb_eRuntimeError, "path must be a string, :stdout or nil");
|
58
|
+
}
|
59
|
+
return Qnil;
|
60
|
+
}
|
61
|
+
|
62
|
+
static VALUE Log_SetLevel(VALUE self, VALUE level)
|
63
|
+
{
|
64
|
+
if (SYMBOL_P(level)) {
|
65
|
+
if (SYM2ID(level) == rb_intern("debug")) {
|
66
|
+
Level = LogLevel::Debug;
|
67
|
+
return Qnil;
|
68
|
+
}
|
69
|
+
if (SYM2ID(level) == rb_intern("info")) {
|
70
|
+
Level = LogLevel::Info;
|
71
|
+
return Qnil;
|
72
|
+
}
|
73
|
+
if (SYM2ID(level) == rb_intern("warn")) {
|
74
|
+
Level = LogLevel::Warn;
|
75
|
+
return Qnil;
|
76
|
+
}
|
77
|
+
if (SYM2ID(level) == rb_intern("error")) {
|
78
|
+
Level = LogLevel::Error;
|
79
|
+
return Qnil;
|
80
|
+
}
|
81
|
+
}
|
82
|
+
rb_raise(rb_eRuntimeError, "level must be :debug, :info, :warn or :error");
|
83
|
+
return Qnil;
|
84
|
+
}
|
85
|
+
|
86
|
+
void Log::Setup()
|
87
|
+
{
|
88
|
+
log_file = stdout;
|
89
|
+
|
90
|
+
LogClass = rb_define_module("Log");
|
91
|
+
|
92
|
+
rb_define_singleton_method(LogClass, "debug", RUBY_METHOD_FUNC(Log_Debug), 1);
|
93
|
+
rb_define_singleton_method(LogClass, "puts", RUBY_METHOD_FUNC(Log_Info), 1);
|
94
|
+
rb_define_singleton_method(LogClass, "warn", RUBY_METHOD_FUNC(Log_Warn), 1);
|
95
|
+
rb_define_singleton_method(LogClass, "error", RUBY_METHOD_FUNC(Log_Error), 1);
|
96
|
+
rb_define_singleton_method(LogClass, "file_path=", RUBY_METHOD_FUNC(Log_SetFilePath), 1);
|
97
|
+
rb_define_singleton_method(LogClass, "level=", RUBY_METHOD_FUNC(Log_SetLevel), 1);
|
98
|
+
}
|
99
|
+
|
100
|
+
void Log::Debug(const char *format, ...)
|
101
|
+
{
|
102
|
+
if (Level < LogLevel::Debug) return;
|
103
|
+
va_list args;
|
104
|
+
va_start(args, format);
|
105
|
+
Print("DEBUG", format, args);
|
106
|
+
va_end(args);
|
107
|
+
}
|
108
|
+
|
109
|
+
void Log::Info(const char *format, ...)
|
110
|
+
{
|
111
|
+
if (Level < LogLevel::Info) return;
|
112
|
+
va_list args;
|
113
|
+
va_start(args, format);
|
114
|
+
Print("INFO", format, args);
|
115
|
+
va_end(args);
|
116
|
+
}
|
117
|
+
|
118
|
+
void Log::Warn(const char *format, ...)
|
119
|
+
{
|
120
|
+
if (Level < LogLevel::Warn) return;
|
121
|
+
va_list args;
|
122
|
+
va_start(args, format);
|
123
|
+
Print("WARN", format, args);
|
124
|
+
va_end(args);
|
125
|
+
}
|
126
|
+
|
127
|
+
void Log::Error(const char *format, ...)
|
128
|
+
{
|
129
|
+
if (Level < LogLevel::Error) return;
|
130
|
+
va_list args;
|
131
|
+
va_start(args, format);
|
132
|
+
Print("ERROR", format, args);
|
133
|
+
va_end(args);
|
134
|
+
}
|