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/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: []
|