fable 0.5.0

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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +30 -0
  3. data/.gitignore +57 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +30 -0
  8. data/LICENSE +21 -0
  9. data/README.md +2 -0
  10. data/Rakefile +10 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/bin/test +8 -0
  14. data/fable.gemspec +34 -0
  15. data/fable.sublime-project +8 -0
  16. data/lib/fable.rb +49 -0
  17. data/lib/fable/call_stack.rb +351 -0
  18. data/lib/fable/choice.rb +31 -0
  19. data/lib/fable/choice_point.rb +65 -0
  20. data/lib/fable/container.rb +218 -0
  21. data/lib/fable/control_command.rb +156 -0
  22. data/lib/fable/debug_metadata.rb +13 -0
  23. data/lib/fable/divert.rb +100 -0
  24. data/lib/fable/glue.rb +7 -0
  25. data/lib/fable/ink_list.rb +425 -0
  26. data/lib/fable/list_definition.rb +44 -0
  27. data/lib/fable/list_definitions_origin.rb +35 -0
  28. data/lib/fable/native_function_call.rb +324 -0
  29. data/lib/fable/native_function_operations.rb +149 -0
  30. data/lib/fable/observer.rb +205 -0
  31. data/lib/fable/path.rb +186 -0
  32. data/lib/fable/pointer.rb +42 -0
  33. data/lib/fable/profiler.rb +287 -0
  34. data/lib/fable/push_pop_type.rb +11 -0
  35. data/lib/fable/runtime_object.rb +159 -0
  36. data/lib/fable/search_result.rb +20 -0
  37. data/lib/fable/serializer.rb +560 -0
  38. data/lib/fable/state_patch.rb +47 -0
  39. data/lib/fable/story.rb +1447 -0
  40. data/lib/fable/story_state.rb +915 -0
  41. data/lib/fable/tag.rb +14 -0
  42. data/lib/fable/value.rb +334 -0
  43. data/lib/fable/variable_assignment.rb +20 -0
  44. data/lib/fable/variable_reference.rb +38 -0
  45. data/lib/fable/variables_state.rb +327 -0
  46. data/lib/fable/version.rb +3 -0
  47. data/lib/fable/void.rb +4 -0
  48. data/zork_mode.rb +23 -0
  49. metadata +149 -0
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+ # from: https://github.com/ruby/observer
3
+ #
4
+ # Implementation of the _Observer_ object-oriented design pattern. The
5
+ # following documentation is copied, with modifications, from "Programming
6
+ # Ruby", by Hunt and Thomas; http://www.ruby-doc.org/docs/ProgrammingRuby/html/lib_patterns.html.
7
+ #
8
+ # See Observable for more info.
9
+
10
+ # The Observer pattern (also known as publish/subscribe) provides a simple
11
+ # mechanism for one object to inform a set of interested third-party objects
12
+ # when its state changes.
13
+ #
14
+ # == Mechanism
15
+ #
16
+ # The notifying class mixes in the +Observable+
17
+ # module, which provides the methods for managing the associated observer
18
+ # objects.
19
+ #
20
+ # The observable object must:
21
+ # * assert that it has +#changed+
22
+ # * call +#notify_observers+
23
+ #
24
+ # An observer subscribes to updates using Observable#add_observer, which also
25
+ # specifies the method called via #notify_observers. The default method for
26
+ # #notify_observers is #update.
27
+ #
28
+ # === Example
29
+ #
30
+ # The following example demonstrates this nicely. A +Ticker+, when run,
31
+ # continually receives the stock +Price+ for its <tt>@symbol</tt>. A +Warner+
32
+ # is a general observer of the price, and two warners are demonstrated, a
33
+ # +WarnLow+ and a +WarnHigh+, which print a warning if the price is below or
34
+ # above their set limits, respectively.
35
+ #
36
+ # The +update+ callback allows the warners to run without being explicitly
37
+ # called. The system is set up with the +Ticker+ and several observers, and the
38
+ # observers do their duty without the top-level code having to interfere.
39
+ #
40
+ # Note that the contract between publisher and subscriber (observable and
41
+ # observer) is not declared or enforced. The +Ticker+ publishes a time and a
42
+ # price, and the warners receive that. But if you don't ensure that your
43
+ # contracts are correct, nothing else can warn you.
44
+ #
45
+ # require "observer"
46
+ #
47
+ # class Ticker ### Periodically fetch a stock price.
48
+ # include Observable
49
+ #
50
+ # def initialize(symbol)
51
+ # @symbol = symbol
52
+ # end
53
+ #
54
+ # def run
55
+ # last_price = nil
56
+ # loop do
57
+ # price = Price.fetch(@symbol)
58
+ # print "Current price: #{price}\n"
59
+ # if price != last_price
60
+ # changed # notify observers
61
+ # last_price = price
62
+ # notify_observers(Time.now, price)
63
+ # end
64
+ # sleep 1
65
+ # end
66
+ # end
67
+ # end
68
+ #
69
+ # class Price ### A mock class to fetch a stock price (60 - 140).
70
+ # def self.fetch(symbol)
71
+ # 60 + rand(80)
72
+ # end
73
+ # end
74
+ #
75
+ # class Warner ### An abstract observer of Ticker objects.
76
+ # def initialize(ticker, limit)
77
+ # @limit = limit
78
+ # ticker.add_observer(self)
79
+ # end
80
+ # end
81
+ #
82
+ # class WarnLow < Warner
83
+ # def update(time, price) # callback for observer
84
+ # if price < @limit
85
+ # print "--- #{time.to_s}: Price below #@limit: #{price}\n"
86
+ # end
87
+ # end
88
+ # end
89
+ #
90
+ # class WarnHigh < Warner
91
+ # def update(time, price) # callback for observer
92
+ # if price > @limit
93
+ # print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
94
+ # end
95
+ # end
96
+ # end
97
+ #
98
+ # ticker = Ticker.new("MSFT")
99
+ # WarnLow.new(ticker, 80)
100
+ # WarnHigh.new(ticker, 120)
101
+ # ticker.run
102
+ #
103
+ # Produces:
104
+ #
105
+ # Current price: 83
106
+ # Current price: 75
107
+ # --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 75
108
+ # Current price: 90
109
+ # Current price: 134
110
+ # +++ Sun Jun 09 00:10:25 CDT 2002: Price above 120: 134
111
+ # Current price: 134
112
+ # Current price: 112
113
+ # Current price: 79
114
+ # --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 79
115
+ module Observable
116
+
117
+ #
118
+ # Add +observer+ as an observer on this object. So that it will receive
119
+ # notifications.
120
+ #
121
+ # +observer+:: the object that will be notified of changes.
122
+ # +func+:: Symbol naming the method that will be called when this Observable
123
+ # has changes.
124
+ #
125
+ # This method must return true for +observer.respond_to?+ and will
126
+ # receive <tt>*arg</tt> when #notify_observers is called, where
127
+ # <tt>*arg</tt> is the value passed to #notify_observers by this
128
+ # Observable
129
+ def add_observer(observer, func=:update)
130
+ @observer_peers = {} unless defined? @observer_peers
131
+ unless observer.respond_to? func
132
+ raise NoMethodError, "observer does not respond to `#{func}'"
133
+ end
134
+ @observer_peers[observer] = func
135
+ end
136
+
137
+ #
138
+ # Remove +observer+ as an observer on this object so that it will no longer
139
+ # receive notifications.
140
+ #
141
+ # +observer+:: An observer of this Observable
142
+ def delete_observer(observer)
143
+ @observer_peers.delete observer if defined? @observer_peers
144
+ end
145
+
146
+ #
147
+ # Remove all observers associated with this object.
148
+ #
149
+ def delete_observers
150
+ @observer_peers.clear if defined? @observer_peers
151
+ end
152
+
153
+ #
154
+ # Return the number of observers associated with this object.
155
+ #
156
+ def count_observers
157
+ if defined? @observer_peers
158
+ @observer_peers.size
159
+ else
160
+ 0
161
+ end
162
+ end
163
+
164
+ #
165
+ # Set the changed state of this object. Notifications will be sent only if
166
+ # the changed +state+ is +true+.
167
+ #
168
+ # +state+:: Boolean indicating the changed state of this Observable.
169
+ #
170
+ def changed(state=true)
171
+ @observer_state = state
172
+ end
173
+
174
+ #
175
+ # Returns true if this object's state has been changed since the last
176
+ # #notify_observers call.
177
+ #
178
+ def changed?
179
+ if defined? @observer_state and @observer_state
180
+ true
181
+ else
182
+ false
183
+ end
184
+ end
185
+
186
+ #
187
+ # Notify observers of a change in state *if* this object's changed state is
188
+ # +true+.
189
+ #
190
+ # This will invoke the method named in #add_observer, passing <tt>*arg</tt>.
191
+ # The changed state is then set to +false+.
192
+ #
193
+ # <tt>*arg</tt>:: Any arguments to pass to the observers.
194
+ def notify_observers(*arg)
195
+ if defined? @observer_state and @observer_state
196
+ if defined? @observer_peers
197
+ @observer_peers.each do |k, v|
198
+ k.send v, *arg
199
+ end
200
+ end
201
+ @observer_state = false
202
+ end
203
+ end
204
+
205
+ end
@@ -0,0 +1,186 @@
1
+ module Fable
2
+ class Path
3
+ PARENT_ID = "^".freeze
4
+
5
+ attr_accessor :components, :relative
6
+
7
+ def relative?
8
+ relative == true
9
+ end
10
+
11
+ def head
12
+ components.first
13
+ end
14
+
15
+ def tail
16
+ if components.size >= 2
17
+ self.class.new(components[1..])
18
+ else
19
+ Path.self
20
+ end
21
+ end
22
+
23
+ def empty?
24
+ length == 0
25
+ end
26
+
27
+ def length
28
+ components.size
29
+ end
30
+
31
+ def contains_named_component?
32
+ components.any?{|x| !x.is_index? }
33
+ end
34
+
35
+ def self.self
36
+ Path.new(".")
37
+ end
38
+
39
+ def initialize(components, relative= false)
40
+ if components.is_a?(String)
41
+ parse_components_string(components)
42
+ else
43
+ self.components = components
44
+ self.relative = relative
45
+ end
46
+ end
47
+
48
+ def path_by_appending_path(path_to_append)
49
+ new_path = Path.new("")
50
+
51
+ upward_moves = 0
52
+
53
+ path_to_append.components.each do |component|
54
+ if component.is_parent?
55
+ upward_moves += 1
56
+ else
57
+ break
58
+ end
59
+ end
60
+
61
+ upward_jumps_to_make = (components.size - upward_moves-1)
62
+ components_to_add_at_this_level = (0..upward_jumps_to_make)
63
+
64
+ components_to_add_at_this_level.each do |i|
65
+ new_path.components << components[i]
66
+ end
67
+
68
+ components_to_add_after_upward_move = (upward_moves..path_to_append.components.size)
69
+
70
+ components_to_add_after_upward_move.each do |i|
71
+ new_path.components << path_to_append.components[i]
72
+ end
73
+
74
+ new_path
75
+ end
76
+
77
+ def path_by_appending_component(component)
78
+ if !component.is_a?(Path::Component)
79
+ component = Component.new(Component.component_type(component))
80
+ end
81
+ new_path = Path.new("")
82
+
83
+ new_path.components += self.components
84
+
85
+ new_path.components << component
86
+ new_path
87
+ end
88
+
89
+ def components_string
90
+ string = components.map{|x| x.to_s}.join('.')
91
+ if relative?
92
+ string = ".#{string}"
93
+ end
94
+
95
+ string
96
+ end
97
+
98
+ def parse_components_string(components_string)
99
+ self.components = []
100
+ return if components_string.strip.empty?
101
+
102
+ # Relative path when components staet with "."
103
+ # example: .^.^.hello.5 is equivalent to filesystem path
104
+ # ../../hello/5
105
+
106
+ if components_string.start_with?(".")
107
+ @relative = true
108
+ else
109
+ @relative = false
110
+ end
111
+
112
+ components_string.split('.').each do |section|
113
+ next if section.empty? #usually the first item in a relative path
114
+
115
+ components << Component.new(Component.component_type(section))
116
+ end
117
+ end
118
+
119
+ def ==(other_path)
120
+ return false if other_path.nil?
121
+ return false if other_path.components.size != components.size
122
+ return false if other_path.relative? != relative?
123
+ return other_path.components == components
124
+ end
125
+
126
+ def to_s
127
+ components_string
128
+ end
129
+
130
+ class Component
131
+ attr_accessor :index, :name
132
+
133
+ def is_index?
134
+ index >= 0
135
+ end
136
+
137
+ def is_parent?
138
+ name == Path::PARENT_ID
139
+ end
140
+
141
+ def self.component_type(value)
142
+ if value.is_a?(Numeric) || value.match?(/^\d+$/)
143
+ return {index: Integer(value)}
144
+ else
145
+ return {name: value}
146
+ end
147
+ end
148
+
149
+ def initialize(options)
150
+ if options[:index]
151
+ self.index = options[:index]
152
+ self.name = nil
153
+ elsif options[:name]
154
+ self.name = options[:name]
155
+ self.index = -1
156
+ end
157
+ end
158
+
159
+ def to_s
160
+ if is_index?
161
+ index.to_s
162
+ else
163
+ name
164
+ end
165
+ end
166
+
167
+ def ==(other_component)
168
+ return false if other_component.nil?
169
+
170
+ if self.is_index? == other_component.is_index?
171
+ if is_index?
172
+ return self.index == other_component.index
173
+ else
174
+ return self.name == other_component.name
175
+ end
176
+ end
177
+
178
+ return false
179
+ end
180
+
181
+ def self.parent_component
182
+ self.new(name: Path::PARENT_ID)
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,42 @@
1
+ module Fable
2
+ class Pointer
3
+ attr_accessor :container, :index
4
+
5
+ def self.start_of(container)
6
+ self.new(container, 0)
7
+ end
8
+
9
+ def self.null_pointer
10
+ self.new(nil, -1)
11
+ end
12
+
13
+ def initialize(container, index)
14
+ self.container = container
15
+ self.index = index
16
+ end
17
+
18
+ def resolve!
19
+ return container if index < 0
20
+ return nil if container.nil?
21
+ return container if container.content.empty?
22
+ return container.content[index]
23
+ end
24
+
25
+ def clone
26
+ self.class.new(self.container, self.index)
27
+ end
28
+
29
+ def null_pointer?
30
+ container.nil?
31
+ end
32
+
33
+ def path
34
+ return nil if null_pointer?
35
+ if index > 0
36
+ return container.path.path_by_appending_component(index)
37
+ else
38
+ return container.path
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,287 @@
1
+ module Fable
2
+ # Simple ink profiler that logs every instruction in the story and counts frequency and timing.
3
+ # To use:
4
+ #
5
+ # profiler = story.start_profiling!
6
+ #
7
+ # (play your story for a bit)
8
+ #
9
+ # report = profiler.report;
10
+ #
11
+ # story.end_profiling!;
12
+ class Profiler
13
+ # The root node in the hierarchical tree of recorded ink timings
14
+ attr_accessor :root_node, :continue_watch, :step_watch, :snapshot_watch,
15
+ :number_of_continues,
16
+ :continue_total, :step_total, :snapshot_total, :current_step_stack,
17
+ :current_step_details, :step_details
18
+
19
+ def initialize
20
+ self.root_node = ProfileNode.new
21
+ self.continue_watch = Stopwatch.new
22
+ self.step_watch = Stopwatch.new
23
+ self.snapshot_watch = Stopwatch.new
24
+ self.step_details = []
25
+ self.number_of_continues = 0
26
+ self.step_total = 0
27
+ self.snapshot_total = 0
28
+ self.continue_total = 0
29
+ end
30
+
31
+ # Generate a printable report based on the data recorded during profiling
32
+ def report
33
+ <<~STR
34
+ #{number_of_continues} CONTINUES / LINES:
35
+ TOTAL TIME: #{self.class.format_milliseconds(continue_total)}
36
+ SNAPSHOTTING: #{self.class.format_milliseconds(snapshot_total)}
37
+ OTHER: #{self.class.format_milliseconds(continue_total - (step_total + snapshot_total))}
38
+ #{root_node.to_s}
39
+ STR
40
+ end
41
+
42
+ def pre_continue!
43
+ self.continue_watch.restart!
44
+ end
45
+
46
+ def post_continue!
47
+ self.continue_watch.stop!
48
+ self.continue_total += continue_watch.elapsed_milliseconds
49
+ self.number_of_continues += 1
50
+ end
51
+
52
+ def pre_step!
53
+ self.current_step_stack = nil
54
+ self.step_watch.restart!
55
+ end
56
+
57
+ def step!(callstack)
58
+ self.step_watch.stop!
59
+
60
+ stack = []
61
+ callstack.elements.each do |element|
62
+ stack_element_name = ""
63
+ if !element.current_pointer.null_pointer?
64
+ path = element.current_pointer.path
65
+ path.components.each do |component|
66
+ if !component.is_index?
67
+ stack_element_name = component.name
68
+ break
69
+ end
70
+ end
71
+ end
72
+
73
+ stack << stack_element_name
74
+ end
75
+
76
+ self.current_step_stack = stack
77
+
78
+ current_object = callstack.current_element.current_pointer.resolve!
79
+
80
+ if current_object.is_a?(ControlCommand)
81
+ step_type = "#{current_object} CC"
82
+ else
83
+ step_type = current_object.class.to_s
84
+ end
85
+
86
+ self.current_step_details = OpenStruct.new(
87
+ type: step_type,
88
+ object: current_object
89
+ )
90
+
91
+ self.step_watch.start!
92
+ end
93
+
94
+ def post_step!
95
+ self.step_watch.stop!
96
+ duration = step_watch.elapsed_milliseconds
97
+ self.step_total += duration
98
+ self.root_node.add_sample(self.current_step_stack, duration)
99
+
100
+ self.current_step_details.time = duration
101
+ self.step_details << self.current_step_details
102
+ end
103
+
104
+ # Generate a printable report specifying the average and maximum times spent
105
+ # stepping over different internal ink instruction types.
106
+ # This report type is primarily used to profile the ink engine itself rather
107
+ # than your own specific ink.
108
+ def step_length_report
109
+ report = StringIO.new
110
+
111
+ report << "TOTAL:#{root_node.total_milliseconds}ms\n"
112
+
113
+ grouped_step_times = step_details.group_by{|x| x.type }
114
+
115
+ average_step_times = grouped_step_times.map do |type, details|
116
+ average = details.sum{|x| x.time }/details.size
117
+ [type, average]
118
+ end.sort_by{|type, average| average}.reverse.map{|type, average| "#{type}: #{average}ms" }
119
+
120
+ report << "AVERAGE STEP TIMES: #{average_step_times.join(", ")}\n"
121
+
122
+ accumulated_step_times = grouped_step_times.map do |type, details|
123
+ sum = details.sum{|x| x.time }
124
+ ["#{type} (x#{details.size})", sum]
125
+ end.sort_by{|type, sum| sum}.reverse.map{|type, sum| "#{type}: #{sum}ms" }
126
+
127
+ report << "ACCUMULATED STEP TIMES: #{accumulated_step_times.join(", ")}\n"
128
+
129
+ report.rewind
130
+ report.read
131
+ end
132
+
133
+ # Create a large log of all the internal instructions that were evaluated while
134
+ # profiling was active. Log is in a tab-separated format, for easing loading into
135
+ # a spreadsheet
136
+ def mega_log
137
+ report = StringIO.new
138
+ report << "Step type\tDescription\tPath\tTime\n"
139
+
140
+ step_details.each do |step|
141
+ report << "#{step.type}\t#{step.object.to_s}\t#{step.object.path.to_s}\t#{step.time.to_s}\n"
142
+ end
143
+
144
+ report.rewind
145
+ report.read
146
+ end
147
+
148
+ def pre_snapshot!
149
+ self.snapshot_watch.restart!
150
+ end
151
+
152
+ def post_snapshot!
153
+ self.snapshot_watch.stop!
154
+ self.snapshot_total += self.snapshot_watch.elapsed_milliseconds
155
+ end
156
+
157
+ def self.format_milliseconds(milliseconds)
158
+ if milliseconds > 1_000
159
+ "#{(milliseconds/1_000.0).round(2)} s"
160
+ else
161
+ "#{(milliseconds).round(3)} ms"
162
+ end
163
+ end
164
+
165
+ # Node used in the hierarchical tree of timings used by the Profiler.
166
+ # Each node corresponds to a single line viewable in a UI-based representation.
167
+ class ProfileNode
168
+ attr_accessor :key, :nodes, :total_milliseconds, :total_sample_count,
169
+ :self_sample_count, :self_milliseconds
170
+
171
+ def has_children?
172
+ !nodes.nil? && nodes.size > 0
173
+ end
174
+
175
+ def initialize(key="")
176
+ self.key = key
177
+ self.total_sample_count = 0
178
+ self.total_milliseconds = 0
179
+ self.self_sample_count = 0
180
+ self.self_milliseconds = 0
181
+ end
182
+
183
+ def add_sample(stack, duration)
184
+ add_sample_with_index(stack, -1, duration)
185
+ end
186
+
187
+ def add_sample_with_index(stack, stack_index, duration)
188
+ self.total_milliseconds += 1
189
+ self.total_milliseconds += duration
190
+
191
+ if stack_index == (stack.size - 1)
192
+ self.self_sample_count += 1
193
+ self.self_milliseconds += duration
194
+ end
195
+
196
+ if stack_index < stack.size
197
+ add_sample_to_node(stack, stack_index + 1, duration)
198
+ end
199
+ end
200
+
201
+ def add_sample_to_node(stack, stack_index, duration)
202
+ node_key = stack[stack_index]
203
+ nodes ||= {node_key => ProfileNode.new(node_key)}
204
+
205
+ nodes[node_key].add_sample_with_index(stack, stack_index, duration)
206
+ end
207
+
208
+ def print_hierarchy(io, indent)
209
+ self.class.pad(io, indent)
210
+
211
+ io << "#{key}: #{own_report}\n"
212
+
213
+ return if nodes.nil?
214
+
215
+ nodes.sort_by{|k,v| v.total_milliseconds }.reverse.each do |key, node|
216
+ node.print_hierarchy(io, indent + 1)
217
+ end
218
+ end
219
+
220
+ # Generates a string giving timing information for this single node, including
221
+ # total milliseconds spent on the piece of ink, the time spent within itself
222
+ # (v.s. spent in children), as well as the number of samples (instruction steps)
223
+ # recorded for both too.
224
+ def own_report
225
+ report = StringIO.new
226
+
227
+ report << "total #{Profiler.format_milliseconds(total_milliseconds)}"
228
+ report << ", self #{Profiler.format_milliseconds(self_milliseconds)}"
229
+ report << " (#{self_sample_count} self samples, #{total_sample_count} total)"
230
+
231
+ report.rewind
232
+ report.read
233
+ end
234
+
235
+ def to_s
236
+ report = StringIO.new
237
+ print_hierarchy(report, 0)
238
+ report.rewind
239
+ report.read
240
+ end
241
+
242
+ def self.pad(io, indent)
243
+ io << " " * indent
244
+ end
245
+ end
246
+
247
+ class Stopwatch
248
+ attr_accessor :start_time, :stop_time, :elapsed_milliseconds
249
+
250
+ def initialize
251
+ @elapsed_milliseconds = 0
252
+ end
253
+
254
+ def start!
255
+ @stop_time = nil
256
+ @start_time = Time.now.utc
257
+ end
258
+
259
+ def reset!
260
+ @elapsed_milliseconds = 0
261
+ end
262
+
263
+ def restart!
264
+ reset!
265
+ start!
266
+ end
267
+
268
+ def stop!
269
+ @stop_time = Time.now.utc
270
+ @elapsed_milliseconds += elapsed_from_start_to_stop
271
+ end
272
+
273
+ def elapsed_milliseconds
274
+ return -1 if start_time.nil?
275
+ if @elapsed_milliseconds == 0 && stop_time.nil?
276
+ return elapsed_from_start_to_stop
277
+ else
278
+ @elapsed_milliseconds
279
+ end
280
+ end
281
+
282
+ def elapsed_from_start_to_stop
283
+ ((@stop_time || Time.now.utc).to_r - @start_time.to_r) * 1000.0
284
+ end
285
+ end
286
+ end
287
+ end