In our Rails application we started to experience serious memory issues. Suddenly.

Well, suddenly once our user base increased and usage pattern changed

1st I thought of memory leaks. Started to look around, found Memory Leak profiling with Rails and Ruby Closures and Memory Usage.

Playing around with MemoryProfiler and method dispatch in class Dispatcher

class Dispatcher

  class << self
    def dispatch(cgi = nil, session_options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
      controller = nil
      if cgi ||= new_cgi(output)
        request, response = ActionController::CgiRequest.new(cgi, session_options), ActionController::CgiResponse.new(cgi)
        prepare_application
        controller = ActionController::Routing::Routes.recognize(request)
        controller.process(request, response).out(output)
      end
    rescue Exception => exception  # errors from CGI dispatch
      failsafe_response(output, '500 Internal Server Error', exception) do
        controller ||= ApplicationController rescue LoadError nil
        controller ||= ActionController::Base
        controller.process_with_exception(request, response, exception).out(output)
      end
    ensure
      # Do not give a failsafe response here.
      reset_after_dispatch
    end
like
       # controller.process(request, response).out(output)
        controller.process(request, response)
        controller.process(request, response)
        controller.process(request, response).out(output)

I noticed that 3 instance of controller stays in memory. Well, thats odd. So I decided to look into these instances. To do so I wrote simple controller and view

Controller

class Admin::LeakController < Admin::BaseController
  include ActionView::Helpers::TextHelper
  def index
    ENV['FIRST_LEAK'] ||= self.__id__.to_s + ' '+self.to_s + ' '+ params.inspect
    @object_ids = []
    GC.start
    ObjectSpace.each_object do |o|
      if o.kind_of?(ApplicationController)
        if o.__id__ != self.__id__
          @object_ids << o.__id__
        end
      end
    end
    render :template => 'admin/leak/index'
  end

  def big_fat
    @big_fat = "qwertyuio" * 50.megabyte
    return index
  end
  
  def nil_my_internal
    GC.start
    if params[:id].to_i != self.object_id
      o = ObjectSpace._id2ref(params[:id].to_i) rescue nil
      if o && o.instance_variables.include?(params[:name].to_s)
        o.instance_variable_set(params[:name],nil)
      end
    end
    GC.start
    return index
  end

  def inspect_me
    if params[:id].to_i != self.object_id && o = ObjectSpace._id2ref(params[:id].to_i) rescue nil
      render :text => '<pre>' + o.__id__.to_s + ' ' +CGI.escapeHTML(truncate(o.inspect,250.kilobytes).gsub(",",",\n")) +'</pre>'
    else
      render :nothing => true
    end
  end
  
  def klass_in_object_space
    if params[:klass]
      obj_ids = {}
      ObjectSpace.each_object do |o|
        next if o.__id__ == self.__id__
        next unless o.class.to_s == params[:klass]
        obj_ids[o.__id__] = CGI.escapeHTML(truncate(o.inspect,250.kilobytes).gsub(",",",\n"))
      end
      text = '<pre>' + obj_ids.keys.sort.map{|k| "#{k} => #{obj_ids[k]}"}  * "<hr/>" + "</pre>"

      render :text => text
    else
      render :nothing => true
    end
  end

    def strings_in_object_space
      obj_ids = {}
      ObjectSpace.each_object do |o|
        next if o.__id__ == self.__id__
        next unless o.class.to_s == 'String'
        obj_ids[o.__id__] = o.size
      end
      text = '<pre>' + obj_ids.keys.sort.map{|k| "#{k} => #{obj_ids[k]}"}  * "<hr/>" + "</pre>"
      render :text => text
  end

  
  def object_space
    GC.start
    h = Hash.new(0)
    ObjectSpace.each_object do |o|
      next if o.__id__ == self.__id__
      h[o.class.to_s] += 1
    end
    render :text => '<pre>' + h.keys.sort_by{|k| h[k]}.reverse.map{|k| CGI.escapeHTML(k) + " (" + h[k].to_s + ")" } * "<br/>" + '</pre>'
  end
  
  def ar_object_space
    GC.start
    h = Hash.new(0)
    ObjectSpace.each_object do |o|
      next if o.__id__ == self.__id__
      next unless o.kind_of? ActiveRecord::Base
      h[o.class.to_s] += 1
    end
    render :text => '<pre>' + h.keys.sort_by{|k| h[k]}.reverse.map{|k| CGI.escapeHTML(k) + " (" + h[k].to_s + ")" } * "<br/>" + '</pre>'
  end
  
end

View

Rails <%= Rails::VERSION::STRING %><br/>
First processing controller <%=h ENV['FIRST_LEAK'] %><br/>
<br/>
<form action="/admin/leak/nil_my_internal" method="get">
object_id <input type=text name='id' size=10> variable name <input type=text name='name' size=10> <input type=submit value='clean'>
</form>
<br/>
leak url_for <%= link_to 'index', :action => :index, :controller => 'admin/leak' %> <%= link_to '50mb', :action => :big_fat, :controller => :leak %> <%= link_to 'Strings', :action => 'strings_in_object_space', :controller => 'admin/leak' %><br/>
<br/>
Current self <%=h controller.__id__ %> <%=h controller %></br><br/>
ObjectSpace Count
<%= link_to 'Space', url_for(:action => 'object_space'), :target => 'space'  %><br/>
<%= link_to 'AR Space', url_for(:action => 'ar_object_space'), :target => 'ar_space'  %><br/>
<hr/>
Controllers in ObjectSpace<br/>
<%for o_id in @object_ids.sort do -%>
Object id <%= o_id%> <%= link_to_function 'Hide/Show', "$('#{o_id}').toggle();"%> <br/>
<div id="<%= o_id %>" style="display:none;">
<% o = ObjectSpace._id2ref(o_id.to_i) rescue next -%>
 instance <%=h o%><br/>
 controller params <%=h o.params.inspect %><br/>
 controller instance variables
 instance variables<br/>
 <% o.instance_variables.each do |iv| %>
 <%= iv %> <%= (oiv = o.instance_variable_get(iv)).__id__ %> <%=h oiv.class %><br/>
 <% end %>
</div>
<%end -%>
<br/>
<hr/>
<form action="/admin/leak/inspect_me" method="get" target='inspect'>
object_id <input type=text name='id' size=10> <input type=submit value='Inspect'><input type=reset>
</form>
<hr/>
<form action="/admin/leak/klass_in_object_space" method="get" target='klass'>
Klass <input type=text name='klass' size=50> <input type=submit value='view inspects'><input type=reset>
</form>
  1. Start application in production mode
  2. Hit any page where AR object loaded in controller instance variables
  3. Check the AR space in Leak controller

Well, there is plenty of AR object there. If you happens to load @account, and @account.transactions and number of transactions is 500 for that @account, all 500 transaction in the memory. However if you load @account and in the view @account.transaction.find(:all) and then iterate or pass it to partial - they are not out there

My conlcusion:

  1. Keep controller instance variables lean
  2. If you dont have to use instance variables - don't use them
  3. If you have to look into the habtm collection, and you don't need this collection in the view - use find(:all), if you need collection through the method life cycle, load it into local scope variable and deal with it
  4. If you need the size of collection - use @ar.collection.count, not @ar.collection.size. Who knows haw many elements in that collection and when woudl be a next time this method called for @ar with smaller collection size
  5. ....

P.S.

I found useful to patch vendor/rails/activesupport/lib/active_support/deprecation.rb to look into rails utensils
===================================================================
--- trunk/vendor/rails/activesupport/lib/active_support/deprecation.rb	2008-05-13 21:34:20 UTC (rev 5118)
+++ trunk/vendor/rails/activesupport/lib/active_support/deprecation.rb	2008-05-13 21:55:02 UTC (rev 5119)
@@ -147,7 +147,12 @@
     # Stand-in for @request, @attributes, @params, etc which emits deprecation
     # warnings on any method call (except #inspect).
     class DeprecatedInstanceVariableProxy #:nodoc:
-      instance_methods.each { |m| undef_method m unless m =~ /^__/ }
+      instance_methods.each { |m| 
+        if m =~ /^(?:__|class|kind_of|inspect|is_a|to_s)/
+          next
+        end
+        undef_method m unless m =~ /^__/ 
+      }
 
       def initialize(instance, method, var = "@#{method}")
         @instance, @method, @var = instance, method, var

Another patch to ActiveRecord (v1.2.3) to make it less aggressive in loading habtm on class/kind_of/is_a/inspect and save after ar.collection.find(:whatever)
Index: activerecord/lib/active_record/associations/association_proxy.rb
===================================================================
--- activerecord/lib/active_record/associations/association_proxy.rb    (revision 5121)
+++ activerecord/lib/active_record/associations/association_proxy.rb    (working copy)
@@ -5,7 +5,11 @@
       alias_method :proxy_respond_to?, :respond_to?
       alias_method :proxy_extend, :extend
       delegate :to_param, :to => :proxy_target
-      instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_)/ }
+      if ENV[:ROR_MEMLEAK.to_s]
+        instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^kind_of\?$|^class$|^is_a?$|^inspect$)/ }
+      else
+        instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_)/ }
+      end

       def initialize(owner, reflection)
         @owner, @reflection = owner, reflection
Index: activerecord/lib/active_record/associations/belongs_to_association.rb
===================================================================
--- activerecord/lib/active_record/associations/belongs_to_association.rb      (revision 5121)
+++ activerecord/lib/active_record/associations/belongs_to_association.rb      (working copy)
@@ -1,6 +1,9 @@
 module ActiveRecord
   module Associations
     class BelongsToAssociation < AssociationProxy #:nodoc:
+      if ENV[:ROR_MEMLEAK.to_s]
+        instance_methods.each { |m| undef_method m if m =~ /(^kind_of\?$|^class$|^is_a?$|^inspect$)/ }
+      end
       def create(attributes = {})
         replace(@reflection.klass.create(attributes))
       end
Index: activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
===================================================================
--- activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb   (revision 5121)
+++ activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb   (working copy)
@@ -1,6 +1,9 @@
 module ActiveRecord
   module Associations
     class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc:
+      if ENV[:ROR_MEMLEAK.to_s]
+        instance_methods.each { |m| undef_method m if m =~ /(^kind_of\?$|^class$|^is_a?$|^inspect$)/ }
+      end
       def replace(record)
         if record.nil?
           @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil
Index: activerecord/lib/active_record/associations.rb
===================================================================
--- activerecord/lib/active_record/associations.rb      (revision 5121)
+++ activerecord/lib/active_record/associations.rb      (working copy)
@@ -981,11 +981,11 @@

           after_callback = <<-end_eval
             association = instance_variable_get("@#{association_name}")
-
+            records_to_save=[]
             if association.respond_to?(:loaded?)
               if @new_record_before_save
                 records_to_save = association
-              else
+              elsif association.loaded?
                 records_to_save = association.select { |record| record.new_record? }
               end
               records_to_save.each { |record| association.send(:insert_record, record) }