Reconstructing Ruby, Part 9: Memory leaks

Read Part 8 in case you missed it.

Right now our ruby program is leaking memory and we should do something about it. In order to figure out what to do, we should use a tool to help us diagnose where we have leaks. One such tool for the job is Valgrind, but unfortunately for Mac users, Valgrind won't work.

That's were Docker is going to come in. Docker will allow us to run one-off commands in a virtual machine. So the idea here will be to setup a Docker image with Valgrind and then run our program through it. The first step is to install Docker from the page above and then if you're on OSX install boot2docker. After installing boot2docker, make sure you run the following commands:

$ boot2docker init
$ boot2docker start
$ $(boot2docker shellinit)

Now we're ready to create our Docker image. First create a file called Dockerfile and put the following into it:

FROM debian
RUN apt-get update && apt-get install --no-install-recommends -y build-essential flex bison valgrind
WORKDIR /usr/src/ruby

That will provide us with all the tools we need. Now to build the image, run the following:

$ docker build -t ruby .

Now we can run Valgrind. To make it easy, add the following to the Makefile:

check:
     docker run -v `pwd`:/usr/src/ruby ruby bash -c "make clean && make && valgrind --leak-check=full --show-reachable=yes ./ruby program.rb"

For the purposes of these memory checks, your program.rb should only contain the digit 1 and nothing else. We'll add more later to catch some additional memory leaks.

This command mounts our current directory on the Docker image and then runs make clean, make, and Valgrind. If we run make check now, we see the following important lines:

definitely lost: 11 bytes in 1 blocks
...
still reachable: 17,026 bytes in 4 blocks

Let's fix the "definitely lost" ones first. If we look at the output you should see something like this:

==25== 11 bytes in 1 blocks are definitely lost in loss record 2 of 5
==25==    at 0x4C28BED: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==25==    by 0x4EAF8E1: strdup (strdup.c:43)
==25==    by 0x400C42: main (in /usr/src/ruby/ruby)

Here we can see that strdup was called from main and it's value is never freed, but it would be nice to have more debugging information. Let's change our Makefile to help us out a bit. Replace:

cc -o ruby ${SRC}

with:

cc -O0 -g -o ruby ${SRC}

The two flags help provide debug information (the second flag is the letter O and the number 0):

   -O0 Means "no optimization": this level compiles the fastest and generates the most debuggable code.
 -g  Generate debug information.  Note that Clang debug information works best at -O0.

Using the -g flag will dump a folder of debug information into ruby.dSYM. You'll want to add the following to your .gitignore:

ruby.dSYM

Now when we run make check we get much more helpful output:

==25== 11 bytes in 1 blocks are definitely lost in loss record 2 of 5
==25==    at 0x4C28BED: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==25==    by 0x4EAF8E1: strdup (strdup.c:43)
==25==    by 0x400C42: main (main.c:14)

We now know our line leaking memory is on main.c:14

state.source_file = strdup(argv[1]);  

In this case, the strdup is unnecessary and can be removed.

The next memory leak we'll look at is this one:

==26== 568 bytes in 1 blocks are still reachable in loss record 3 of 4
==26==    at 0x4C28BED: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==26==    by 0x4E972AA: __fopen_internal (iofopen.c:76)
==26==    by 0x400C53: main (main.c:15)

And our violating line is:

yyin = fopen(argv[1], "r");  

We've opened a file, but we've failed to close it. We'll fix this by adding the following to main.c after yyparse is called:

if(argc > 1) {  
  fclose(yyin);
}

The next three errors are a bit trickier. Let's take a look at one of them:

==25== 8 bytes in 1 blocks are still reachable in loss record 1 of 3
==25==    at 0x4C28BED: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==25==    by 0x40312E: yyalloc (lex.yy.c:2020)
==25==    by 0x402C3B: yyensure_buffer_stack (lex.yy.c:1717)
==25==    by 0x401404: yylex (lex.yy.c:758)
==25==    by 0x400F46: yyparse (parse.tab.c:1364)
==25==    by 0x400CB6: main (main.c:18)

Here we can see that after we call yyparse from main, it calls yylex. The meaning of this is that we haven't freed up some of the memory the lexer has used. In order to fix this, we need to tell the lexer we're done. We'll do this by adding two things. With our other externs in main, add the following:

extern int yylex_destroy(void);  

and then right before the return add this:

yylex_destroy();  

Now when we run make check we come back all clear. But we are leaking memory elsewhere. Let's change our program.rb to cover all of our cases. Make you program.rb look like this:

class Foo  
  def initialize(arg1)
    x = arg1
    x.length          
  end
end

Foo.new("bar")  
Foo.new('bar')  

When we run make check again we see 4 more memory leaks. If you look in ruby.l you'll notice we have 4 calls to strdup. We'll need to free this memory. This is going to take a bit of work, which we'll end up undoing later, but it's a good exercise for the time being. Every line we have a tID, tCONSTANT, and tSTRING and free it. For example, change this line:

| tCONSTANT tDOT tID tLPAREN tSTRING tRPAREN

to:

| tCONSTANT tDOT tID tLPAREN tSTRING tRPAREN {
  free($<sval>1);
  free($<sval>3);
  free($<sval>5);
}

This is a way of typecasting our values. There is however an easier way of handling this. If we define our tokens to have a particular type then we won't need to typecast it later. Up where we've defined our tokens extract out the following:

%token <sval> tSTRING tCONSTANT tID

In which case we can write our previous line as follows:

| tCONSTANT tDOT tID tLPAREN tSTRING tRPAREN { 
  free($1);
  free($3);
  free($5);
}

If we do the same to all the other declarations with a tSTRING, tCONSTANT, and tID and then run make check we'll see we've eliminated all other memory issues. We'll want to run our make check frequently to make sure we don't introduce any memory leaks in our implementation.

At this point our next steps will be to generate an AST of nodes from our grammar.

If you're having any problems you can check the reference implementation on GitHub or look specifically at the diff to see what I've changed. Additionally, if you have any comments or feedback I'd greatly appreciate if you left a comment!