flipsasser-bullet 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.markdown +197 -0
- data/Rakefile +32 -0
- data/VERSION +1 -0
- data/bullet.gemspec +55 -0
- data/lib/bullet.rb +21 -0
- data/lib/bullet/active_record.rb +62 -0
- data/lib/bullet/association.rb +277 -0
- data/lib/bullet/logger.rb +9 -0
- data/lib/bulletware.rb +25 -0
- data/rails/init.rb +1 -0
- data/spec/bullet_association_spec.rb +696 -0
- data/spec/spec.opts +8 -0
- data/spec/spec_helper.rb +10 -0
- data/tasks/bullet_tasks.rake +9 -0
- metadata +69 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Richard Huang (flyerhzm@gmail.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
*Please note!* This is the Bullet gem as written by Richard Huang, with a few added niceties:
|
2
|
+
|
3
|
+
- It has been refactored to support Rails initializers
|
4
|
+
- Growl, console.log, and Rails.logger support have been added
|
5
|
+
|
6
|
+
# Bullet #
|
7
|
+
|
8
|
+
The Bullet plugin 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) or when you're using eager loading that isn't necessary.
|
9
|
+
|
10
|
+
Best practice is to use Bullet while building your application, but UNINSTALL OR DEACTIVATE IT when you deploy to a production server. The last thing you want is your clients getting alerts about how lazy you are.
|
11
|
+
|
12
|
+
## Installation ##
|
13
|
+
|
14
|
+
### Get the source ##
|
15
|
+
|
16
|
+
You can add Bullet to your Rails gem requirements:
|
17
|
+
|
18
|
+
config.gem 'flipsasser-bullet', :lib => nil, :source => 'http://gems.github.com'
|
19
|
+
|
20
|
+
Or you can install it as a gem like so:
|
21
|
+
|
22
|
+
sudo gem install flipsasser-bullet --source http://gems.github.com
|
23
|
+
|
24
|
+
Finally, you can install it as a Rails plugins:
|
25
|
+
|
26
|
+
ruby script/plugin install git://github.com/flipsasser/bullet.git
|
27
|
+
|
28
|
+
### Configure Bullet ###
|
29
|
+
|
30
|
+
Bullet boots up from a Rails initializer. It won't do ANYTHING unless you tell it to explicitly. Add a RAILS_ROOT/config/initializers/bullet.rb initializer with the following code:
|
31
|
+
|
32
|
+
# Only use Bullet in development...
|
33
|
+
if Bullet.enable = RAILS_ENV == 'development'
|
34
|
+
Bullet::Association.alert = true
|
35
|
+
Bullet::Association.bullet_logger = true
|
36
|
+
Bullet::Association.console = true
|
37
|
+
Bullet::Association.growl = true
|
38
|
+
Bullet::Association.rails_logger = true
|
39
|
+
end
|
40
|
+
|
41
|
+
The code above will enable all five of the Bullet notification systems:
|
42
|
+
|
43
|
+
- `Bullet::Association.alert`: pop up a JavaScript alert in the browser
|
44
|
+
- `Bullet::Association.bullet_logger`: log to the Bullet log file (RAILS_ROOT/log/bullet.log)
|
45
|
+
- `Bullet::Association.rails_logger`: add warnings directly to the Rails log
|
46
|
+
- `Bullet::Association.console`: log warnings to your browser's console.log (Safari/Webkit browsers or Firefox w/Firebug installed)
|
47
|
+
- `Bullet::Association.growl`: pop up Growl warnings if your system has Growl installed. Requires a little bit of configuration
|
48
|
+
|
49
|
+
### The Bullet log ###
|
50
|
+
|
51
|
+
The Bullet log (log/bullet.log) will look something like this:
|
52
|
+
|
53
|
+
2009-08-25 20:40:17[INFO] N+1 Query: PATH_INFO: /posts; model: Post => associations: [comments]·
|
54
|
+
Add to your finder: :include => [:comments]
|
55
|
+
2009-08-25 20:40:17[INFO] N+1 Query: method call stack:·
|
56
|
+
/Users/richard/Downloads/test/app/views/posts/index.html.erb:11:in `_run_erb_app47views47posts47index46html46erb'
|
57
|
+
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
|
58
|
+
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb'
|
59
|
+
/Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
|
60
|
+
|
61
|
+
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.
|
62
|
+
|
63
|
+
Bullet also has a Rake task, `rake bullet:log:clear`, which will clear out the Bullet log.
|
64
|
+
|
65
|
+
### Growl Support ###
|
66
|
+
|
67
|
+
To get Growl support up-and-running for Bullet, follow the steps below:
|
68
|
+
|
69
|
+
1. Install the ruby-growl gem: `sudo gem install ruby-growl`
|
70
|
+
2. Open the Growl preference pane in Systems Preferences
|
71
|
+
3. Click the "Network" tab
|
72
|
+
4. Make sure both "Listen for incoming notifications" and "Allow remote application registration" are checked. *Note:* If you set a password, you will need to set `Bullet::Association.growl_password = 'your_growl_password'` in the initializer.
|
73
|
+
5. Restart Growl ("General" tab -> Stop Growl -> Start Growl)
|
74
|
+
6. 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.
|
75
|
+
|
76
|
+
## Step by step example ##
|
77
|
+
|
78
|
+
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.
|
79
|
+
|
80
|
+
*Important*: It is strongly recommended you disable your browser's cache.
|
81
|
+
|
82
|
+
1. Setup your test environment
|
83
|
+
|
84
|
+
$ rails test
|
85
|
+
$ cd test
|
86
|
+
$ script/generate scaffold post name:string
|
87
|
+
$ script/generate scaffold comment name:string post_id:integer
|
88
|
+
$ rake db:migrate
|
89
|
+
|
90
|
+
2. Add relationships to `app/model/post.rb` and `app/model/comment.rb`
|
91
|
+
|
92
|
+
class Post < ActiveRecord::Base
|
93
|
+
has_many :comments
|
94
|
+
end
|
95
|
+
|
96
|
+
class Comment < ActiveRecord::Base
|
97
|
+
belongs_to :post
|
98
|
+
end
|
99
|
+
|
100
|
+
3. Go to script/console and execute
|
101
|
+
|
102
|
+
post1 = Post.create(:name => 'first')
|
103
|
+
post2 = Post.create(:name => 'second')
|
104
|
+
post1.comments.create(:name => 'first')
|
105
|
+
post1.comments.create(:name => 'second')
|
106
|
+
post2.comments.create(:name => 'third')
|
107
|
+
post2.comments.create(:name => 'fourth')
|
108
|
+
|
109
|
+
4. Change the `app/views/posts/index.html.erb` to produce an N+1 query
|
110
|
+
|
111
|
+
<% @posts.each do |post| %>
|
112
|
+
<tr>
|
113
|
+
<td><%= h post.name %></td>
|
114
|
+
<td><%= post.comments.collect(&:name) %></td>
|
115
|
+
<td><%= link_to 'Show', post %></td>
|
116
|
+
<td><%= link_to 'Edit', edit_post_path(post) %></td>
|
117
|
+
<td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
|
118
|
+
</tr>
|
119
|
+
<% end %>
|
120
|
+
|
121
|
+
5. Install the Bullet plugin
|
122
|
+
|
123
|
+
$ script/plugin install git://github.com/flipsasser/bullet.git
|
124
|
+
|
125
|
+
6. Enable the bullet plugin in development with a Rails initializer (RAILS_ROOT/config/intializers/bullet.rb):
|
126
|
+
|
127
|
+
if Bullet.enable = RAILS_ENV == 'development'
|
128
|
+
Bullet::Association.alert = true
|
129
|
+
end
|
130
|
+
|
131
|
+
7. Boot up your development server
|
132
|
+
|
133
|
+
$ script/server
|
134
|
+
|
135
|
+
8. Visit http://localhost:3000/posts in your browser. Bullet will alert you that an N+1 query has occurred.
|
136
|
+
|
137
|
+
The request has N+1 queries as follows:
|
138
|
+
model: Post => associations: [comment]
|
139
|
+
|
140
|
+
... which means there is an N+1 query from the Post object to the comments association.
|
141
|
+
|
142
|
+
9. Fix the N+1 query. Change `app/controllers/posts_controller.rb`:
|
143
|
+
|
144
|
+
def index
|
145
|
+
@posts = Post.find(:all, :include => :comments)
|
146
|
+
|
147
|
+
respond_to do |format|
|
148
|
+
format.html # index.html.erb
|
149
|
+
format.xml { render :xml => @posts }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
10. Refresh your browser. No alert should show up.
|
154
|
+
|
155
|
+
This is because the original query, which read:
|
156
|
+
|
157
|
+
Post Load (1.0ms) SELECT * FROM "posts"
|
158
|
+
Comment Load (0.4ms) SELECT * FROM "comments" WHERE ("comments".post_id = 1)
|
159
|
+
Comment Load (0.3ms) SELECT * FROM "comments" WHERE ("comments".post_id = 2)
|
160
|
+
|
161
|
+
... would cause one additional query for each post it found. The new SQL should look like this:
|
162
|
+
|
163
|
+
Post Load (0.5ms) SELECT * FROM "posts"
|
164
|
+
Comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE ("comments".post_id IN (1,2))
|
165
|
+
|
166
|
+
And your N+1 query is fixed. Cool!
|
167
|
+
|
168
|
+
11. Now simulate unused eager loading. Change `app/controllers/posts_controller.rb` and `app/views/posts/index.html.erb`
|
169
|
+
|
170
|
+
app/controllers/posts_controller.rb:
|
171
|
+
|
172
|
+
def index
|
173
|
+
@posts = Post.find(:all, :include => :comments)
|
174
|
+
|
175
|
+
respond_to do |format|
|
176
|
+
format.html # index.html.erb
|
177
|
+
format.xml { render :xml => @posts }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
app/views/posts/index.html.erb:
|
182
|
+
|
183
|
+
<% @posts.each do |post| %>
|
184
|
+
<tr>
|
185
|
+
<td><%=h post.name %></td>
|
186
|
+
<td><%= link_to 'Show', post %></td>
|
187
|
+
<td><%= link_to 'Edit', edit_post_path(post) %></td>
|
188
|
+
<td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
|
189
|
+
</tr>
|
190
|
+
<% end %>
|
191
|
+
|
192
|
+
12. Refresh your browser. Bullet will alert you that you have unused, eager-loaded objects.
|
193
|
+
|
194
|
+
The request has unused preload associations as follows:
|
195
|
+
model: Post => associations: [comment]
|
196
|
+
|
197
|
+
Copyright (c) 2009 Richard Huang (flyerhzm@gmail.com), released under the MIT license.
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'jeweler'
|
5
|
+
|
6
|
+
desc 'Default: run unit tests.'
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
desc 'Generate documentation for the sitemap plugin.'
|
10
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
11
|
+
rdoc.rdoc_dir = 'rdoc'
|
12
|
+
rdoc.title = 'Bullet'
|
13
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
14
|
+
rdoc.rdoc_files.include('README')
|
15
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Run all specs in spec directory"
|
19
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
20
|
+
t.spec_files = FileList['spec/spec_helper.rb', 'spec/**/*_spec.rb']
|
21
|
+
end
|
22
|
+
|
23
|
+
Jeweler::Tasks.new do |gemspec|
|
24
|
+
gemspec.name = "bullet"
|
25
|
+
gemspec.summary = "A plugin to kill N+1 queries and unused eager loading"
|
26
|
+
gemspec.description = "The Bullet plugin 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) or when you're using eager loading that isn't necessary."
|
27
|
+
gemspec.email = "flip@x451.com"
|
28
|
+
gemspec.homepage = "http://github.com/flipsasser/bullet"
|
29
|
+
gemspec.authors = ["Richard Huang", "Flip Sasser"]
|
30
|
+
gemspec.files.exclude '.gitignore'
|
31
|
+
gemspec.files.exclude 'log/'
|
32
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.1.0
|
data/bullet.gemspec
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{bullet}
|
8
|
+
s.version = "1.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Richard Huang", "Flip Sasser"]
|
12
|
+
s.date = %q{2009-08-27}
|
13
|
+
s.description = %q{The Bullet plugin 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) or when you're using eager loading that isn't necessary.}
|
14
|
+
s.email = %q{flip@x451.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.markdown"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
"MIT-LICENSE",
|
20
|
+
"README.markdown",
|
21
|
+
"Rakefile",
|
22
|
+
"VERSION",
|
23
|
+
"bullet.gemspec",
|
24
|
+
"lib/bullet.rb",
|
25
|
+
"lib/bullet/active_record.rb",
|
26
|
+
"lib/bullet/association.rb",
|
27
|
+
"lib/bullet/logger.rb",
|
28
|
+
"lib/bulletware.rb",
|
29
|
+
"rails/init.rb",
|
30
|
+
"spec/bullet_association_spec.rb",
|
31
|
+
"spec/spec.opts",
|
32
|
+
"spec/spec_helper.rb",
|
33
|
+
"tasks/bullet_tasks.rake"
|
34
|
+
]
|
35
|
+
s.has_rdoc = true
|
36
|
+
s.homepage = %q{http://github.com/flipsasser/bullet}
|
37
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
38
|
+
s.require_paths = ["lib"]
|
39
|
+
s.rubygems_version = %q{1.3.1}
|
40
|
+
s.summary = %q{A plugin to kill N+1 queries and unused eager loading}
|
41
|
+
s.test_files = [
|
42
|
+
"spec/bullet_association_spec.rb",
|
43
|
+
"spec/spec_helper.rb"
|
44
|
+
]
|
45
|
+
|
46
|
+
if s.respond_to? :specification_version then
|
47
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
48
|
+
s.specification_version = 2
|
49
|
+
|
50
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
51
|
+
else
|
52
|
+
end
|
53
|
+
else
|
54
|
+
end
|
55
|
+
end
|
data/lib/bullet.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Bullet
|
2
|
+
@@enable = nil
|
3
|
+
|
4
|
+
class <<self
|
5
|
+
def enable=(enable)
|
6
|
+
if enable != @@enable && @@enable = enable
|
7
|
+
Bullet::ActiveRecord.enable
|
8
|
+
ActionController::Dispatcher.middleware.use Bulletware
|
9
|
+
end
|
10
|
+
@@enable
|
11
|
+
end
|
12
|
+
|
13
|
+
def enable?
|
14
|
+
class_variables.include?('@@enable') and @@enable == true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
autoload :ActiveRecord, 'bullet/active_record'
|
19
|
+
autoload :Association, 'bullet/association'
|
20
|
+
autoload :BulletLogger, 'bullet/logger'
|
21
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Bullet
|
2
|
+
module ActiveRecord
|
3
|
+
def self.enable
|
4
|
+
::ActiveRecord::Base.class_eval do
|
5
|
+
class << self
|
6
|
+
alias_method :bullet_find_every, :find_every
|
7
|
+
# if select a collection of objects, then these objects have possible to cause N+1 query
|
8
|
+
# if select only one object, then the only one object has impossible to cause N+1 query
|
9
|
+
def find_every(options)
|
10
|
+
records = bullet_find_every(options)
|
11
|
+
|
12
|
+
if records
|
13
|
+
if records.size > 1
|
14
|
+
Bullet::Association.add_possible_objects(records)
|
15
|
+
elsif records.size == 1
|
16
|
+
Bullet::Association.add_impossible_object(records.first)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
records
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method :bullet_preload_associations, :preload_associations
|
24
|
+
# add include for one to many associations query
|
25
|
+
def preload_associations(records, associations, preload_options={})
|
26
|
+
records = [records].flatten.compact.uniq
|
27
|
+
return if records.empty?
|
28
|
+
records.each do |record|
|
29
|
+
Bullet::Association.add_association(record, associations)
|
30
|
+
end
|
31
|
+
bullet_preload_associations(records, associations, preload_options={})
|
32
|
+
end
|
33
|
+
|
34
|
+
# define one to many associations
|
35
|
+
alias_method :bullet_collection_reader_method, :collection_reader_method
|
36
|
+
def collection_reader_method(reflection, association_proxy_class)
|
37
|
+
Bullet::Association.define_association(self, reflection.name)
|
38
|
+
bullet_collection_reader_method(reflection, association_proxy_class)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
::ActiveRecord::Associations::AssociationCollection.class_eval do
|
44
|
+
# call one to many associations
|
45
|
+
alias_method :bullet_load_target, :load_target
|
46
|
+
def load_target
|
47
|
+
Bullet::Association.call_association(@owner, @reflection.name)
|
48
|
+
bullet_load_target
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
::ActiveRecord::Associations::AssociationProxy.class_eval do
|
53
|
+
# call has_one association
|
54
|
+
alias_method :bullet_load_target, :load_target
|
55
|
+
def load_target
|
56
|
+
Bullet::Association.call_association(@owner, @reflection.name)
|
57
|
+
bullet_load_target
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
module Bullet
|
2
|
+
class BulletAssociationError < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class Association
|
6
|
+
class <<self
|
7
|
+
@@alert = nil
|
8
|
+
@@bullet_logger = nil
|
9
|
+
@@console = nil
|
10
|
+
@@growl = nil
|
11
|
+
@@growl_password = nil
|
12
|
+
@@rails_logger = nil
|
13
|
+
|
14
|
+
def start_request
|
15
|
+
# puts "start request"
|
16
|
+
end
|
17
|
+
|
18
|
+
def end_request
|
19
|
+
# puts "end request"
|
20
|
+
@@object_associations = nil
|
21
|
+
@@unpreload_associations = nil
|
22
|
+
@@unused_preload_associations = nil
|
23
|
+
@@callers = nil
|
24
|
+
@@possible_objects = nil
|
25
|
+
@@impossible_objects = nil
|
26
|
+
@@call_object_associations = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def alert=(alert)
|
30
|
+
@@alert = alert
|
31
|
+
end
|
32
|
+
|
33
|
+
def bullet_logger=(bullet_logger)
|
34
|
+
if @@bullet_logger = bullet_logger
|
35
|
+
@@logger_file = File.open(Bullet::BulletLogger::LOG_FILE, 'a+')
|
36
|
+
@@logger = Bullet::BulletLogger.new(@@logger_file)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def console=(console)
|
41
|
+
@@console = console
|
42
|
+
end
|
43
|
+
|
44
|
+
def growl=(growl)
|
45
|
+
if growl
|
46
|
+
begin
|
47
|
+
require 'ruby-growl'
|
48
|
+
growl = Growl.new('localhost', 'ruby-growl', ['Bullet Notification'], nil, @@growl_password)
|
49
|
+
growl.notify('Bullet Notification', 'Bullet Notification', 'Bullet Growl notifications have been turned on')
|
50
|
+
rescue MissingSourceFile
|
51
|
+
raise BulletAssociationError.new('You must install the ruby-growl gem to use Growl notifications: `sudo gem install ruby-growl`')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
@@growl = growl
|
55
|
+
end
|
56
|
+
|
57
|
+
def growl_password=(growl_password)
|
58
|
+
@@growl_password = growl_password
|
59
|
+
end
|
60
|
+
|
61
|
+
def rails_logger=(rails_logger)
|
62
|
+
@@rails_logger = rails_logger
|
63
|
+
end
|
64
|
+
|
65
|
+
def check_unused_preload_associations
|
66
|
+
object_associations.each do |object, association|
|
67
|
+
call_object_association = call_object_associations[object] || []
|
68
|
+
add_unused_preload_associations(object.class, association - call_object_association) unless (association - call_object_association).empty?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def has_bad_assocations?
|
73
|
+
check_unused_preload_associations
|
74
|
+
has_unpreload_associations? or has_unused_preload_associations?
|
75
|
+
end
|
76
|
+
|
77
|
+
def has_unused_preload_associations?
|
78
|
+
!unused_preload_associations.empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
def has_unpreload_associations?
|
82
|
+
!unpreload_associations.empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
def bad_associations_alert
|
86
|
+
str = ''
|
87
|
+
if @@alert || @@console || @@growl
|
88
|
+
response = []
|
89
|
+
if has_unused_preload_associations?
|
90
|
+
response.push("Unused preload associations detected:\n")
|
91
|
+
response.push(*@@unused_preload_associations.to_a.collect{|klazz, associations| klazz_associations_str(klazz, associations)}.join('\n'))
|
92
|
+
end
|
93
|
+
if has_unpreload_associations?
|
94
|
+
response.push("#{"\n" unless response.empty?}N+1 queries detected:\n")
|
95
|
+
response.push(*@@unpreload_associations.to_a.collect{|klazz, associations| " #{klazz} => [#{associations.map(&:inspect).join(', ')}]"}.join('\n'))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
if @@alert
|
99
|
+
str << wrap_js_association("alert(#{response.join("\n").inspect});")
|
100
|
+
end
|
101
|
+
if @@console
|
102
|
+
str << wrap_js_association("if (typeof(console) != 'undefined' && console.log) console.log(#{response.join("\n").inspect});")
|
103
|
+
end
|
104
|
+
if @@growl
|
105
|
+
begin
|
106
|
+
growl = Growl.new('localhost', 'ruby-growl', ['Bullet Notification'], nil, @@growl_password)
|
107
|
+
growl.notify('Bullet Notification', 'Bullet Notification', response.join("\n"))
|
108
|
+
rescue
|
109
|
+
end
|
110
|
+
str << '<!-- Sent Growl notification -->'
|
111
|
+
end
|
112
|
+
str
|
113
|
+
end
|
114
|
+
|
115
|
+
def wrap_js_association(message)
|
116
|
+
str = ''
|
117
|
+
str << "<script type=\"text/javascript\">/*<![CDATA[*/"
|
118
|
+
str << message
|
119
|
+
str << "/*]]>*/</script>\n"
|
120
|
+
end
|
121
|
+
|
122
|
+
def log_bad_associations(path)
|
123
|
+
if (@@bullet_logger || @@rails_logger) && (!unpreload_associations.empty? || !unused_preload_associations.empty?)
|
124
|
+
Rails.logger.warn '' if @@rails_logger
|
125
|
+
unused_preload_associations.each do |klazz, associations|
|
126
|
+
log = ["Unused preload associations: #{path}", klazz_associations_str(klazz, associations), " Remove from your finder: #{associations_str(associations)}"].join("\n")
|
127
|
+
@@logger.info(log) if @@bullet_logger
|
128
|
+
Rails.logger.warn(log) if @@rails_logger
|
129
|
+
end
|
130
|
+
unpreload_associations.each do |klazz, associations|
|
131
|
+
log = ["N+1 Query in #{path}", klazz_associations_str(klazz, associations), " Add to your finder: #{associations_str(associations)}"].join("\n")
|
132
|
+
@@logger.info(log) if @@bullet_logger
|
133
|
+
Rails.logger.warn(log) if @@rails_logger
|
134
|
+
end
|
135
|
+
callers.each do |c|
|
136
|
+
log = ["N+1 Query method call stack", c.map{|line| " #{line}"}].flatten.join("\n")
|
137
|
+
@@logger.info(log) if @@bullet_logger
|
138
|
+
Rails.logger.warn(log) if @@rails_logger
|
139
|
+
end
|
140
|
+
@@logger_file.flush if @@bullet_logger
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def bad_associations_str(bad_associations)
|
145
|
+
# puts bad_associations.inspect
|
146
|
+
bad_associations.to_a.collect{|klazz, associations| klazz_associations_str(klazz, associations)}.join('\\n')
|
147
|
+
end
|
148
|
+
|
149
|
+
def klazz_associations_str(klazz, associations)
|
150
|
+
" #{klazz} => [#{associations.map(&:inspect).join(', ')}]"
|
151
|
+
end
|
152
|
+
|
153
|
+
def associations_str(associations)
|
154
|
+
":include => #{associations.map{|a| a.to_sym unless a.is_a? Hash}.inspect}"
|
155
|
+
end
|
156
|
+
|
157
|
+
def has_klazz_association(klazz)
|
158
|
+
!klazz_associations[klazz].nil? and klazz_associations.keys.include?(klazz)
|
159
|
+
end
|
160
|
+
|
161
|
+
def define_association(klazz, associations)
|
162
|
+
# puts "define association, #{klazz} => #{associations.inspect}"
|
163
|
+
add_klazz_associations(klazz, associations)
|
164
|
+
end
|
165
|
+
|
166
|
+
def call_association(object, associations)
|
167
|
+
# puts "call association, #{object} => #{associations.inspect}"
|
168
|
+
add_call_object_associations(object, associations)
|
169
|
+
if unpreload_associations?(object, associations)
|
170
|
+
add_unpreload_associations(object.class, associations)
|
171
|
+
caller_in_project
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def unpreload_associations?(object, associations)
|
176
|
+
klazz = object.class
|
177
|
+
(!possible_objects[klazz].nil? and possible_objects[klazz].include?(object)) and
|
178
|
+
(impossible_objects[klazz].nil? or !impossible_objects[klazz].include?(object)) and
|
179
|
+
(object_associations[object].nil? or !object_associations[object].include?(associations))
|
180
|
+
end
|
181
|
+
|
182
|
+
def add_unpreload_associations(klazz, associations)
|
183
|
+
# puts "add unpreload associations, #{klazz} => #{associations.inspect}"
|
184
|
+
unpreload_associations[klazz] ||= []
|
185
|
+
unpreload_associations[klazz] << associations
|
186
|
+
unique(unpreload_associations[klazz])
|
187
|
+
end
|
188
|
+
|
189
|
+
def add_unused_preload_associations(klazz, associations)
|
190
|
+
# puts "add unused preload associations, #{klazz} => #{associations.inspect}"
|
191
|
+
unused_preload_associations[klazz] ||= []
|
192
|
+
unused_preload_associations[klazz] << associations
|
193
|
+
unique(unused_preload_associations[klazz])
|
194
|
+
end
|
195
|
+
|
196
|
+
def add_association(object, associations)
|
197
|
+
# puts "add associations, #{object} => #{associations.inspect}"
|
198
|
+
object_associations[object] ||= []
|
199
|
+
object_associations[object] << associations
|
200
|
+
unique(object_associations[object])
|
201
|
+
end
|
202
|
+
|
203
|
+
def add_call_object_associations(object, associations)
|
204
|
+
# puts "add call object associations, #{object} => #{associations.inspect}"
|
205
|
+
call_object_associations[object] ||= []
|
206
|
+
call_object_associations[object] << associations
|
207
|
+
unique(call_object_associations[object])
|
208
|
+
end
|
209
|
+
|
210
|
+
def add_possible_objects(objects)
|
211
|
+
# puts "add possible objects, #{objects.inspect}"
|
212
|
+
klazz= objects.first.class
|
213
|
+
possible_objects[klazz] ||= []
|
214
|
+
possible_objects[klazz] << objects
|
215
|
+
unique(possible_objects[klazz])
|
216
|
+
end
|
217
|
+
|
218
|
+
def add_impossible_object(object)
|
219
|
+
# puts "add impossible object, #{object}"
|
220
|
+
klazz = object.class
|
221
|
+
impossible_objects[klazz] ||= []
|
222
|
+
impossible_objects[klazz] << object
|
223
|
+
impossible_objects[klazz].uniq!
|
224
|
+
end
|
225
|
+
|
226
|
+
def add_klazz_associations(klazz, associations)
|
227
|
+
# puts "define associations, #{klazz} => #{associations.inspect}"
|
228
|
+
klazz_associations[klazz] ||= []
|
229
|
+
klazz_associations[klazz] << associations
|
230
|
+
unique(klazz_associations[klazz])
|
231
|
+
end
|
232
|
+
|
233
|
+
def unique(array)
|
234
|
+
array.flatten!
|
235
|
+
array.uniq!
|
236
|
+
end
|
237
|
+
|
238
|
+
def unpreload_associations
|
239
|
+
@@unpreload_associations ||= {}
|
240
|
+
end
|
241
|
+
|
242
|
+
def unused_preload_associations
|
243
|
+
@@unused_preload_associations ||= {}
|
244
|
+
end
|
245
|
+
|
246
|
+
def object_associations
|
247
|
+
@@object_associations ||= {}
|
248
|
+
end
|
249
|
+
|
250
|
+
def call_object_associations
|
251
|
+
@@call_object_associations ||= {}
|
252
|
+
end
|
253
|
+
|
254
|
+
def possible_objects
|
255
|
+
@@possible_objects ||= {}
|
256
|
+
end
|
257
|
+
|
258
|
+
def impossible_objects
|
259
|
+
@@impossible_objects ||= {}
|
260
|
+
end
|
261
|
+
|
262
|
+
def klazz_associations
|
263
|
+
@@klazz_associations ||= {}
|
264
|
+
end
|
265
|
+
|
266
|
+
VENDOR_ROOT = File.join(RAILS_ROOT, 'vendor')
|
267
|
+
def caller_in_project
|
268
|
+
callers << caller.select {|c| c =~ /#{RAILS_ROOT}/}.reject {|c| c =~ /#{VENDOR_ROOT}/}
|
269
|
+
callers.uniq!
|
270
|
+
end
|
271
|
+
|
272
|
+
def callers
|
273
|
+
@@callers ||= []
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|