~ read.

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
end  

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  
  Money.new total_cents
end  

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
    total
  end
end

def total  
  Money.new total_cents
end  

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  
  invoice_items.sum(:price_cents)
end  

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
      end
    end

In the rails console I added the following Benchmark code:

require 'benchmark'

 Benchmark.bm do |benchmark|
      benchmark.report('Invoice#total_cents_second') do
        Invoice.all.each do |invoice|
            invoice.total_cents_second
        end
      end
      benchmark.report('Invoice#total_cents') do
        Invoice.all.each do |invoice|
            invoice.total_cents
        end
      end
    end

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.

Conclusion

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.