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.