flyerhzm-bullet 1.4.1 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +82 -21
- data/VERSION +1 -1
- data/bullet.gemspec +6 -5
- data/lib/bullet.rb +52 -4
- data/lib/bullet/association.rb +95 -224
- data/lib/bullet/counter.rb +41 -4
- data/lib/bullet/notification.rb +79 -0
- data/lib/bulletware.rb +6 -6
- data/spec/bullet_counter_spec.rb +9 -2
- data/spec/spec_helper.rb +1 -0
- metadata +4 -3
data/README.textile
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
h1. Bullet
|
2
2
|
|
3
|
-
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)
|
3
|
+
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), when you're using eager loading that isn't necessary and when you should use counter cache.
|
4
4
|
|
5
5
|
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.
|
6
6
|
|
@@ -32,11 +32,11 @@ Bullet won't do ANYTHING unless you tell it to explicitly. Append to <code>confi
|
|
32
32
|
<pre><code>
|
33
33
|
config.after_initialize do
|
34
34
|
Bullet.enable = true
|
35
|
-
Bullet
|
36
|
-
Bullet
|
37
|
-
Bullet
|
38
|
-
Bullet
|
39
|
-
Bullet
|
35
|
+
Bullet.alert = true
|
36
|
+
Bullet.bullet_logger = true
|
37
|
+
Bullet.console = true
|
38
|
+
Bullet.growl = true
|
39
|
+
Bullet.rails_logger = true
|
40
40
|
end
|
41
41
|
</code></pre>
|
42
42
|
|
@@ -44,18 +44,18 @@ It is recommended to config growl notification as follows if your collaborators
|
|
44
44
|
<pre><code>
|
45
45
|
begin
|
46
46
|
require 'ruby-growl'
|
47
|
-
Bullet
|
47
|
+
Bullet.growl = true
|
48
48
|
rescue MissingSourceFile
|
49
49
|
end
|
50
50
|
</code></pre>
|
51
51
|
|
52
52
|
The code above will enable all five of the Bullet notification systems:
|
53
53
|
* <code>Bullet.enable</code>: enable Bullet plugin, otherwise do nothing
|
54
|
-
* <code>Bullet
|
55
|
-
* <code>Bullet
|
56
|
-
* <code>Bullet
|
57
|
-
* <code>Bullet
|
58
|
-
* <code>Bullet
|
54
|
+
* <code>Bullet.alert</code>: pop up a JavaScript alert in the browser
|
55
|
+
* <code>Bullet.bullet_logger</code>: log to the Bullet log file (RAILS_ROOT/log/bullet.log)
|
56
|
+
* <code>Bullet.rails_logger</code>: add warnings directly to the Rails log
|
57
|
+
* <code>Bullet.console</code>: log warnings to your browser's console.log (Safari/Webkit browsers or Firefox w/Firebug installed)
|
58
|
+
* <code>Bullet.growl</code>: pop up Growl warnings if your system has Growl installed. Requires a little bit of configuration
|
59
59
|
|
60
60
|
****************************************************************************
|
61
61
|
|
@@ -84,6 +84,12 @@ Remove from your finder: :include => [:comments]
|
|
84
84
|
|
85
85
|
These two lines are notifications that unused eager loadings have been encountered.
|
86
86
|
|
87
|
+
* Need counter cache:
|
88
|
+
<pre><code>
|
89
|
+
2009-09-11 09:46:50[INFO] Need Counter Cache
|
90
|
+
Post => [:comments]
|
91
|
+
</code></pre>
|
92
|
+
|
87
93
|
****************************************************************************
|
88
94
|
|
89
95
|
h2. Growl Support
|
@@ -92,23 +98,41 @@ To get Growl support up-and-running for Bullet, follow the steps below:
|
|
92
98
|
* Install the ruby-growl gem: <code>sudo gem install ruby-growl</code>
|
93
99
|
* Open the Growl preference pane in Systems Preferences
|
94
100
|
* Click the "Network" tab
|
95
|
-
* 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 <code>Bullet
|
101
|
+
* 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 <code>Bullet.growl_password = 'your_growl_password</code>' in the config file.
|
96
102
|
* Restart Growl ("General" tab -> Stop Growl -> Start Growl)
|
97
103
|
* 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.
|
98
104
|
|
99
105
|
****************************************************************************
|
100
106
|
|
107
|
+
h2. Important
|
108
|
+
|
109
|
+
If you encounter the following errors in development environment:
|
110
|
+
|
111
|
+
<pre><code>
|
112
|
+
You might have expected an instance of Array.
|
113
|
+
The error occurred while evaluating nil.include?
|
114
|
+
/usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/attribute_methods.rb:142:in `create_time_zone_conversion_attribute?'
|
115
|
+
/usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/attribute_methods.rb:75:in `define_attribute_methods'
|
116
|
+
/usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/attribute_methods.rb:71:in `each'
|
117
|
+
/usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/attribute_methods.rb:71:in `define_attribute_methods'
|
118
|
+
/usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/attribute_methods.rb:242:in `method_missing'
|
119
|
+
</code></pre>
|
120
|
+
|
121
|
+
Or any strange behavior of bullet plugin/gem, *please disable your browser's cache*.
|
122
|
+
|
123
|
+
****************************************************************************
|
124
|
+
|
101
125
|
h2. Advance
|
102
126
|
|
103
127
|
The bullet plugin use rack middleware for http request. If you want to bullet for without http server, such as job server. You can do like this:
|
104
128
|
|
105
129
|
<pre><code>
|
106
|
-
Bullet
|
130
|
+
Bullet.start_request if Bullet.enable?
|
107
131
|
# run job
|
108
132
|
if Bullet.enable?
|
109
|
-
Bullet
|
110
|
-
Bullet
|
111
|
-
Bullet
|
133
|
+
Bullet.growl_notification
|
134
|
+
Bullet.log_notificatioin('JobServer: ')
|
135
|
+
Bullet.end_request
|
112
136
|
end
|
113
137
|
</code></pre>
|
114
138
|
|
@@ -118,8 +142,6 @@ h2. Step by step example
|
|
118
142
|
|
119
143
|
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.
|
120
144
|
|
121
|
-
*Important*: It is strongly recommended you disable your browser's cache.
|
122
|
-
|
123
145
|
1. setup test environment
|
124
146
|
|
125
147
|
<pre><code>
|
@@ -178,8 +200,8 @@ $ script/plugin install git://github.com/flyerhzm/bullet.git
|
|
178
200
|
<pre><code>
|
179
201
|
config.after_initialize do
|
180
202
|
Bullet.enable = true
|
181
|
-
Bullet
|
182
|
-
Bullet
|
203
|
+
Bullet.alert = true
|
204
|
+
Bullet.bullet_logger = true
|
183
205
|
end
|
184
206
|
</code></pre>
|
185
207
|
|
@@ -285,5 +307,44 @@ In the meanwhile, there's a log appended into <code>log/bullet.log</code> file
|
|
285
307
|
Remove from your finder: :include => [:comments]
|
286
308
|
</code></pre>
|
287
309
|
|
310
|
+
13. simulate counter_cache. Change <code>app/controllers/posts_controller.rb</code> and <code>app/views/posts/index.html.erb</code>
|
311
|
+
|
312
|
+
<pre><code>
|
313
|
+
def index
|
314
|
+
@posts = Post.find(:all)
|
315
|
+
|
316
|
+
respond_to do |format|
|
317
|
+
format.html # index.html.erb
|
318
|
+
format.xml { render :xml => @posts }
|
319
|
+
end
|
320
|
+
end
|
321
|
+
</code></pre>
|
322
|
+
|
323
|
+
<pre><code>
|
324
|
+
<% @posts.each do |post| %>
|
325
|
+
<tr>
|
326
|
+
<td><%=h post.name %></td>
|
327
|
+
<td><%=h post.comments.size %></td>
|
328
|
+
<td><%= link_to 'Show', post %></td>
|
329
|
+
<td><%= link_to 'Edit', edit_post_path(post) %></td>
|
330
|
+
<td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
|
331
|
+
</tr>
|
332
|
+
<% end %>
|
333
|
+
</code></pre>
|
334
|
+
|
335
|
+
14. refresh http://localhost:3000/posts page, then you will see a popup alert box says
|
336
|
+
|
337
|
+
<pre><code>
|
338
|
+
Need counter cache
|
339
|
+
Post => [:comments]
|
340
|
+
</code></pre>
|
341
|
+
|
342
|
+
In the meanwhile, there's a log appended into <code>log/bullet.log</code> file.
|
343
|
+
|
344
|
+
<pre><code>
|
345
|
+
2009-09-11 10:07:10[INFO] Need Counter Cache
|
346
|
+
Post => [:comments]
|
347
|
+
</code></pre>
|
348
|
+
|
288
349
|
|
289
350
|
Copyright (c) 2009 Richard Huang (flyerhzm@gmail.com), released under the MIT license
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.5.0
|
data/bullet.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{bullet}
|
8
|
-
s.version = "1.
|
8
|
+
s.version = "1.5.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Richard Huang"]
|
12
|
-
s.date = %q{2009-09-
|
12
|
+
s.date = %q{2009-09-11}
|
13
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
14
|
s.email = %q{flyerhzm@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -26,6 +26,7 @@ Gem::Specification.new do |s|
|
|
26
26
|
"lib/bullet/association.rb",
|
27
27
|
"lib/bullet/counter.rb",
|
28
28
|
"lib/bullet/logger.rb",
|
29
|
+
"lib/bullet/notification.rb",
|
29
30
|
"lib/bulletware.rb",
|
30
31
|
"rails/init.rb",
|
31
32
|
"spec/bullet_association_spec.rb",
|
@@ -40,9 +41,9 @@ Gem::Specification.new do |s|
|
|
40
41
|
s.rubygems_version = %q{1.3.5}
|
41
42
|
s.summary = %q{A plugin to kill N+1 queries and unused eager loading}
|
42
43
|
s.test_files = [
|
43
|
-
"spec/
|
44
|
-
"spec/
|
45
|
-
"spec/
|
44
|
+
"spec/bullet_counter_spec.rb",
|
45
|
+
"spec/spec_helper.rb",
|
46
|
+
"spec/bullet_association_spec.rb"
|
46
47
|
]
|
47
48
|
|
48
49
|
if s.respond_to? :specification_version then
|
data/lib/bullet.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
require 'bulletware'
|
2
2
|
|
3
3
|
module Bullet
|
4
|
-
@@enable = nil
|
5
|
-
|
6
4
|
class <<self
|
5
|
+
attr_accessor :enable, :alert, :console, :growl, :growl_password, :rails_logger, :bullet_logger, :logger, :logger_file
|
6
|
+
|
7
7
|
def enable=(enable)
|
8
|
-
|
8
|
+
@enable = enable
|
9
9
|
if enable?
|
10
10
|
Bullet::ActiveRecord.enable
|
11
11
|
ActionController::Dispatcher.middleware.use Bulletware
|
@@ -13,11 +13,59 @@ module Bullet
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def enable?
|
16
|
-
|
16
|
+
@enable == true
|
17
|
+
end
|
18
|
+
|
19
|
+
def growl=(growl)
|
20
|
+
if growl
|
21
|
+
begin
|
22
|
+
require 'ruby-growl'
|
23
|
+
growl = Growl.new('localhost', 'ruby-growl', ['Bullet Notification'], nil, @@growl_password)
|
24
|
+
growl.notify('Bullet Notification', 'Bullet Notification', 'Bullet Growl notifications have been turned on')
|
25
|
+
rescue MissingSourceFile
|
26
|
+
raise NotificationError.new('You must install the ruby-growl gem to use Growl notifications: `sudo gem install ruby-growl`')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@growl = growl
|
30
|
+
end
|
31
|
+
|
32
|
+
def bullet_logger=(bullet_logger)
|
33
|
+
if @bullet_logger = bullet_logger
|
34
|
+
@logger_file = File.open(Bullet::BulletLogger::LOG_FILE, 'a+')
|
35
|
+
@logger = Bullet::BulletLogger.new(@logger_file)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
BULLETS = [Bullet::Association, Bullet::Counter]
|
40
|
+
|
41
|
+
def start_request
|
42
|
+
BULLETS.each {|bullet| bullet.start_request}
|
43
|
+
end
|
44
|
+
|
45
|
+
def end_request
|
46
|
+
BULLETS.each {|bullet| bullet.end_request}
|
47
|
+
end
|
48
|
+
|
49
|
+
def notification?
|
50
|
+
BULLETS.any? {|bullet| bullet.notification?}
|
51
|
+
end
|
52
|
+
|
53
|
+
def javascript_notification
|
54
|
+
BULLETS.collect {|bullet| bullet.javascript_notification if bullet.notification?}.join("\n")
|
55
|
+
end
|
56
|
+
|
57
|
+
def growl_notification
|
58
|
+
BULLETS.each {|bullet| bullet.growl_notification if bullet.notification?}
|
59
|
+
end
|
60
|
+
|
61
|
+
def log_notification(path)
|
62
|
+
BULLETS.each {|bullet| bullet.log_notification(path) if bullet.notification?}
|
17
63
|
end
|
18
64
|
end
|
19
65
|
|
20
66
|
autoload :ActiveRecord, 'bullet/active_record'
|
21
67
|
autoload :Association, 'bullet/association'
|
68
|
+
autoload :Counter, 'bullet/counter'
|
22
69
|
autoload :BulletLogger, 'bullet/logger'
|
70
|
+
autoload :Notification, 'bullet/notification'
|
23
71
|
end
|
data/lib/bullet/association.rb
CHANGED
@@ -1,64 +1,12 @@
|
|
1
1
|
module Bullet
|
2
|
-
class BulletAssociationError < StandardError
|
3
|
-
end
|
4
|
-
|
5
2
|
class Association
|
6
3
|
class <<self
|
7
|
-
|
8
|
-
@@bullet_logger = nil
|
9
|
-
@@console = nil
|
10
|
-
@@growl = nil
|
11
|
-
@@growl_password = nil
|
12
|
-
@@rails_logger = nil
|
13
|
-
|
14
|
-
###################################################
|
15
|
-
# Configurations
|
16
|
-
###################################################
|
17
|
-
def alert=(alert)
|
18
|
-
@@alert = alert
|
19
|
-
end
|
20
|
-
|
21
|
-
def console=(console)
|
22
|
-
@@console = console
|
23
|
-
end
|
24
|
-
|
25
|
-
def growl=(growl)
|
26
|
-
if growl
|
27
|
-
begin
|
28
|
-
require 'ruby-growl'
|
29
|
-
growl = Growl.new('localhost', 'ruby-growl', ['Bullet Notification'], nil, @@growl_password)
|
30
|
-
growl.notify('Bullet Notification', 'Bullet Notification', 'Bullet Growl notifications have been turned on')
|
31
|
-
rescue MissingSourceFile
|
32
|
-
raise BulletAssociationError.new('You must install the ruby-growl gem to use Growl notifications: `sudo gem install ruby-growl`')
|
33
|
-
end
|
34
|
-
end
|
35
|
-
@@growl = growl
|
36
|
-
end
|
37
|
-
|
38
|
-
def growl_password=(growl_password)
|
39
|
-
@@growl_password = growl_password
|
40
|
-
end
|
4
|
+
include Bullet::Notification
|
41
5
|
|
42
|
-
def bullet_logger=(bullet_logger)
|
43
|
-
if @@bullet_logger = bullet_logger
|
44
|
-
@@logger_file = File.open(Bullet::BulletLogger::LOG_FILE, 'a+')
|
45
|
-
@@logger = Bullet::BulletLogger.new(@@logger_file)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def rails_logger=(rails_logger)
|
50
|
-
@@rails_logger = rails_logger
|
51
|
-
end
|
52
|
-
|
53
|
-
#####################################################
|
54
|
-
# login control interface
|
55
|
-
#####################################################
|
56
6
|
def start_request
|
57
|
-
# puts "start request"
|
58
7
|
end
|
59
8
|
|
60
9
|
def end_request
|
61
|
-
# puts "end request"
|
62
10
|
@@object_associations = nil
|
63
11
|
@@unpreload_associations = nil
|
64
12
|
@@unused_preload_associations = nil
|
@@ -69,110 +17,36 @@ module Bullet
|
|
69
17
|
@@eager_loadings = nil
|
70
18
|
end
|
71
19
|
|
72
|
-
def
|
20
|
+
def notification?
|
73
21
|
check_unused_preload_associations
|
74
22
|
has_unpreload_associations? or has_unused_preload_associations?
|
75
23
|
end
|
76
24
|
|
77
|
-
######################################################
|
78
|
-
# Notifications
|
79
|
-
######################################################
|
80
|
-
def javascript_notification
|
81
|
-
str = ''
|
82
|
-
if @@alert || @@console
|
83
|
-
response = notification_response
|
84
|
-
end
|
85
|
-
if @@alert
|
86
|
-
str << wrap_js_association("alert(#{response.join("\n").inspect});")
|
87
|
-
end
|
88
|
-
if @@console
|
89
|
-
title = []
|
90
|
-
title << unused_preload_messages.first.first unless unused_preload_messages.empty?
|
91
|
-
title << unpreload_messages.first.first unless unpreload_messages.empty?
|
92
|
-
code = <<-CODE
|
93
|
-
if (typeof(console) !== 'undefined') {
|
94
|
-
|
95
|
-
if (console.groupCollapsed && console.groupEnd && console.log) {
|
96
|
-
|
97
|
-
console.groupCollapsed(#{title.join(', ').inspect});
|
98
|
-
console.log(#{response.join("\n").inspect});
|
99
|
-
console.log(#{call_stack_messages.join("\n").inspect});
|
100
|
-
console.groupEnd();
|
101
|
-
|
102
|
-
} else if (console.log) {
|
103
|
-
|
104
|
-
console.log(#{response.join("\n").inspect});
|
105
|
-
}
|
106
|
-
}
|
107
|
-
CODE
|
108
|
-
str << wrap_js_association(code)
|
109
|
-
end
|
110
|
-
str
|
111
|
-
end
|
112
|
-
|
113
|
-
def growl_notification
|
114
|
-
if @@growl
|
115
|
-
response = notification_response
|
116
|
-
begin
|
117
|
-
growl = Growl.new('localhost', 'ruby-growl', ['Bullet Notification'], nil, @@growl_password)
|
118
|
-
growl.notify('Bullet Notification', 'Bullet Notification', response.join("\n"))
|
119
|
-
rescue
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def log_notificatioin(path)
|
125
|
-
if (@@bullet_logger || @@rails_logger) && (!unpreload_associations.empty? || !unused_preload_associations.empty?)
|
126
|
-
Rails.logger.warn '' if @@rails_logger
|
127
|
-
unused_preload_messages(path).each do |message|
|
128
|
-
@@logger.info(message.join("\n")) if @@bullet_logger
|
129
|
-
Rails.logger.warn(message.join("\n")) if @@rails_logger
|
130
|
-
end
|
131
|
-
unpreload_messages(path).each do |message|
|
132
|
-
@@logger.info(message.join("\n")) if @@bullet_logger
|
133
|
-
Rails.logger.warn(message.join("\n")) if @@rails_logger
|
134
|
-
end
|
135
|
-
call_stack_messages.each do |message|
|
136
|
-
@@logger.info(message.join("\n")) if @@bullet_logger
|
137
|
-
Rails.logger.warn(message.join("\n")) if @@rails_logger
|
138
|
-
end
|
139
|
-
@@logger_file.flush if @@bullet_logger
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
25
|
def add_unpreload_associations(klazz, associations)
|
144
|
-
# puts "add unpreload associations, #{klazz} => #{associations.inspect}"
|
145
26
|
unpreload_associations[klazz] ||= []
|
146
27
|
unpreload_associations[klazz] << associations
|
147
28
|
unique(unpreload_associations[klazz])
|
148
29
|
end
|
149
30
|
|
150
31
|
def add_unused_preload_associations(klazz, associations)
|
151
|
-
# puts "add unused preload associations, #{klazz} => #{associations.inspect}"
|
152
32
|
unused_preload_associations[klazz] ||= []
|
153
33
|
unused_preload_associations[klazz] << associations
|
154
34
|
unique(unused_preload_associations[klazz])
|
155
35
|
end
|
156
36
|
|
157
37
|
def add_association(object, associations)
|
158
|
-
# puts "add associations, #{object} => #{associations.inspect}"
|
159
38
|
object_associations[object] ||= []
|
160
39
|
object_associations[object] << associations
|
161
40
|
unique(object_associations[object])
|
162
41
|
end
|
163
42
|
|
164
|
-
###################################################
|
165
|
-
# interface for active record
|
166
|
-
###################################################
|
167
43
|
def add_call_object_associations(object, associations)
|
168
|
-
# puts "add call object associations, #{object} => #{associations.inspect}"
|
169
44
|
call_object_associations[object] ||= []
|
170
45
|
call_object_associations[object] << associations
|
171
46
|
unique(call_object_associations[object])
|
172
47
|
end
|
173
48
|
|
174
49
|
def add_possible_objects(objects)
|
175
|
-
# puts "add possible objects, #{objects.inspect}"
|
176
50
|
klazz= objects.first.class
|
177
51
|
possible_objects[klazz] ||= []
|
178
52
|
possible_objects[klazz] << objects
|
@@ -180,7 +54,6 @@ module Bullet
|
|
180
54
|
end
|
181
55
|
|
182
56
|
def add_impossible_object(object)
|
183
|
-
# puts "add impossible object, #{object}"
|
184
57
|
klazz = object.class
|
185
58
|
impossible_objects[klazz] ||= []
|
186
59
|
impossible_objects[klazz] << object
|
@@ -188,14 +61,12 @@ module Bullet
|
|
188
61
|
end
|
189
62
|
|
190
63
|
def add_klazz_associations(klazz, associations)
|
191
|
-
# puts "define associations, #{klazz} => #{associations.inspect}"
|
192
64
|
klazz_associations[klazz] ||= []
|
193
65
|
klazz_associations[klazz] << associations
|
194
66
|
unique(klazz_associations[klazz])
|
195
67
|
end
|
196
68
|
|
197
69
|
def add_eager_loadings(objects, associations)
|
198
|
-
# puts "add eager loadings, #{objects.inspect} => #{associations.inspect}"
|
199
70
|
objects = Array(objects)
|
200
71
|
eager_loadings[objects] ||= []
|
201
72
|
eager_loadings[objects] << associations
|
@@ -203,12 +74,10 @@ module Bullet
|
|
203
74
|
end
|
204
75
|
|
205
76
|
def define_association(klazz, associations)
|
206
|
-
# puts "define association, #{klazz} => #{associations.inspect}"
|
207
77
|
add_klazz_associations(klazz, associations)
|
208
78
|
end
|
209
79
|
|
210
80
|
def call_association(object, associations)
|
211
|
-
# puts "call association, #{object} => #{associations.inspect}"
|
212
81
|
add_call_object_associations(object, associations)
|
213
82
|
if unpreload_associations?(object, associations)
|
214
83
|
add_unpreload_associations(object.class, associations)
|
@@ -216,9 +85,6 @@ module Bullet
|
|
216
85
|
end
|
217
86
|
end
|
218
87
|
|
219
|
-
############################################
|
220
|
-
# for rspec
|
221
|
-
############################################
|
222
88
|
def check_unused_preload_associations
|
223
89
|
object_associations.each do |object, association|
|
224
90
|
related_objects = eager_loadings.select {|key, value| key.include?(object) and value == association}.collect(&:first).flatten
|
@@ -236,117 +102,122 @@ module Bullet
|
|
236
102
|
!unpreload_associations.empty?
|
237
103
|
end
|
238
104
|
|
239
|
-
|
240
105
|
private
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
106
|
+
def unpreload_associations?(object, associations)
|
107
|
+
klazz = object.class
|
108
|
+
(!possible_objects[klazz].nil? and possible_objects[klazz].include?(object)) and
|
109
|
+
(impossible_objects[klazz].nil? or !impossible_objects[klazz].include?(object)) and
|
110
|
+
(object_associations[object].nil? or !object_associations[object].include?(associations))
|
111
|
+
end
|
247
112
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
113
|
+
def notification_response
|
114
|
+
response = []
|
115
|
+
if has_unused_preload_associations?
|
116
|
+
response << unused_preload_messages.join("\n")
|
117
|
+
end
|
118
|
+
if has_unpreload_associations?
|
119
|
+
response << unpreload_messages.join("\n")
|
120
|
+
end
|
121
|
+
response
|
252
122
|
end
|
253
|
-
|
254
|
-
|
123
|
+
|
124
|
+
def console_title
|
125
|
+
title = []
|
126
|
+
title << unused_preload_messages.first.first unless unused_preload_messages.empty?
|
127
|
+
title << unpreload_messages.first.first unless unpreload_messages.empty?
|
255
128
|
end
|
256
|
-
response
|
257
|
-
end
|
258
129
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
messages <<
|
263
|
-
|
264
|
-
klazz_associations_str(klazz, associations),
|
265
|
-
" Remove from your finder: #{associations_str(associations)}"
|
266
|
-
]
|
130
|
+
def log_messages(path = nil)
|
131
|
+
messages = []
|
132
|
+
messages << unused_preload_messages(path)
|
133
|
+
messages << unpreload_messages(path)
|
134
|
+
messages << call_stack_messages
|
267
135
|
end
|
268
|
-
messages
|
269
|
-
end
|
270
136
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
137
|
+
def unused_preload_messages(path = nil)
|
138
|
+
messages = []
|
139
|
+
unused_preload_associations.each do |klazz, associations|
|
140
|
+
messages << [
|
141
|
+
"Unused Eager Loading #{path ? "in #{path}" : 'detected'}",
|
142
|
+
klazz_associations_str(klazz, associations),
|
143
|
+
" Remove from your finder: #{associations_str(associations)}"
|
144
|
+
]
|
145
|
+
end
|
146
|
+
messages
|
279
147
|
end
|
280
|
-
messages
|
281
|
-
end
|
282
148
|
|
283
|
-
|
284
|
-
|
285
|
-
|
149
|
+
def unpreload_messages(path = nil)
|
150
|
+
messages = []
|
151
|
+
unpreload_associations.each do |klazz, associations|
|
152
|
+
messages << [
|
153
|
+
"N+1 Query #{path ? "in #{path}" : 'detected'}",
|
154
|
+
klazz_associations_str(klazz, associations),
|
155
|
+
" Add to your finder: #{associations_str(associations)}"
|
156
|
+
]
|
157
|
+
end
|
158
|
+
messages
|
159
|
+
end
|
286
160
|
|
287
|
-
|
288
|
-
|
289
|
-
|
161
|
+
def call_stack_messages
|
162
|
+
callers.inject([]) do |messages, c|
|
163
|
+
messages << ['N+1 Query method call stack', c.collect {|line| " #{line}"}].flatten
|
164
|
+
end
|
165
|
+
end
|
290
166
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
str << message
|
295
|
-
str << "/*]]>*/</script>\n"
|
296
|
-
end
|
167
|
+
def klazz_associations_str(klazz, associations)
|
168
|
+
" #{klazz} => [#{associations.map(&:inspect).join(', ')}]"
|
169
|
+
end
|
297
170
|
|
298
|
-
|
299
|
-
|
300
|
-
|
171
|
+
def associations_str(associations)
|
172
|
+
":include => #{associations.map{|a| a.to_sym unless a.is_a? Hash}.inspect}"
|
173
|
+
end
|
174
|
+
|
175
|
+
def unique(array)
|
176
|
+
array.flatten!
|
177
|
+
array.uniq!
|
301
178
|
end
|
302
|
-
end
|
303
|
-
|
304
|
-
def unique(array)
|
305
|
-
array.flatten!
|
306
|
-
array.uniq!
|
307
|
-
end
|
308
179
|
|
309
|
-
|
310
|
-
|
311
|
-
|
180
|
+
def unpreload_associations
|
181
|
+
@@unpreload_associations ||= {}
|
182
|
+
end
|
312
183
|
|
313
|
-
|
314
|
-
|
315
|
-
|
184
|
+
def unused_preload_associations
|
185
|
+
@@unused_preload_associations ||= {}
|
186
|
+
end
|
316
187
|
|
317
|
-
|
318
|
-
|
319
|
-
|
188
|
+
def object_associations
|
189
|
+
@@object_associations ||= {}
|
190
|
+
end
|
320
191
|
|
321
|
-
|
322
|
-
|
323
|
-
|
192
|
+
def call_object_associations
|
193
|
+
@@call_object_associations ||= {}
|
194
|
+
end
|
324
195
|
|
325
|
-
|
326
|
-
|
327
|
-
|
196
|
+
def possible_objects
|
197
|
+
@@possible_objects ||= {}
|
198
|
+
end
|
328
199
|
|
329
|
-
|
330
|
-
|
331
|
-
|
200
|
+
def impossible_objects
|
201
|
+
@@impossible_objects ||= {}
|
202
|
+
end
|
332
203
|
|
333
|
-
|
334
|
-
|
335
|
-
|
204
|
+
def klazz_associations
|
205
|
+
@@klazz_associations ||= {}
|
206
|
+
end
|
336
207
|
|
337
|
-
|
338
|
-
|
339
|
-
|
208
|
+
def eager_loadings
|
209
|
+
@@eager_loadings ||= {}
|
210
|
+
end
|
340
211
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
212
|
+
VENDOR_ROOT = File.join(RAILS_ROOT, 'vendor')
|
213
|
+
def caller_in_project
|
214
|
+
callers << caller.select {|c| c =~ /#{RAILS_ROOT}/}.reject {|c| c =~ /#{VENDOR_ROOT}/}
|
215
|
+
callers.uniq!
|
216
|
+
end
|
346
217
|
|
347
|
-
|
348
|
-
|
349
|
-
|
218
|
+
def callers
|
219
|
+
@@callers ||= []
|
220
|
+
end
|
350
221
|
end
|
351
222
|
end
|
352
223
|
end
|
data/lib/bullet/counter.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
module Bullet
|
2
2
|
class Counter
|
3
3
|
class <<self
|
4
|
-
|
4
|
+
include Bullet::Notification
|
5
5
|
|
6
|
+
def start_request
|
6
7
|
end
|
7
8
|
|
8
9
|
def end_request
|
@@ -12,6 +13,26 @@ module Bullet
|
|
12
13
|
def need_counter_caches?
|
13
14
|
!klazz_associations.empty?
|
14
15
|
end
|
16
|
+
|
17
|
+
def notification?
|
18
|
+
need_counter_caches?
|
19
|
+
end
|
20
|
+
|
21
|
+
def notification_response
|
22
|
+
response = []
|
23
|
+
if need_counter_caches?
|
24
|
+
response << counter_cache_messages.join("\n")
|
25
|
+
end
|
26
|
+
response
|
27
|
+
end
|
28
|
+
|
29
|
+
def console_title
|
30
|
+
title = ["Need Counter Cache"]
|
31
|
+
end
|
32
|
+
|
33
|
+
def log_messages(path = nil)
|
34
|
+
[counter_cache_messages(path)]
|
35
|
+
end
|
15
36
|
|
16
37
|
def add_counter_cache(object, associations)
|
17
38
|
klazz = object.class
|
@@ -21,9 +42,25 @@ module Bullet
|
|
21
42
|
klazz_associations[klazz].uniq!
|
22
43
|
end
|
23
44
|
|
24
|
-
|
25
|
-
|
26
|
-
|
45
|
+
private
|
46
|
+
def counter_cache_messages(path = nil)
|
47
|
+
messages = []
|
48
|
+
klazz_associations.each do |klazz, associations|
|
49
|
+
messages << [
|
50
|
+
"Need Counter Cache",
|
51
|
+
" #{klazz} => [#{associations.map(&:inspect).join(', ')}]"
|
52
|
+
]
|
53
|
+
end
|
54
|
+
messages
|
55
|
+
end
|
56
|
+
|
57
|
+
def call_stack_messages
|
58
|
+
[]
|
59
|
+
end
|
60
|
+
|
61
|
+
def klazz_associations
|
62
|
+
@@klazz_associations ||= {}
|
63
|
+
end
|
27
64
|
end
|
28
65
|
end
|
29
66
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Bullet
|
2
|
+
class NotificationError < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
module Notification
|
6
|
+
def notification?
|
7
|
+
end
|
8
|
+
|
9
|
+
def notification_response
|
10
|
+
end
|
11
|
+
|
12
|
+
def console_title
|
13
|
+
end
|
14
|
+
|
15
|
+
def log_message(path = nil)
|
16
|
+
end
|
17
|
+
|
18
|
+
def javascript_notification
|
19
|
+
str = ''
|
20
|
+
if Bullet.alert || Bullet.console
|
21
|
+
response = notification_response
|
22
|
+
end
|
23
|
+
if Bullet.alert
|
24
|
+
str << wrap_js_association("alert(#{response.join("\n").inspect});")
|
25
|
+
end
|
26
|
+
if Bullet.console
|
27
|
+
code = <<-CODE
|
28
|
+
if (typeof(console) !== 'undefined') {
|
29
|
+
|
30
|
+
if (console.groupCollapsed && console.groupEnd && console.log) {
|
31
|
+
|
32
|
+
console.groupCollapsed(#{console_title.join(', ').inspect});
|
33
|
+
console.log(#{response.join("\n").inspect});
|
34
|
+
console.log(#{call_stack_messages.join("\n").inspect});
|
35
|
+
console.groupEnd();
|
36
|
+
|
37
|
+
} else if (console.log) {
|
38
|
+
|
39
|
+
console.log(#{response.join("\n").inspect});
|
40
|
+
}
|
41
|
+
}
|
42
|
+
CODE
|
43
|
+
str << wrap_js_association(code)
|
44
|
+
end
|
45
|
+
str
|
46
|
+
end
|
47
|
+
|
48
|
+
def growl_notification
|
49
|
+
if Bullet.growl
|
50
|
+
response = notification_response
|
51
|
+
begin
|
52
|
+
growl = Growl.new('localhost', 'ruby-growl', ['Bullet Notification'], nil, Bullet.growl_password)
|
53
|
+
growl.notify('Bullet Notification', 'Bullet Notification', response.join("\n"))
|
54
|
+
rescue
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def log_notification(path)
|
60
|
+
if Bullet.bullet_logger || Bullet.rails_logger
|
61
|
+
Rails.logger.warn '' if Bullet.rails_logger
|
62
|
+
messages = log_messages(path)
|
63
|
+
messages.each do |message|
|
64
|
+
Bullet.logger.info(message.join("\n")) if Bullet.bullet_logger
|
65
|
+
Rails.logger.warn(message.join("\n")) if Bullet.rails_logger
|
66
|
+
end
|
67
|
+
Bullet.logger_file.flush if Bullet.bullet_logger
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def wrap_js_association(message)
|
73
|
+
str = ''
|
74
|
+
str << "<script type=\"text/javascript\">/*<![CDATA[*/"
|
75
|
+
str << message
|
76
|
+
str << "/*]]>*/</script>\n"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/bulletware.rb
CHANGED
@@ -6,21 +6,21 @@ class Bulletware
|
|
6
6
|
def call(env)
|
7
7
|
return @app.call(env) unless Bullet.enable?
|
8
8
|
|
9
|
-
Bullet
|
9
|
+
Bullet.start_request
|
10
10
|
status, headers, response = @app.call(env)
|
11
11
|
return [status, headers, response] if response.empty?
|
12
12
|
|
13
|
-
if Bullet
|
13
|
+
if Bullet.notification?
|
14
14
|
if check_html?(headers, response)
|
15
|
-
response_body = response.body << Bullet
|
15
|
+
response_body = response.body << Bullet.javascript_notification
|
16
16
|
headers['Content-Length'] = response_body.length.to_s
|
17
17
|
end
|
18
18
|
|
19
|
-
Bullet
|
20
|
-
Bullet
|
19
|
+
Bullet.growl_notification
|
20
|
+
Bullet.log_notification(env['PATH_INFO'])
|
21
21
|
end
|
22
22
|
response_body ||= response.body
|
23
|
-
Bullet
|
23
|
+
Bullet.end_request
|
24
24
|
[status, headers, response_body]
|
25
25
|
end
|
26
26
|
|
data/spec/bullet_counter_spec.rb
CHANGED
@@ -54,12 +54,19 @@ describe Bullet::Counter do
|
|
54
54
|
Bullet::Counter.end_request
|
55
55
|
end
|
56
56
|
|
57
|
-
it "should need counter cache with
|
57
|
+
it "should need counter cache with all cities" do
|
58
58
|
Country.all.each do |country|
|
59
59
|
country.cities.size
|
60
60
|
end
|
61
61
|
Bullet::Counter.should be_need_counter_caches
|
62
62
|
end
|
63
|
+
|
64
|
+
it "should not need counter cache with part of cities" do
|
65
|
+
Country.all.each do |country|
|
66
|
+
country.cities(:conditions => ["name = ?", 'first']).size
|
67
|
+
end
|
68
|
+
Bullet::Counter.should_not be_need_counter_cache
|
69
|
+
end
|
63
70
|
end
|
64
71
|
|
65
72
|
describe Bullet::Counter do
|
@@ -115,7 +122,7 @@ describe Bullet::Counter do
|
|
115
122
|
Bullet::Counter.end_request
|
116
123
|
end
|
117
124
|
|
118
|
-
it "should need counter cache
|
125
|
+
it "should not need counter cache" do
|
119
126
|
Person.all.each do |person|
|
120
127
|
person.pets.size
|
121
128
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -4,6 +4,7 @@ require 'active_record'
|
|
4
4
|
require 'action_controller'
|
5
5
|
|
6
6
|
RAILS_ROOT = File.expand_path(__FILE__).split('/')[0..-3].join('/') unless defined? RAILS_ROOT
|
7
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '../lib/bullet/notification'))
|
7
8
|
require File.expand_path(File.join(File.dirname(__FILE__), '../lib/bullet/logger'))
|
8
9
|
require File.expand_path(File.join(File.dirname(__FILE__), '../lib/bullet/active_record'))
|
9
10
|
require File.expand_path(File.join(File.dirname(__FILE__), '../lib/bullet/association'))
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flyerhzm-bullet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Richard Huang
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-09-
|
12
|
+
date: 2009-09-11 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -32,6 +32,7 @@ files:
|
|
32
32
|
- lib/bullet/association.rb
|
33
33
|
- lib/bullet/counter.rb
|
34
34
|
- lib/bullet/logger.rb
|
35
|
+
- lib/bullet/notification.rb
|
35
36
|
- lib/bulletware.rb
|
36
37
|
- rails/init.rb
|
37
38
|
- spec/bullet_association_spec.rb
|
@@ -66,6 +67,6 @@ signing_key:
|
|
66
67
|
specification_version: 3
|
67
68
|
summary: A plugin to kill N+1 queries and unused eager loading
|
68
69
|
test_files:
|
69
|
-
- spec/bullet_association_spec.rb
|
70
70
|
- spec/bullet_counter_spec.rb
|
71
71
|
- spec/spec_helper.rb
|
72
|
+
- spec/bullet_association_spec.rb
|