The performance_promise
gem enables you to annotate and validate the performance of your Rails actions.
You can declare the performance characteristics of your Rails actions in code (right next to the action definition itself), and performance_promise
will monitor and validate the promise. If the action breaks the promise, the performance_promise
gem will alert you and provide a helpful suggestion with a passing performance annotation.
Example syntax:
class ArticlesController < ApplicationController
Performance :makes => 1.query,
:full_table_scans => [Article]
def index
# ...
end
Performance :makes => 1.query + Article.N.queries,
:full_table_scans => [Article, Comment]
def expensive_action
# ...
end
end
You may also choose to enable a default/minimum performance promise for all actions by turning on the untagged_methods_are_speedy
config parameter (see here). You can reap all the benefits of performance validation without having to make any changes to code except tagging expensive actions.
You can install it as a gem:
gem install performance_promise
or add it to a Gemfile (Bundler):
group :development, :test do
gem 'performance_promise'
end
You can build the gem yourself:
gem build performance_promise.gemspec
For safety, performance_promise
is disabled by default. To enable it, create a new file config/initializers/performance_promise.rb
with the following code:
require 'performance_promise/performance.rb'
PerformancePromise.configure do |config|
config.enable = true
# config.validations = [
# :makes, # validate the number of DB queries made
# ]
# config.untagged_methods_are_speedy = true
# config.speedy_promise = {
# :makes => 2,
# }
# config.allowed_environments = [
# 'development',
# 'test',
# ]
# config.logger = Rails.logger
# config.throw_exception = true
end
PerformancePromise.start
To understand how to use performance_promise
, let's use a simple Blog App. A Blog
has Article
s, each of which may have one or more Comment
s.
Here is a simple controller:
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
end
Assuming your routes and views are setup, you should be able to successfully visit /articles
.
You can annotate this action with a promise of how many database queries the action will make so:
class ArticlesController < ApplicationController
Performance :makes => 1.query
def index
@articles = Article.all
end
end
Visit /articles
to confirm that the view is rendered successfully again.
Now suppose, you make the view more complex, causing it to execute more database queries
Performance :makes => 1.query
def index
@articles = Article.all
@total_comments = 0
@articles.each do |article|
@total_comments += article.comments.length
end
puts @total_comments
end
Since the performance annotation has not been updated, visiting /articles
now will throw an exception. The exception tells you that the performance of your view does not respect the annotation promise.
Let's update the annotation:
Performance :makes => 1.query + Article.N.queries
def index
@articles = Article.all
@total_comments = 0
@articles.each do |article|
@total_comments += article.comments.length
end
puts @total_comments
end
Now that you have annotated the action correctly, visiting /articles
renders successfully.
The experienced code-reviewer however might ask the author to get rid of the N + 1
query here, and use .includes
instead:
Performance :makes => 2.queries
def index
@articles = Article.all.includes(:comments)
@total_comments = 0
@articles.each do |article|
@total_comments += article.comments.length
end
puts @total_comments
end
And now, we've successfully caught and averted a bad code commit!
performance_promise
opens up more functionality through configuration variables:
By default, performance_promise
runs only in development
and testing
. This ensures that you can identify issues when developing or running your test-suite. Be very careful about enabling this in production
– you almost certainly don't want to.
Tells performance_promise
whether to throw an exception. Set to true
by default, but can be overridden if you simply want to ignore failing cases (they will still be written to the log).
If you do not care to determine the exact performance of your action, you can still simply mark it as Speedy
:
Speedy()
def index
...
end
A Speedy
action is supposed to be well behaved, making lesser than x
database queries, and taking less than y
to complete. You can set these defaults using this configuration parameter.
By default, actions that are not annotated aren't validated by performance_promise
. If you'd like to force all actions to be validated, one option is to simply default them all to be Speedy
. This allows developers to make no change to their code, while still reaping the benefits of performance validation. If a view fails to be Speedy
, then the developer is forced to acknowledge it in code.
What is the strange syntax? Is it a function call? Is it a method?
We borrow the coding style from Python's decorators
. This style allows for a function to be wrapped by another. This is a great use case for that style since it allows for us to express the annotation right above the function definition.
Credit goes to Yehuda Katz for the port of decorators into Ruby.
Will this affect my production service?
By default, performance_promise
is applied only in development
and test
environments. You can choose to override this, but is strongly discouraged.
What are some other kinds of performance guarantees that I can make with
performance_promise
?
In addition to promises about the number of database queries, you can also make promises on how long the entire view will take to render, and whether it performs any table scans.
Performance :makes => 1.query + Article.N.queries,
:takes => 1.second,
:full_tables_scans => [Article]
def index
...
end
If you come up with other validations that you think will be useful, please consider sharing it with the community by writing your own plugin here, and raising a Pull Request.
Is this the same as Bullet gem?
Bullet is a great piece of software that allows developers to help identify N + 1 queries and unused eager loading. It does this by watching your application in development mode, and alerting you when it does either of those things.
performance_promise
can be tuned to not only identify N + 1 queries, but can also alert whenever there's any change in performance. It allows you to identify expensive actions irrespective of their database query profile.
performance_promise
also has access to the entire database query object. In the future, performance_promise
can be tuned to perform additional checks like how long the most expensive query took, whether the action performed any table scans (available through an EXPLAIN
) etc.
Finally, the difference between bullet
and performance_promise
is akin to testing by refreshing your browser and testing by writing specs. performance_promise
encourages you to specify your action's performance by declaring it in code itself. This allows both code-reviewers as well as automated tests to verify your code's performance.