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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +30 -0
- data/.gitignore +57 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +30 -0
- data/LICENSE +21 -0
- data/README.md +2 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/test +8 -0
- data/fable.gemspec +34 -0
- data/fable.sublime-project +8 -0
- data/lib/fable.rb +49 -0
- data/lib/fable/call_stack.rb +351 -0
- data/lib/fable/choice.rb +31 -0
- data/lib/fable/choice_point.rb +65 -0
- data/lib/fable/container.rb +218 -0
- data/lib/fable/control_command.rb +156 -0
- data/lib/fable/debug_metadata.rb +13 -0
- data/lib/fable/divert.rb +100 -0
- data/lib/fable/glue.rb +7 -0
- data/lib/fable/ink_list.rb +425 -0
- data/lib/fable/list_definition.rb +44 -0
- data/lib/fable/list_definitions_origin.rb +35 -0
- data/lib/fable/native_function_call.rb +324 -0
- data/lib/fable/native_function_operations.rb +149 -0
- data/lib/fable/observer.rb +205 -0
- data/lib/fable/path.rb +186 -0
- data/lib/fable/pointer.rb +42 -0
- data/lib/fable/profiler.rb +287 -0
- data/lib/fable/push_pop_type.rb +11 -0
- data/lib/fable/runtime_object.rb +159 -0
- data/lib/fable/search_result.rb +20 -0
- data/lib/fable/serializer.rb +560 -0
- data/lib/fable/state_patch.rb +47 -0
- data/lib/fable/story.rb +1447 -0
- data/lib/fable/story_state.rb +915 -0
- data/lib/fable/tag.rb +14 -0
- data/lib/fable/value.rb +334 -0
- data/lib/fable/variable_assignment.rb +20 -0
- data/lib/fable/variable_reference.rb +38 -0
- data/lib/fable/variables_state.rb +327 -0
- data/lib/fable/version.rb +3 -0
- data/lib/fable/void.rb +4 -0
- data/zork_mode.rb +23 -0
- 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
|
data/lib/fable/path.rb
ADDED
@@ -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
|