flyerhzm-bullet 1.0.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.textile +243 -0
- data/Rakefile +32 -0
- data/VERSION +1 -0
- data/bullet.gemspec +54 -0
- data/lib/bullet/association.rb +150 -0
- data/lib/bullet/logger.rb +9 -0
- data/lib/bullet.rb +14 -0
- data/lib/bulletware.rb +25 -0
- data/lib/hack/active_record.rb +95 -0
- data/rails/init.rb +4 -0
- data/spec/bullet_association_spec.rb +679 -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.textile
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
h1. Bullet
|
2
|
+
|
3
|
+
This plugin is aimed to give you some performance suggestion about ActiveRecord usage, what should use but not use, such as eager loading, counter cache and so on, what should not use but use, such as unused eager loading.
|
4
|
+
|
5
|
+
Now it provides you the suggestion of eager loading and unused eager loading.
|
6
|
+
|
7
|
+
The others are todo, next may be couter cache.
|
8
|
+
|
9
|
+
****************************************************************************
|
10
|
+
|
11
|
+
h2. Install
|
12
|
+
|
13
|
+
install as gem:
|
14
|
+
<pre><code>
|
15
|
+
gem sources -a http://gems.github.com
|
16
|
+
gem install flyerhzm-bullet
|
17
|
+
</code></pre>
|
18
|
+
|
19
|
+
install as plugin:
|
20
|
+
<pre><code>script/plugin install git://github.com/flyerhzm/bullet.git</code></pre>
|
21
|
+
|
22
|
+
****************************************************************************
|
23
|
+
|
24
|
+
h2. Usage
|
25
|
+
|
26
|
+
* "Eager Loading, protect N+1 query and protect from unused eager loading":#EagerLoading
|
27
|
+
|
28
|
+
****************************************************************************
|
29
|
+
|
30
|
+
h2. Step by step example
|
31
|
+
|
32
|
+
* "Eager Loading, protect N+1 query and protect from unused eager loading":#EagerLoadingExample
|
33
|
+
|
34
|
+
|
35
|
+
****************************************************************************
|
36
|
+
|
37
|
+
h3. Eager Loading, protect N+1 query and protect from unused eager loading, usage
|
38
|
+
<a id="EagerLoading"/></a>
|
39
|
+
|
40
|
+
*important*: It is strongly recommended to disable cache in browser.
|
41
|
+
|
42
|
+
* add configuration to environment
|
43
|
+
<pre><code>
|
44
|
+
Bullet.enable = true
|
45
|
+
Bullet::Association.logger = true
|
46
|
+
Bullet::Association.alert = true
|
47
|
+
</code></pre>
|
48
|
+
** Bullet.enable (required), if enable is true (default is false), Bullet plugin is enabled. Otherwise, Bullet plugin is disabled.
|
49
|
+
** Bullet::Association.logger (optional), if logger is true (default is true), the N+1 query hints will be appended to <code>log/bullet.log</code> with N+1 query method call stack. Otherwise, no hint to log/bullet.log.
|
50
|
+
** Bullet::Association.alert (optional), if alert is true (default value), alert box will popup if there is N+1 query when browsing web page. Otherwise, no alert box.
|
51
|
+
|
52
|
+
* browse the webpage, if there are N+1 queries or unused eager loading, alert box and bullet log will generate according to configurations. Alert box will only popup when the request's Content-Type is text/html, and <code>log/bullet.log</code> will produce whatever the request is.
|
53
|
+
|
54
|
+
* example of <code>log/bullet.log</code>
|
55
|
+
<pre><code>
|
56
|
+
2009-08-25 20:40:17[INFO] N+1 Query: PATH_INFO: /posts; model: Post => associations: [comments]·
|
57
|
+
Add to your finder: :include => [:comments]
|
58
|
+
2009-08-25 20:40:17[INFO] N+1 Query: method call stack:·
|
59
|
+
/Users/richard/Downloads/test/app/views/posts/index.html.erb:11:in `_run_erb_app47views47posts47index46html46erb'
|
60
|
+
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
|
61
|
+
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb'
|
62
|
+
/Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
|
63
|
+
</code></pre>
|
64
|
+
It represents that in request '/posts', there is a N+1 query from Post to comments. It means you may have a logic code in controller <code>@posts = Post.find(:all)</code> should be changed to <code>@posts = Post.find(:all, :include => :comments)</code>
|
65
|
+
<pre><code>
|
66
|
+
2009-08-25 20:53:56[INFO] Unused preload associations: PATH_INFO: /posts; model: Post => associations: [comments]·
|
67
|
+
Remove from your finder: :include => [:comments]
|
68
|
+
</code></pre>
|
69
|
+
It represents that in request '/posts', there is a used eager loading from Post to comments. It means you may have a logic code in controller <code>@posts = Post.find(:all, :include => :comments)</code> should be changed to <code>@posts = Post.find(:all)</code>
|
70
|
+
|
71
|
+
* To see what causes N+1 queries, check the <code>spec/bullet_association_spec.rb</code>
|
72
|
+
|
73
|
+
* Rake tasks
|
74
|
+
<code>rake bullet:log:clear</code>, clear the <code>log/bullet.log</code>
|
75
|
+
|
76
|
+
****************************************************************************
|
77
|
+
|
78
|
+
h3. Eager Loading, protect N+1 query and protect from unused eager loading, step by step example
|
79
|
+
<a id="EagerLoadingExample"/></a>
|
80
|
+
|
81
|
+
1. setup test environment
|
82
|
+
|
83
|
+
<pre><code>
|
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
|
+
</code></pre>
|
90
|
+
|
91
|
+
2. change <code>app/model/post.rb</code> and <code>app/model/comment.rb</code>
|
92
|
+
|
93
|
+
<pre><code>
|
94
|
+
class Post < ActiveRecord::Base
|
95
|
+
has_many :comments
|
96
|
+
end
|
97
|
+
|
98
|
+
class Comment < ActiveRecord::Base
|
99
|
+
belongs_to :post
|
100
|
+
end
|
101
|
+
</code></pre>
|
102
|
+
|
103
|
+
3. go to script/console and execute
|
104
|
+
|
105
|
+
<pre><code>
|
106
|
+
post1 = Post.create(:name => 'first')
|
107
|
+
post2 = Post.create(:name => 'second')
|
108
|
+
post1.comments.create(:name => 'first')
|
109
|
+
post1.comments.create(:name => 'second')
|
110
|
+
post2.comments.create(:name => 'third')
|
111
|
+
post2.comments.create(:name => 'fourth')
|
112
|
+
</code></pre>
|
113
|
+
|
114
|
+
4. change the <code>app/views/posts/index.html.erb</code> to produce a N+1 query
|
115
|
+
|
116
|
+
<pre><code>
|
117
|
+
<% @posts.each do |post| %>
|
118
|
+
<tr>
|
119
|
+
<td><%=h post.name %></td>
|
120
|
+
<td><%= post.comments.collect(&:name) %></td>
|
121
|
+
<td><%= link_to 'Show', post %></td>
|
122
|
+
<td><%= link_to 'Edit', edit_post_path(post) %></td>
|
123
|
+
<td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
|
124
|
+
</tr>
|
125
|
+
<% end %>
|
126
|
+
</code></pre>
|
127
|
+
|
128
|
+
5. add bullet plugin
|
129
|
+
|
130
|
+
<pre><code>
|
131
|
+
$ script/plugin install git://github.com/flyerhzm/bullet.git
|
132
|
+
</code></pre>
|
133
|
+
|
134
|
+
6. enable the bullet plugin in development, add a line to <code>config/environments/development.rb</code>
|
135
|
+
|
136
|
+
<pre><code>
|
137
|
+
Bullet.enable = true
|
138
|
+
</code></pre>
|
139
|
+
|
140
|
+
7. start server
|
141
|
+
|
142
|
+
<pre><code>
|
143
|
+
$ script/server
|
144
|
+
</code></pre>
|
145
|
+
|
146
|
+
8. input http://localhost:3000/posts in browser, then you will see a popup alert box says
|
147
|
+
|
148
|
+
<pre><code>
|
149
|
+
The request has unused preload associations as follows:
|
150
|
+
None
|
151
|
+
The request has N+1 queries as follows:
|
152
|
+
model: Post => associations: [comment]
|
153
|
+
</code></pre>
|
154
|
+
|
155
|
+
which means there is a N+1 query from post object to comments associations.
|
156
|
+
|
157
|
+
In the meanwhile, there's a log appended into <code>log/bullet.log</code> file
|
158
|
+
|
159
|
+
<pre><code>
|
160
|
+
2009-08-20 09:12:19[INFO] N+1 Query: PATH_INFO: /posts; model: Post => assocations: [comments]
|
161
|
+
Add your finder: :include => [:comments]
|
162
|
+
2009-08-20 09:12:19[INFO] N+1 Query: method call stack:
|
163
|
+
/Users/richard/Downloads/test/app/views/posts/index.html.erb:11:in `_run_erb_app47views47posts47index46html46erb'
|
164
|
+
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
|
165
|
+
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb'
|
166
|
+
/Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
|
167
|
+
</code></pre>
|
168
|
+
|
169
|
+
The generated SQLs are
|
170
|
+
|
171
|
+
<pre><code>
|
172
|
+
Post Load (1.0ms) SELECT * FROM "posts"
|
173
|
+
Comment Load (0.4ms) SELECT * FROM "comments" WHERE ("comments".post_id = 1)
|
174
|
+
Comment Load (0.3ms) SELECT * FROM "comments" WHERE ("comments".post_id = 2)
|
175
|
+
</code></pre>
|
176
|
+
|
177
|
+
|
178
|
+
9. fix the N+1 query, change <code>app/controllers/posts_controller.rb</code> file
|
179
|
+
|
180
|
+
<pre><code>
|
181
|
+
def index
|
182
|
+
@posts = Post.find(:all, :include => :comments)
|
183
|
+
|
184
|
+
respond_to do |format|
|
185
|
+
format.html # index.html.erb
|
186
|
+
format.xml { render :xml => @posts }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
</code></pre>
|
190
|
+
|
191
|
+
10. refresh http://localhost:3000/posts page, no alert box and no log appended.
|
192
|
+
|
193
|
+
The generated SQLs are
|
194
|
+
|
195
|
+
<pre><code>
|
196
|
+
Post Load (0.5ms) SELECT * FROM "posts"
|
197
|
+
Comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE ("comments".post_id IN (1,2))
|
198
|
+
</code></pre>
|
199
|
+
|
200
|
+
a N+1 query fixed. Cool!
|
201
|
+
|
202
|
+
11. now simulate unused eager loading. Change <code>app/controllers/posts_controller.rb</code> and <code>app/views/posts/index.html.erb</code>
|
203
|
+
|
204
|
+
<pre><code>
|
205
|
+
def index
|
206
|
+
@posts = Post.find(:all, :include => :comments)
|
207
|
+
|
208
|
+
respond_to do |format|
|
209
|
+
format.html # index.html.erb
|
210
|
+
format.xml { render :xml => @posts }
|
211
|
+
end
|
212
|
+
end
|
213
|
+
</code></pre>
|
214
|
+
|
215
|
+
<pre><code>
|
216
|
+
<% @posts.each do |post| %>
|
217
|
+
<tr>
|
218
|
+
<td><%=h post.name %></td>
|
219
|
+
<td><%= link_to 'Show', post %></td>
|
220
|
+
<td><%= link_to 'Edit', edit_post_path(post) %></td>
|
221
|
+
<td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
|
222
|
+
</tr>
|
223
|
+
<% end %>
|
224
|
+
</code></pre>
|
225
|
+
|
226
|
+
12. refresh http://localhost:3000/posts page, then you will see a popup alert box says
|
227
|
+
|
228
|
+
<pre><code>
|
229
|
+
The request has unused preload associations as follows:
|
230
|
+
model: Post => associations: [comment]
|
231
|
+
The request has N+1 queries as follows:
|
232
|
+
None
|
233
|
+
</code></pre>
|
234
|
+
|
235
|
+
In the meanwhile, there's a log appended into <code>log/bullet.log</code> file
|
236
|
+
|
237
|
+
<pre><code>
|
238
|
+
2009-08-25 21:13:22[INFO] Unused preload associations: PATH_INFO: /posts; model: Post => associations: [comments]·
|
239
|
+
Remove from your finder: :include => [:comments]
|
240
|
+
</code></pre>
|
241
|
+
|
242
|
+
|
243
|
+
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 = "This plugin is aimed to give you some performance suggestion about ActiveRecord usage, what should use but not use, such as eager loading, counter cache and so on, what should not use but use, such as unused eager loading. Now it provides you the suggestion of eager loading and unused eager loading. The others are todo, next may be couter cache."
|
27
|
+
gemspec.email = "flyerhzm@gmail.com"
|
28
|
+
gemspec.homepage = "http://www.huangzhimin.com/projects/4-bullet"
|
29
|
+
gemspec.authors = ["Richard Huang"]
|
30
|
+
gemspec.files.exclude '.gitignore'
|
31
|
+
gemspec.files.exclude 'log/'
|
32
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/bullet.gemspec
ADDED
@@ -0,0 +1,54 @@
|
|
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.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Richard Huang"]
|
12
|
+
s.date = %q{2009-08-26}
|
13
|
+
s.description = %q{This plugin is aimed to give you some performance suggestion about ActiveRecord usage, what should use but not use, such as eager loading, counter cache and so on, what should not use but use, such as unused eager loading. Now it provides you the suggestion of eager loading and unused eager loading. The others are todo, next may be couter cache.}
|
14
|
+
s.email = %q{flyerhzm@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.textile"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
"MIT-LICENSE",
|
20
|
+
"README.textile",
|
21
|
+
"Rakefile",
|
22
|
+
"VERSION",
|
23
|
+
"bullet.gemspec",
|
24
|
+
"lib/bullet.rb",
|
25
|
+
"lib/bullet/association.rb",
|
26
|
+
"lib/bullet/logger.rb",
|
27
|
+
"lib/bulletware.rb",
|
28
|
+
"lib/hack/active_record.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.homepage = %q{http://www.huangzhimin.com/projects/4-bullet}
|
36
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubygems_version = %q{1.3.5}
|
39
|
+
s.summary = %q{A plugin to kill N+1 queries and unused eager loading}
|
40
|
+
s.test_files = [
|
41
|
+
"spec/spec_helper.rb",
|
42
|
+
"spec/bullet_association_spec.rb"
|
43
|
+
]
|
44
|
+
|
45
|
+
if s.respond_to? :specification_version then
|
46
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
47
|
+
s.specification_version = 3
|
48
|
+
|
49
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
50
|
+
else
|
51
|
+
end
|
52
|
+
else
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Bullet
|
2
|
+
class Association
|
3
|
+
class <<self
|
4
|
+
@@logger_file = File.open(Bullet::BulletLogger::LOG_FILE, 'a+')
|
5
|
+
@@logger = Bullet::BulletLogger.new(@@logger_file)
|
6
|
+
@@alert = true
|
7
|
+
|
8
|
+
def start_request
|
9
|
+
# puts "start request"
|
10
|
+
@@object_associations ||= {}
|
11
|
+
@@call_object_associations ||= {}
|
12
|
+
@@unpreload_associations ||= {}
|
13
|
+
@@unused_preload_associations ||= {}
|
14
|
+
@@callers ||= []
|
15
|
+
@@possible_objects ||= {}
|
16
|
+
@@impossible_objects ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def end_request
|
20
|
+
# puts "end request"
|
21
|
+
@@object_associations = nil
|
22
|
+
@@unpreload_associations = nil
|
23
|
+
@@unused_preload_associations = nil
|
24
|
+
@@callers = nil
|
25
|
+
@@possible_objects = nil
|
26
|
+
@@impossible_objects = nil
|
27
|
+
@@call_object_associations = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def alert=(alert)
|
31
|
+
@@alert = alert
|
32
|
+
end
|
33
|
+
|
34
|
+
def logger=(logger)
|
35
|
+
if logger == false
|
36
|
+
@@logger = nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def check_unused_preload_associations
|
41
|
+
@@object_associations.each do |object, association|
|
42
|
+
call_association = @@call_object_associations[object] || []
|
43
|
+
association.uniq! unless association.flatten!.nil?
|
44
|
+
call_association.uniq! unless call_association.flatten!.nil?
|
45
|
+
klazz = object.class
|
46
|
+
unless (association - call_association).empty?
|
47
|
+
@@unused_preload_associations[klazz] ||= []
|
48
|
+
@@unused_preload_associations[klazz] << (association - call_association)
|
49
|
+
@@unused_preload_associations[klazz].flatten!.uniq!
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def has_bad_assocations?
|
55
|
+
check_unused_preload_associations
|
56
|
+
has_unpreload_associations? or has_unused_preload_associations?
|
57
|
+
end
|
58
|
+
|
59
|
+
def has_unused_preload_associations?
|
60
|
+
!@@unused_preload_associations.empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
def has_unpreload_associations?
|
64
|
+
!@@unpreload_associations.empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
def bad_associations_alert
|
68
|
+
str = ''
|
69
|
+
if @@alert
|
70
|
+
str = "<script type='text/javascript'>"
|
71
|
+
str << "alert('The request has unused preload assocations as follows:\\n"
|
72
|
+
str << (has_unused_preload_associations? ? @@unused_preload_associations.to_a.collect{|klazz, associations| "model: #{klazz} => associations: [#{associations.join(', ')}]"}.join('\\n') : "None")
|
73
|
+
str << "\\nThe request has N+1 queries as follows:\\n"
|
74
|
+
str << (has_unpreload_associations? ? @@unpreload_associations.to_a.collect{|klazz, associations| "model: #{klazz} => associations: [#{associations.join(', ')}]"}.join('\\n') : "None")
|
75
|
+
str << "')"
|
76
|
+
str << "</script>\n"
|
77
|
+
end
|
78
|
+
str
|
79
|
+
end
|
80
|
+
|
81
|
+
def log_bad_associations(path)
|
82
|
+
if @@logger
|
83
|
+
@@unused_preload_associations.each do |klazz, associations|
|
84
|
+
@@logger.info "Unused preload associations: PATH_INFO: #{path}; model: #{klazz} => associations: [#{associations.join(', ')}] \n Remove from your finder: :include => #{associations.map{|a| a.to_sym}.inspect}"
|
85
|
+
end
|
86
|
+
@@unpreload_associations.each do |klazz, associations|
|
87
|
+
@@logger.info "N+1 Query: PATH_INFO: #{path}; model: #{klazz} => associations: [#{associations.join(', ')}] \n Add to your finder: :include => #{associations.map{|a| a.to_sym}.inspect}"
|
88
|
+
end
|
89
|
+
@@callers.each do |c|
|
90
|
+
@@logger.info "N+1 Query: method call stack: \n" + c.join("\n")
|
91
|
+
end
|
92
|
+
@@logger_file.flush
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def has_klazz_association(klazz)
|
97
|
+
!@@klazz_associations[klazz].nil? and @@klazz_associations.keys.include?(klazz)
|
98
|
+
end
|
99
|
+
|
100
|
+
def define_association(klazz, associations)
|
101
|
+
# puts "define association, #{klazz} => #{associations}"
|
102
|
+
@@klazz_associations ||= {}
|
103
|
+
@@klazz_associations[klazz] ||= []
|
104
|
+
@@klazz_associations[klazz] << associations
|
105
|
+
end
|
106
|
+
|
107
|
+
def add_possible_objects(objects)
|
108
|
+
# puts "add possible object, #{objects}"
|
109
|
+
klazz= objects.first.class
|
110
|
+
@@possible_objects[klazz] ||= []
|
111
|
+
@@possible_objects[klazz] << objects
|
112
|
+
@@possible_objects[klazz].flatten!.uniq!
|
113
|
+
end
|
114
|
+
|
115
|
+
def add_impossible_object(object)
|
116
|
+
# puts "add impossible object, #{object}"
|
117
|
+
klazz = object.class
|
118
|
+
@@impossible_objects[klazz] ||= []
|
119
|
+
@@impossible_objects[klazz] << object
|
120
|
+
end
|
121
|
+
|
122
|
+
def add_association(object, associations)
|
123
|
+
# puts "add association, #{object} => #{associations}"
|
124
|
+
@@object_associations[object] ||= []
|
125
|
+
@@object_associations[object] << associations
|
126
|
+
end
|
127
|
+
|
128
|
+
def call_association(object, associations)
|
129
|
+
# puts "call association, #{object} => #{associations}"
|
130
|
+
klazz = object.class
|
131
|
+
@@possible_objects ||= {}
|
132
|
+
@@impossible_objects ||= {}
|
133
|
+
if (!@@possible_objects[klazz].nil? and @@possible_objects[klazz].include?(object)) and (@@impossible_objects[klazz].nil? or !@@impossible_objects[klazz].include?(object)) and (@@object_associations[object].nil? or !@@object_associations[object].include?(associations))
|
134
|
+
@@unpreload_associations[klazz] ||= []
|
135
|
+
@@unpreload_associations[klazz] << associations
|
136
|
+
@@unpreload_associations[klazz].uniq!
|
137
|
+
@@call_object_associations[object] ||= []
|
138
|
+
@@call_object_associations[object] << associations
|
139
|
+
caller_in_project
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
VENDOR_ROOT = File.join(RAILS_ROOT, 'vendor')
|
144
|
+
def caller_in_project
|
145
|
+
@@callers << caller.select {|c| c =~ /#{RAILS_ROOT}/}.reject {|c| c =~ /#{VENDOR_ROOT}/}
|
146
|
+
@@callers.uniq!
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/bullet.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Bullet
|
2
|
+
class <<self
|
3
|
+
def enable=(enable)
|
4
|
+
@@enable = enable
|
5
|
+
end
|
6
|
+
|
7
|
+
def enable?
|
8
|
+
class_variables.include?('@@enable') and @@enable == true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
autoload :Association, 'bullet/association'
|
13
|
+
autoload :BulletLogger, 'bullet/logger'
|
14
|
+
end
|
data/lib/bulletware.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
class Bulletware
|
2
|
+
def initialize(app)
|
3
|
+
@app = app
|
4
|
+
end
|
5
|
+
|
6
|
+
def call(env)
|
7
|
+
return @app.call(env) unless Bullet.enable?
|
8
|
+
|
9
|
+
Bullet::Association.start_request
|
10
|
+
status, headers, response = @app.call(env)
|
11
|
+
return [status, headers, response] if response.empty?
|
12
|
+
|
13
|
+
if Bullet::Association.has_bad_assocations?
|
14
|
+
if !headers['Content-Type'].nil? and headers['Content-Type'].include? 'text/html'
|
15
|
+
response_body = response.body.insert(-17, Bullet::Association.bad_associations_alert)
|
16
|
+
headers['Content-Length'] = response_body.length.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
Bullet::Association.log_bad_associations(env['PATH_INFO'])
|
20
|
+
end
|
21
|
+
response_body ||= response.body
|
22
|
+
Bullet::Association.end_request
|
23
|
+
[status, headers, response_body]
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
if Bullet.enable?
|
2
|
+
ActiveRecord::ActiveRecordError # An ActiveRecord bug
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
class Base
|
6
|
+
class <<self
|
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
|
+
alias_method :origin_find_every, :find_every
|
10
|
+
|
11
|
+
def find_every(options)
|
12
|
+
records = origin_find_every(options)
|
13
|
+
|
14
|
+
if records
|
15
|
+
if records.size > 1
|
16
|
+
Bullet::Association.add_possible_objects(records)
|
17
|
+
elsif records.size == 1
|
18
|
+
Bullet::Association.add_impossible_object(records.first)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
records
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module AssociationPreload
|
28
|
+
module ClassMethods
|
29
|
+
# add include for one to many associations query
|
30
|
+
alias_method :origin_preload_associations, :preload_associations
|
31
|
+
|
32
|
+
def preload_associations(records, associations, preload_options={})
|
33
|
+
records = [records].flatten.compact.uniq
|
34
|
+
return if records.empty?
|
35
|
+
records.each do |record|
|
36
|
+
Bullet::Association.add_association(record, associations)
|
37
|
+
end
|
38
|
+
origin_preload_associations(records, associations, preload_options={})
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module Associations
|
44
|
+
module ClassMethods
|
45
|
+
# define one to many associations
|
46
|
+
alias_method :origin_collection_reader_method, :collection_reader_method
|
47
|
+
|
48
|
+
def collection_reader_method(reflection, association_proxy_class)
|
49
|
+
Bullet::Association.define_association(self, reflection.name)
|
50
|
+
origin_collection_reader_method(reflection, association_proxy_class)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class AssociationCollection
|
55
|
+
# call one to many associations
|
56
|
+
alias_method :origin_load_target, :load_target
|
57
|
+
|
58
|
+
def load_target
|
59
|
+
Bullet::Association.call_association(@owner, @reflection.name)
|
60
|
+
origin_load_target
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class HasOneAssociation
|
65
|
+
# call has_one association
|
66
|
+
alias_method :origin_find_target, :find_target
|
67
|
+
|
68
|
+
def find_target
|
69
|
+
Bullet::Association.call_association(@owner, @reflection.name)
|
70
|
+
origin_find_target
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class BelongsToAssociation
|
75
|
+
# call belongs_to association
|
76
|
+
alias_method :origin_find_target, :find_target
|
77
|
+
|
78
|
+
def find_target
|
79
|
+
Bullet::Association.call_association(@owner, @reflection.name)
|
80
|
+
origin_find_target
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class BelongsToPolymorphicAssociation
|
85
|
+
# call belongs_to association
|
86
|
+
alias_method :origin_find_target, :find_target
|
87
|
+
|
88
|
+
def find_target
|
89
|
+
Bullet::Association.call_association(@owner, @reflection.name)
|
90
|
+
origin_find_target
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/rails/init.rb
ADDED