From 4f37b97033f596ec2c95eb53e9964e051c224981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 8 Sep 2009 10:10:14 -0500 Subject: Changed ActiveRecord to use new callbacks and speed up observers by only notifying events that are actually being consumed. Signed-off-by: Joshua Peek --- activesupport/lib/active_support/new_callbacks.rb | 99 ++++++++++++----------- 1 file changed, 53 insertions(+), 46 deletions(-) (limited to 'activesupport/lib/active_support/new_callbacks.rb') diff --git a/activesupport/lib/active_support/new_callbacks.rb b/activesupport/lib/active_support/new_callbacks.rb index 56b510d52e..61651caa09 100644 --- a/activesupport/lib/active_support/new_callbacks.rb +++ b/activesupport/lib/active_support/new_callbacks.rb @@ -15,17 +15,17 @@ module ActiveSupport # end # # class ConfigStorage < Storage - # save_callback :before, :saving_message + # set_callback :save, :before, :saving_message # def saving_message # puts "saving..." # end # - # save_callback :after do |object| + # set_callback :save, :after do |object| # puts "saved" # end # # def save - # _run_save_callbacks do + # _run_set_callback :save,s do # puts "- save" # end # end @@ -47,24 +47,24 @@ module ActiveSupport # # define_callbacks :save # - # save_callback :before, :prepare + # set_callback :save, :before, :prepare # def prepare # puts "preparing save" # end # end # # class ConfigStorage < Storage - # save_callback :before, :saving_message + # set_callback :save, :before, :saving_message # def saving_message # puts "saving..." # end # - # save_callback :after do |object| + # set_callback :save, :after do |object| # puts "saved" # end # # def save - # _run_save_callbacks do + # _run_set_callback :save,s do # puts "- save" # end # end @@ -78,6 +78,7 @@ module ActiveSupport # saving... # - save # saved + # module NewCallbacks def self.included(klass) klass.extend ClassMethods @@ -242,7 +243,7 @@ module ActiveSupport # Options support the same options as filters themselves (and support # symbols, string, procs, and objects), so compile a conditional # expression based on the options - def _compile_options(options) + def _compile_options(options) return [] if options[:if].empty? && options[:unless].empty? conditions = [] @@ -259,6 +260,7 @@ module ActiveSupport end # Filters support: + # # Arrays:: Used in conditions. This is used to specify # multiple conditions. Used internally to # merge conditions from skip_* filters @@ -269,6 +271,7 @@ module ActiveSupport # # All of these objects are compiled into methods and handled # the same after this point: + # # Arrays:: Merged together into a single filter # Symbols:: Already methods # Strings:: class_eval'ed into methods @@ -276,6 +279,7 @@ module ActiveSupport # Objects:: # a method is created that calls the before_foo method # on the object. + # def _compile_filter(filter) method_name = "_callback_#{@kind}_#{next_id}" case filter @@ -329,14 +333,18 @@ module ActiveSupport def compile(key = nil, options = {}) method = [] + method << "value = nil" method << "halted = false" each do |callback| method << callback.start(key, options) end - method << "yield self if block_given? && !halted" + method << "value = yield if block_given? && !halted" + # TODO Make each and reverse each part of the callbacks definition. + # TODO Make halted on after part of the callbacks definition. reverse_each do |callback| method << callback.end(key, options) end + method << "halted ? false : (block_given? ? value : true)" method.compact.join("\n") end @@ -345,22 +353,21 @@ module ActiveSupport chain.push(*map {|c| c.clone(klass)}) end end - + module ClassMethods - CHAINS = {:before => :before, :around => :before, :after => :after} - - # Make the _run_save_callbacks method. The generated method takes + # Make the _run_set_callback :save method. The generated method takes # a block that it'll yield to. It'll call the before and around filters # in order, yield the block, and then run the after filters. # - # _run_save_callbacks do + # _run_set_callback :save,s do # save # end # - # The _run_save_callbacks method can optionally take a key, which + # The _run_set_callback :save,s method can optionally take a key, which # will be used to compile an optimized callback method for each # key. See #define_callbacks for more information. - def _define_runner(symbol) + # + def __define_runner(symbol) #:nodoc: body = send("_#{symbol}_callbacks"). compile(nil, :terminator => send("_#{symbol}_terminator")) @@ -370,7 +377,7 @@ module ActiveSupport name = "_run__\#{self.class.name.hash.abs}__#{symbol}__\#{key.hash.abs}__callbacks" unless respond_to?(name) - self.class._create_keyed_callback(name, :#{symbol}, self, &blk) + self.class.__create_keyed_callback(name, :#{symbol}, self, &blk) end send(name, &blk) @@ -387,32 +394,39 @@ module ActiveSupport # This is called the first time a callback is called with a particular # key. It creates a new callback method for the key, calculating # which callbacks can be omitted because of per_key conditions. - def _create_keyed_callback(name, kind, obj, &blk) + # + def __create_keyed_callback(name, kind, obj, &blk) #:nodoc: @_keyed_callbacks ||= {} @_keyed_callbacks[name] ||= begin str = send("_#{kind}_callbacks"). compile(name, :object => obj, :terminator => send("_#{kind}_terminator")) class_eval "def #{name}() #{str} end", __FILE__, __LINE__ - + true end end - + + def __update_callbacks(name, filters = CallbackChain.new(name), block = nil) + type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before + options = filters.last.is_a?(Hash) ? filters.pop : {} + filters.unshift(block) if block + + callbacks = send("_#{name}_callbacks") + yield callbacks, type, filters, options if block_given? + + __define_runner(name) + end + # Define callbacks. # - # Creates a _callback method that you can use to add callbacks. - # # Syntax: - # save_callback :before, :before_meth - # save_callback :after, :after_meth, :if => :condition - # save_callback :around {|r| stuff; yield; stuff } - # - # The _callback method also updates the _run__callbacks - # method, which is the public API to run the callbacks. + # set_callback :save, :before, :before_meth + # set_callback :save, :after, :after_meth, :if => :condition + # set_callback :save, :around {|r| stuff; yield; stuff } # - # Also creates a skip__callback method that you can use to skip - # callbacks. + # It also updates the _run__callbacks method, which is the public + # API to run the callbacks. Use skip_callback to skip any defined one. # # When creating or skipping callbacks, you can specify conditions that # are always the same for a given key. For instance, in ActionPack, @@ -430,21 +444,9 @@ module ActiveSupport # In that case, each action_name would get its own compiled callback # method that took into consideration the per_key conditions. This # is a speed improvement for ActionPack. - def _update_callbacks(name, filters = CallbackChain.new(name), block = nil) - type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before - options = filters.last.is_a?(Hash) ? filters.pop : {} - filters.unshift(block) if block - - callbacks = send("_#{name}_callbacks") - yield callbacks, type, filters, options if block_given? - - _define_runner(name) - end - - alias_method :_reset_callbacks, :_update_callbacks - + # def set_callback(name, *filters, &block) - _update_callbacks(name, filters, block) do |callbacks, type, filters, options| + __update_callbacks(name, filters, block) do |callbacks, type, filters, options| filters.map! do |filter| # overrides parent class callbacks.delete_if {|c| c.matches?(type, filter) } @@ -456,7 +458,7 @@ module ActiveSupport end def skip_callback(name, *filters, &block) - _update_callbacks(name, filters, block) do |callbacks, type, filters, options| + __update_callbacks(name, filters, block) do |callbacks, type, filters, options| filters.each do |filter| callbacks = send("_#{name}_callbacks=", callbacks.clone(self)) @@ -471,6 +473,11 @@ module ActiveSupport end end + def reset_callbacks(symbol) + send("_#{symbol}_callbacks").clear + __define_runner(symbol) + end + def define_callbacks(*symbols) terminator = symbols.pop if symbols.last.is_a?(String) symbols.each do |symbol| @@ -480,7 +487,7 @@ module ActiveSupport CallbackChain.new(symbol) end - _define_runner(symbol) + __define_runner(symbol) end end end -- cgit v1.2.3 From 2ea1d684d93bd59887a9fd12e647941f0d1f4868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 8 Sep 2009 10:22:45 -0500 Subject: Refactor new callbacks and AR implementation. Signed-off-by: Joshua Peek --- activesupport/lib/active_support/new_callbacks.rb | 114 +++++++++++----------- 1 file changed, 58 insertions(+), 56 deletions(-) (limited to 'activesupport/lib/active_support/new_callbacks.rb') diff --git a/activesupport/lib/active_support/new_callbacks.rb b/activesupport/lib/active_support/new_callbacks.rb index 61651caa09..ce08ea8660 100644 --- a/activesupport/lib/active_support/new_callbacks.rb +++ b/activesupport/lib/active_support/new_callbacks.rb @@ -83,18 +83,18 @@ module ActiveSupport def self.included(klass) klass.extend ClassMethods end - + def run_callbacks(kind, options = {}, &blk) send("_run_#{kind}_callbacks", &blk) end - + class Callback @@_callback_sequence = 0 - - attr_accessor :filter, :kind, :name, :options, :per_key, :klass - def initialize(filter, kind, options, klass) - @kind, @klass = kind, klass - + + attr_accessor :name, :filter, :kind, :options, :per_key, :klass + + def initialize(name, filter, kind, options, klass) + @name, @kind, @klass = name, kind, klass normalize_options!(options) @per_key = options.delete(:per_key) @@ -105,9 +105,10 @@ module ActiveSupport _compile_per_key_options end - + def clone(klass) obj = super() + obj.name = name obj.klass = klass obj.per_key = @per_key.dup obj.options = @options.dup @@ -115,36 +116,39 @@ module ActiveSupport obj.per_key[:unless] = @per_key[:unless].dup obj.options[:if] = @options[:if].dup obj.options[:unless] = @options[:unless].dup + obj.options[:scope] = @options[:scope].dup obj end - + def normalize_options!(options) options[:if] = Array.wrap(options[:if]) options[:unless] = Array.wrap(options[:unless]) + options[:scope] ||= [:kind] + options[:scope] = Array.wrap(options[:scope]) + options[:per_key] ||= {} options[:per_key][:if] = Array.wrap(options[:per_key][:if]) options[:per_key][:unless] = Array.wrap(options[:per_key][:unless]) end - + def next_id @@_callback_sequence += 1 end - + def matches?(_kind, _filter) - @kind == _kind && - @filter == _filter + @kind == _kind && @filter == _filter end def _update_filter(filter_options, new_options) filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless) filter_options[:unless].push(new_options[:if]) if new_options.key?(:if) end - + def recompile!(_options, _per_key) _update_filter(self.options, _options) _update_filter(self.per_key, _per_key) - + @callback_id = next_id @filter = _compile_filter(@raw_filter) @compiled_options = _compile_options(@options) @@ -165,14 +169,13 @@ module ActiveSupport # contents for after filters (for the forward pass). def start(key = nil, options = {}) object, terminator = (options || {}).values_at(:object, :terminator) - return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?") - + terminator ||= false - + # options[0] is the compiled form of supplied conditions # options[1] is the "end" for the conditional - + if @kind == :before || @kind == :around if @kind == :before # if condition # before_save :filter_name, :if => :condition @@ -184,7 +187,7 @@ module ActiveSupport halted = (#{terminator}) end RUBY_EVAL - + [@compiled_options[0], filter, @compiled_options[1]].compact.join("\n") else # Compile around filters with conditions into proxy methods @@ -201,7 +204,7 @@ module ActiveSupport # yield self # end # end - + # name = "_conditional_callback_#{@kind}_#{next_id}" txt, line = <<-RUBY_EVAL, __LINE__ + 1 def #{name}(halted) @@ -224,9 +227,8 @@ module ActiveSupport # before filters (for the backward pass). def end(key = nil, options = {}) object = (options || {})[:object] - return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?") - + if @kind == :around || @kind == :after # if condition # after_save :filter_name, :if => :condition # filter_name @@ -238,27 +240,28 @@ module ActiveSupport end end end - + private + # Options support the same options as filters themselves (and support # symbols, string, procs, and objects), so compile a conditional # expression based on the options def _compile_options(options) return [] if options[:if].empty? && options[:unless].empty? - + conditions = [] - + unless options[:if].empty? conditions << Array.wrap(_compile_filter(options[:if])) end - + unless options[:unless].empty? conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"} end - + ["if #{conditions.flatten.join(" && ")}", "end"] end - + # Filters support: # # Arrays:: Used in conditions. This is used to specify @@ -298,10 +301,11 @@ module ActiveSupport @klass.send(:define_method, "#{method_name}_object") { filter } _normalize_legacy_filter(kind, filter) + method_to_call = @options[:scope].map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_") @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{method_name}(&blk) - #{method_name}_object.send(:#{kind}, self, &blk) + #{method_name}_object.send(:#{method_to_call}, self, &blk) end RUBY_EVAL @@ -322,34 +326,40 @@ module ActiveSupport end end end - end # An Array with a compile method class CallbackChain < Array - def initialize(symbol) + attr_reader :symbol, :config + + def initialize(symbol, config) @symbol = symbol + @config = config end - - def compile(key = nil, options = {}) + + def compile(key=nil, options={}) + options = config.merge(options) + method = [] method << "value = nil" method << "halted = false" + each do |callback| method << callback.start(key, options) end + method << "value = yield if block_given? && !halted" - # TODO Make each and reverse each part of the callbacks definition. - # TODO Make halted on after part of the callbacks definition. + reverse_each do |callback| method << callback.end(key, options) end + method << "halted ? false : (block_given? ? value : true)" method.compact.join("\n") end - + def clone(klass) - chain = CallbackChain.new(@symbol) + chain = CallbackChain.new(@symbol, @config.dup) chain.push(*map {|c| c.clone(klass)}) end end @@ -359,7 +369,7 @@ module ActiveSupport # a block that it'll yield to. It'll call the before and around filters # in order, yield the block, and then run the after filters. # - # _run_set_callback :save,s do + # _run_set_callback :save do # save # end # @@ -368,14 +378,13 @@ module ActiveSupport # key. See #define_callbacks for more information. # def __define_runner(symbol) #:nodoc: - body = send("_#{symbol}_callbacks"). - compile(nil, :terminator => send("_#{symbol}_terminator")) + body = send("_#{symbol}_callbacks").compile(nil) body, line = <<-RUBY_EVAL, __LINE__ def _run_#{symbol}_callbacks(key = nil, &blk) if key name = "_run__\#{self.class.name.hash.abs}__#{symbol}__\#{key.hash.abs}__callbacks" - + unless respond_to?(name) self.class.__create_keyed_callback(name, :#{symbol}, self, &blk) end @@ -386,7 +395,7 @@ module ActiveSupport end end RUBY_EVAL - + undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks") class_eval body, __FILE__, line end @@ -398,16 +407,13 @@ module ActiveSupport def __create_keyed_callback(name, kind, obj, &blk) #:nodoc: @_keyed_callbacks ||= {} @_keyed_callbacks[name] ||= begin - str = send("_#{kind}_callbacks"). - compile(name, :object => obj, :terminator => send("_#{kind}_terminator")) - + str = send("_#{kind}_callbacks").compile(name, :object => obj) class_eval "def #{name}() #{str} end", __FILE__, __LINE__ - true end end - def __update_callbacks(name, filters = CallbackChain.new(name), block = nil) + def __update_callbacks(name, filters = CallbackChain.new(name, {}), block = nil) type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before options = filters.last.is_a?(Hash) ? filters.pop : {} filters.unshift(block) if block @@ -446,11 +452,10 @@ module ActiveSupport # is a speed improvement for ActionPack. # def set_callback(name, *filters, &block) - __update_callbacks(name, filters, block) do |callbacks, type, filters, options| + __update_callbacks(name, filters, block) do |callbacks, type, filters, options| filters.map! do |filter| - # overrides parent class callbacks.delete_if {|c| c.matches?(type, filter) } - Callback.new(filter, type, options.dup, self) + Callback.new(name, filter, type, options.merge(callbacks.config), self) end options[:prepend] ? callbacks.unshift(*filters) : callbacks.push(*filters) @@ -461,7 +466,6 @@ module ActiveSupport __update_callbacks(name, filters, block) do |callbacks, type, filters, options| filters.each do |filter| callbacks = send("_#{name}_callbacks=", callbacks.clone(self)) - filter = callbacks.find {|c| c.matches?(type, filter) } if filter && options.any? @@ -479,12 +483,10 @@ module ActiveSupport end def define_callbacks(*symbols) - terminator = symbols.pop if symbols.last.is_a?(String) + config = symbols.last.is_a?(Hash) ? symbols.pop : {} symbols.each do |symbol| - extlib_inheritable_accessor("_#{symbol}_terminator") { terminator } - extlib_inheritable_accessor("_#{symbol}_callbacks") do - CallbackChain.new(symbol) + CallbackChain.new(symbol, config) end __define_runner(symbol) -- cgit v1.2.3