GraphQL Performance Testing (Part 2): Updating Rails Models

When last we left off, we had just created a rails api site, set up the models and added some sample data generation.

You can download the github repo and checkout the Part2 branch to begin with this article.
_(any file paths references are all relative to the rails\api folder)

Now, we need to make sure we update the Rails models so we have the ability to get totals for Invoice and InvoiceItem

Since an Invoice is made up of many items, let's start with the items first

Calculating totals for invoice items

We have the amount and price_cents fields from the database. Creating a total is as easy as multiplying the two.

# app/models/invoice_item.rb

def total_cents
  amount * price_cents

It's worth noting that we are using cents here to avoid any inconsistencies with floating point math. This will not matter much for performance. But, it is a valid consideration in a real-world app. It also gives us a chance to evaluate values calculated by the application layer, rather simply pulling values from the database.

Since we added the rails-money gem during setup, we should also add a method that returns a money object for easy manipulation or currency conversion. By convention, this should have the same method name without the _cents suffix.

# app/models/invoice_item.rb

def total total_cents

Calculating totals for invoices

Draft 1: Loop over the collection

For our first draft, we can simply use the reduce method to loop over the invoice items collection and add everything together

# app/models/invoice.rb

def total_cents
  invoice_items.reduce(0) do |total, item|
    total + item.total_cents

def total total_cents

For a single record, this works reasonably fast, but it is making a database request for each record. This adds ~0.1 - 0.3ms for each record. Let's see if we can do better.

Second Draft

# app/models/invoice.rb

def total_cents_second

The difference with this method is that we are doing the summation in the database, rather than in the application layer.

Surprisingly, the results are about the same. The second draft still adds ~0.1 - 0.3ms for each record. I suspect that rails 6 has included enhancements under the hood.

Time to test the difference!

Benchmarking our first drafts

First, I added another 2,000 records in order to get a good statistical sample, using the same code for setting up the initial data. I also increased the possible number of generated invoice items from 25 to 250.

2000.times do
      invoice = Invoice.create date: Faker::Date.in_date_period(month: 12),
                               number: Faker::IDNumber.spanish_citizen_number,
                               creator: User.order(Arel.sql('RANDOM()')).first

      # Invoice Items
      (15..250).to_a.sample.times do
        invoice.invoice_items.create amount: (5..250).to_a.sample,
                                     description: Faker::Hipster.sentence(word_count: 3),
                                     price_cents: (25..250_000).to_a.sample

In the rails console I added the following Benchmark code:

require 'benchmark' do |benchmark|'Invoice#total_cents_second') do
        Invoice.all.each do |invoice|
      end'Invoice#total_cents') do
        Invoice.all.each do |invoice|

The Result:

| method                     | real     | stime    | utime    | total    |
| Invoice#total_cents        | 9.646610 | 0.384991 | 8.691589 | 9.076581 |
| Invoice#total_cents_second | 1.147491 | 0.099628 | 0.824540 | 0.924168 |

comparing total times shows the second draft to be about 9.8 times faster

clearing the cache, quitting the console and running the tests in the opposite order resulted in a similar result

| method                     | real      | stime    | utime    | total    |
| Invoice#total_cents        | 10.072655 | 0.383990 | 9.031934 | 9.415925 |
| Invoice#total_cents_second | 1.68221   | 0.143680 | 1.175744 | 1.319424 |

The total time went up for the second draft. But it's still about 7 times faster under load.


After some performance testing. It seems preferable to go with the second draft of the method.

You can check out the final version in the Part2-Final branch of this repository

In the next installment, we'll actually create the GraphQL API to take advantage of our models.