ruck 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+
2
+ module Ruck
3
+ class EventClock
4
+ attr_reader :now
5
+
6
+ def initialize
7
+ @now = 0
8
+ @waiting = Hash.new { |hash, event| hash[event] = [] }
9
+ @raised = []
10
+ end
11
+
12
+ # fast-forward this clock by the given time delta
13
+ def fast_forward(dt)
14
+ @now += dt
15
+ end
16
+
17
+ def schedule(obj, event = nil)
18
+ @waiting[event] << obj
19
+ end
20
+
21
+ def unschedule(obj)
22
+ @waiting.each { |event, objs| objs.delete(obj) }
23
+ @raised.delete(obj)
24
+ end
25
+
26
+ def next
27
+ [@raised.first, 0] if @raised.length > 0
28
+ end
29
+
30
+ def unschedule_next
31
+ [@raised.shift, 0] if @raised.length > 0
32
+ end
33
+
34
+ def raise_all(event)
35
+ @raised += @waiting[event]
36
+ @waiting[event].clear
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,135 @@
1
+
2
+ module Ruck
3
+
4
+ # A resumable Proc implemented using continuation. If the given
5
+ # block calls #pause during its execution, its execution is paused
6
+ # and the caller resumed. The second time the Shred is called, it
7
+ # resumes where it left off.
8
+ #
9
+ # If #pause is called anywhere but inside the given block, I can
10
+ # almost guarantee that strange things will happen.
11
+
12
+ class CallccShred
13
+ # I don't mean to actually expose @proc. I noticed that Ruby 1.8's
14
+ # garbage collection cycles become much longer when @proc (a
15
+ # Continuation) is returned from a custom method, but not
16
+ # if returned from an attr_reader. I use attr_reader and alias it
17
+ # to running? to avoid this cost.
18
+ attr_reader :proc
19
+ alias running? proc
20
+
21
+ @@current_shreds = []
22
+
23
+ # the currently executing shred
24
+ def self.current
25
+ @@current_shreds.last
26
+ end
27
+
28
+ def initialize(&block)
29
+ @proc = block || Proc.new{}
30
+ end
31
+
32
+ # pause execution by saving this execution point and returning
33
+ # to the point where go was called
34
+ def pause
35
+ return unless Shred.current == self
36
+
37
+ @@current_shreds.pop
38
+
39
+ callcc do |cont|
40
+ @proc = cont
41
+ @caller.call
42
+ end
43
+ end
44
+
45
+ # begin or resume execution
46
+ def call(*args)
47
+ return unless @proc
48
+
49
+ callcc do |cont|
50
+ @caller = cont
51
+
52
+ @@current_shreds << self
53
+ @proc.call
54
+
55
+ # if we made it here, we're done
56
+ @@current_shreds.pop
57
+ @proc = nil
58
+ @caller.call
59
+ end
60
+ end
61
+
62
+ # alias for call. It takes arguments, but ignores them.
63
+ def [](*args)
64
+ call(*args)
65
+ end
66
+
67
+ # returns true if calling this Shred again will have no effect
68
+ def finished?
69
+ !running?
70
+ end
71
+
72
+ # makes it so calling this Shred in the future will have no effect
73
+ def kill
74
+ @proc = nil
75
+ end
76
+ end
77
+
78
+ # See the documentation for CallccShred
79
+ class FiberShred
80
+ @@current_shreds = []
81
+
82
+ def self.current
83
+ @@current_shreds.last
84
+ end
85
+
86
+ def initialize(&block)
87
+ @fiber = Fiber.new(&block)
88
+ end
89
+
90
+ def pause
91
+ return unless Shred.current == self
92
+
93
+ @@current_shreds.pop
94
+
95
+ Fiber.yield
96
+ end
97
+
98
+ def call(*args)
99
+ return unless @fiber
100
+ @@current_shreds << self
101
+ @fiber.resume
102
+ rescue FiberError
103
+ @fiber = nil
104
+ ensure
105
+ @@current_shreds.pop
106
+ end
107
+
108
+ def [](*args)
109
+ call(*args)
110
+ end
111
+
112
+ def finished?
113
+ @fiber.nil?
114
+ end
115
+
116
+ def running?
117
+ !finished?
118
+ end
119
+
120
+ def kill
121
+ @fiber = nil
122
+ end
123
+ end
124
+
125
+ # Fiber was introduced in Ruby 1.9 and supports a cleaner implementation
126
+ # of Shred than the callcc-based version, but I would like to support
127
+ # Ruby 1.8 as well.
128
+ if defined? Fiber
129
+ class Shred < FiberShred
130
+ end
131
+ else
132
+ class Shred < CallccShred
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,149 @@
1
+
2
+ module Ruck
3
+ class Shreduler
4
+ attr_reader :clock
5
+ attr_reader :event_clock
6
+
7
+ def initialize
8
+ @clock = Clock.new
9
+ @event_clock = EventClock.new
10
+ @clock.add_child_clock(@event_clock)
11
+ end
12
+
13
+ # this Shreduler's idea of the current time
14
+ def now
15
+ @clock.now
16
+ end
17
+
18
+ # schedules the given Shred at the given time, on the given Clock.
19
+ # if no time is given, it is scheduled for immediate execution.
20
+ # if no Clock is given, it is scheduled on the default Clock.
21
+ def shredule(shred, time = nil, clock = nil)
22
+ (clock || @clock).schedule(shred, time)
23
+ shred
24
+ end
25
+
26
+ # unschedules the provided Shred
27
+ def unshredule(shred)
28
+ @clock.unschedule(shred)
29
+ end
30
+
31
+ # wakes up all Shreds waiting on the given event
32
+ def raise_all(event)
33
+ event_clock.raise_all(event)
34
+ end
35
+
36
+ # runs the next scheduled Shred, if one exists, returning that Shred
37
+ def run_one
38
+ shred, relative_time = @clock.unschedule_next
39
+ return nil unless shred
40
+
41
+ fast_forward(relative_time) if relative_time > 0
42
+ invoke_shred(shred)
43
+ end
44
+
45
+ # runs until all Shreds have died, or are all waiting on events
46
+ def run
47
+ loop { return unless run_one }
48
+ end
49
+
50
+ # runs shreds until the given target time, then fast-forwards to
51
+ # that time
52
+ def run_until(target_time)
53
+ return if target_time < now
54
+
55
+ loop do
56
+ shred, relative_time = next_shred
57
+ break unless shred
58
+ break unless now + relative_time <= target_time
59
+ run_one
60
+ end
61
+
62
+ # I hope rounding errors are okay
63
+ fast_forward(target_time - now)
64
+ end
65
+
66
+ # makes this the global shreduler, adding convenience methods to
67
+ # Object and Shred to make it easier to use
68
+ def make_convenient
69
+ $shreduler = self
70
+
71
+ Shred.module_eval do
72
+ class << self
73
+ include ShredConvenienceMethods
74
+ end
75
+ end
76
+ Object.module_eval { include ObjectConvenienceMethods }
77
+ end
78
+
79
+ protected
80
+
81
+ def invoke_shred(shred)
82
+ begin
83
+ shred.call
84
+ rescue Exception => e
85
+ puts e.inspect
86
+ end
87
+ shred
88
+ end
89
+
90
+ # if you override this method, you should probably call super
91
+ def fast_forward(dt)
92
+ @clock.fast_forward(dt)
93
+ end
94
+
95
+ def next_shred
96
+ clock.next
97
+ end
98
+ end
99
+
100
+ module ShredConvenienceMethods
101
+ # yields the given amount of time on the global Shreduler, using the
102
+ # provided Clock if given
103
+ def yield(dt, clock = nil)
104
+ clock ||= $shreduler.clock
105
+ $shreduler.shredule(Shred.current, clock.now + dt, clock)
106
+ Shred.current.pause
107
+ end
108
+
109
+ # sleeps, waiting on the given event on the default EventClock of
110
+ # the global Shreduler
111
+ def wait_on(event)
112
+ $shreduler.shredule(Shred.current, event, $shreduler.event_clock)
113
+ Shred.current.pause
114
+ end
115
+ end
116
+
117
+ module ObjectConvenienceMethods
118
+ # creates a new Shred with the given block on the global Shreduler
119
+ def spork(&block)
120
+ $shreduler.shredule(Shred.new(&block))
121
+ end
122
+
123
+ # creates a new Shred with the given block on the global Shreduler,
124
+ # automatically surrounded by loop { }. If the delay_or_event parameter
125
+ # is given, a Shred.yield(delay) or Shred.wait_on(event) is inserted
126
+ # before the call to your block.
127
+ def spork_loop(delay_or_event = nil, clock = nil, &block)
128
+ shred = Shred.new do
129
+ while Shred.current.running?
130
+ if delay_or_event
131
+ if delay_or_event.is_a?(Numeric)
132
+ Shred.yield(delay_or_event, clock)
133
+ else
134
+ Shred.wait_on(delay_or_event)
135
+ end
136
+ end
137
+
138
+ block.call
139
+ end
140
+ end
141
+ $shreduler.shredule(shred)
142
+ end
143
+
144
+ # raises an event on the default EventClock of the global Shreduler.
145
+ def raise_event(event)
146
+ $shreduler.event_clock.raise_all(event)
147
+ end
148
+ end
149
+ end
@@ -5,44 +5,81 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{ruck}
8
- s.version = "0.2.0"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Tom Lieber"]
12
- s.date = %q{2010-07-10}
12
+ s.date = %q{2010-08-15}
13
13
  s.description = %q{ Ruck uses continuations and a simple scheduler to ensure "shreds"
14
14
  (Ruck threads) are woken at precisely the right time according
15
15
  to its virtual clock. Schedulers can map virtual time to samples
16
16
  in a WAV file, real time, time in a MIDI file, or anything else
17
17
  by overriding "sim_to" in the Shreduler class.
18
+
19
+ A small library of useful unit generators and plenty of examples
20
+ are provided. See the README or the web page for details.
18
21
  }
19
22
  s.email = %q{tom@alltom.com}
20
23
  s.extra_rdoc_files = [
21
- "README"
24
+ "LICENSE",
25
+ "README.markdown"
22
26
  ]
23
27
  s.files = [
24
28
  ".gitignore",
25
- "README",
29
+ "LICENSE",
30
+ "README.markdown",
26
31
  "Rakefile",
27
32
  "VERSION",
33
+ "examples/ex01.rb",
34
+ "examples/ex02.rb",
35
+ "examples/ex03.rb",
36
+ "examples/ex04.rb",
37
+ "examples/ex05.rb",
38
+ "examples/ex06.rb",
39
+ "examples/space/media/Beep.wav",
40
+ "examples/space/media/Space.png",
41
+ "examples/space/media/Star.png",
42
+ "examples/space/media/Starfighter.bmp",
43
+ "examples/space/space.rb",
28
44
  "lib/ruck.rb",
29
- "lib/ruck/shreduling.rb",
30
- "ruck.gemspec"
45
+ "lib/ruck/clock.rb",
46
+ "lib/ruck/event_clock.rb",
47
+ "lib/ruck/shred.rb",
48
+ "lib/ruck/shreduler.rb",
49
+ "ruck.gemspec",
50
+ "spec/clock_spec.rb",
51
+ "spec/shred_spec.rb",
52
+ "spec/shreduler_spec.rb"
31
53
  ]
32
54
  s.homepage = %q{http://github.com/alltom/ruck}
33
55
  s.rdoc_options = ["--charset=UTF-8"]
34
56
  s.require_paths = ["lib"]
35
- s.rubygems_version = %q{1.3.6}
57
+ s.rubygems_version = %q{1.3.7}
36
58
  s.summary = %q{strong timing for Ruby: cooperative threads on a virtual clock}
59
+ s.test_files = [
60
+ "spec/clock_spec.rb",
61
+ "spec/shred_spec.rb",
62
+ "spec/shreduler_spec.rb",
63
+ "examples/ex01.rb",
64
+ "examples/ex02.rb",
65
+ "examples/ex03.rb",
66
+ "examples/ex04.rb",
67
+ "examples/ex05.rb",
68
+ "examples/ex06.rb",
69
+ "examples/space/space.rb"
70
+ ]
37
71
 
38
72
  if s.respond_to? :specification_version then
39
73
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
40
74
  s.specification_version = 3
41
75
 
42
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
76
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
77
+ s.add_runtime_dependency(%q<PriorityQueue>, [">= 0"])
43
78
  else
79
+ s.add_dependency(%q<PriorityQueue>, [">= 0"])
44
80
  end
45
81
  else
82
+ s.add_dependency(%q<PriorityQueue>, [">= 0"])
46
83
  end
47
84
  end
48
85
 
@@ -0,0 +1,218 @@
1
+
2
+ require "ruck"
3
+
4
+ include Ruck
5
+
6
+ class MockOccurrenceObj
7
+ def self.next_name
8
+ @@next_name ||= "a"
9
+ name = @@next_name
10
+ @@next_name = @@next_name.succ
11
+ name
12
+ end
13
+
14
+ def initialize
15
+ @name = MockOccurrenceObj.next_name
16
+ end
17
+
18
+ def inspect
19
+ "MockOccurrenceObj<#{@name}>"
20
+ end
21
+ end
22
+
23
+ describe Clock do
24
+ before(:each) do
25
+ @clock = Clock.new
26
+ @clocks = [@clock]
27
+ end
28
+
29
+ context "when creating" do
30
+ it "starts with now = 0" do
31
+ Clock.new.now.should == 0
32
+ end
33
+ end
34
+
35
+ context "when fast-forwarding" do
36
+ it "works" do
37
+ @clock.fast_forward(1)
38
+ @clock.now.should == 1
39
+ @clock.fast_forward(1)
40
+ @clock.now.should == 2
41
+ end
42
+ end
43
+
44
+ context "when scheduling" do
45
+ it "should default to using the current time" do
46
+ @clock.fast_forward(3)
47
+ @occurrence = MockOccurrenceObj.new
48
+ @clock.schedule(@occurrence)
49
+ @clock.next.should == [@occurrence, 0]
50
+ end
51
+
52
+ it "should use the given time if provided" do
53
+ @clock.fast_forward(3)
54
+ @occurrence = MockOccurrenceObj.new
55
+ @clock.schedule(@occurrence, 5)
56
+ @clock.next.should == [@occurrence, 2]
57
+ end
58
+
59
+ context "with no occurrences" do
60
+ it "next should be nil" do
61
+ @clock.next.should == nil
62
+ end
63
+ end
64
+
65
+ context "with multiple occurrences" do
66
+ before(:each) do
67
+ @next_occurrence = MockOccurrenceObj.new
68
+ @occurrence_after = MockOccurrenceObj.new
69
+ @clock.schedule(@occurrence_after, 2)
70
+ @clock.schedule(@next_occurrence, 1)
71
+ end
72
+
73
+ it "knows the next scheduled occurrence" do
74
+ @clock.next.should == [@next_occurrence, 1]
75
+ end
76
+
77
+ it "can dequeue the next scheduled occurrence" do
78
+ @clock.unschedule_next.should == [@next_occurrence, 1]
79
+ @clock.next.should == [@occurrence_after, 2]
80
+ end
81
+
82
+ it "can enqueue and dequeue a new occurrence" do
83
+ @last_occurrence = MockOccurrenceObj.new
84
+ @clock.schedule(@last_occurrence, 3)
85
+ @clock.unschedule_next.should == [@next_occurrence, 1]
86
+ @clock.unschedule_next.should == [@occurrence_after, 2]
87
+ @clock.next.should == [@last_occurrence, 3]
88
+ end
89
+
90
+ it "can interleavedly enqueue and dequeue a new occurrence" do
91
+ @last_occurrence = MockOccurrenceObj.new
92
+ @clock.unschedule_next.should == [@next_occurrence, 1]
93
+ @clock.schedule(@last_occurrence, 1)
94
+ @clock.unschedule_next.should == [@last_occurrence, 1]
95
+ @clock.next.should == [@occurrence_after, 2]
96
+ end
97
+ end
98
+ end
99
+
100
+ context "with sub-clocks" do
101
+ before do
102
+ # clock/clocks[0]
103
+ # - clocks[1] x1
104
+ # - clocks[2] x2
105
+ # - clocks[3] x2
106
+ @clocks << @clock.add_child_clock(Clock.new(1))
107
+ @clocks << @clock.add_child_clock(Clock.new(2))
108
+ @clocks << @clocks[2].add_child_clock(Clock.new(2))
109
+ end
110
+
111
+ context "when fast-forwarding" do
112
+ it "fast-forwards children clocks" do
113
+ @clock.fast_forward(1)
114
+ @clocks[1].now.should == 1
115
+ @clocks[2].now.should == 2
116
+ @clocks[3].now.should == 4
117
+ end
118
+ end
119
+
120
+ context "when finding the next occurrence" do
121
+ it "should return the correct time offset" do
122
+ @occurrence = MockOccurrenceObj.new
123
+ @clocks[2].schedule(@occurrence, 4)
124
+ @clock.next.should == [@occurrence, 2]
125
+ end
126
+
127
+ it "should return the correct time offset in a sub-clock 2 levels deep" do
128
+ @occurrence = MockOccurrenceObj.new
129
+ @clocks[3].schedule(@occurrence, 8)
130
+ @clock.next.should == [@occurrence, 2]
131
+ end
132
+
133
+ it "should return the correct time offset after a fast-forward" do
134
+ @occurrence = MockOccurrenceObj.new
135
+ @clocks[2].schedule(@occurrence, 4)
136
+ @clock.fast_forward(1)
137
+ @clock.next.should == [@occurrence, 1]
138
+ end
139
+
140
+ it "should return the correct time offset in a sub-clock 2 levels deep after a fast-forward" do
141
+ @occurrence = MockOccurrenceObj.new
142
+ @clocks[3].schedule(@occurrence, 8)
143
+ @clock.fast_forward(1)
144
+ @clock.next.should == [@occurrence, 1]
145
+ end
146
+ end
147
+
148
+ context "when dequeuing the next occurrence" do
149
+ it "should work when the occurrence is on the parent clock" do
150
+ @occurrence = MockOccurrenceObj.new
151
+ @clocks[0].schedule(@occurrence, 4)
152
+ @clock.unschedule_next.should == [@occurrence, 4]
153
+ end
154
+
155
+ it "should work when the occurrence is one clock deep" do
156
+ @occurrence = MockOccurrenceObj.new
157
+ @clocks[1].schedule(@occurrence, 4)
158
+ @clock.unschedule_next.should == [@occurrence, 4]
159
+ end
160
+
161
+ it "should work when the occurrence is one clock deep and account for rate" do
162
+ @occurrence = MockOccurrenceObj.new
163
+ @clocks[2].schedule(@occurrence, 4)
164
+ @clock.unschedule_next.should == [@occurrence, 2]
165
+ end
166
+
167
+ it "should work when the occurrence is two clocks deep and account for rate" do
168
+ @occurrence = MockOccurrenceObj.new
169
+ @clocks[3].schedule(@occurrence, 4)
170
+ @clock.unschedule_next.should == [@occurrence, 1]
171
+ end
172
+ end
173
+ end
174
+
175
+ context "when dequeuing occurrences" do
176
+ it "should work" do
177
+ @occurrence = MockOccurrenceObj.new
178
+ @clock.schedule(@occurrence, 2)
179
+ @clock.unschedule(@occurrence).should == 2
180
+ end
181
+
182
+ context "with sub-clocks" do
183
+ before(:each) do
184
+ # clock/clocks[0]
185
+ # - clocks[1] x1
186
+ # - clocks[2] x2
187
+ # - clocks[3] x2
188
+ @clocks << @clock.add_child_clock(Clock.new(1))
189
+ @clocks << @clock.add_child_clock(Clock.new(2))
190
+ @clocks << @clocks[2].add_child_clock(Clock.new(2))
191
+ end
192
+
193
+ it "should work with the parent clock" do
194
+ @occurrence = MockOccurrenceObj.new
195
+ @clocks[0].schedule(@occurrence, 2)
196
+ @clocks[0].unschedule(@occurrence).should == 2
197
+ end
198
+
199
+ it "should work one clock deep" do
200
+ @occurrence = MockOccurrenceObj.new
201
+ @clocks[1].schedule(@occurrence, 2)
202
+ @clocks[0].unschedule(@occurrence).should == 2
203
+ end
204
+
205
+ it "should work one clock deep and adjust for rate" do
206
+ @occurrence = MockOccurrenceObj.new
207
+ @clocks[2].schedule(@occurrence, 2)
208
+ @clocks[0].unschedule(@occurrence).should == 1
209
+ end
210
+
211
+ it "should work two clocks deep and adjust for rate" do
212
+ @occurrence = MockOccurrenceObj.new
213
+ @clocks[3].schedule(@occurrence, 4)
214
+ @clocks[0].unschedule(@occurrence).should == 1
215
+ end
216
+ end
217
+ end
218
+ end