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
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
|
+
}
|