How do I track down a memory leak in my Ruby code?
Asked Answered
B

4

34

Question

I'm debugging a memory leak in a rake task. I want to see a call stack of:

  • Living objects
  • What object or line originally allocated those objects

Is this possible with ruby-prof?

If not, what tool should I use?

Setup

Gems

Rake task

  • Imports a CSV file directly into a MySql database using DATA LOAD INFILE and Active Record objects.

What I've Tried

I've tried the modes

  • RubyProf::ALLOCATIONS
  • RubyProf::MEMORY

All it says in the documentation is:

RubyProf::ALLOCATIONS Object allocation reports show how many objects each method in a program allocates.

RubyProf::MEMORY Memory usage reports show how much memory each method in a program uses.

This implies that ruby-prof just reports on the total allocation of objects, not just the ones that are living.

I've tried Ruby-Mass and Bloat Check but neither seem to be able to do what I want. Ruby-Mass also crashes because it's finding FactoryGirl objects in memory for some reason...

Brokaw answered 6/1, 2014 at 18:9 Comment(4)
First question: Are you using any gems that have known leak problems like ImageMagick / RMagick?Lovell
I'm using EventBus and activerecord-fast-import within the rake task. I'm also running Rails 3.2.16. I've got a lot of other gems installed but none that I'm running from the rake task.Brokaw
You can always try calling GC.start once in a while to see if that liberates some memory. It makes your app slower but the memory footprint is usually considerably lower.Lovell
@Lovell I've already done this quite a bit - it reduces memory a tiny bit but it still seems to be holding onto a lot somewhere...Brokaw
L
40

I did not find ruby-prof very useful when it came to locating memory leaks, because you need a patched Ruby interpreter. Tracking object allocation has become easier in Ruby 2.1. Maybe it is the best choice to explore this yourself.

I recommend the blog post Ruby 2.1: objspace.so by tmml who is one of the Ruby core developers. Basically you can fetch a lot of information while debugging your application:

ObjectSpace.each_object{ |o| ... }
ObjectSpace.count_objects #=> {:TOTAL=>55298, :FREE=>10289, :T_OBJECT=>3371, ...}

require 'objspace'
ObjectSpace.memsize_of(o) #=> 0 /* additional bytes allocated by object */
ObjectSpace.count_tdata_objects #=> {Encoding=>100, Time=>87, RubyVM::Env=>17, ...}
ObjectSpace.count_nodes #=> {:NODE_SCOPE=>2, :NODE_BLOCK=>688, :NODE_IF=>9, ...}
ObjectSpace.reachable_objects_from(o) #=> [referenced, objects, ...]
ObjectSpace.reachable_objects_from_root #=> {"symbols"=>..., "global_tbl"=>...} /* in 2.1 */

With Ruby 2.1 you can even start to track allocation of new objects and gather metadata about every new object:

require 'objspace'
ObjectSpace.trace_object_allocations_start

class MyApp
  def perform
    "foobar"
  end
end

o = MyApp.new.perform
ObjectSpace.allocation_sourcefile(o) #=> "example.rb"
ObjectSpace.allocation_sourceline(o) #=> 6
ObjectSpace.allocation_generation(o) #=> 1
ObjectSpace.allocation_class_path(o) #=> "MyApp"
ObjectSpace.allocation_method_id(o)  #=> :perform

Use pry and pry-byebug and start exploring the memory heap where you think it will probably grow, respectively try different segments in your code. Before Ruby 2.1 I always relied on ObjectSpace.count_objects and calculated the result's difference, to see if one object type grows in particularly.

The garbage collection works properly when the number of objects growing are retested back to a much smaller amount during the iterations as opposed to keep growing. The garbage collector should run all the time anyway, you can reassure yourself by looking into the Garbage Collector statistics.

From my experience this is either String or Symbol (T_STRING). Symbols before ruby 2.2.0 were not garbage collected so make sure your CSV or parts of it is not converted into symbols on the way.

If you do not feel comfortable, try to run your code on the JVM with JRuby. At least the memory profiling is a lot better supported with tools like VisualVM.

Latrell answered 6/1, 2014 at 18:43 Comment(3)
fyi I just blogged about this as well samsaffron.com/archive/2015/03/31/…Roman
TIL: Symbols are not garbage collectedMiltiades
Symbols are GC'd starting with Ruby 2.2Lulualaba
O
5

Consider the memory_profiler gem for use with Ruby 2.1.

Outsider answered 10/11, 2014 at 15:41 Comment(0)
E
5

To save time you can check a list of Ruby gems that have memory leaks first. https://github.com/ASoftCo/leaky-gems

Emendation answered 23/10, 2015 at 16:13 Comment(0)
F
3

There is a ruby-mass gem, which provides a nice api over ObjectSpace.

One of the ways to approach the problem is to check references after you've done with your object.

object = ...
# more logic
puts Mass.references(object)

If there is at least one reference, the object is not garbage collected and you need to figure out how to drop that reference. For example:

object.instance_variable_set("@example", nil)

# or

ObjectSpace.each_object(Your::Object::Class::Name).each do |obj|
  obj.instance_variable_set("@example", nil)
end
Fibrosis answered 24/10, 2014 at 6:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.