bullet 2.0.0.beta.2 → 2.1.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 +9 -0
- data/.rspec +2 -0
- data/.rvmrc +2 -0
- data/.rvmrc.example +2 -0
- data/.watchr +24 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +105 -0
- data/Hacking.textile +69 -0
- data/MIT-LICENSE +1 -1
- data/README.textile +61 -49
- data/README_for_rails2.textile +41 -42
- data/Rakefile +40 -29
- data/bullet.gemspec +17 -60
- data/lib/bullet/action_controller2.rb +6 -6
- data/lib/bullet/active_record2.rb +17 -16
- data/lib/bullet/active_record3.rb +17 -25
- data/lib/bullet/active_record31.rb +96 -0
- data/lib/bullet/detector/association.rb +144 -0
- data/lib/bullet/detector/base.rb +12 -0
- data/lib/bullet/detector/counter.rb +43 -0
- data/lib/bullet/detector/n_plus_one_query.rb +39 -0
- data/lib/bullet/detector/unused_eager_association.rb +41 -0
- data/lib/bullet/detector.rb +9 -0
- data/lib/bullet/notification/base.rb +61 -0
- data/lib/bullet/notification/counter_cache.rb +13 -0
- data/lib/bullet/notification/n_plus_one_query.rb +32 -0
- data/lib/bullet/notification/unused_eager_loading.rb +14 -0
- data/lib/bullet/notification.rb +4 -79
- data/lib/bullet/notification_collector.rb +25 -0
- data/lib/bullet/rack.rb +44 -0
- data/lib/bullet/registry/association.rb +15 -0
- data/lib/bullet/registry/base.rb +37 -0
- data/lib/bullet/registry/object.rb +15 -0
- data/lib/bullet/registry.rb +7 -0
- data/lib/bullet/version.rb +5 -0
- data/lib/bullet.rb +64 -43
- data/perf/benchmark.rb +106 -0
- data/spec/bullet/association_for_chris_spec.rb +6 -6
- data/spec/bullet/association_for_peschkaj_spec.rb +6 -6
- data/spec/bullet/association_spec.rb +140 -294
- data/spec/bullet/counter_spec.rb +10 -10
- data/spec/spec_helper.rb +51 -17
- metadata +72 -57
- data/VERSION +0 -1
- data/lib/bullet/association.rb +0 -294
- data/lib/bullet/counter.rb +0 -101
- data/lib/bullet/logger.rb +0 -9
- data/lib/bulletware.rb +0 -42
- data/spec/spec.opts +0 -3
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
LOAD_TARGET_RGX = /load_target/
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
def self.enable
|
|
5
|
+
require 'active_record'
|
|
6
|
+
::ActiveRecord::Relation.class_eval do
|
|
7
|
+
alias_method :origin_to_a, :to_a
|
|
8
|
+
# if select a collection of objects, then these objects have possible to cause N+1 query.
|
|
9
|
+
# if select only one object, then the only one object has impossible to cause N+1 query.
|
|
10
|
+
def to_a
|
|
11
|
+
records = origin_to_a
|
|
12
|
+
if records.size > 1
|
|
13
|
+
Bullet::Detector::Association.add_possible_objects(records)
|
|
14
|
+
Bullet::Detector::Counter.add_possible_objects(records)
|
|
15
|
+
elsif records.size == 1
|
|
16
|
+
Bullet::Detector::Association.add_impossible_object(records.first)
|
|
17
|
+
Bullet::Detector::Counter.add_impossible_object(records.first)
|
|
18
|
+
end
|
|
19
|
+
records
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
::ActiveRecord::Associations::Preloader.class_eval do
|
|
24
|
+
# include query for one to many associations.
|
|
25
|
+
# keep this eager loadings.
|
|
26
|
+
alias_method :origin_initialize, :initialize
|
|
27
|
+
def initialize(records, associations, preload_options = {})
|
|
28
|
+
origin_initialize(records, associations, preload_options)
|
|
29
|
+
records = [records].flatten.compact.uniq
|
|
30
|
+
return if records.empty?
|
|
31
|
+
records.each do |record|
|
|
32
|
+
Bullet::Detector::Association.add_object_associations(record, associations)
|
|
33
|
+
end
|
|
34
|
+
Bullet::Detector::Association.add_eager_loadings(records, associations)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
::ActiveRecord::FinderMethods.class_eval do
|
|
39
|
+
# add includes in scope
|
|
40
|
+
alias_method :origin_find_with_associations, :find_with_associations
|
|
41
|
+
def find_with_associations
|
|
42
|
+
records = origin_find_with_associations
|
|
43
|
+
associations = (@eager_load_values + @includes_values).uniq
|
|
44
|
+
records.each do |record|
|
|
45
|
+
Bullet::Detector::Association.add_object_associations(record, associations)
|
|
46
|
+
Bullet::Detector::NPlusOneQuery.call_association(record, associations)
|
|
47
|
+
end
|
|
48
|
+
Bullet::Detector::Association.add_eager_loadings(records, associations)
|
|
49
|
+
records
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
::ActiveRecord::Associations::JoinDependency.class_eval do
|
|
54
|
+
alias_method :origin_construct_association, :construct_association
|
|
55
|
+
# call join associations
|
|
56
|
+
def construct_association(record, join, row)
|
|
57
|
+
associations = join.reflection.name
|
|
58
|
+
Bullet::Detector::Association.add_object_associations(record, associations)
|
|
59
|
+
Bullet::Detector::NPlusOneQuery.call_association(record, associations)
|
|
60
|
+
origin_construct_association(record, join, row)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
::ActiveRecord::Associations::CollectionAssociation.class_eval do
|
|
65
|
+
# call one to many associations
|
|
66
|
+
alias_method :origin_load_target, :load_target
|
|
67
|
+
def load_target
|
|
68
|
+
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
|
|
69
|
+
origin_load_target
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
::ActiveRecord::Associations::Association.class_eval do
|
|
74
|
+
# call has_one and belong_to association
|
|
75
|
+
alias_method :origin_load_target, :load_target
|
|
76
|
+
def load_target
|
|
77
|
+
# avoid stack level too deep
|
|
78
|
+
result = origin_load_target
|
|
79
|
+
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless caller.find {|c| c =~ LOAD_TARGET_RGX }
|
|
80
|
+
Bullet::Detector::Association.add_possible_objects(result)
|
|
81
|
+
result
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
::ActiveRecord::Associations::HasManyAssociation.class_eval do
|
|
86
|
+
alias_method :origin_has_cached_counter?, :has_cached_counter?
|
|
87
|
+
|
|
88
|
+
def has_cached_counter?
|
|
89
|
+
result = origin_has_cached_counter?
|
|
90
|
+
Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
module Detector
|
|
3
|
+
class Association < Base
|
|
4
|
+
class <<self
|
|
5
|
+
def start_request
|
|
6
|
+
@@checked = false
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def clear
|
|
10
|
+
# Note that under ruby class variables are shared among the class
|
|
11
|
+
# that declares them and all classes derived from that class.
|
|
12
|
+
# The following variables are accessible by all classes that
|
|
13
|
+
# derive from Bullet::Detector::Association - changing the variable
|
|
14
|
+
# in one subclass will make the change visible to all subclasses!
|
|
15
|
+
@@object_associations = nil
|
|
16
|
+
@@callers = nil
|
|
17
|
+
@@possible_objects = nil
|
|
18
|
+
@@impossible_objects = nil
|
|
19
|
+
@@call_object_associations = nil
|
|
20
|
+
@@eager_loadings = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_object_associations(object, associations)
|
|
24
|
+
object_associations.add( object, associations )
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_call_object_associations(object, associations)
|
|
28
|
+
call_object_associations.add( object, associations )
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def add_possible_objects(objects)
|
|
32
|
+
possible_objects.add objects
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add_impossible_object(object)
|
|
36
|
+
impossible_objects.add object
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_eager_loadings(objects, associations)
|
|
40
|
+
objects = Array(objects)
|
|
41
|
+
|
|
42
|
+
to_add = nil
|
|
43
|
+
to_merge, to_delete = [], []
|
|
44
|
+
eager_loadings.each do |k, v|
|
|
45
|
+
key_objects_overlap = k & objects
|
|
46
|
+
|
|
47
|
+
next if key_objects_overlap.empty?
|
|
48
|
+
|
|
49
|
+
if key_objects_overlap == k
|
|
50
|
+
to_add = [k, associations]
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
else
|
|
54
|
+
to_merge << [key_objects_overlap, ( eager_loadings[k].dup << associations )]
|
|
55
|
+
|
|
56
|
+
keys_without_objects = k - objects
|
|
57
|
+
to_merge << [keys_without_objects, eager_loadings[k]]
|
|
58
|
+
to_delete << k
|
|
59
|
+
objects = objects - k
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
if to_add
|
|
63
|
+
eager_loadings.add *to_add
|
|
64
|
+
end
|
|
65
|
+
to_merge.each do |k,val|
|
|
66
|
+
eager_loadings.merge k, val
|
|
67
|
+
end
|
|
68
|
+
to_delete.each do |k|
|
|
69
|
+
eager_loadings.delete k
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
eager_loadings.add objects, associations unless objects.empty?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
def possible?(object)
|
|
77
|
+
possible_objects.contains? object
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def impossible?(object)
|
|
81
|
+
impossible_objects.contains? object
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# check if object => associations already exists in object_associations.
|
|
85
|
+
def association?(object, associations)
|
|
86
|
+
value = object_associations[object]
|
|
87
|
+
if value
|
|
88
|
+
value.each do |v|
|
|
89
|
+
result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
|
|
90
|
+
return true if result
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# object_associations keep the object relationships
|
|
98
|
+
# that the object has many associations.
|
|
99
|
+
# e.g. { <Post id:1> => [:comments] }
|
|
100
|
+
# the object_associations keep all associations that may be or may no be
|
|
101
|
+
# unpreload associations or unused preload associations.
|
|
102
|
+
def object_associations
|
|
103
|
+
@@object_associations ||= Bullet::Registry::Base.new
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# call_object_assciations keep the object relationships
|
|
107
|
+
# that object.associations is called.
|
|
108
|
+
# e.g. { <Post id:1> => [:comments] }
|
|
109
|
+
# they are used to detect unused preload associations.
|
|
110
|
+
def call_object_associations
|
|
111
|
+
@@call_object_associations ||= Bullet::Registry::Base.new
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# possible_objects keep the class to object relationships
|
|
115
|
+
# that the objects may cause N+1 query.
|
|
116
|
+
# e.g. { Post => [<Post id:1>, <Post id:2>] }
|
|
117
|
+
def possible_objects
|
|
118
|
+
@@possible_objects ||= Bullet::Registry::Object.new
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# impossible_objects keep the class to objects relationships
|
|
122
|
+
# that the objects may not cause N+1 query.
|
|
123
|
+
# e.g. { Post => [<Post id:1>, <Post id:2>] }
|
|
124
|
+
# Notice: impossible_objects are not accurate,
|
|
125
|
+
# if find collection returns only one object, then the object is impossible object,
|
|
126
|
+
# impossible_objects are used to avoid treating 1+1 query to N+1 query.
|
|
127
|
+
def impossible_objects
|
|
128
|
+
@@impossible_objects ||= Bullet::Registry::Object.new
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# eager_loadings keep the object relationships
|
|
132
|
+
# that the associations are preloaded by find :include.
|
|
133
|
+
# e.g. { [<Post id:1>, <Post id:2>] => [:comments, :user] }
|
|
134
|
+
def eager_loadings
|
|
135
|
+
@@eager_loadings ||= Bullet::Registry::Association.new
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def callers
|
|
139
|
+
@@callers ||= []
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
module Detector
|
|
3
|
+
class Counter < Base
|
|
4
|
+
def self.clear
|
|
5
|
+
@@possible_objects = nil
|
|
6
|
+
@@impossible_objects = nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.add_counter_cache(object, associations)
|
|
10
|
+
if conditions_met?( object, associations )
|
|
11
|
+
create_notification object.class, associations
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.add_possible_objects(objects)
|
|
16
|
+
possible_objects.add objects
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.add_impossible_object(object)
|
|
20
|
+
impossible_objects.add object
|
|
21
|
+
end
|
|
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
|
|
28
|
+
|
|
29
|
+
def self.possible_objects
|
|
30
|
+
@@possible_objects ||= Bullet::Registry::Object.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.impossible_objects
|
|
34
|
+
@@impossible_objects ||= Bullet::Registry::Object.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.conditions_met?( object, associations )
|
|
38
|
+
possible_objects.contains?( object ) and
|
|
39
|
+
!impossible_objects.contains?( object )
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
module Detector
|
|
3
|
+
class NPlusOneQuery < Association
|
|
4
|
+
# executed when object.assocations is called.
|
|
5
|
+
# first, it keeps this method call for object.association.
|
|
6
|
+
# then, it checks if this associations call is unpreload.
|
|
7
|
+
# if it is, keeps this unpreload associations and caller.
|
|
8
|
+
def self.call_association(object, associations)
|
|
9
|
+
@@checked = true
|
|
10
|
+
add_call_object_associations(object, associations)
|
|
11
|
+
|
|
12
|
+
if conditions_met?(object, associations)
|
|
13
|
+
caller_in_project
|
|
14
|
+
create_notification object.class, associations
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
def self.create_notification(klazz, associations)
|
|
20
|
+
notice = Bullet::Notification::NPlusOneQuery.new( callers, klazz, associations )
|
|
21
|
+
Bullet.notification_collector.add( notice )
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# decide whether the object.associations is unpreloaded or not.
|
|
25
|
+
def self.conditions_met?(object, associations)
|
|
26
|
+
possible?(object) and
|
|
27
|
+
!impossible?(object) and
|
|
28
|
+
!association?(object, associations)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.caller_in_project
|
|
32
|
+
vender_root ||= File.join(Rails.root, 'vendor')
|
|
33
|
+
callers << caller.select { |c| c =~ /#{Rails.root}/ }.
|
|
34
|
+
reject { |c| c =~ /#{vender_root}/ }
|
|
35
|
+
callers.uniq!
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
module Detector
|
|
3
|
+
class UnusedEagerAssociation < Association
|
|
4
|
+
# check if there are unused preload associations.
|
|
5
|
+
# for each object => association
|
|
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 self.check_unused_preload_associations
|
|
10
|
+
@@checked = true
|
|
11
|
+
object_associations.each do |object, association|
|
|
12
|
+
object_association_diff = diff_object_association object, association
|
|
13
|
+
next if object_association_diff.empty?
|
|
14
|
+
|
|
15
|
+
create_notification object.class, object_association_diff
|
|
16
|
+
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
|
+
|
|
25
|
+
def self.call_object_association( object, association )
|
|
26
|
+
all = Set.new
|
|
27
|
+
eager_loadings.similarly_associated( object, association ).each do |related_object|
|
|
28
|
+
coa = call_object_associations[related_object]
|
|
29
|
+
next if coa.nil?
|
|
30
|
+
all.merge coa
|
|
31
|
+
end
|
|
32
|
+
all.to_a
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.diff_object_association( object, association )
|
|
36
|
+
potential_objects = association - call_object_association( object, association )
|
|
37
|
+
potential_objects.reject {|a| a.is_a?( Hash ) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
module Detector
|
|
3
|
+
autoload :Base, 'bullet/detector/base'
|
|
4
|
+
autoload :Association, 'bullet/detector/association'
|
|
5
|
+
autoload :NPlusOneQuery, 'bullet/detector/n_plus_one_query'
|
|
6
|
+
autoload :UnusedEagerAssociation, 'bullet/detector/unused_eager_association'
|
|
7
|
+
autoload :Counter, 'bullet/detector/counter'
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
module Notification
|
|
3
|
+
class Base
|
|
4
|
+
attr_accessor :notifier, :url
|
|
5
|
+
attr_reader :base_class, :associations, :path
|
|
6
|
+
|
|
7
|
+
def initialize( base_class, associations, path = nil )
|
|
8
|
+
@base_class = base_class
|
|
9
|
+
@associations = associations.is_a?( Array ) ? associations : [ associations ]
|
|
10
|
+
@path = path
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def title
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def body
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def whoami
|
|
20
|
+
"user: " << `whoami`.chomp
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def body_with_caller
|
|
24
|
+
body
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def standard_notice
|
|
28
|
+
@standard_notifice ||= title + "\n" + body
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def full_notice
|
|
32
|
+
[whoami, url, title, body_with_caller].compact.join("\n")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def notify_inline
|
|
36
|
+
self.notifier.inline_notify( self.full_notice )
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def notify_out_of_channel
|
|
40
|
+
self.notifier.out_of_channel_notify( self.full_notice )
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def eql?( other )
|
|
44
|
+
klazz_associations_str == other.klazz_associations_str
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def hash
|
|
48
|
+
klazz_associations_str.hash
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
protected
|
|
52
|
+
def klazz_associations_str
|
|
53
|
+
" #{@base_class} => [#{@associations.map(&:inspect).join(', ')}]"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def associations_str
|
|
57
|
+
":include => #{@associations.map{|a| a.to_s.to_sym unless a.is_a? Hash}.inspect}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
module Notification
|
|
3
|
+
class NPlusOneQuery < Base
|
|
4
|
+
def initialize( callers, base_class, associations, path = nil )
|
|
5
|
+
super( base_class, associations, path )
|
|
6
|
+
|
|
7
|
+
@callers = callers
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def body_with_caller
|
|
11
|
+
"#{body}\n#{call_stack_messages}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def body
|
|
15
|
+
"#{klazz_associations_str}\n Add to your finder: #{associations_str}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def title
|
|
19
|
+
"N+1 Query #{@path ? "in #{@path}" : 'detected'}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
def call_stack_messages
|
|
24
|
+
@callers.collect do |c|
|
|
25
|
+
[ 'N+1 Query method call stack',
|
|
26
|
+
c.collect {|line| " #{line}"} ].flatten
|
|
27
|
+
end.join( "\n" )
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
module Notification
|
|
3
|
+
class UnusedEagerLoading < Base
|
|
4
|
+
def body
|
|
5
|
+
"#{klazz_associations_str}\n Remove from your finder: #{associations_str}"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def title
|
|
9
|
+
"Unused Eager Loading #{@path ? "in #{@path}" : 'detected'}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/bullet/notification.rb
CHANGED
|
@@ -1,83 +1,8 @@
|
|
|
1
1
|
module Bullet
|
|
2
|
-
class NotificationError < StandardError
|
|
3
|
-
end
|
|
4
|
-
|
|
5
2
|
module Notification
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
unless response.blank?
|
|
24
|
-
if Bullet.alert
|
|
25
|
-
str << wrap_js_association("alert(#{response.join("\n").inspect});")
|
|
26
|
-
end
|
|
27
|
-
if Bullet.console
|
|
28
|
-
code = <<-CODE
|
|
29
|
-
if (typeof(console) !== 'undefined') {
|
|
30
|
-
|
|
31
|
-
if (console.groupCollapsed && console.groupEnd && console.log) {
|
|
32
|
-
|
|
33
|
-
console.groupCollapsed(#{console_title.join(', ').inspect});
|
|
34
|
-
console.log(#{response.join("\n").inspect});
|
|
35
|
-
console.log(#{call_stack_messages.join("\n").inspect});
|
|
36
|
-
console.groupEnd();
|
|
37
|
-
|
|
38
|
-
} else if (console.log) {
|
|
39
|
-
|
|
40
|
-
console.log(#{response.join("\n").inspect});
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
CODE
|
|
44
|
-
str << wrap_js_association(code)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
str
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def growl_notification
|
|
51
|
-
if Bullet.growl
|
|
52
|
-
response = notification_response
|
|
53
|
-
unless response.blank?
|
|
54
|
-
begin
|
|
55
|
-
growl = Growl.new('localhost', 'ruby-growl', ['Bullet Notification'], nil, Bullet.growl_password)
|
|
56
|
-
growl.notify('Bullet Notification', 'Bullet Notification', response.join("\n"))
|
|
57
|
-
rescue
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def log_notification(path)
|
|
64
|
-
if Bullet.bullet_logger || Bullet.rails_logger
|
|
65
|
-
Rails.logger.warn '' if Bullet.rails_logger
|
|
66
|
-
messages = log_messages(path)
|
|
67
|
-
messages.each do |message|
|
|
68
|
-
Bullet.logger.info(message.join("\n")) if Bullet.bullet_logger
|
|
69
|
-
Rails.logger.warn(message.join("\n")) if Bullet.rails_logger
|
|
70
|
-
end
|
|
71
|
-
Bullet.logger_file.flush if Bullet.bullet_logger
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
private
|
|
76
|
-
def wrap_js_association(message)
|
|
77
|
-
str = ''
|
|
78
|
-
str << "<script type=\"text/javascript\">/*<![CDATA[*/"
|
|
79
|
-
str << message
|
|
80
|
-
str << "/*]]>*/</script>\n"
|
|
81
|
-
end
|
|
3
|
+
autoload :Base, 'bullet/notification/base'
|
|
4
|
+
autoload :UnusedEagerLoading, 'bullet/notification/unused_eager_loading'
|
|
5
|
+
autoload :NPlusOneQuery, 'bullet/notification/n_plus_one_query'
|
|
6
|
+
autoload :CounterCache, 'bullet/notification/counter_cache'
|
|
82
7
|
end
|
|
83
8
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
module Bullet
|
|
4
|
+
class NotificationCollector
|
|
5
|
+
attr_reader :collection
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
reset
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def reset
|
|
12
|
+
@collection = Set.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add( value )
|
|
16
|
+
@collection << value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def notifications_present?
|
|
20
|
+
!@collection.empty?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
data/lib/bullet/rack.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
class Rack
|
|
3
|
+
def initialize(app)
|
|
4
|
+
@app = app
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def call(env)
|
|
8
|
+
return @app.call(env) unless Bullet.enable?
|
|
9
|
+
|
|
10
|
+
Bullet.start_request
|
|
11
|
+
status, headers, response = @app.call(env)
|
|
12
|
+
return [status, headers, response] if empty?(response)
|
|
13
|
+
|
|
14
|
+
if Bullet.notification?
|
|
15
|
+
if status == 200 and !response.body.frozen? and check_html?(headers, response)
|
|
16
|
+
response_body = response.body << Bullet.gather_inline_notifications
|
|
17
|
+
headers['Content-Length'] = response_body.length.to_s
|
|
18
|
+
end
|
|
19
|
+
Bullet.perform_out_of_channel_notifications(env)
|
|
20
|
+
end
|
|
21
|
+
response_body ||= response.body
|
|
22
|
+
Bullet.end_request
|
|
23
|
+
no_browser_cache(headers) if Bullet.disable_browser_cache
|
|
24
|
+
[status, headers, [response_body]]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# fix issue if response's body is a Proc
|
|
28
|
+
def empty?(response)
|
|
29
|
+
# response may be ["Not Found"], ["Move Permanently"], etc.
|
|
30
|
+
(response.is_a?(Array) && response.size <= 1) ||
|
|
31
|
+
!response.body.is_a?(String) || response.body.empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def check_html?(headers, response)
|
|
35
|
+
headers['Content-Type'] and headers['Content-Type'].include? 'text/html' and response.body =~ %r{<html.*</html>}m
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def no_browser_cache(headers)
|
|
39
|
+
headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
|
|
40
|
+
headers["Pragma"] = "no-cache"
|
|
41
|
+
headers["Expires"] = "Wed, 09 Sep 2009 09:09:09 GMT"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Bullet
|
|
2
|
+
module Registry
|
|
3
|
+
class Association < Base
|
|
4
|
+
def merge( base, associations )
|
|
5
|
+
@registry.merge!( { base => associations } )
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def similarly_associated( base, associations )
|
|
9
|
+
@registry.select do |key, value|
|
|
10
|
+
key.include?( base ) and value == associations
|
|
11
|
+
end.collect( &:first ).flatten
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|