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>
- Start application in production mode
- Hit any page where AR object loaded in controller instance variables
- 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:
- Keep controller instance variables lean
- If you dont have to use instance variables - don't use them
- 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
- 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
- ....
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) }