Hello,

I've been trying to track-down a pretty significant memory bloat issue with 
ActiveRecord objects. Running some generic experiments e.g. an empty model 
with 6 enum_fields, created_at, updated_at - and fetching 72000 of these 
with ActiveRecord from a Postgres Database allocates approximately 120mb of 
memory or (1747 bytes per object) - measured using Instruments.On the other 
hand fetching the same 72000 objects into a list of hashes directly with 
Ruby-PG is only about 10mb of memory.

One particular thing I found which was surprising is that enum is not any 
more efficient than string. This appears to be because the Postgresql 
Adapter for ActiveRecord uses TypeMapAllString on the RubyPG level and than 
performs all of the TypeMapping in Ruby. 
<https://github.com/rails/rails/blob/master/activerecord%2Flib%2Factive_record%2Fconnection_adapters%2Fpostgresql%2Fdatabase_statements.rb#L168>
  
<https://github.com/rails/rails/blob/master/activerecord%2Flib%2Factive_record%2Fconnection_adapters%2Fpostgresql%2Fdatabase_statements.rb#L168>This
 
seems to be grossly inefficient since Ruby-PG has support for both basic 
and custom type-mapping on the C-level.Here is some notable output from the 
memory_profiler gem - this is for *only* 729 objects.


1617  "0"
1458  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.3/lib/
active_record/connection_adapters/postgresql/database_statements.rb:169
159  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.3/lib/
active_record/connection_adapters/postgresql/oid/type_map_initializer.rb:
161459  "2"
1459  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.3/lib/
active_record/connection_adapters/postgresql/database_statements.rb:1691459 
 "1"
1459  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.3/lib/
active_record/connection_adapters/postgresql/database_statements.rb:169729  
"_initialize_callbacks"
729  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.3/lib/
active_support/callbacks.rb:81729  "_find_callbacks"
729  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.3/lib/
active_support/callbacks.rb:81729  "initialize"
729  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.3/lib/
active_support/callbacks.rb:81729  "find"
729  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.3/lib/
active_support/callbacks.rb:81505  "\n"
262  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.3/lib/
active_record/relation.rb:17
87  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.3/lib/
active_record/relation/delegation.rb:15
42  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.3/lib/
active_record/relation/delegation.rb:16
34  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.3/lib/
active_record/connection_adapters/abstract/database_statements.rb:228
31  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.3/lib/
active_support/dependencies.rb:274
20  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.3/lib/
active_record/relation.rb:654
20  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.3/lib/
active_record/connection_adapters/postgresql/database_statements.rb:169
 7  /Users/user_name/.rvm/gems/ruby-2.2.2/gems/activerecord-4.2.3/lib/
active_record/relation/delegation.rb:17
 2  /Users/user_name/.rvm/rubies/ruby-2.2.2/lib/ruby/2.2.0/mutex_m.rb:41


It seems to me that potentially two improvements could be made here:

a) Freezing the callback strings. While "_initialize_callbacks" and 
"_find_callbacks" are dynamically generated strings I think the variations 
are quite few in number, using hash lookup for this could be a large cost 
saver when fetching multiple objects.

b) Switching from TypeMapAllString in the ActiveRecord Postgres Adapter to 
native C type-mapping in Ruby-PG. 

I have experimented with a patch for Freezing the callback strings 
(attached). It may not be very nice but perhaps still preferable to 
dynamically generating the callback strings per object.

P.S. I have only about ~3 months of experience with Ruby, so I am quite apt 
to have some serious misunderstandings, please take it easy on me :) 

-- 
You received this message because you are subscribed to the Google Groups "Ruby 
on Rails: Core" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/rubyonrails-core.
For more options, visit https://groups.google.com/d/optout.
diff --git a/lib/active_support/callbacks.rb b/lib/active_support/callbacks.rb
index 1e98547..4006beb 100644
--- a/lib/active_support/callbacks.rb
+++ b/lib/active_support/callbacks.rb
@@ -77,8 +77,13 @@ module ActiveSupport
     #   run_callbacks :save do
     #     save
     #   end
+
+    # Store a frozen copy of a callback string so it doesn't need to be
+    # allocated and de-allocated for every call
+    @@_callbacks_store = {}
+
     def run_callbacks(kind, &block)
-      callbacks = send("_#{kind}_callbacks")
+      callbacks = send(callbacks_store(kind))
 
       if callbacks.empty?
         yield if block_given?
@@ -91,6 +96,10 @@ module ActiveSupport
 
     private
 
+    def callbacks_store(kind)
+      @@_callbacks_store[kind] ? @@_callbacks_store[kind] : @@_callbacks_store[kind]  = "_#{kind}_callbacks".freeze
+    end
+
     # A hook invoked every time a before callback is halted.
     # This can be overridden in AS::Callback implementors in order
     # to provide better debugging/logging.
@@ -764,6 +773,7 @@ module ActiveSupport
       #
       # NOTE: +method_name+ passed to `define_model_callbacks` must not end with
       # `!`, `?` or `=`.
+
       def define_callbacks(*names)
         options = names.extract_options!
 

Reply via email to