When Your Fat Models Need to Go on a Diet, Part 2

In part 1 of this post, I talked about using modules to help break source code of large models into smaller, comprehensible pieces. As Ariel Valentin pointed out, we need to do more to lighten up the runtime memory footprint when large models are instantiated.

Let’s imagine we have an application with a User model (might not be a stretch) and we’re building a page that loads a bunch of user gravatars. Our User model has accumulated a ton of attributes, but we may only need a limited set to render the gravatar properly. We might add a line like this in the controller action to select only the needed attributes:

@users = User.select(%w[ id name gravatar_id ]).all

Perhaps to make this more readable, we’ll extract our select statement into a scope in the User model:

# user.rb
scope :select_gravatar_attributes, select(%w[ id name gravatar_id ])

# controller action
@users = User.select_gravatar_attributes.all

We’ll incur less memory overhead during this action at runtime since the app won’t have to load as much data. The caveat is that our @users won’t have access to the data we left behind, so we need to take care only to call methods that rely on the data we have actually loaded. It would be easier to test our lighter users if we could treat them as a different type.

 

Applying the model-slimming refactoring we discussed in part 1, we can extract gravatar related functionality into a UserExtensions::Gravatar module and create a Gravatar::User that inherits from our User model. Furthermore, we can change our select_gravatar_attributes to a default_scope in the subclass, so whenever we grab Gravatar::User from the db, it’ll have only the attributes we need. Our controller action would now look like this:

@users = Gravatar::User.all

To put our savings to the test, we can run some basic benchmark tests. I created a sample rails 3.1 app and created ~2000 users records in my development database and ran the rails benchmarker script to load all users both normally and as gravatars. Here are the results:

All users

All as gravatars

The memory footprint for gravatar users is half of that for our ‘fat’ users. Obviously results will vary greatly depending on architecture, application and the attributes selected, but this anecdotal case demonstrates potential for some big wins in instantiating slimmer subclasses of your heavy models.

Posted Monday, October 3rd, 2011 under Performance, Ruby.

3 comments

  1. So the main idea of this article is to select only attributes that you need? We all know that, something else?

  2. You could also explore using DCI (Data-Context-I don’t remember what the F ;-) ) to extract behavior from the classes based on the Role the class is serving at the time. This behavior can then be mixed into instances at runtime on an as-needed basis. It has the nifty side-effect of allowing the controller code to neatly document the role that the object is acting in at the time of the controller action.

    See https://github.com/rubypair/rubypair/commit/00ade397c66a7f0e4ebb8163aec0256b5e6d4e77 for an example refactoring where ugly controller logic was extracted into a DCI-based model logic. Another awesome side effect: these mixins can run in super-fast tests (though this is also true with the approach that you proposed above).

    We talked about this idea at length at Ruby DCamp this year, initially led by Jim Gay. He’s written some good posts introducing the concepts here (http://saturnflyer.com/blog/jim/2011/09/28/4-simple-steps-extending-ruby-objects-the-tip-of-the-iceberg-with-dci/) and here (http://saturnflyer.com/blog/jim/2011/10/04/oop-dci-and-ruby-what-your-system-is-vs-what-your-system-does/)

  3. Good thoughts on DCI, Evan. I’m interested to look into this more. Speedier tests sounds like a big win. Seems like developers need to take extra care to apply all the needed roles for a given context. Without having played with it much yet, I’m curious about the overhead of extending objects in process during a request, especially when pulling back larger sets.

Leave a Reply