bullet 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.textile +381 -0
- data/Rakefile +33 -0
- data/VERSION +1 -0
- data/bullet.gemspec +59 -0
- data/lib/bullet.rb +77 -0
- data/lib/bullet/action_controller.rb +16 -0
- data/lib/bullet/active_record.rb +105 -0
- data/lib/bullet/association.rb +267 -0
- data/lib/bullet/counter.rb +101 -0
- data/lib/bullet/logger.rb +9 -0
- data/lib/bullet/notification.rb +83 -0
- data/lib/bulletware.rb +42 -0
- data/rails/init.rb +1 -0
- data/spec/bullet_association_spec.rb +1044 -0
- data/spec/bullet_counter_spec.rb +136 -0
- data/spec/spec.opts +8 -0
- data/spec/spec_helper.rb +50 -0
- data/tasks/bullet_tasks.rake +9 -0
- metadata +75 -0
data/lib/bullet.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'bulletware'
|
2
|
+
|
3
|
+
module Bullet
|
4
|
+
autoload :ActiveRecord, 'bullet/active_record'
|
5
|
+
autoload :ActionController, 'bullet/action_controller'
|
6
|
+
autoload :Association, 'bullet/association'
|
7
|
+
autoload :Counter, 'bullet/counter'
|
8
|
+
autoload :BulletLogger, 'bullet/logger'
|
9
|
+
autoload :Notification, 'bullet/notification'
|
10
|
+
|
11
|
+
class <<self
|
12
|
+
attr_accessor :enable, :alert, :console, :growl, :growl_password, :rails_logger, :bullet_logger, :logger, :logger_file, :disable_browser_cache
|
13
|
+
|
14
|
+
def enable=(enable)
|
15
|
+
@enable = enable
|
16
|
+
if enable?
|
17
|
+
Bullet::ActiveRecord.enable
|
18
|
+
Bullet::ActionController.enable
|
19
|
+
::ActionController::Dispatcher.middleware.use Bulletware
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def enable?
|
24
|
+
@enable == true
|
25
|
+
end
|
26
|
+
|
27
|
+
def growl=(growl)
|
28
|
+
if growl
|
29
|
+
begin
|
30
|
+
require 'ruby-growl'
|
31
|
+
growl = Growl.new('localhost', 'ruby-growl', ['Bullet Notification'], nil, @growl_password)
|
32
|
+
growl.notify('Bullet Notification', 'Bullet Notification', 'Bullet Growl notifications have been turned on')
|
33
|
+
rescue MissingSourceFile
|
34
|
+
raise NotificationError.new('You must install the ruby-growl gem to use Growl notifications: `sudo gem install ruby-growl`')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
@growl = growl
|
38
|
+
end
|
39
|
+
|
40
|
+
def bullet_logger=(bullet_logger)
|
41
|
+
if @bullet_logger = bullet_logger
|
42
|
+
@logger_file = File.open(Bullet::BulletLogger::LOG_FILE, 'a+')
|
43
|
+
@logger = Bullet::BulletLogger.new(@logger_file)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
BULLETS = [Bullet::Association, Bullet::Counter]
|
48
|
+
|
49
|
+
def start_request
|
50
|
+
BULLETS.each {|bullet| bullet.start_request}
|
51
|
+
end
|
52
|
+
|
53
|
+
def end_request
|
54
|
+
BULLETS.each {|bullet| bullet.end_request}
|
55
|
+
end
|
56
|
+
|
57
|
+
def clear
|
58
|
+
BULLETS.each {|bullet| bullet.clear}
|
59
|
+
end
|
60
|
+
|
61
|
+
def notification?
|
62
|
+
BULLETS.any? {|bullet| bullet.notification?}
|
63
|
+
end
|
64
|
+
|
65
|
+
def javascript_notification
|
66
|
+
BULLETS.collect {|bullet| bullet.javascript_notification if bullet.notification?}.join("\n")
|
67
|
+
end
|
68
|
+
|
69
|
+
def growl_notification
|
70
|
+
BULLETS.each {|bullet| bullet.growl_notification if bullet.notification?}
|
71
|
+
end
|
72
|
+
|
73
|
+
def log_notification(path)
|
74
|
+
BULLETS.each {|bullet| bullet.log_notification(path) if bullet.notification?}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Bullet
|
2
|
+
class ActionController
|
3
|
+
def self.enable
|
4
|
+
::ActionController::Dispatcher.class_eval do
|
5
|
+
class <<self
|
6
|
+
alias_method :origin_reload_application, :reload_application
|
7
|
+
|
8
|
+
def reload_application
|
9
|
+
origin_reload_application
|
10
|
+
Bullet.clear
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Bullet
|
2
|
+
module ActiveRecord
|
3
|
+
def self.enable
|
4
|
+
::ActiveRecord::Base.class_eval do
|
5
|
+
class << self
|
6
|
+
alias_method :origin_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 = origin_find_every(options)
|
11
|
+
|
12
|
+
if records
|
13
|
+
if records.size > 1
|
14
|
+
Bullet::Association.add_possible_objects(records)
|
15
|
+
Bullet::Counter.add_possible_objects(records)
|
16
|
+
elsif records.size == 1
|
17
|
+
Bullet::Association.add_impossible_object(records.first)
|
18
|
+
Bullet::Counter.add_impossible_object(records.first)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
records
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
::ActiveRecord::AssociationPreload::ClassMethods.class_eval do
|
28
|
+
alias_method :origin_preload_associations, :preload_associations
|
29
|
+
# add include for one to many associations query
|
30
|
+
def preload_associations(records, associations, preload_options={})
|
31
|
+
records = [records].flatten.compact.uniq
|
32
|
+
return if records.empty?
|
33
|
+
records.each do |record|
|
34
|
+
Bullet::Association.add_object_associations(record, associations)
|
35
|
+
end
|
36
|
+
Bullet::Association.add_eager_loadings(records, associations)
|
37
|
+
origin_preload_associations(records, associations, preload_options={})
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
::ActiveRecord::Associations::ClassMethods.class_eval do
|
42
|
+
# define one to many associations
|
43
|
+
alias_method :origin_collection_reader_method, :collection_reader_method
|
44
|
+
def collection_reader_method(reflection, association_proxy_class)
|
45
|
+
Bullet::Association.define_association(self, reflection.name)
|
46
|
+
origin_collection_reader_method(reflection, association_proxy_class)
|
47
|
+
end
|
48
|
+
|
49
|
+
# add include in named_scope
|
50
|
+
alias_method :origin_find_with_associations, :find_with_associations
|
51
|
+
def find_with_associations(options)
|
52
|
+
records = origin_find_with_associations(options)
|
53
|
+
associations = merge_includes(scope(:find, :include), options[:include])
|
54
|
+
records.each do |record|
|
55
|
+
Bullet::Association.add_object_associations(record, associations)
|
56
|
+
Bullet::Association.call_association(record, associations)
|
57
|
+
end
|
58
|
+
Bullet::Association.add_eager_loadings(records, associations)
|
59
|
+
records
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
::ActiveRecord::Associations::ClassMethods::JoinDependency.class_eval do
|
64
|
+
# call join associations
|
65
|
+
alias_method :origin_construct_association, :construct_association
|
66
|
+
def construct_association(record, join, row)
|
67
|
+
associations = join.reflection.name
|
68
|
+
Bullet::Association.add_object_associations(record, associations)
|
69
|
+
Bullet::Association.call_association(record, associations)
|
70
|
+
origin_construct_association(record, join, row)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
::ActiveRecord::Associations::AssociationCollection.class_eval do
|
75
|
+
# call one to many associations
|
76
|
+
alias_method :origin_load_target, :load_target
|
77
|
+
def load_target
|
78
|
+
Bullet::Association.call_association(@owner, @reflection.name)
|
79
|
+
origin_load_target
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
::ActiveRecord::Associations::AssociationProxy.class_eval do
|
84
|
+
# call has_one and belong_to association
|
85
|
+
alias_method :origin_load_target, :load_target
|
86
|
+
def load_target
|
87
|
+
# avoid stack level too deep
|
88
|
+
result = origin_load_target
|
89
|
+
Bullet::Association.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
|
90
|
+
Bullet::Association.add_possible_objects(result)
|
91
|
+
result
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
::ActiveRecord::Associations::HasManyAssociation.class_eval do
|
96
|
+
alias_method :origin_has_cached_counter?, :has_cached_counter?
|
97
|
+
def has_cached_counter?
|
98
|
+
result = origin_has_cached_counter?
|
99
|
+
Bullet::Counter.add_counter_cache(@owner, @reflection.name) unless result
|
100
|
+
result
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,267 @@
|
|
1
|
+
module Bullet
|
2
|
+
class Association
|
3
|
+
class <<self
|
4
|
+
include Bullet::Notification
|
5
|
+
|
6
|
+
def start_request
|
7
|
+
end
|
8
|
+
|
9
|
+
def end_request
|
10
|
+
clear
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear
|
14
|
+
@@object_associations = nil
|
15
|
+
@@unpreload_associations = nil
|
16
|
+
@@unused_preload_associations = nil
|
17
|
+
@@callers = nil
|
18
|
+
@@possible_objects = nil
|
19
|
+
@@impossible_objects = nil
|
20
|
+
@@call_object_associations = nil
|
21
|
+
@@eager_loadings = nil
|
22
|
+
@@klazz_associations = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def notification?
|
26
|
+
check_unused_preload_associations
|
27
|
+
has_unpreload_associations? or has_unused_preload_associations?
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_unpreload_associations(klazz, associations)
|
31
|
+
unpreload_associations[klazz] ||= []
|
32
|
+
unpreload_associations[klazz] << associations
|
33
|
+
unique(unpreload_associations[klazz])
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_unused_preload_associations(klazz, associations)
|
37
|
+
unused_preload_associations[klazz] ||= []
|
38
|
+
unused_preload_associations[klazz] << associations
|
39
|
+
unique(unused_preload_associations[klazz])
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_object_associations(object, associations)
|
43
|
+
object_associations[object] ||= []
|
44
|
+
object_associations[object] << associations
|
45
|
+
unique(object_associations[object])
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_call_object_associations(object, associations)
|
49
|
+
call_object_associations[object] ||= []
|
50
|
+
call_object_associations[object] << associations
|
51
|
+
unique(call_object_associations[object])
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_possible_objects(objects)
|
55
|
+
klazz = objects.is_a?(Array) ? objects.first.class : objects.class
|
56
|
+
possible_objects[klazz] ||= []
|
57
|
+
possible_objects[klazz] << objects
|
58
|
+
unique(possible_objects[klazz])
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_impossible_object(object)
|
62
|
+
klazz = object.class
|
63
|
+
impossible_objects[klazz] ||= []
|
64
|
+
impossible_objects[klazz] << object
|
65
|
+
impossible_objects[klazz].uniq!
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_klazz_associations(klazz, associations)
|
69
|
+
klazz_associations[klazz] ||= []
|
70
|
+
klazz_associations[klazz] << associations
|
71
|
+
unique(klazz_associations[klazz])
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_eager_loadings(objects, associations)
|
75
|
+
objects = Array(objects)
|
76
|
+
eager_loadings[objects] ||= []
|
77
|
+
eager_loadings.each do |k, v|
|
78
|
+
unless (k & objects).empty?
|
79
|
+
if (k & objects) == k
|
80
|
+
eager_loadings[k] = (eager_loadings[k] + Array(associations))
|
81
|
+
unique(eager_loadings[k])
|
82
|
+
break
|
83
|
+
else
|
84
|
+
eager_loadings.merge!({(k & objects) => (eager_loadings[k] + Array(associations))})
|
85
|
+
unique(eager_loadings[(k & objects)])
|
86
|
+
eager_loadings.merge!({(k - objects) => eager_loadings[k]}) unless (k - objects).empty?
|
87
|
+
unique(eager_loadings[(k - objects)])
|
88
|
+
eager_loadings.delete(k)
|
89
|
+
objects = objects - k
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
unless objects.empty?
|
94
|
+
eager_loadings[objects] << Array(associations)
|
95
|
+
unique(eager_loadings[objects])
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def define_association(klazz, associations)
|
100
|
+
add_klazz_associations(klazz, associations)
|
101
|
+
end
|
102
|
+
|
103
|
+
def call_association(object, associations)
|
104
|
+
add_call_object_associations(object, associations)
|
105
|
+
if unpreload_associations?(object, associations)
|
106
|
+
add_unpreload_associations(object.class, associations)
|
107
|
+
caller_in_project
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def check_unused_preload_associations
|
112
|
+
object_associations.each do |object, association|
|
113
|
+
related_objects = eager_loadings.select {|key, value| key.include?(object) and value == association}.collect(&:first).flatten
|
114
|
+
call_object_association = related_objects.collect { |related_object| call_object_associations[related_object] }.compact.flatten.uniq
|
115
|
+
diff_object_association = (association - call_object_association).reject {|a| a.is_a? Hash}
|
116
|
+
add_unused_preload_associations(object.class, diff_object_association) unless diff_object_association.empty?
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def has_unused_preload_associations?
|
121
|
+
!unused_preload_associations.empty?
|
122
|
+
end
|
123
|
+
|
124
|
+
def has_unpreload_associations?
|
125
|
+
!unpreload_associations.empty?
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
def unpreload_associations?(object, associations)
|
130
|
+
possible?(object) and !impossible?(object) and !association?(object, associations)
|
131
|
+
end
|
132
|
+
|
133
|
+
def possible?(object)
|
134
|
+
klazz = object.class
|
135
|
+
possible_objects[klazz] and possible_objects[klazz].include?(object)
|
136
|
+
end
|
137
|
+
|
138
|
+
def impossible?(object)
|
139
|
+
klazz = object.class
|
140
|
+
impossible_objects[klazz] and impossible_objects[klazz].include?(object)
|
141
|
+
end
|
142
|
+
|
143
|
+
def association?(object, associations)
|
144
|
+
object_associations.each do |key, value|
|
145
|
+
if key == object
|
146
|
+
value.each do |v|
|
147
|
+
result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
|
148
|
+
return true if result
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
return false
|
153
|
+
end
|
154
|
+
|
155
|
+
def notification_response
|
156
|
+
response = []
|
157
|
+
if has_unused_preload_associations?
|
158
|
+
response << unused_preload_messages.join("\n")
|
159
|
+
end
|
160
|
+
if has_unpreload_associations?
|
161
|
+
response << unpreload_messages.join("\n")
|
162
|
+
end
|
163
|
+
response
|
164
|
+
end
|
165
|
+
|
166
|
+
def console_title
|
167
|
+
title = []
|
168
|
+
title << unused_preload_messages.first.first unless unused_preload_messages.empty?
|
169
|
+
title << unpreload_messages.first.first unless unpreload_messages.empty?
|
170
|
+
title
|
171
|
+
end
|
172
|
+
|
173
|
+
def log_messages(path = nil)
|
174
|
+
messages = []
|
175
|
+
messages << unused_preload_messages(path)
|
176
|
+
messages << unpreload_messages(path)
|
177
|
+
messages << call_stack_messages
|
178
|
+
messages
|
179
|
+
end
|
180
|
+
|
181
|
+
def unused_preload_messages(path = nil)
|
182
|
+
messages = []
|
183
|
+
unused_preload_associations.each do |klazz, associations|
|
184
|
+
messages << [
|
185
|
+
"Unused Eager Loading #{path ? "in #{path}" : 'detected'}",
|
186
|
+
klazz_associations_str(klazz, associations),
|
187
|
+
" Remove from your finder: #{associations_str(associations)}"
|
188
|
+
]
|
189
|
+
end
|
190
|
+
messages
|
191
|
+
end
|
192
|
+
|
193
|
+
def unpreload_messages(path = nil)
|
194
|
+
messages = []
|
195
|
+
unpreload_associations.each do |klazz, associations|
|
196
|
+
messages << [
|
197
|
+
"N+1 Query #{path ? "in #{path}" : 'detected'}",
|
198
|
+
klazz_associations_str(klazz, associations),
|
199
|
+
" Add to your finder: #{associations_str(associations)}"
|
200
|
+
]
|
201
|
+
end
|
202
|
+
messages
|
203
|
+
end
|
204
|
+
|
205
|
+
def call_stack_messages
|
206
|
+
callers.inject([]) do |messages, c|
|
207
|
+
messages << ['N+1 Query method call stack', c.collect {|line| " #{line}"}].flatten
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def klazz_associations_str(klazz, associations)
|
212
|
+
" #{klazz} => [#{associations.map(&:inspect).join(', ')}]"
|
213
|
+
end
|
214
|
+
|
215
|
+
def associations_str(associations)
|
216
|
+
":include => #{associations.map{|a| a.to_sym unless a.is_a? Hash}.inspect}"
|
217
|
+
end
|
218
|
+
|
219
|
+
def unique(array)
|
220
|
+
array.flatten!
|
221
|
+
array.uniq!
|
222
|
+
end
|
223
|
+
|
224
|
+
def unpreload_associations
|
225
|
+
@@unpreload_associations ||= {}
|
226
|
+
end
|
227
|
+
|
228
|
+
def unused_preload_associations
|
229
|
+
@@unused_preload_associations ||= {}
|
230
|
+
end
|
231
|
+
|
232
|
+
def object_associations
|
233
|
+
@@object_associations ||= {}
|
234
|
+
end
|
235
|
+
|
236
|
+
def call_object_associations
|
237
|
+
@@call_object_associations ||= {}
|
238
|
+
end
|
239
|
+
|
240
|
+
def possible_objects
|
241
|
+
@@possible_objects ||= {}
|
242
|
+
end
|
243
|
+
|
244
|
+
def impossible_objects
|
245
|
+
@@impossible_objects ||= {}
|
246
|
+
end
|
247
|
+
|
248
|
+
def klazz_associations
|
249
|
+
@@klazz_associations ||= {}
|
250
|
+
end
|
251
|
+
|
252
|
+
def eager_loadings
|
253
|
+
@@eager_loadings ||= {}
|
254
|
+
end
|
255
|
+
|
256
|
+
VENDOR_ROOT = File.join(RAILS_ROOT, 'vendor')
|
257
|
+
def caller_in_project
|
258
|
+
callers << caller.select {|c| c =~ /#{RAILS_ROOT}/}.reject {|c| c =~ /#{VENDOR_ROOT}/}
|
259
|
+
callers.uniq!
|
260
|
+
end
|
261
|
+
|
262
|
+
def callers
|
263
|
+
@@callers ||= []
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|