thaold-bullet 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/bullet.rb ADDED
@@ -0,0 +1,105 @@
1
+ require 'set'
2
+ require 'uniform_notifier'
3
+
4
+ module Bullet
5
+ if Rails.version =~ /^3.0/
6
+ autoload :ActiveRecord, 'bullet/active_record3'
7
+ else
8
+ autoload :ActiveRecord, 'bullet/active_record2'
9
+ autoload :ActionController, 'bullet/action_controller2'
10
+ end
11
+ autoload :Rack, 'bullet/rack'
12
+ autoload :BulletLogger, 'bullet/logger'
13
+ autoload :Notification, 'bullet/notification'
14
+ autoload :Detector, 'bullet/detector'
15
+ autoload :Registry, 'bullet/registry'
16
+ autoload :NotificationCollector, 'bullet/notification_collector'
17
+
18
+ if defined? Rails::Railtie
19
+ class BulletRailtie < Rails::Railtie
20
+ initializer "bullet.configure_rails_initialization" do |app|
21
+ app.middleware.use Bullet::Rack
22
+ end
23
+ end
24
+ end
25
+
26
+ class <<self
27
+ attr_accessor :enable, :disable_browser_cache
28
+ attr_reader :notification_collector
29
+
30
+ delegate :alert=, :console=, :growl=, :rails_logger=, :xmpp=, :to => UniformNotifier
31
+
32
+ DETECTORS = [ Bullet::Detector::NPlusOneQuery,
33
+ Bullet::Detector::UnusedEagerAssociation,
34
+ Bullet::Detector::Counter ]
35
+
36
+ def enable=(enable)
37
+ @enable = enable
38
+ if enable?
39
+ Bullet::ActiveRecord.enable
40
+ if Rails.version =~ /^2./
41
+ Bullet::ActionController.enable
42
+ end
43
+ end
44
+ end
45
+
46
+ def enable?
47
+ @enable == true
48
+ end
49
+
50
+ def bullet_logger=(active)
51
+ if active
52
+ bullet_log_file = File.open( 'log/bullet.log', 'a+' )
53
+ bullet_log_file.sync
54
+ UniformNotifier.customized_logger = bullet_log_file
55
+ end
56
+ end
57
+
58
+ def start_request
59
+ notification_collector.reset
60
+ DETECTORS.each {|bullet| bullet.start_request}
61
+ end
62
+
63
+ def end_request
64
+ DETECTORS.each {|bullet| bullet.end_request}
65
+ end
66
+
67
+ def clear
68
+ DETECTORS.each {|bullet| bullet.clear}
69
+ end
70
+
71
+ def notification_collector
72
+ @notification_collector ||= Bullet::NotificationCollector.new
73
+ end
74
+
75
+ def notification?
76
+ Bullet::Detector::UnusedEagerAssociation.check_unused_preload_associations
77
+ notification_collector.notifications_present?
78
+ end
79
+
80
+ def gather_inline_notifications
81
+ responses = []
82
+ for_each_active_notifier_with_notification do |notification|
83
+ responses << notification.notify_inline
84
+ end
85
+ responses.join( "\n" )
86
+ end
87
+
88
+ def perform_out_of_channel_notifications
89
+ for_each_active_notifier_with_notification do |notification|
90
+ notification.notify_out_of_channel
91
+ end
92
+ end
93
+
94
+ private
95
+ def for_each_active_notifier_with_notification
96
+ UniformNotifier.active_notifiers.each do |notifier|
97
+ notification_collector.collection.each do |notification|
98
+ notification.notifier = notifier
99
+ yield notification
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ end
@@ -0,0 +1,49 @@
1
+ module Bullet
2
+ class ActionController
3
+ def self.enable
4
+ require 'action_controller'
5
+ case Rails.version
6
+ when /^2.3/
7
+ ::ActionController::Dispatcher.middleware.use Bullet::Rack
8
+ ::ActionController::Dispatcher.class_eval do
9
+ class <<self
10
+ alias_method :origin_reload_application, :reload_application
11
+ def reload_application
12
+ origin_reload_application
13
+ Bullet.clear
14
+ end
15
+ end
16
+ end
17
+ when /^2.[2|1]/
18
+ ::ActionController::Dispatcher.class_eval do
19
+ alias_method :origin_reload_application, :reload_application
20
+ def reload_application
21
+ origin_reload_application
22
+ Bullet.clear
23
+ end
24
+ end
25
+
26
+ ::ActionController::Base.class_eval do
27
+ alias_method :origin_process, :process
28
+ def process(request, response, method = :perform_action, *arguments)
29
+ Bullet.start_request
30
+ response = origin_process(request, response, method = :perform_action, *arguments)
31
+
32
+ if Bullet.notification?
33
+ if response.headers["type"] and response.headers["type"].include? 'text/html' and response.body =~ %r{<html.*</html>}m
34
+ response.body <<= Bullet.gather_inline_notifications
35
+ response.headers["Content-Length"] = response.body.length.to_s
36
+ end
37
+
38
+ Bullet.perform_bullet_out_of_channel_notifications
39
+ end
40
+ Bullet.end_request
41
+ response
42
+ end
43
+ end
44
+ else
45
+ puts "Gem Bullet: Unsupported rails version"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,109 @@
1
+ module Bullet
2
+ module ActiveRecord
3
+ def self.enable
4
+ require 'active_record'
5
+ ::ActiveRecord::Base.class_eval do
6
+ class << self
7
+ alias_method :origin_find_every, :find_every
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 find_every(options)
11
+ records = origin_find_every(options)
12
+
13
+ if records
14
+ if records.size > 1
15
+ Bullet::Detector::Association.add_possible_objects(records)
16
+ Bullet::Detector::Counter.add_possible_objects(records)
17
+ elsif records.size == 1
18
+ Bullet::Detector::Association.add_impossible_object(records.first)
19
+ Bullet::Detector::Counter.add_impossible_object(records.first)
20
+ end
21
+ end
22
+
23
+ records
24
+ end
25
+ end
26
+ end
27
+
28
+ ::ActiveRecord::AssociationPreload::ClassMethods.class_eval do
29
+ alias_method :origin_preload_associations, :preload_associations
30
+ # include query for one to many associations.
31
+ # keep this eager loadings.
32
+ def preload_associations(records, associations, preload_options={})
33
+ records = [records].flatten.compact.uniq
34
+ return if records.empty?
35
+ records.each do |record|
36
+ Bullet::Detector::Association.add_object_associations(record, associations)
37
+ end
38
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
39
+ origin_preload_associations(records, associations, preload_options={})
40
+ end
41
+ end
42
+
43
+ ::ActiveRecord::Associations::ClassMethods.class_eval do
44
+ # add include in named_scope
45
+ alias_method :origin_find_with_associations, :find_with_associations
46
+ def find_with_associations(options)
47
+ records = origin_find_with_associations(options)
48
+ associations = merge_includes(scope(:find, :include), options[:include])
49
+ records.each do |record|
50
+ Bullet::Detector::Association.add_object_associations(record, associations)
51
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
52
+ end
53
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
54
+ records
55
+ end
56
+ end
57
+
58
+ ::ActiveRecord::Associations::ClassMethods::JoinDependency.class_eval do
59
+ # call join associations
60
+ alias_method :origin_construct_association, :construct_association
61
+ def construct_association(record, join, row)
62
+ associations = join.reflection.name
63
+ Bullet::Detector::Association.add_object_associations(record, associations)
64
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
65
+ origin_construct_association(record, join, row)
66
+ end
67
+ end
68
+
69
+ ::ActiveRecord::Associations::AssociationCollection.class_eval do
70
+ # call one to many associations
71
+ alias_method :origin_load_target, :load_target
72
+ def load_target
73
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
74
+ origin_load_target
75
+ end
76
+ end
77
+
78
+ ::ActiveRecord::Associations::AssociationProxy.class_eval do
79
+ # call has_one and belong_to association
80
+ alias_method :origin_load_target, :load_target
81
+ def load_target
82
+ # avoid stack level too deep
83
+ result = origin_load_target
84
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
85
+ Bullet::Detector::Association.add_possible_objects(result)
86
+ result
87
+ end
88
+ end
89
+
90
+ ::ActiveRecord::Associations::HasManyAssociation.class_eval do
91
+ alias_method :origin_has_cached_counter?, :has_cached_counter?
92
+ def has_cached_counter?
93
+ result = origin_has_cached_counter?
94
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
95
+ result
96
+ end
97
+ end
98
+
99
+ ::ActiveRecord::Associations::HasManyThroughAssociation.class_eval do
100
+ alias_method :origin_has_cached_counter?, :has_cached_counter?
101
+ def has_cached_counter?
102
+ result = origin_has_cached_counter?
103
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
104
+ result
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,104 @@
1
+ module Bullet
2
+ module ActiveRecord
3
+ def self.enable
4
+ require 'active_record'
5
+ ::ActiveRecord::Relation.class_eval do
6
+ alias_method :origin_to_a, :to_a
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 to_a
10
+ records = origin_to_a
11
+ if records.size > 1
12
+ Bullet::Detector::Association.add_possible_objects(records)
13
+ Bullet::Detector::Counter.add_possible_objects(records)
14
+ elsif records.size == 1
15
+ Bullet::Detector::Association.add_impossible_object(records.first)
16
+ Bullet::Detector::Counter.add_impossible_object(records.first)
17
+ end
18
+ records
19
+ end
20
+ end
21
+
22
+ ::ActiveRecord::AssociationPreload::ClassMethods.class_eval do
23
+ alias_method :origin_preload_associations, :preload_associations
24
+ # include query for one to many associations.
25
+ # keep this eager loadings.
26
+ def preload_associations(records, associations, preload_options={})
27
+ records = [records].flatten.compact.uniq
28
+ return if records.empty?
29
+ records.each do |record|
30
+ Bullet::Detector::Association.add_object_associations(record, associations)
31
+ end
32
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
33
+ origin_preload_associations(records, associations, preload_options={})
34
+ end
35
+ end
36
+
37
+ ::ActiveRecord::FinderMethods.class_eval do
38
+ # add includes in scope
39
+ alias_method :origin_find_with_associations, :find_with_associations
40
+ def find_with_associations
41
+ records = origin_find_with_associations
42
+ associations = (@eager_load_values + @includes_values).uniq
43
+ records.each do |record|
44
+ Bullet::Detector::Association.add_object_associations(record, associations)
45
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
46
+ end
47
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
48
+ records
49
+ end
50
+ end
51
+
52
+ ::ActiveRecord::Associations::ClassMethods::JoinDependency.class_eval do
53
+ alias_method :origin_construct_association, :construct_association
54
+ # call join associations
55
+ def construct_association(record, join, row)
56
+ associations = join.reflection.name
57
+ Bullet::Detector::Association.add_object_associations(record, associations)
58
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
59
+ origin_construct_association(record, join, row)
60
+ end
61
+ end
62
+
63
+ ::ActiveRecord::Associations::AssociationCollection.class_eval do
64
+ # call one to many associations
65
+ alias_method :origin_load_target, :load_target
66
+ def load_target
67
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
68
+ origin_load_target
69
+ end
70
+ end
71
+
72
+ ::ActiveRecord::Associations::AssociationProxy.class_eval do
73
+ # call has_one and belong_to association
74
+ alias_method :origin_load_target, :load_target
75
+ def load_target
76
+ # avoid stack level too deep
77
+ result = origin_load_target
78
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
79
+ Bullet::Detector::Association.add_possible_objects(result)
80
+ result
81
+ end
82
+ end
83
+
84
+ ::ActiveRecord::Associations::HasManyAssociation.class_eval do
85
+ alias_method :origin_has_cached_counter?, :has_cached_counter?
86
+
87
+ def has_cached_counter?
88
+ result = origin_has_cached_counter?
89
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
90
+ result
91
+ end
92
+ end
93
+
94
+ ::ActiveRecord::Associations::HasManyThroughAssociation.class_eval do
95
+ alias_method :origin_has_cached_counter?, :has_cached_counter?
96
+ def has_cached_counter?
97
+ result = origin_has_cached_counter?
98
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
99
+ result
100
+ end
101
+ end
102
+ end
103
+ end
104
+ 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,135 @@
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
+ eager_loadings.each do |k, v|
43
+ key_objects_overlap = k & objects
44
+
45
+ next if key_objects_overlap.empty?
46
+
47
+ if key_objects_overlap == k
48
+ eager_loadings.add k, associations
49
+ break
50
+
51
+ else
52
+ eager_loadings.merge key_objects_overlap, ( eager_loadings[k].dup << associations )
53
+
54
+ keys_without_objects = k - objects
55
+ eager_loadings.merge keys_without_objects, eager_loadings[k] unless keys_without_objects.empty?
56
+
57
+ eager_loadings.delete(k)
58
+ objects = objects - k
59
+ end
60
+ end
61
+
62
+ eager_loadings.add objects, associations unless objects.empty?
63
+ end
64
+
65
+ private
66
+ def possible?(object)
67
+ possible_objects.contains? object
68
+ end
69
+
70
+ def impossible?(object)
71
+ impossible_objects.contains? object
72
+ end
73
+
74
+ # check if object => associations already exists in object_associations.
75
+ def association?(object, associations)
76
+ object_associations.each do |key, value|
77
+ next unless key == object
78
+
79
+ value.each do |v|
80
+ result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
81
+ return true if result
82
+ end
83
+
84
+ end
85
+ return false
86
+ end
87
+
88
+ # object_associations keep the object relationships
89
+ # that the object has many associations.
90
+ # e.g. { <Post id:1> => [:comments] }
91
+ # the object_associations keep all associations that may be or may no be
92
+ # unpreload associations or unused preload associations.
93
+ def object_associations
94
+ @@object_associations ||= Bullet::Registry::Base.new
95
+ end
96
+
97
+ # call_object_assciations keep the object relationships
98
+ # that object.associations is called.
99
+ # e.g. { <Post id:1> => [:comments] }
100
+ # they are used to detect unused preload associations.
101
+ def call_object_associations
102
+ @@call_object_associations ||= Bullet::Registry::Base.new
103
+ end
104
+
105
+ # possible_objects keep the class to object relationships
106
+ # that the objects may cause N+1 query.
107
+ # e.g. { Post => [<Post id:1>, <Post id:2>] }
108
+ def possible_objects
109
+ @@possible_objects ||= Bullet::Registry::Object.new
110
+ end
111
+
112
+ # impossible_objects keep the class to objects relationships
113
+ # that the objects may not cause N+1 query.
114
+ # e.g. { Post => [<Post id:1>, <Post id:2>] }
115
+ # Notice: impossible_objects are not accurate,
116
+ # if find collection returns only one object, then the object is impossible object,
117
+ # impossible_objects are used to avoid treating 1+1 query to N+1 query.
118
+ def impossible_objects
119
+ @@impossible_objects ||= Bullet::Registry::Object.new
120
+ end
121
+
122
+ # eager_loadings keep the object relationships
123
+ # that the associations are preloaded by find :include.
124
+ # e.g. { [<Post id:1>, <Post id:2>] => [:comments, :user] }
125
+ def eager_loadings
126
+ @@eager_loadings ||= Bullet::Registry::Association.new
127
+ end
128
+
129
+ def callers
130
+ @@callers ||= []
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end