Copy
My favorite hot take of the week: you don't understand how to write tests well, which is why they're so full of tech debt.
One metric that you should be paying more attention to: the number of Ruby objects allocated per transaction.

This number tells you how many objects are allocated during every web response or background job (NewRelic uses the word "transaction" to describe one run of a controller action or one execution of a background job). In general, most Ruby programmers never really think about object allocation because, well, that's why we chose to write in a memory-managed and garbage-collected language right - I don't want to have to think about memory!

And you still don't have to think about memory - until things get out of hand.

In my experience of working with hundreds of clients, most Rails applications allocate between 25,000 and 75,000 objects per transaction.

If you are using NewRelic, this metric is available in the "Ruby VM" tab, in the left hand menu. Other tools do not provide an average across the entire application, but may provide different metrics. In Skylight, click any transaction and then click the "allocations" tab. In Scout, similarly, a global "average objects per transaction" is also unavailable, but per-trace allocation counts are available and also "maximum allocations" per action is also available.

Rails 6, recently released, includes a new feature where object allocation counts will be logged for web responses, like this (output from BigBinary's blog on the topic):

Started GET "/articles" for ::1 at 2019-04-15 17:24:09 +0530
Processing by ArticlesController#index as HTML
  Rendering articles/index.html.erb within layouts/application
  Rendered shared/_ad_banner.html.erb (Duration: 0.1ms | Allocations: 6)
  Article Load (1.3ms)  SELECT "articles".* FROM "articles"
  ↳ app/views/articles/index.html.erb:5
  Rendered collection of articles/_article.html.erb [100 times] (Duration: 6.1ms | Allocations: 805)
  Rendered articles/index.html.erb within layouts/application (Duration: 17.6ms | Allocations: 3901)
Completed 200 OK in 86ms (Views: 83.6ms | ActiveRecord: 1.3ms | Allocations: 29347)


However, these new numbers can't be relied upon if you're using Puma with multiple threads in production.

Underneath, the feature is implemented using GC.stat. GC.stat is a hash of statistics related to the garbage collector and the current state of the ObjectSpace. I wrote about this a pretty lengthy blog here.

However, GC.stat isn't "thread-safe" - that is, what's happening in one thread will affect the state of GC.stat from the perspective of another thread. Scout, Skylight and NewRelic get around this by using their own thread-safe C or Rust extensions to measure object allocation.

In any case, I wouldn't pay too much attention to object allocation numbers in ordinary development work, when the app is running in "development mode". In dev mode, you're creating all kinds of objects that you don't in production: asset pipeline stuff, dev-mode-only gems like Bullet and better-errors, etc etc. It's not uncommon to be creating 2-5x as many objects in development. Instead, pay attention to what is happening in production.

So, why do we care about the number of objects allocated? There are two reasons: time and total memory usage.

One thing that hasn't gotten much faster in Ruby since Ruby 2.0 is how long it takes to allocate an object. Allocating most objects results in calls to the memory allocator, which is not super zippy. An excessive amount of object allocations means that you could speed up the app by reducing those - either by finding a way to do the same work with fewer objects, or just by doing something else entirely.

As for total memory usage, controller actions or background jobs that allocate extremely high numbers of objects (1 million or more) increase total process memory usage. This is due to a ratcheting mechanism in the memory allocator (described in detail on my blog) that causes Ruby's memory usage over time to be roughly equal to how many objects it needs at *any particular instant*. Reducing high "max allocation" controller actions or background jobs always tends to reduce total memory usage in terms of RSS (resident set size), which usually means we can fit more processes onto a machine or server.

So, what do you once you've figured out you have a problem with the number of allocated objects? Dig in with the memory_profiler gem and figure out what's causing it. More tools and process here can be found in The Complete Guide to Rails Performance. Also, my frequent collaborator Richard Schneeman and co-worker Caleb Thompson gave a great talk at Railsconf this year on this very topic.

Until next time, Rubyists!

-Nate
You can share this email with this permalink: https://mailchi.mp/railsspeed/one-ruby-performance-metric-you-should-be-paying-attention-to?e=[UNIQID]

Copyright © 2019 Nate Berkopec, All rights reserved.


Want to change how you receive these emails?
You can update your preferences or unsubscribe from this list.