h1. Bullet The Bullet plugin/gem is designed to help you increase your application's performance by reducing the number of queries it makes. It will watch your queries while you develop your application and notify you when you should add eager loading (N+1 queries), when you're using eager loading that isn't necessary and when you should use counter cache. Best practice is to use Bullet in development mode or custom mode (staging, profile, etc.). The last thing you want is your clients getting alerts about how lazy you are. The Bullet plugin/gem now supports rails 2.1, 2.2, 2.3 and 3.0. **************************************************************************** h2. Install You can install it as a gem:
gem install bullet
or add it into a Gemfile (Bundler):
gem "bullet", :group => "development"
**************************************************************************** h2. Configuration Bullet won't do ANYTHING unless you tell it to explicitly. Append toconfig/environments/development.rb
initializer with the following code:The notifier of bullet is a wrap of "uniform_notifier":https://github.com/flyerhzm/uniform_notifier The code above will enable all six of the Bullet notification systems: *config.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.console = true Bullet.growl = true Bullet.xmpp = { :account => 'bullets_account@jabber.org', :password => 'bullets_password_for_jabber', :receiver => 'your_account@jabber.org', :show_online_status => true } Bullet.rails_logger = true Bullet.disable_browser_cache = true end
Bullet.enable
: enable Bullet plugin/gem, otherwise do nothing *Bullet.alert
: pop up a JavaScript alert in the browser *Bullet.bullet_logger
: log to the Bullet log file (Rails.root/log/bullet.log) *Bullet.rails_logger
: add warnings directly to the Rails log *Bullet.console
: log warnings to your browser's console.log (Safari/Webkit browsers or Firefox w/Firebug installed) *Bullet.growl
: pop up Growl warnings if your system has Growl installed. Requires a little bit of configuration *Bullet.xmpp
: send XMPP/Jabber notifications to the receiver indicated. Note that the code will currently not handle the adding of contacts, so you will need to make both accounts indicated know each other manually before you will receive any notifications. If you restart the development server frequently, the 'coming online' sound for the bullet account may start to annoy - in this case set :show_online_status to false; you will still get notifications, but the bullet account won't announce it's online status anymore. *Bullet.disable_browser_cache
: disable browser cache which usually causes unexpected problems **************************************************************************** h2. Log The Bullet loglog/bullet.log
will look something like this: * N+1 Query:The first two lines are notifications that N+1 queries have been encountered. The remaining lines are stack traces so you can find exactly where the queries were invoked in your code, and fix them. * Unused eager loading:2009-08-25 20:40:17[INFO] N+1 Query: PATH_INFO: /posts; model: Post => associations: [comments]· Add to your finder: :include => [:comments] 2009-08-25 20:40:17[INFO] N+1 Query: method call stack:· /Users/richard/Downloads/test/app/views/posts/index.html.erb:11:in `_run_erb_app47views47posts47index46html46erb' /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each' /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb' /Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
These two lines are notifications that unused eager loadings have been encountered. * Need counter cache:2009-08-25 20:53:56[INFO] Unused eager loadings: PATH_INFO: /posts; model: Post => associations: [comments]· Remove from your finder: :include => [:comments]
**************************************************************************** h2. Growl Support To get Growl support up-and-running for Bullet, follow the steps below: * Install the ruby-growl gem:2009-09-11 09:46:50[INFO] Need Counter Cache Post => [:comments]
gem install ruby-growl
* Open the Growl preference pane in Systems Preferences * Click the "Network" tab * Make sure both "Listen for incoming notifications" and "Allow remote application registration" are checked. *Note*: If you set a password, you will need to setBullet.growl_password = { :password => 'growl password' }
in the config file. * Restart Growl ("General" tab -> Stop Growl -> Start Growl) * Boot up your application. Bullet will automatically send a Growl notification when Growl is turned on. If you do not see it when your application loads, make sure it is enabled in your initializer and double-check the steps above. **************************************************************************** h2. Ruby 1.9 issue ruby-growl gem has an issue about md5 in ruby 1.9, if you use growl and ruby 1.9, check this gist http://gist.github.com/300184 **************************************************************************** h2. XMPP/Jabber Support To get XMPP support up-and-running for Bullet, follow the steps below: * Install the xmpp4r gem:sudo gem install xmpp4r
* Make both the bullet and the receipient account add each other as contacts. This will require you to manually log into both accounts, add each other as contact and confirm each others contact request. * Boot up your application. Bullet will automatically send an XMPP notification when XMPP is turned on. **************************************************************************** h2. Important If you find bullet does not work for you, *please disable your browser's cache*. **************************************************************************** h2. Advance The bullet plugin/gem use rack middleware for http request. If you want to bullet for without http server, such as job server. You can do like this:Or you want to use it in test modeBullet.start_request if Bullet.enable? # run job if Bullet.enable? Bullet.growl_notification Bullet.log_notification('JobServer: ') Bullet.end_request end
Don't forget enabling bullet in test environment. **************************************************************************** h2. Links * "http://weblog.rubyonrails.org/2009/10/22/community-highlights":http://weblog.rubyonrails.org/2009/10/22/community-highlights * "http://ruby5.envylabs.com/episodes/9-episode-8-september-8-2009":http://ruby5.envylabs.com/episodes/9-episode-8-september-8-2009 * "http://railslab.newrelic.com/2009/10/23/episode-19-on-the-edge-part-1":http://railslab.newrelic.com/2009/10/23/episode-19-on-the-edge-part-1 **************************************************************************** h2. Contributors sriedel did a lot of awesome refactors, added xmpp notification support, and added Hacking.textile about how to extend to bullet plugin. flipsasser added Growl, console.log and Rails.log support, very awesome. And he also improved README. rainux added group style console.log. 2collegebums added some great specs to generate red bar. **************************************************************************** h2. Step by step example Bullet is designed to function as you browse through your application in development. It will alert you whenever it encounters N+1 queries or unused eager loading. 1. setup test environmentbefore(:each) Bullet.start_request if Bullet.enable? end after(:each) if Bullet.enable? Bullet.perform_out_of_channel_notifications Bullet.end_request end end
2. change$ rails test $ cd test $ script/rails g scaffold post name:string $ script/rails g scaffold comment name:string post_id:integer $ rake db:migrate
app/model/post.rb
andapp/model/comment.rb
3. go toclass Post < ActiveRecord::Base has_many :comments end class Comment < ActiveRecord::Base belongs_to :post end
script/rails c
and execute4. change thepost1 = Post.create(:name => 'first') post2 = Post.create(:name => 'second') post1.comments.create(:name => 'first') post1.comments.create(:name => 'second') post2.comments.create(:name => 'third') post2.comments.create(:name => 'fourth')
app/views/posts/index.html.erb
to produce a N+1 query5. add bullet gem to<% @posts.each do |post| %>
<% end %> <%= post.name %> <%= post.comments.collect(&:name) %> <%= link_to 'Show', post %> <%= link_to 'Edit', edit_post_path(post) %> <%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %> Gemfile
And rungem "bullet", "2.0.0.beta.2"
6. enable the bullet plugin in development, add a line tobundle install
config/environments/development.rb
7. start serverconfig.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.console = true # Bullet.growl = true Bullet.rails_logger = true Bullet.disable_browser_cache = true end
8. input http://localhost:3000/posts in browser, then you will see a popup alert box says$ script/rails s
which means there is a N+1 query from post object to comments associations. In the meanwhile, there's a log appended intoThe request has unused preload associations as follows: None The request has N+1 queries as follows: model: Post => associations: [comment]
log/bullet.log
fileThe generated SQLs are2010-03-07 14:12:18[INFO] N+1 Query in /posts Post => [:comments] Add to your finder: :include => [:comments] 2010-03-07 14:12:18[INFO] N+1 Query method call stack /home/flyerhzm/NetBeansProjects/test_bullet2/app/views/posts/index.html.erb:14:in `_render_template__600522146_80203160_0' /home/flyerhzm/NetBeansProjects/test_bullet2/app/views/posts/index.html.erb:11:in `each' /home/flyerhzm/NetBeansProjects/test_bullet2/app/views/posts/index.html.erb:11:in `_render_template__600522146_80203160_0' /home/flyerhzm/NetBeansProjects/test_bullet2/app/controllers/posts_controller.rb:7:in `index'
9. fix the N+1 query, changePost Load (1.0ms) SELECT * FROM "posts" Comment Load (0.4ms) SELECT * FROM "comments" WHERE ("comments".post_id = 1) Comment Load (0.3ms) SELECT * FROM "comments" WHERE ("comments".post_id = 2)
app/controllers/posts_controller.rb
file10. refresh http://localhost:3000/posts page, no alert box and no log appended. The generated SQLs aredef index @posts = Post.includes(:comments) respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end
a N+1 query fixed. Cool! 11. now simulate unused eager loading. ChangePost Load (0.5ms) SELECT * FROM "posts" Comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE ("comments".post_id IN (1,2))
app/controllers/posts_controller.rb
andapp/views/posts/index.html.erb
def index @posts = Post.includes(:comments) respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end
12. refresh http://localhost:3000/posts page, then you will see a popup alert box says<% @posts.each do |post| %>
<% end %> <%= post.name %> <%= link_to 'Show', post %> <%= link_to 'Edit', edit_post_path(post) %> <%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %> In the meanwhile, there's a log appended intoThe request has unused preload associations as follows: model: Post => associations: [comment] The request has N+1 queries as follows: None
log/bullet.log
file13. simulate counter_cache. Change2009-08-25 21:13:22[INFO] Unused preload associations: PATH_INFO: /posts; model: Post => associations: [comments]· Remove from your finder: :include => [:comments]
app/controllers/posts_controller.rb
andapp/views/posts/index.html.erb
def index @posts = Post.all respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end
14. refresh http://localhost:3000/posts page, then you will see a popup alert box says<% @posts.each do |post| %>
<% end %> <%= post.name %> <%= post.comments.size %> <%= link_to 'Show', post %> <%= link_to 'Edit', edit_post_path(post) %> <%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %> In the meanwhile, there's a log appended intoNeed counter cache Post => [:comments]
log/bullet.log
file.**************************************************************************** Copyright (c) 2009 - 2010 Richard Huang (flyerhzm@gmail.com), released under the MIT license2009-09-11 10:07:10[INFO] Need Counter Cache Post => [:comments]