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.
@@ -1,10 +1,11 @@
1
1
  require 'set'
2
2
  require 'active_support/concern'
3
- require 'core_ext/proc'
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 &&= call_proc_or_symbol(options[:if])
19
+ conditions_met &&= contexticute(options[:if])
19
20
  elsif options.key? :unless
20
- conditions_met &&= !call_proc_or_symbol(options[:unless])
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
- # E.g.
77
+ # @param callback_name [String, Symbol]
78
+ # @example
79
+ # add_callbacks_accessor(:after_save)
117
80
  #
118
- # def self.after_save_callbacks
119
- # @after_save_callbacks ||= []
120
- # end
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
- call_proc_or_symbol(callback, args, options)
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
- TIME_FORMAT = "%02d:%02d:%02d"
7
+ extend ActiveSupport::Concern
8
+ # Creates a default UltraMarathon::Instrumentation::Store and stores
9
+ # all instrumented profiles there
6
10
 
7
- ## Public Instance Methods
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 ||= Hash.new
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
- # Instruments given block, setting its start time and end time
18
- # Returns the result of the block
19
- def instrument(name, &block)
20
- profile = Profile.new(name, &block)
21
- instrumentations[name] = profile
22
- profile.call
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
- # returns the total time, in seconds
33
+ # @return [Float] the total time in seconds to the nanosecond
28
34
  def total_time
29
- (end_time - start_time).to_i
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
- duration = total_time
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
- sprintf(TIME_FORMAT, time.hour, time.min, time.sec)
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