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/test/setup_test.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'minitest'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
|
4
|
+
#require_relative '../lib/actuator'
|
5
|
+
gem 'actuator'
|
6
|
+
require 'actuator'
|
7
|
+
|
8
|
+
module Minitest
|
9
|
+
class << self
|
10
|
+
alias_method :run_without_actuator, :run
|
11
|
+
|
12
|
+
def run(args=[])
|
13
|
+
result = nil
|
14
|
+
Actuator.run do
|
15
|
+
Actuator.defer do
|
16
|
+
begin
|
17
|
+
result = run_without_actuator(args)
|
18
|
+
rescue => ex
|
19
|
+
Kernel.puts "#{ex.class}: #{ex.message}\n#{ex.backtrace.join "\n"}"
|
20
|
+
raise ex
|
21
|
+
end
|
22
|
+
Actuator.next_tick { Actuator.stop }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
result
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Actuator
|
31
|
+
class Test < Minitest::Test
|
32
|
+
@@_assert_queue = Queue.new
|
33
|
+
@@assert_thread = nil
|
34
|
+
|
35
|
+
# Defer to a thread so that we can sleep reliably without the reactor
|
36
|
+
def assert_async
|
37
|
+
fiber = Fiber.current
|
38
|
+
assert_threaded do
|
39
|
+
begin
|
40
|
+
yield
|
41
|
+
rescue => ex
|
42
|
+
Actuator.next_tick { fiber.resume(ex) }
|
43
|
+
else
|
44
|
+
Actuator.next_tick { fiber.resume }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
result = Fiber.yield
|
48
|
+
raise result if result.is_a? Exception
|
49
|
+
end
|
50
|
+
|
51
|
+
# Reuse a single thread to reduce the variance of start delay
|
52
|
+
def assert_threaded(&block)
|
53
|
+
if @@assert_thread
|
54
|
+
@@_assert_queue << block
|
55
|
+
else
|
56
|
+
@@_assert_queue << block
|
57
|
+
@@assert_thread = Thread.new do
|
58
|
+
@@_assert_queue.pop.() while true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require_relative "setup_test"
|
2
|
+
|
3
|
+
module Actuator
|
4
|
+
class TestActuator < Test
|
5
|
+
PrecisionTestDuration = 10.0
|
6
|
+
PrecisionTestInterval = 0.002
|
7
|
+
PrecisionTotalSamples = (PrecisionTestDuration / PrecisionTestInterval).to_i
|
8
|
+
|
9
|
+
def setup
|
10
|
+
# If GC desyncs the assert thread too much it will cause false positives
|
11
|
+
GC.start full_mark: true, immediate_sweep: true
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_once_off_timer
|
15
|
+
called = called2 = 0
|
16
|
+
timer = Timer.in(0.1) { called += 1 }
|
17
|
+
timer2 = Timer.in(0.1) { called2 += 1 }
|
18
|
+
assert_async do
|
19
|
+
timer2.destroy
|
20
|
+
assert timer2.destroyed?, 'destroyed? returned false after calling Timer#destroy'
|
21
|
+
Kernel.sleep 0.02
|
22
|
+
assert !timer.destroyed?, 'timer destroyed too early'
|
23
|
+
Kernel.sleep 0.1
|
24
|
+
assert timer.destroyed?, 'timer not destroyed within 20ms'
|
25
|
+
Kernel.sleep 0.1
|
26
|
+
assert called == 1, 'timer callback not called'
|
27
|
+
assert called2 == 0, 'timer callback called after being destroyed'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_interval_timer
|
32
|
+
called = called2 = 0
|
33
|
+
timer = Timer.every(0.005) { called += 1 }
|
34
|
+
timer2 = Timer.every(0.005) { called2 += 1; timer2.destroy }
|
35
|
+
assert_async do
|
36
|
+
Kernel.sleep 0.1
|
37
|
+
assert called > 10, "5ms interval timer only fired #{called} times in 100ms"
|
38
|
+
timer.destroy
|
39
|
+
total_called = called
|
40
|
+
Kernel.sleep 0.02
|
41
|
+
assert called == total_called, 'interval timer fired after being destroyed'
|
42
|
+
assert called2 > 0, '5ms interval timer never called'
|
43
|
+
assert called2 == 1, 'Timer#destroy did not work in interval callback'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
#TODO: Implement sampling in the C++ extension to eliminate profiling overhead
|
48
|
+
def test_timer_precision
|
49
|
+
fiber = Fiber.current
|
50
|
+
timer = nil
|
51
|
+
lates = Array.new(PrecisionTotalSamples)
|
52
|
+
# Try skip initial warmup slowdowns so that they aren't included in the samples
|
53
|
+
Timer.in(0.5) do
|
54
|
+
attempts = 0
|
55
|
+
i = -1
|
56
|
+
scheduled_at = Actuator.now
|
57
|
+
timer = Timer.every(PrecisionTestInterval) do
|
58
|
+
now = Actuator.now
|
59
|
+
lates[i += 1] = (now - scheduled_at - PrecisionTestInterval) * 1_000_000
|
60
|
+
scheduled_at = Actuator.now
|
61
|
+
next if i < PrecisionTotalSamples - 1
|
62
|
+
lates.sort!
|
63
|
+
below_1000_count = 0
|
64
|
+
sum = 0.0; lates.each {|late| below_1000_count += 1 if late < 1000; sum += late }
|
65
|
+
mean = sum / lates.size
|
66
|
+
median = lates.size % 2 == 0 ? (lates[lates.size/2] + lates[lates.size/2-1]) / 2.0 : lates[(lates.size-1)/2]
|
67
|
+
sum = 0.0; lates.each {|late| sum += (late - mean) ** 2 }
|
68
|
+
std_deviation = Math.sqrt(sum / (lates.size - 1).to_f)
|
69
|
+
Log.puts "[Timer Precision] Total: #{lates.size}, Over 1ms: #{lates.size - below_1000_count}, Low: #{lates.first.round(1)} us, High: #{lates.last.round(1)} us, Median: #{median.round(1)} us, Variance: #{std_deviation.round(1)} us"
|
70
|
+
begin
|
71
|
+
# Most hardware will provide good precision but this should be able to pass on a Raspberry Pi v1 or a VM
|
72
|
+
assert lates.last < 10000, "a timer was over 10 ms late (#{lates.last.round(2)} us)"
|
73
|
+
assert median < 200, "average timer precision is worse than 200 us (#{median.round(2)} us)"
|
74
|
+
assert std_deviation < 500, "too much jitter, variance is over 500 us (#{std_deviation.round(2)} us)"
|
75
|
+
rescue Minitest::Assertion => ex
|
76
|
+
Log.puts "Retrying due to bad precision most likely caused by loaded hardware: #{ex.message}"
|
77
|
+
attempts += 1
|
78
|
+
raise if attempts > 9
|
79
|
+
i = -1
|
80
|
+
scheduled_at = Actuator.now
|
81
|
+
next
|
82
|
+
end
|
83
|
+
timer.destroy
|
84
|
+
fiber.resume
|
85
|
+
end
|
86
|
+
end
|
87
|
+
Fiber.yield
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: actuator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- bawNg
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-01-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake-compiler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rdoc
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: hoe
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.16'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.16'
|
69
|
+
description: ''
|
70
|
+
email:
|
71
|
+
- bawng@intoxicated.co.za
|
72
|
+
executables: []
|
73
|
+
extensions:
|
74
|
+
- ext/actuator/extconf.rb
|
75
|
+
extra_rdoc_files:
|
76
|
+
- History.txt
|
77
|
+
- Manifest.txt
|
78
|
+
- README.md
|
79
|
+
files:
|
80
|
+
- ".autotest"
|
81
|
+
- History.txt
|
82
|
+
- Manifest.txt
|
83
|
+
- README.md
|
84
|
+
- Rakefile
|
85
|
+
- ext/actuator/actuator.c
|
86
|
+
- ext/actuator/actuator.h
|
87
|
+
- ext/actuator/clock.c
|
88
|
+
- ext/actuator/clock.h
|
89
|
+
- ext/actuator/debug.c
|
90
|
+
- ext/actuator/debug.h
|
91
|
+
- ext/actuator/extconf.rb
|
92
|
+
- ext/actuator/log.cpp
|
93
|
+
- ext/actuator/log.h
|
94
|
+
- ext/actuator/reactor.cpp
|
95
|
+
- ext/actuator/reactor.h
|
96
|
+
- ext/actuator/ruby_helpers.c
|
97
|
+
- ext/actuator/ruby_helpers.h
|
98
|
+
- ext/actuator/timer.cpp
|
99
|
+
- ext/actuator/timer.h
|
100
|
+
- lib/actuator.rb
|
101
|
+
- lib/actuator/fiber.rb
|
102
|
+
- lib/actuator/fiber_pool.rb
|
103
|
+
- lib/actuator/job.rb
|
104
|
+
- lib/actuator/mutex.rb
|
105
|
+
- lib/actuator/mutex/replace.rb
|
106
|
+
- test/setup_test.rb
|
107
|
+
- test/test_actuator.rb
|
108
|
+
homepage:
|
109
|
+
licenses:
|
110
|
+
- MIT
|
111
|
+
metadata: {}
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options:
|
114
|
+
- "--main"
|
115
|
+
- README.md
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
requirements: []
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 2.6.12
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: ''
|
134
|
+
test_files: []
|