bullet 2.2.1 → 2.3.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/.gitignore +1 -0
- data/.travis.yml +4 -1
- data/Gemfile +3 -2
- data/Gemfile.lock +85 -69
- data/Guardfile +8 -0
- data/lib/bullet.rb +13 -8
- data/lib/bullet/action_controller2.rb +4 -4
- data/lib/bullet/active_record2.rb +5 -6
- data/lib/bullet/active_record3.rb +2 -3
- data/lib/bullet/active_record31.rb +6 -8
- data/lib/bullet/detector/association.rb +27 -53
- data/lib/bullet/detector/counter.rb +34 -29
- data/lib/bullet/detector/n_plus_one_query.rb +47 -28
- data/lib/bullet/detector/unused_eager_association.rb +31 -30
- data/lib/bullet/notification/base.rb +14 -12
- data/lib/bullet/notification/n_plus_one_query.rb +6 -10
- data/lib/bullet/notification/unused_eager_loading.rb +1 -2
- data/lib/bullet/notification_collector.rb +1 -2
- data/lib/bullet/rack.rb +6 -3
- data/lib/bullet/registry/association.rb +4 -6
- data/lib/bullet/registry/base.rb +10 -7
- data/lib/bullet/registry/object.rb +6 -6
- data/lib/bullet/version.rb +1 -1
- data/perf/benchmark.rb +30 -12
- data/spec/bullet/detector/association_spec.rb +90 -0
- data/spec/bullet/detector/base_spec.rb +14 -0
- data/spec/bullet/detector/counter_spec.rb +65 -0
- data/spec/bullet/detector/n_plus_one_query_spec.rb +94 -0
- data/spec/bullet/detector/unused_eager_association_spec.rb +62 -0
- data/spec/bullet/notification/base_spec.rb +67 -0
- data/spec/bullet/notification/counter_cache_spec.rb +12 -0
- data/spec/bullet/notification/n_plus_one_query_spec.rb +13 -0
- data/spec/bullet/notification/unused_eager_loading_spec.rb +12 -0
- data/spec/bullet/notification_collector_spec.rb +32 -0
- data/spec/bullet/rack_spec.rb +80 -24
- data/spec/bullet/registry/association_spec.rb +26 -0
- data/spec/bullet/registry/base_spec.rb +44 -0
- data/spec/bullet/registry/object_spec.rb +25 -0
- data/spec/integration/association_for_chris_spec.rb +37 -0
- data/spec/integration/association_for_peschkaj_spec.rb +26 -0
- data/spec/{bullet → integration}/association_spec.rb +1 -359
- data/spec/integration/counter_spec.rb +37 -0
- data/spec/models/address.rb +3 -0
- data/spec/models/author.rb +3 -0
- data/spec/models/base_user.rb +5 -0
- data/spec/models/category.rb +7 -0
- data/spec/models/city.rb +3 -0
- data/spec/models/client.rb +4 -0
- data/spec/models/comment.rb +4 -0
- data/spec/models/company.rb +3 -0
- data/spec/models/contact.rb +3 -0
- data/spec/models/country.rb +3 -0
- data/spec/models/deal.rb +4 -0
- data/spec/models/document.rb +5 -0
- data/spec/models/email.rb +3 -0
- data/spec/models/entry.rb +3 -0
- data/spec/models/firm.rb +4 -0
- data/spec/models/folder.rb +2 -0
- data/spec/models/hotel.rb +4 -0
- data/spec/models/location.rb +3 -0
- data/spec/models/newspaper.rb +3 -0
- data/spec/models/page.rb +2 -0
- data/spec/models/person.rb +3 -0
- data/spec/models/pet.rb +3 -0
- data/spec/models/post.rb +11 -0
- data/spec/models/relationship.rb +4 -0
- data/spec/models/student.rb +3 -0
- data/spec/models/submission.rb +4 -0
- data/spec/models/teacher.rb +3 -0
- data/spec/models/user.rb +4 -0
- data/spec/models/writer.rb +2 -0
- data/spec/spec_helper.rb +16 -106
- data/spec/support/bullet_ext.rb +55 -0
- data/spec/support/rack_double.rb +55 -0
- data/spec/support/seed.rb +256 -0
- metadata +104 -14
- data/.watchr +0 -24
- data/spec/bullet/association_for_chris_spec.rb +0 -96
- data/spec/bullet/association_for_peschkaj_spec.rb +0 -86
- data/spec/bullet/counter_spec.rb +0 -136
@@ -1,42 +1,47 @@
|
|
1
1
|
module Bullet
|
2
2
|
module Detector
|
3
3
|
class Counter < Base
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
class <<self
|
5
|
+
def clear
|
6
|
+
@@possible_objects = nil
|
7
|
+
@@impossible_objects = nil
|
8
|
+
end
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
def add_counter_cache(object, associations)
|
11
|
+
if conditions_met?(object.ar_key, associations)
|
12
|
+
create_notification object.class.to_s, associations
|
13
|
+
end
|
12
14
|
end
|
13
|
-
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
16
|
+
def add_possible_objects(object_or_objects)
|
17
|
+
if object_or_objects.is_a? Array
|
18
|
+
object_or_objects.each { |object| possible_objects.add object.ar_key }
|
19
|
+
else
|
20
|
+
possible_objects.add object_or_objects.ar_key
|
21
|
+
end
|
22
|
+
end
|
18
23
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
private
|
24
|
-
def self.create_notification( klazz, associations )
|
25
|
-
notice = Bullet::Notification::CounterCache.new klazz, associations
|
26
|
-
Bullet.notification_collector.add notice
|
27
|
-
end
|
24
|
+
def add_impossible_object(object)
|
25
|
+
impossible_objects.add object.ar_key
|
26
|
+
end
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
private
|
29
|
+
def create_notification(klazz, associations)
|
30
|
+
notice = Bullet::Notification::CounterCache.new klazz, associations
|
31
|
+
Bullet.notification_collector.add notice
|
32
|
+
end
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
34
|
+
def possible_objects
|
35
|
+
@@possible_objects ||= Bullet::Registry::Object.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def impossible_objects
|
39
|
+
@@impossible_objects ||= Bullet::Registry::Object.new
|
40
|
+
end
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
|
42
|
+
def conditions_met?(object_ar_key, associations)
|
43
|
+
possible_objects.include?(object_ar_key) && !impossible_objects.include?(object_ar_key)
|
44
|
+
end
|
40
45
|
end
|
41
46
|
end
|
42
47
|
end
|
@@ -1,38 +1,57 @@
|
|
1
1
|
module Bullet
|
2
2
|
module Detector
|
3
3
|
class NPlusOneQuery < Association
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
4
|
+
class <<self
|
5
|
+
# executed when object.assocations is called.
|
6
|
+
# first, it keeps this method call for object.association.
|
7
|
+
# then, it checks if this associations call is unpreload.
|
8
|
+
# if it is, keeps this unpreload associations and caller.
|
9
|
+
def call_association(object, associations)
|
10
|
+
@@checked = true
|
11
|
+
add_call_object_associations(object, associations)
|
12
|
+
|
13
|
+
if conditions_met?(object.ar_key, associations)
|
14
|
+
create_notification caller_in_project, object.class.to_s, associations
|
15
|
+
end
|
15
16
|
end
|
16
|
-
end
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
private
|
19
|
+
def create_notification(callers, klazz, associations)
|
20
|
+
notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, associations)
|
21
|
+
Bullet.notification_collector.add(notice)
|
22
|
+
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
24
|
+
# decide whether the object.associations is unpreloaded or not.
|
25
|
+
def conditions_met?(object_ar_key, associations)
|
26
|
+
possible?(object_ar_key) && !impossible?(object_ar_key) && !association?(object_ar_key, associations)
|
27
|
+
end
|
28
|
+
|
29
|
+
def caller_in_project
|
30
|
+
rails_root = Rails.root.to_s
|
31
|
+
vendor_root = rails_root + "/vendor"
|
32
|
+
caller.select { |c| c.include?(rails_root) && !c.include?(vendor_root) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def possible?(object_ar_key)
|
36
|
+
possible_objects.include? object_ar_key
|
37
|
+
end
|
38
|
+
|
39
|
+
def impossible?(object_ar_key)
|
40
|
+
impossible_objects.include? object_ar_key
|
41
|
+
end
|
42
|
+
|
43
|
+
# check if object => associations already exists in object_associations.
|
44
|
+
def association?(object_ar_key, associations)
|
45
|
+
value = object_associations[object_ar_key]
|
46
|
+
if value
|
47
|
+
value.each do |v|
|
48
|
+
result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
|
49
|
+
return true if result
|
50
|
+
end
|
51
|
+
end
|
30
52
|
|
31
|
-
|
32
|
-
|
33
|
-
callers << caller.select { |c| c =~ /#{Rails.root}/ }.
|
34
|
-
reject { |c| c =~ /#{vender_root}/ }
|
35
|
-
callers.uniq!
|
53
|
+
return false
|
54
|
+
end
|
36
55
|
end
|
37
56
|
end
|
38
57
|
end
|
@@ -1,40 +1,41 @@
|
|
1
1
|
module Bullet
|
2
2
|
module Detector
|
3
3
|
class UnusedEagerAssociation < Association
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
4
|
+
class <<self
|
5
|
+
# check if there are unused preload associations.
|
6
|
+
# get related_objects from eager_loadings associated with object and associations
|
7
|
+
# get call_object_association from associations of call_object_associations whose object is in related_objects
|
8
|
+
# if association not in call_object_association, then the object => association - call_object_association is ununsed preload assocations
|
9
|
+
def check_unused_preload_associations
|
10
|
+
@@checked = true
|
11
|
+
object_associations.each do |object_ar_key, association|
|
12
|
+
object_association_diff = diff_object_association object_ar_key, association
|
13
|
+
next if object_association_diff.empty?
|
14
14
|
|
15
|
-
|
15
|
+
create_notification object_ar_key.split(":").first, object_association_diff
|
16
|
+
end
|
16
17
|
end
|
17
|
-
end
|
18
|
-
|
19
|
-
protected
|
20
|
-
def self.create_notification(klazz, associations)
|
21
|
-
notice = Bullet::Notification::UnusedEagerLoading.new( klazz, associations )
|
22
|
-
Bullet.notification_collector.add( notice )
|
23
|
-
end
|
24
18
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
19
|
+
private
|
20
|
+
def create_notification(klazz, associations)
|
21
|
+
notice = Bullet::Notification::UnusedEagerLoading.new(klazz, associations)
|
22
|
+
Bullet.notification_collector.add(notice)
|
23
|
+
end
|
24
|
+
|
25
|
+
def call_associations(object_ar_key, association)
|
26
|
+
all = Set.new
|
27
|
+
eager_loadings.similarly_associated(object_ar_key, association).each do |related_object_ar_key|
|
28
|
+
coa = call_object_associations[related_object_ar_key]
|
29
|
+
next if coa.nil?
|
30
|
+
all.merge coa
|
31
|
+
end
|
32
|
+
all.to_a
|
33
|
+
end
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
def diff_object_association(object_ar_key, association)
|
36
|
+
potential_objects = association - call_associations(object_ar_key, association)
|
37
|
+
potential_objects.reject { |a| a.is_a?(Hash) }
|
38
|
+
end
|
38
39
|
end
|
39
40
|
end
|
40
41
|
end
|
@@ -4,18 +4,20 @@ module Bullet
|
|
4
4
|
attr_accessor :notifier, :url
|
5
5
|
attr_reader :base_class, :associations, :path
|
6
6
|
|
7
|
-
def initialize(
|
7
|
+
def initialize(base_class, association_or_associations, path = nil)
|
8
8
|
@base_class = base_class
|
9
|
-
@associations =
|
9
|
+
@associations = association_or_associations.is_a?(Array) ? association_or_associations : [association_or_associations]
|
10
10
|
@path = path
|
11
11
|
end
|
12
12
|
|
13
13
|
def title
|
14
|
+
raise NoMethodError.new("no method title defined")
|
14
15
|
end
|
15
16
|
|
16
17
|
def body
|
18
|
+
raise NoMethodError.new("no method body defined")
|
17
19
|
end
|
18
|
-
|
20
|
+
|
19
21
|
def whoami
|
20
22
|
"user: " << `whoami`.chomp
|
21
23
|
end
|
@@ -33,14 +35,14 @@ module Bullet
|
|
33
35
|
end
|
34
36
|
|
35
37
|
def notify_inline
|
36
|
-
self.notifier.inline_notify(
|
38
|
+
self.notifier.inline_notify(self.full_notice)
|
37
39
|
end
|
38
40
|
|
39
41
|
def notify_out_of_channel
|
40
|
-
self.notifier.out_of_channel_notify(
|
42
|
+
self.notifier.out_of_channel_notify(self.full_notice)
|
41
43
|
end
|
42
44
|
|
43
|
-
def eql?(
|
45
|
+
def eql?(other)
|
44
46
|
klazz_associations_str == other.klazz_associations_str
|
45
47
|
end
|
46
48
|
|
@@ -49,13 +51,13 @@ module Bullet
|
|
49
51
|
end
|
50
52
|
|
51
53
|
protected
|
52
|
-
|
53
|
-
|
54
|
-
|
54
|
+
def klazz_associations_str
|
55
|
+
" #{@base_class} => [#{@associations.map(&:inspect).join(', ')}]"
|
56
|
+
end
|
55
57
|
|
56
|
-
|
57
|
-
|
58
|
-
|
58
|
+
def associations_str
|
59
|
+
":include => #{@associations.map{ |a| a.to_s.to_sym unless a.is_a? Hash }.inspect}"
|
60
|
+
end
|
59
61
|
end
|
60
62
|
end
|
61
63
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
module Bullet
|
2
2
|
module Notification
|
3
3
|
class NPlusOneQuery < Base
|
4
|
-
def initialize(
|
5
|
-
super(
|
4
|
+
def initialize(callers, base_class, associations, path = nil)
|
5
|
+
super(base_class, associations, path)
|
6
6
|
|
7
7
|
@callers = callers
|
8
8
|
end
|
@@ -10,7 +10,7 @@ module Bullet
|
|
10
10
|
def body_with_caller
|
11
11
|
"#{body}\n#{call_stack_messages}"
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
def body
|
15
15
|
"#{klazz_associations_str}\n Add to your finder: #{associations_str}"
|
16
16
|
end
|
@@ -20,13 +20,9 @@ module Bullet
|
|
20
20
|
end
|
21
21
|
|
22
22
|
protected
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
c.collect {|line| " #{line}"} ].flatten
|
27
|
-
end.join( "\n" )
|
28
|
-
end
|
29
|
-
|
23
|
+
def call_stack_messages
|
24
|
+
@callers.unshift('N+1 Query method call stack').join( "\n " )
|
25
|
+
end
|
30
26
|
end
|
31
27
|
end
|
32
28
|
end
|
data/lib/bullet/rack.rb
CHANGED
@@ -13,7 +13,7 @@ module Bullet
|
|
13
13
|
|
14
14
|
response_body = nil
|
15
15
|
if Bullet.notification?
|
16
|
-
if status == 200
|
16
|
+
if status == 200 && !response.body.frozen? && html_request?(headers, response)
|
17
17
|
response_body = response.body << Bullet.gather_inline_notifications
|
18
18
|
headers['Content-Length'] = response_body.length.to_s
|
19
19
|
end
|
@@ -31,8 +31,11 @@ module Bullet
|
|
31
31
|
!response.body.is_a?(String) || response.body.empty?
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
35
|
-
headers['Content-Type']
|
34
|
+
def html_request?(headers, response)
|
35
|
+
headers['Content-Type'] &&
|
36
|
+
headers['Content-Type'].include?('text/html') &&
|
37
|
+
response.body.include?("<html>") &&
|
38
|
+
response.body.include?("</html>")
|
36
39
|
end
|
37
40
|
|
38
41
|
def no_browser_cache(headers)
|
@@ -1,14 +1,12 @@
|
|
1
1
|
module Bullet
|
2
2
|
module Registry
|
3
3
|
class Association < Base
|
4
|
-
def merge(
|
5
|
-
@registry.merge!(
|
4
|
+
def merge(base, associations)
|
5
|
+
@registry.merge!(base => associations)
|
6
6
|
end
|
7
7
|
|
8
|
-
def similarly_associated(
|
9
|
-
@registry.select
|
10
|
-
key.include?( base ) and value == associations
|
11
|
-
end.collect( &:first ).flatten
|
8
|
+
def similarly_associated(base, associations)
|
9
|
+
@registry.select { |key, value| key.include?(base) && value == associations }.collect(&:first).flatten
|
12
10
|
end
|
13
11
|
end
|
14
12
|
end
|
data/lib/bullet/registry/base.rb
CHANGED
@@ -11,19 +11,19 @@ module Bullet
|
|
11
11
|
@registry[key]
|
12
12
|
end
|
13
13
|
|
14
|
-
def each(
|
15
|
-
@registry.each(
|
14
|
+
def each(&block)
|
15
|
+
@registry.each(&block)
|
16
16
|
end
|
17
17
|
|
18
|
-
def delete(
|
19
|
-
@registry.delete(
|
18
|
+
def delete(base)
|
19
|
+
@registry.delete(base)
|
20
20
|
end
|
21
21
|
|
22
|
-
def select(
|
23
|
-
@registry.select(
|
22
|
+
def select(*args, &block)
|
23
|
+
@registry.select(*args, &block)
|
24
24
|
end
|
25
25
|
|
26
|
-
def add(
|
26
|
+
def add(key, value)
|
27
27
|
@registry[key] ||= Set.new
|
28
28
|
if value.is_a? Array
|
29
29
|
@registry[key] += value
|
@@ -32,6 +32,9 @@ module Bullet
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
+
def include?(key, value)
|
36
|
+
@registry[key] && @registry[key].include?(value)
|
37
|
+
end
|
35
38
|
end
|
36
39
|
end
|
37
40
|
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
module Bullet
|
2
2
|
module Registry
|
3
3
|
class Object < Base
|
4
|
-
def add(
|
5
|
-
klazz =
|
6
|
-
|
7
|
-
super( klazz, object_or_objects )
|
4
|
+
def add(object_ar_key)
|
5
|
+
klazz = object_ar_key.split(":").first
|
6
|
+
super(klazz, object_ar_key)
|
8
7
|
end
|
9
8
|
|
10
|
-
def
|
11
|
-
|
9
|
+
def include?(object_ar_key)
|
10
|
+
klazz = object_ar_key.split(":").first
|
11
|
+
super(klazz, object_ar_key)
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|