ultra_marathon 0.1.7 → 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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