ultra_marathon 0.1.7 → 0.1.10
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 +4 -4
- data/README.md +97 -1
- data/lib/core_ext/class.rb +13 -0
- data/lib/core_ext/extensions.rb +4 -0
- data/lib/core_ext/object.rb +12 -0
- data/lib/core_ext/string.rb +9 -0
- data/lib/ultra_marathon.rb +2 -1
- data/lib/ultra_marathon/abstract_runner.rb +29 -144
- data/lib/ultra_marathon/base_runner.rb +216 -0
- data/lib/ultra_marathon/callbacks.rb +15 -50
- data/lib/ultra_marathon/collection_runner.rb +111 -0
- data/lib/ultra_marathon/contexticution.rb +53 -0
- data/lib/ultra_marathon/instrumentation.rb +32 -9
- data/lib/ultra_marathon/instrumentation/profile.rb +38 -9
- data/lib/ultra_marathon/instrumentation/store.rb +103 -0
- data/lib/ultra_marathon/logging.rb +1 -1
- data/lib/ultra_marathon/store.rb +13 -4
- data/lib/ultra_marathon/sub_context.rb +36 -0
- data/lib/ultra_marathon/sub_runner.rb +116 -60
- data/lib/ultra_marathon/version.rb +1 -1
- data/spec/spec_helper.rb +11 -0
- data/spec/ultra_marathon/abstract_runner_spec.rb +44 -1
- data/spec/ultra_marathon/collection_runner_spec.rb +96 -0
- data/spec/ultra_marathon/instrumentation/profile_spec.rb +5 -4
- data/spec/ultra_marathon/instrumentation/store_spec.rb +73 -0
- data/spec/ultra_marathon/instrumentation_spec.rb +1 -1
- data/spec/ultra_marathon/sub_runner_spec.rb +5 -1
- metadata +15 -1
@@ -1,10 +1,11 @@
|
|
1
1
|
require 'set'
|
2
2
|
require 'active_support/concern'
|
3
|
-
require '
|
3
|
+
require 'ultra_marathon/contexticution'
|
4
4
|
|
5
5
|
module UltraMarathon
|
6
6
|
module Callbacks
|
7
7
|
extend ActiveSupport::Concern
|
8
|
+
include Contexticution
|
8
9
|
|
9
10
|
private
|
10
11
|
|
@@ -15,56 +16,16 @@ module UltraMarathon
|
|
15
16
|
def callback_conditions_met?(options)
|
16
17
|
conditions_met = true
|
17
18
|
if options.key? :if
|
18
|
-
conditions_met &&=
|
19
|
+
conditions_met &&= contexticute(options[:if])
|
19
20
|
elsif options.key? :unless
|
20
|
-
conditions_met &&= !
|
21
|
+
conditions_met &&= !contexticute(options[:unless])
|
21
22
|
end
|
22
23
|
conditions_met
|
23
24
|
end
|
24
25
|
|
25
|
-
# Exectutes the object in the context of the instance,
|
26
|
-
# whether an explicitly callable object or a string/symbol
|
27
|
-
# representation of one
|
28
|
-
def call_proc_or_symbol(object, args=[], options={})
|
29
|
-
options = options.dup
|
30
|
-
options[:context] ||= self
|
31
|
-
bound_proc = bind_to_context(object, options[:context])
|
32
|
-
evaluate_block_with_arguments(bound_proc, args)
|
33
|
-
end
|
34
|
-
|
35
|
-
# Binds a proc to the given context. If a symbol is passed in,
|
36
|
-
# retrieves the bound method.
|
37
|
-
def bind_to_context(symbol_or_proc, context)
|
38
|
-
if symbol_or_proc.is_a?(Symbol)
|
39
|
-
context.method(symbol_or_proc)
|
40
|
-
elsif symbol_or_proc.respond_to? :call
|
41
|
-
symbol_or_proc.bind(context)
|
42
|
-
else
|
43
|
-
raise ArgumentError.new("Cannot bind #{callback.class} to #{context}. Expected callable object or symbol.")
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
# Applies a block in context with the correct number of arguments.
|
48
|
-
# If there are more arguments than the arity, takes the first n
|
49
|
-
# arguments where (n) is the arity of the block.
|
50
|
-
# If there are the same number of arguments, splats them into the block.
|
51
|
-
# Otherwise throws an argument error.
|
52
|
-
def evaluate_block_with_arguments(block, args)
|
53
|
-
# If block.arity < 0, when a block takes a variable number of args,
|
54
|
-
# the one's complement (-n-1) is the number of required arguments
|
55
|
-
required_arguments = block.arity < 0 ? ~block.arity : block.arity
|
56
|
-
if args.length >= required_arguments
|
57
|
-
if block.arity < 0
|
58
|
-
instance_exec(*args, &block)
|
59
|
-
else
|
60
|
-
instance_exec(*args.first(block.arity), &block)
|
61
|
-
end
|
62
|
-
else
|
63
|
-
raise ArgumentError.new("wrong number of arguments (#{args.size} for #{required_arguments})")
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
26
|
module ClassMethods
|
27
|
+
include Contexticution
|
28
|
+
|
68
29
|
## Public Class Methods
|
69
30
|
|
70
31
|
# Add one or more new callbacks for class
|
@@ -113,11 +74,15 @@ module UltraMarathon
|
|
113
74
|
end
|
114
75
|
|
115
76
|
# Defines class level accessor that memoizes the set of callbacks
|
116
|
-
#
|
77
|
+
# @param callback_name [String, Symbol]
|
78
|
+
# @example
|
79
|
+
# add_callbacks_accessor(:after_save)
|
117
80
|
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
#
|
81
|
+
# # Equivalent to
|
82
|
+
# #
|
83
|
+
# # def self.after_save_callbacks
|
84
|
+
# # @after_save_callbacks ||= []
|
85
|
+
# # end
|
121
86
|
def add_callbacks_accessor(callback_name)
|
122
87
|
accessor_name = "#{callback_name}_callbacks"
|
123
88
|
instance_variable_name = :"@#{accessor_name}"
|
@@ -167,7 +132,7 @@ module UltraMarathon
|
|
167
132
|
callbacks = self.class.send :"#{callback_name}_callbacks"
|
168
133
|
callbacks.each do |callback, options|
|
169
134
|
next unless callback_conditions_met?(options)
|
170
|
-
|
135
|
+
contexticute(callback, args, options[:context] || self)
|
171
136
|
end
|
172
137
|
end
|
173
138
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'ultra_marathon/base_runner'
|
3
|
+
require 'ultra_marathon/store'
|
4
|
+
require 'ultra_marathon/sub_context'
|
5
|
+
require 'ultra_marathon/sub_runner'
|
6
|
+
|
7
|
+
module UltraMarathon
|
8
|
+
class CollectionRunner < BaseRunner
|
9
|
+
attr_reader :collection, :options, :run_block, :name
|
10
|
+
attr_memo_reader :sub_runner_class, -> { options[:sub_runner].try_call }
|
11
|
+
|
12
|
+
after_run :write_logs
|
13
|
+
instrumentation_prefix lambda { |collection| "collection.#{collection.name}." }
|
14
|
+
|
15
|
+
# Takes a collection, each of which will be run in its own subrunner. The collection
|
16
|
+
# Also takes a number of options:
|
17
|
+
#
|
18
|
+
# name: The name of the collection run block
|
19
|
+
# sub_name: A callable object (passed the index of the collection) that
|
20
|
+
# should return a unique (to the collection) name for that subrunner
|
21
|
+
# Defaults to :"#{options[:name]}__#{index}"
|
22
|
+
# sub_runner: Class inheiriting from UltraMarathon::SubRunner in which each
|
23
|
+
# run_block will be run
|
24
|
+
# Defaults to UltraMarathon::SubRunner
|
25
|
+
#
|
26
|
+
# iterator: Method called to iterate over collection. For example, a Rails
|
27
|
+
# application may wish to use :find_each with an ActiveRecord::Relation
|
28
|
+
# to batch queries
|
29
|
+
# Defaults to :each
|
30
|
+
#
|
31
|
+
# threaded: Run each iteration in its own thread
|
32
|
+
def initialize(collection, options={}, &run_block)
|
33
|
+
@collection, @run_block = collection, run_block
|
34
|
+
@name = options[:name]
|
35
|
+
@options = {
|
36
|
+
sub_name: proc { |index| :"#{options[:name]}__#{index}" },
|
37
|
+
sub_runner: SubRunner,
|
38
|
+
iterator: :each,
|
39
|
+
threaded: false
|
40
|
+
}.merge(options)
|
41
|
+
end
|
42
|
+
|
43
|
+
def unrun_sub_runners
|
44
|
+
@unrun_sub_runners ||= begin
|
45
|
+
store = Store.new
|
46
|
+
index = 0
|
47
|
+
collection.send(options[:iterator]) do |item|
|
48
|
+
this_index = index
|
49
|
+
index += 1
|
50
|
+
store << new_item_sub_runner(item, this_index)
|
51
|
+
end
|
52
|
+
store
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def threaded?
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
def complete?
|
61
|
+
unrun_sub_runners.empty? && running_sub_runners.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
# Set of all sub runners that should be run before this one.
|
65
|
+
# This class cannot do anything with this information, but it is useful
|
66
|
+
# to the enveloping runner.
|
67
|
+
def parents
|
68
|
+
@parents ||= Set.new(options[:requires])
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def write_logs
|
74
|
+
log_header
|
75
|
+
log_all_sub_runners
|
76
|
+
log_summary
|
77
|
+
end
|
78
|
+
|
79
|
+
def log_header
|
80
|
+
logger.info "Running Collection #{options[:name]}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def new_item_sub_runner(item, index)
|
84
|
+
item_options = sub_runner_item_options(item, index)
|
85
|
+
item_sub_context = build_item_sub_context(item, item_options)
|
86
|
+
sub_runner_class.new(item_options, item_sub_context)
|
87
|
+
end
|
88
|
+
|
89
|
+
# By default, run the sub runner inside this context. Unlikely to be what you
|
90
|
+
# want
|
91
|
+
def sub_runner_base_options
|
92
|
+
{
|
93
|
+
context: options[:context] || self,
|
94
|
+
threaded: options[:threaded]
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
def sub_runner_item_options(item, index)
|
99
|
+
name = options[:sub_name].try_call(index, item)
|
100
|
+
sub_runner_base_options.merge(name: name)
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_item_sub_context(item, options)
|
104
|
+
Proc.new do |run_block|
|
105
|
+
SubContext.new(options[:context]) do
|
106
|
+
instance_exec(*item, &run_block)
|
107
|
+
end
|
108
|
+
end.call(run_block)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'core_ext/proc'
|
2
|
+
|
3
|
+
module UltraMarathon
|
4
|
+
module Contexticution
|
5
|
+
|
6
|
+
# @param object [Symbol, String, Proc] a symbol representing a method name,
|
7
|
+
# a string to be eval'd, or a proc to be called in the given context
|
8
|
+
# @param args [Array] arguments to be passed to +object+
|
9
|
+
# @param context [Object] the context in which to evaluate +object+. Defaults
|
10
|
+
# to self
|
11
|
+
def contexticute(object, args=[], context=self)
|
12
|
+
bound_proc = bind_to_context(object, context)
|
13
|
+
evaluate_block_with_arguments(bound_proc, args)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
# Binds a proc to the given context. If a symbol is passed in,
|
18
|
+
# retrieves the bound method. If it is a string, generates a lambda wrapping
|
19
|
+
# it.
|
20
|
+
def bind_to_context(object, context)
|
21
|
+
if object.is_a?(Symbol)
|
22
|
+
context.method(object)
|
23
|
+
elsif object.respond_to?(:call)
|
24
|
+
object.bind(context)
|
25
|
+
elsif object.is_a?(String)
|
26
|
+
eval("lambda { #{object} }").bind(context)
|
27
|
+
else
|
28
|
+
raise ArgumentError.new("Cannot bind #{callback.class} to #{context}. Expected Symbol, String, or object responding to #call.")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Applies a block in context with the correct number of arguments.
|
33
|
+
# If there are more arguments than the arity, takes the first n
|
34
|
+
# arguments where (n) is the arity of the block.
|
35
|
+
# If there are the same number of arguments, splats them into the block.
|
36
|
+
# Otherwise throws an argument error.
|
37
|
+
def evaluate_block_with_arguments(block, args)
|
38
|
+
# If block.arity < 0, when a block takes a variable number of args,
|
39
|
+
# the one's complement (-n-1) is the number of required arguments
|
40
|
+
required_arguments = block.arity < 0 ? ~block.arity : block.arity
|
41
|
+
if args.length >= required_arguments
|
42
|
+
if block.arity < 0
|
43
|
+
instance_exec(*args, &block)
|
44
|
+
else
|
45
|
+
instance_exec(*args.first(block.arity), &block)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
raise ArgumentError.new("wrong number of arguments (#{args.size} for #{required_arguments})")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -1,25 +1,48 @@
|
|
1
|
+
require 'active_support/concern'
|
1
2
|
require 'ultra_marathon/instrumentation/profile'
|
3
|
+
require 'ultra_marathon/instrumentation/store'
|
2
4
|
|
3
5
|
module UltraMarathon
|
4
6
|
module Instrumentation
|
5
|
-
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
# Creates a default UltraMarathon::Instrumentation::Store and stores
|
9
|
+
# all instrumented profiles there
|
6
10
|
|
7
|
-
|
11
|
+
module ClassMethods
|
12
|
+
## Public Class Methods
|
8
13
|
|
14
|
+
# @param prefix_or_proc [String, Proc] the prefix. If a Proc, it will be
|
15
|
+
# passed self for each instance
|
16
|
+
# @return [String, Proc, nil]
|
17
|
+
def instrumentation_prefix(prefix_or_proc=nil)
|
18
|
+
if prefix_or_proc
|
19
|
+
@instrumentation_prefix = prefix_or_proc
|
20
|
+
else
|
21
|
+
@instrumentation_prefix
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# The default instrumentation store for the included class
|
27
|
+
# @return [UltraMarathon::Instrumentation::Store]
|
9
28
|
def instrumentations
|
10
|
-
@instrumentations ||=
|
29
|
+
@instrumentations ||= UltraMarathon::Instrumentation::Store.new([], prefix: instrumentation_prefix)
|
11
30
|
end
|
12
31
|
|
13
32
|
private
|
14
33
|
|
15
34
|
## Private Instance Methods
|
16
35
|
|
17
|
-
#
|
18
|
-
#
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
|
36
|
+
# @return [String] the prefix for the default instrumentation store passed
|
37
|
+
# to {.instrumentation_prefix}
|
38
|
+
def instrumentation_prefix
|
39
|
+
self.class.instrumentation_prefix.try_call(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Object]
|
43
|
+
# @see UltraMarathon::Instrumentation::Store#instrument
|
44
|
+
def instrument(*args, &block)
|
45
|
+
instrumentations.instrument(*args, &block)
|
23
46
|
end
|
24
47
|
end
|
25
48
|
end
|
@@ -1,19 +1,25 @@
|
|
1
|
+
require 'ultra_marathon/instrumentation/profile'
|
1
2
|
module UltraMarathon
|
2
3
|
module Instrumentation
|
3
4
|
class Profile
|
5
|
+
DATETIME_FORMAT = '%H:%M:%S:%L'.freeze
|
6
|
+
RAW_TIME_FORMAT = '%02d:%02d:%02d:%03d'.freeze
|
4
7
|
attr_reader :name, :start_time, :end_time
|
5
8
|
|
6
9
|
## Public Instance Methods
|
7
10
|
|
11
|
+
# @param name [String] name of the instrumented block
|
12
|
+
# @param block [Proc] block to be instrumented
|
8
13
|
def initialize(name, &block)
|
9
14
|
@name = name
|
10
15
|
# Ruby cannot marshal procs or lambdas, so we need to define a method.
|
11
|
-
# Binding to self allows us to intercept logging calls.
|
12
16
|
define_singleton_method :instrumented_block do
|
13
17
|
block.call
|
14
18
|
end
|
15
19
|
end
|
16
20
|
|
21
|
+
# Sets {#start_time}, runs the initialized block, then sets {#end_time}
|
22
|
+
# @return [Object] the return value of the initialized block
|
17
23
|
def call
|
18
24
|
@start_time = Time.now
|
19
25
|
begin
|
@@ -24,31 +30,54 @@ module UltraMarathon
|
|
24
30
|
return_value
|
25
31
|
end
|
26
32
|
|
27
|
-
#
|
33
|
+
# @return [Float] the total time in seconds to the nanosecond
|
28
34
|
def total_time
|
29
|
-
|
35
|
+
@total_time ||= end_time - start_time
|
30
36
|
end
|
31
37
|
|
38
|
+
# @return [String] {#total_time} formatted per {RAW_TIME_FORMAT}
|
32
39
|
def formatted_total_time
|
33
|
-
|
34
|
-
seconds = (duration % 60).floor
|
35
|
-
minutes = (duration / 60).floor
|
36
|
-
hours = (duration / 3600).floor
|
37
|
-
sprintf(TIME_FORMAT, hours, minutes, seconds)
|
40
|
+
format_seconds(total_time)
|
38
41
|
end
|
39
42
|
|
43
|
+
# @return [String] {#start_time} formatted per {DATETIME_FORMAT}
|
40
44
|
def formatted_start_time
|
41
45
|
format_time(start_time)
|
42
46
|
end
|
43
47
|
|
48
|
+
# @return [String] {#end_time} formatted per {DATETIME_FORMAT}
|
44
49
|
def formatted_end_time
|
45
50
|
format_time(end_time)
|
46
51
|
end
|
47
52
|
|
53
|
+
# Comparison delegated to {#total_time}
|
54
|
+
# @param other_profile [Profile]
|
55
|
+
# @return [Integer] {#total_time} <=> other_profile.total_time
|
56
|
+
def <=>(other_profile)
|
57
|
+
total_time <=> other_profile.total_time
|
58
|
+
end
|
59
|
+
|
60
|
+
# Profiles are considered equal if their names are `eql?`
|
61
|
+
# @param other_profile [Profile]
|
62
|
+
# @return [Boolean] delegates to {#name}
|
63
|
+
def eql?(other_profile)
|
64
|
+
name.eql? other_profile.name
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
48
69
|
## Private Instance Methods
|
49
70
|
|
71
|
+
def format_seconds(total_seconds)
|
72
|
+
seconds = (total_seconds % 60).floor
|
73
|
+
minutes = (total_seconds / 60).floor
|
74
|
+
hours = (total_seconds / 3600).floor
|
75
|
+
milliseconds = (total_seconds - total_seconds.to_i) * 1000.0
|
76
|
+
sprintf(RAW_TIME_FORMAT, hours, minutes, seconds, milliseconds)
|
77
|
+
end
|
78
|
+
|
50
79
|
def format_time(time)
|
51
|
-
|
80
|
+
time.strftime(DATETIME_FORMAT)
|
52
81
|
end
|
53
82
|
end
|
54
83
|
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'set'
|
2
|
+
module UltraMarathon
|
3
|
+
module Instrumentation
|
4
|
+
class Store < SortedSet
|
5
|
+
attr_reader :options
|
6
|
+
|
7
|
+
# @param new_members [Array, Set]
|
8
|
+
# @param options [Hash]
|
9
|
+
# @option options [String] :prefix ('') will prefix the name of every
|
10
|
+
# name passed into {#instrument}
|
11
|
+
def initialize(new_members=[], options={})
|
12
|
+
super(new_members)
|
13
|
+
@options = {
|
14
|
+
prefix: ''
|
15
|
+
}.merge(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Instruments given block, setting its start time and end time
|
19
|
+
# Stores the resulting profile in in itself
|
20
|
+
# @param name [String] name of the instrumented block
|
21
|
+
# @param block [Proc] block to instrument
|
22
|
+
# @return [Object] return value of the instrumented block
|
23
|
+
def instrument(name, &block)
|
24
|
+
profile = Profile.new(full_name(name), &block)
|
25
|
+
return_value = profile.call
|
26
|
+
self.add(profile)
|
27
|
+
return_value
|
28
|
+
end
|
29
|
+
|
30
|
+
# The passed in prefix
|
31
|
+
# @return [String]
|
32
|
+
def prefix
|
33
|
+
options[:prefix]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Access a profile by name. Instrumentations shouldn't have to know about
|
37
|
+
# their fully qualified name, so the unprefixed version should be passed
|
38
|
+
# @param name [String] name of the profile that was passed to
|
39
|
+
# {#instrument}
|
40
|
+
# @return [UltraMarathon::Instrumentation::Profile, nil]
|
41
|
+
def [](name)
|
42
|
+
full_name = full_name(name)
|
43
|
+
detect do |profile|
|
44
|
+
profile.name == full_name
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Accumulated total time for all stored profiles
|
49
|
+
# @return [Float]
|
50
|
+
def total_time
|
51
|
+
total_times.reduce(0.0, :+)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Float] the mean time for all profiles
|
55
|
+
def mean_runtime
|
56
|
+
total_time / size
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [UltraMarthon::Instrumentation::Profile] the profile in the
|
60
|
+
# middle of the pack per
|
61
|
+
# {UltraMarthon::Instrumentation::Profile#total_time}
|
62
|
+
def median
|
63
|
+
to_a[size / 2]
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# Please forgive me Mr. Brooks, I had to Google it
|
68
|
+
# @return [Float] the standard deviation from the mean
|
69
|
+
def standard_deviation
|
70
|
+
sum_of_squares = total_times.reduce(0) do |sum, total_time|
|
71
|
+
sum + (mean_runtime - total_time) ** 2
|
72
|
+
end
|
73
|
+
Math.sqrt(sum_of_squares / size)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Adds all profiles from the other_store
|
77
|
+
# @param other_store [UltraMarathon::Instrumentation::Profile]
|
78
|
+
# @return [self] the other_store
|
79
|
+
def merge!(other_store)
|
80
|
+
other_store.each do |member|
|
81
|
+
add(member)
|
82
|
+
end
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
## Private Instance Methods
|
89
|
+
|
90
|
+
# Adds the prefix to the name
|
91
|
+
# @param name [String] the raw name
|
92
|
+
# @return [String] the prefixed name
|
93
|
+
def full_name(name)
|
94
|
+
"#{prefix}#{name}"
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [Array<Float>] the total times for all stored profiles
|
98
|
+
def total_times
|
99
|
+
map(&:total_time)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|