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.
@@ -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