bullet 2.0.0.beta.2 → 2.0.0.beta.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.rspec +1 -0
  2. data/Hacking.textile +100 -0
  3. data/README.textile +36 -5
  4. data/README_for_rails2.textile +17 -0
  5. data/Rakefile +33 -16
  6. data/VERSION +1 -1
  7. data/autotest/discover.rb +1 -0
  8. data/bullet.gemspec +32 -9
  9. data/lib/bullet.rb +69 -38
  10. data/lib/bullet/action_controller2.rb +4 -4
  11. data/lib/bullet/active_record2.rb +16 -16
  12. data/lib/bullet/active_record3.rb +16 -25
  13. data/lib/bullet/detector.rb +9 -0
  14. data/lib/bullet/detector/association.rb +135 -0
  15. data/lib/bullet/detector/base.rb +19 -0
  16. data/lib/bullet/detector/counter.rb +43 -0
  17. data/lib/bullet/detector/n_plus_one_query.rb +39 -0
  18. data/lib/bullet/detector/unused_eager_association.rb +39 -0
  19. data/lib/bullet/notification.rb +4 -79
  20. data/lib/bullet/notification/base.rb +59 -0
  21. data/lib/bullet/notification/counter_cache.rb +13 -0
  22. data/lib/bullet/notification/n_plus_one_query.rb +32 -0
  23. data/lib/bullet/notification/unused_eager_loading.rb +14 -0
  24. data/lib/bullet/notification_collector.rb +25 -0
  25. data/lib/bullet/presenter.rb +13 -0
  26. data/lib/bullet/presenter/base.rb +9 -0
  27. data/lib/bullet/presenter/bullet_logger.rb +28 -0
  28. data/lib/bullet/presenter/growl.rb +40 -0
  29. data/lib/bullet/presenter/javascript_alert.rb +15 -0
  30. data/lib/bullet/presenter/javascript_console.rb +28 -0
  31. data/lib/bullet/presenter/javascript_helpers.rb +13 -0
  32. data/lib/bullet/presenter/rails_logger.rb +15 -0
  33. data/lib/bullet/presenter/xmpp.rb +56 -0
  34. data/lib/bullet/rack.rb +42 -0
  35. data/lib/bullet/registry.rb +7 -0
  36. data/lib/bullet/registry/association.rb +16 -0
  37. data/lib/bullet/registry/base.rb +39 -0
  38. data/lib/bullet/registry/object.rb +15 -0
  39. data/spec/bullet/association_for_chris_spec.rb +6 -6
  40. data/spec/bullet/association_for_peschkaj_spec.rb +6 -6
  41. data/spec/bullet/association_spec.rb +118 -262
  42. data/spec/bullet/counter_spec.rb +10 -10
  43. data/spec/spec_helper.rb +51 -17
  44. metadata +32 -9
  45. data/lib/bullet/association.rb +0 -294
  46. data/lib/bullet/counter.rb +0 -101
  47. data/lib/bullet/logger.rb +0 -9
  48. data/lib/bulletware.rb +0 -42
  49. data/spec/spec.opts +0 -3
@@ -2,7 +2,7 @@ require File.dirname(__FILE__) + '/../spec_helper'
2
2
 
3
3
  ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
4
4
 
5
- describe Bullet::Counter do
5
+ describe Bullet::Detector::Counter do
6
6
  def setup_db
7
7
  ActiveRecord::Schema.define(:version => 1) do
8
8
  create_table :countries do |t|
@@ -47,34 +47,34 @@ describe Bullet::Counter do
47
47
  end
48
48
 
49
49
  before(:each) do
50
- Bullet::Counter.start_request
50
+ Bullet.start_request
51
51
  end
52
52
 
53
53
  after(:each) do
54
- Bullet::Counter.end_request
54
+ Bullet.end_request
55
55
  end
56
56
 
57
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
- Bullet::Counter.should be_need_counter_caches
61
+ Bullet.collected_counter_cache_notifications.should_not be_empty
62
62
  end
63
63
 
64
64
  it "should not need coounter cache with only one object" do
65
65
  Country.first.cities.size
66
- Bullet::Counter.should_not be_need_counter_caches
66
+ Bullet.collected_counter_cache_notifications.should be_empty
67
67
  end
68
68
 
69
69
  it "should not need counter cache with part of cities" do
70
70
  Country.all.each do |country|
71
71
  country.cities.where(:name => 'first').size
72
72
  end
73
- Bullet::Counter.should_not be_need_counter_caches
73
+ Bullet.collected_counter_cache_notifications.should be_empty
74
74
  end
75
75
  end
76
76
 
77
- describe Bullet::Counter do
77
+ describe Bullet::Detector::Counter do
78
78
  def setup_db
79
79
  ActiveRecord::Schema.define(:version => 1) do
80
80
  create_table :people do |t|
@@ -120,17 +120,17 @@ describe Bullet::Counter do
120
120
  end
121
121
 
122
122
  before(:each) do
123
- Bullet::Counter.start_request
123
+ Bullet.start_request
124
124
  end
125
125
 
126
126
  after(:each) do
127
- Bullet::Counter.end_request
127
+ Bullet.end_request
128
128
  end
129
129
 
130
130
  it "should not need counter cache" do
131
131
  Person.all.each do |person|
132
132
  person.pets.size
133
133
  end
134
- Bullet::Counter.should_not be_need_counter_caches
134
+ Bullet.collected_counter_cache_notifications.should be_empty
135
135
  end
136
136
  end
data/spec/spec_helper.rb CHANGED
@@ -19,26 +19,60 @@ Bullet.enable = true
19
19
  ActiveRecord::Migration.verbose = false
20
20
 
21
21
  module Bullet
22
- class Association
23
- class <<self
24
- # returns true if all associations are preloaded
25
- def completely_preloading_associations?
26
- !has_unpreload_associations?
27
- end
22
+ def self.collected_notifications_of_class( notification_class )
23
+ Bullet.notification_collector.collection.select do |notification|
24
+ notification.is_a? notification_class
25
+ end
26
+ end
28
27
 
29
- # returns true if a given object has a specific association
30
- def creating_object_association_for?(object, association)
31
- object_associations[object].present? && object_associations[object].include?(association)
32
- end
28
+ def self.collected_counter_cache_notifications
29
+ collected_notifications_of_class Bullet::Notification::CounterCache
30
+ end
33
31
 
34
- # returns true if a given class includes the specific unpreloaded association
35
- def detecting_unpreloaded_association_for?(klazz, association)
36
- unpreload_associations[klazz].present? && unpreload_associations[klazz].include?(association)
37
- end
32
+ def self.collected_n_plus_one_query_notifications
33
+ collected_notifications_of_class Bullet::Notification::NPlusOneQuery
34
+ end
35
+
36
+ def self.collected_unused_eager_association_notifications
37
+ collected_notifications_of_class Bullet::Notification::UnusedEagerLoading
38
+ end
39
+ end
40
+
41
+ module Bullet
42
+ module Detector
43
+ class Association
44
+ class <<self
45
+ # returns true if all associations are preloaded
46
+ def completely_preloading_associations?
47
+ Bullet.collected_n_plus_one_query_notifications.empty?
48
+ end
49
+
50
+ def has_unused_preload_associations?
51
+ Bullet.collected_unused_eager_association_notifications.present?
52
+ end
53
+
54
+ # returns true if a given object has a specific association
55
+ def creating_object_association_for?(object, association)
56
+ object_associations[object].present? && object_associations[object].include?(association)
57
+ end
58
+
59
+ # returns true if a given class includes the specific unpreloaded association
60
+ def detecting_unpreloaded_association_for?(klass, association)
61
+ for_class_and_assoc = Bullet.collected_n_plus_one_query_notifications.select do |notification|
62
+ notification.base_class == klass and
63
+ notification.associations.include?( association )
64
+ end
65
+ for_class_and_assoc.present?
66
+ end
38
67
 
39
- # returns true if the given class includes the specific unused preloaded association
40
- def unused_preload_associations_for?(klazz, association)
41
- unused_preload_associations[klazz].present? && unused_preload_associations[klazz].include?(association)
68
+ # returns true if the given class includes the specific unused preloaded association
69
+ def unused_preload_associations_for?(klazz, association)
70
+ for_class_and_assoc = Bullet.collected_unused_eager_association_notifications.select do |notification|
71
+ notification.base_class == klass and
72
+ notification.associations.include?( association )
73
+ end
74
+ for_class_and_assoc.present?
75
+ end
42
76
  end
43
77
  end
44
78
  end
metadata CHANGED
@@ -7,8 +7,8 @@ version: !ruby/object:Gem::Version
7
7
  - 0
8
8
  - 0
9
9
  - beta
10
- - 2
11
- version: 2.0.0.beta.2
10
+ - 3
11
+ version: 2.0.0.beta.3
12
12
  platform: ruby
13
13
  authors:
14
14
  - Richard Huang
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2010-03-07 00:00:00 +08:00
19
+ date: 2010-07-14 00:00:00 +08:00
20
20
  default_executable:
21
21
  dependencies: []
22
22
 
@@ -30,27 +30,50 @@ extra_rdoc_files:
30
30
  - README.textile
31
31
  - README_for_rails2.textile
32
32
  files:
33
+ - .rspec
34
+ - Hacking.textile
33
35
  - MIT-LICENSE
34
36
  - README.textile
35
37
  - README_for_rails2.textile
36
38
  - Rakefile
37
39
  - VERSION
40
+ - autotest/discover.rb
38
41
  - bullet.gemspec
39
42
  - lib/bullet.rb
40
43
  - lib/bullet/action_controller2.rb
41
44
  - lib/bullet/active_record2.rb
42
45
  - lib/bullet/active_record3.rb
43
- - lib/bullet/association.rb
44
- - lib/bullet/counter.rb
45
- - lib/bullet/logger.rb
46
+ - lib/bullet/detector.rb
47
+ - lib/bullet/detector/association.rb
48
+ - lib/bullet/detector/base.rb
49
+ - lib/bullet/detector/counter.rb
50
+ - lib/bullet/detector/n_plus_one_query.rb
51
+ - lib/bullet/detector/unused_eager_association.rb
46
52
  - lib/bullet/notification.rb
47
- - lib/bulletware.rb
53
+ - lib/bullet/notification/base.rb
54
+ - lib/bullet/notification/counter_cache.rb
55
+ - lib/bullet/notification/n_plus_one_query.rb
56
+ - lib/bullet/notification/unused_eager_loading.rb
57
+ - lib/bullet/notification_collector.rb
58
+ - lib/bullet/presenter.rb
59
+ - lib/bullet/presenter/base.rb
60
+ - lib/bullet/presenter/bullet_logger.rb
61
+ - lib/bullet/presenter/growl.rb
62
+ - lib/bullet/presenter/javascript_alert.rb
63
+ - lib/bullet/presenter/javascript_console.rb
64
+ - lib/bullet/presenter/javascript_helpers.rb
65
+ - lib/bullet/presenter/rails_logger.rb
66
+ - lib/bullet/presenter/xmpp.rb
67
+ - lib/bullet/rack.rb
68
+ - lib/bullet/registry.rb
69
+ - lib/bullet/registry/association.rb
70
+ - lib/bullet/registry/base.rb
71
+ - lib/bullet/registry/object.rb
48
72
  - rails/init.rb
49
73
  - spec/bullet/association_for_chris_spec.rb
50
74
  - spec/bullet/association_for_peschkaj_spec.rb
51
75
  - spec/bullet/association_spec.rb
52
76
  - spec/bullet/counter_spec.rb
53
- - spec/spec.opts
54
77
  - spec/spec_helper.rb
55
78
  - tasks/bullet_tasks.rake
56
79
  has_rdoc: true
@@ -84,7 +107,7 @@ rubyforge_project:
84
107
  rubygems_version: 1.3.6
85
108
  signing_key:
86
109
  specification_version: 3
87
- summary: A plugin to kill N+1 queries and unused eager loading
110
+ summary: A rails plugin to kill N+1 queries and unused eager loading
88
111
  test_files:
89
112
  - spec/spec_helper.rb
90
113
  - spec/bullet/counter_spec.rb
@@ -1,294 +0,0 @@
1
- module Bullet
2
- class Association
3
- class <<self
4
- include Bullet::Notification
5
-
6
- def start_request
7
- @@checked = false
8
- end
9
-
10
- def end_request
11
- clear
12
- end
13
-
14
- def clear
15
- @@object_associations = nil
16
- @@unpreload_associations = nil
17
- @@unused_preload_associations = nil
18
- @@callers = nil
19
- @@possible_objects = nil
20
- @@impossible_objects = nil
21
- @@call_object_associations = nil
22
- @@eager_loadings = nil
23
- end
24
-
25
- def notification?
26
- check_unused_preload_associations unless @@checked
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_eager_loadings(objects, associations)
69
- objects = Array(objects)
70
- eager_loadings[objects] ||= []
71
- eager_loadings.each do |k, v|
72
- unless (k & objects).empty?
73
- if (k & objects) == k
74
- eager_loadings[k] << associations
75
- unique(eager_loadings[k])
76
- break
77
- else
78
- eager_loadings.merge!({(k & objects) => (eager_loadings[k].dup << associations)})
79
- unique(eager_loadings[(k & objects)])
80
- eager_loadings.merge!({(k - objects) => eager_loadings[k]}) unless (k - objects).empty?
81
- unique(eager_loadings[(k - objects)])
82
- eager_loadings.delete(k)
83
- objects = objects - k
84
- end
85
- end
86
- end
87
- unless objects.empty?
88
- eager_loadings[objects] << associations
89
- unique(eager_loadings[objects])
90
- end
91
- end
92
-
93
- # executed when object.assocations is called.
94
- # first, it keeps this method call for object.association.
95
- # then, it checks if this associations call is unpreload.
96
- # if it is, keeps this unpreload associations and caller.
97
- def call_association(object, associations)
98
- add_call_object_associations(object, associations)
99
- if unpreload_associations?(object, associations)
100
- add_unpreload_associations(object.class, associations)
101
- caller_in_project
102
- end
103
- end
104
-
105
- # check if there are unused preload associations.
106
- # for each object => association
107
- # get related_objects from eager_loadings associated with object and associations
108
- # get call_object_association from associations of call_object_associations whose object is in related_objects
109
- # if association not in call_object_association, then the object => association - call_object_association is ununsed preload assocations
110
- def check_unused_preload_associations
111
- @@checked = true
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
- # decide whether the object.associations is unpreloaded or not.
130
- def unpreload_associations?(object, associations)
131
- possible?(object) and !impossible?(object) and !association?(object, associations)
132
- end
133
-
134
- def possible?(object)
135
- klazz = object.class
136
- possible_objects[klazz] and possible_objects[klazz].include?(object)
137
- end
138
-
139
- def impossible?(object)
140
- klazz = object.class
141
- impossible_objects[klazz] and impossible_objects[klazz].include?(object)
142
- end
143
-
144
- # check if object => associations already exists in object_associations.
145
- def association?(object, associations)
146
- object_associations.each do |key, value|
147
- if key == object
148
- value.each do |v|
149
- result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
150
- return true if result
151
- end
152
- end
153
- end
154
- return false
155
- end
156
-
157
- def notification_response
158
- response = []
159
- if has_unused_preload_associations?
160
- response << unused_preload_messages.join("\n")
161
- end
162
- if has_unpreload_associations?
163
- response << unpreload_messages.join("\n")
164
- end
165
- response
166
- end
167
-
168
- def console_title
169
- title = []
170
- title << unused_preload_messages.first.first unless unused_preload_messages.empty?
171
- title << unpreload_messages.first.first unless unpreload_messages.empty?
172
- title
173
- end
174
-
175
- def log_messages(path = nil)
176
- messages = []
177
- messages << unused_preload_messages(path)
178
- messages << unpreload_messages(path)
179
- messages << call_stack_messages
180
- messages
181
- end
182
-
183
- def unused_preload_messages(path = nil)
184
- messages = []
185
- unused_preload_associations.each do |klazz, associations|
186
- messages << [
187
- "Unused Eager Loading #{path ? "in #{path}" : 'detected'}",
188
- klazz_associations_str(klazz, associations),
189
- " Remove from your finder: #{associations_str(associations)}"
190
- ]
191
- end
192
- messages
193
- end
194
-
195
- def unpreload_messages(path = nil)
196
- messages = []
197
- unpreload_associations.each do |klazz, associations|
198
- messages << [
199
- "N+1 Query #{path ? "in #{path}" : 'detected'}",
200
- klazz_associations_str(klazz, associations),
201
- " Add to your finder: #{associations_str(associations)}"
202
- ]
203
- end
204
- messages
205
- end
206
-
207
- def call_stack_messages
208
- callers.inject([]) do |messages, c|
209
- messages << ['N+1 Query method call stack', c.collect {|line| " #{line}"}].flatten
210
- end
211
- end
212
-
213
- def klazz_associations_str(klazz, associations)
214
- " #{klazz} => [#{associations.map(&:inspect).join(', ')}]"
215
- end
216
-
217
- def associations_str(associations)
218
- ":include => #{associations.map{|a| a.to_sym unless a.is_a? Hash}.inspect}"
219
- end
220
-
221
- def unique(array)
222
- array.flatten!
223
- array.uniq!
224
- end
225
-
226
- # unpreload_associations keep the class relationships
227
- # that the associations, belongs to the class, are used but not preloaded.
228
- # e.g. { Post => [:comments] }
229
- # so the unpreload_associations should be preloaded by find :include.
230
- def unpreload_associations
231
- @@unpreload_associations ||= {}
232
- end
233
-
234
- # unused_preload_associations keep the class relationships
235
- # that the associations, belongs to the class, are preloaded but not used.
236
- # e.g. { Post => [:comments] }
237
- # so the unused_preload_associations should be removed from find :include.
238
- def unused_preload_associations
239
- @@unused_preload_associations ||= {}
240
- end
241
-
242
- # object_associations keep the object relationships
243
- # that the object has many associations.
244
- # e.g. { <Post id:1> => [:comments] }
245
- # the object_associations keep all associations that may be or may no be
246
- # unpreload associations or unused preload associations.
247
- def object_associations
248
- @@object_associations ||= {}
249
- end
250
-
251
- # call_object_assciations keep the object relationships
252
- # that object.associations is called.
253
- # e.g. { <Post id:1> => [:comments] }
254
- # they are used to detect unused preload associations.
255
- def call_object_associations
256
- @@call_object_associations ||= {}
257
- end
258
-
259
- # possible_objects keep the class to object relationships
260
- # that the objects may cause N+1 query.
261
- # e.g. { Post => [<Post id:1>, <Post id:2>] }
262
- def possible_objects
263
- @@possible_objects ||= {}
264
- end
265
-
266
- # impossible_objects keep the class to objects relationships
267
- # that the objects may not cause N+1 query.
268
- # e.g. { Post => [<Post id:1>, <Post id:2>] }
269
- # Notice: impossible_objects are not accurate,
270
- # if find collection returns only one object, then the object is impossible object,
271
- # impossible_objects are used to avoid treating 1+1 query to N+1 query.
272
- def impossible_objects
273
- @@impossible_objects ||= {}
274
- end
275
-
276
- # eager_loadings keep the object relationships
277
- # that the associations are preloaded by find :include.
278
- # e.g. { [<Post id:1>, <Post id:2>] => [:comments, :user] }
279
- def eager_loadings
280
- @@eager_loadings ||= {}
281
- end
282
-
283
- def caller_in_project
284
- vender_root ||= File.join(Rails.root, 'vendor')
285
- callers << caller.select {|c| c =~ /#{Rails.root}/}.reject {|c| c =~ /#{vender_root}/}
286
- callers.uniq!
287
- end
288
-
289
- def callers
290
- @@callers ||= []
291
- end
292
- end
293
- end
294
- end