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.
- data/.rspec +1 -0
- data/Hacking.textile +100 -0
- data/README.textile +36 -5
- data/README_for_rails2.textile +17 -0
- data/Rakefile +33 -16
- data/VERSION +1 -1
- data/autotest/discover.rb +1 -0
- data/bullet.gemspec +32 -9
- data/lib/bullet.rb +69 -38
- data/lib/bullet/action_controller2.rb +4 -4
- data/lib/bullet/active_record2.rb +16 -16
- data/lib/bullet/active_record3.rb +16 -25
- data/lib/bullet/detector.rb +9 -0
- data/lib/bullet/detector/association.rb +135 -0
- data/lib/bullet/detector/base.rb +19 -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 +39 -0
- data/lib/bullet/notification.rb +4 -79
- data/lib/bullet/notification/base.rb +59 -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_collector.rb +25 -0
- data/lib/bullet/presenter.rb +13 -0
- data/lib/bullet/presenter/base.rb +9 -0
- data/lib/bullet/presenter/bullet_logger.rb +28 -0
- data/lib/bullet/presenter/growl.rb +40 -0
- data/lib/bullet/presenter/javascript_alert.rb +15 -0
- data/lib/bullet/presenter/javascript_console.rb +28 -0
- data/lib/bullet/presenter/javascript_helpers.rb +13 -0
- data/lib/bullet/presenter/rails_logger.rb +15 -0
- data/lib/bullet/presenter/xmpp.rb +56 -0
- data/lib/bullet/rack.rb +42 -0
- data/lib/bullet/registry.rb +7 -0
- data/lib/bullet/registry/association.rb +16 -0
- data/lib/bullet/registry/base.rb +39 -0
- data/lib/bullet/registry/object.rb +15 -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 +118 -262
- data/spec/bullet/counter_spec.rb +10 -10
- data/spec/spec_helper.rb +51 -17
- metadata +32 -9
- 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,39 @@
|
|
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
|
+
eager_loadings.similarly_associated( object, association ).
|
27
|
+
collect { |related_object| call_object_associations[related_object] }.
|
28
|
+
compact.
|
29
|
+
flatten.
|
30
|
+
uniq
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.diff_object_association( object, association )
|
34
|
+
potential_objects = association - call_object_association( object, association )
|
35
|
+
potential_objects.reject {|a| a.is_a?( Hash ) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
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,59 @@
|
|
1
|
+
module Bullet
|
2
|
+
module Notification
|
3
|
+
class Base
|
4
|
+
attr_accessor :presenter
|
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 body_with_caller
|
20
|
+
body
|
21
|
+
end
|
22
|
+
|
23
|
+
def standard_notice
|
24
|
+
@standard_notifice ||= title + "\n" + body
|
25
|
+
end
|
26
|
+
|
27
|
+
def full_notice
|
28
|
+
@full_notice ||= title + "\n" + body_with_caller
|
29
|
+
end
|
30
|
+
|
31
|
+
def present_inline
|
32
|
+
return unless self.presenter.respond_to? :inline
|
33
|
+
self.presenter.send( :inline, self )
|
34
|
+
end
|
35
|
+
|
36
|
+
def present_out_of_channel
|
37
|
+
return unless self.presenter.respond_to? :out_of_channel
|
38
|
+
self.presenter.send( :out_of_channel, self )
|
39
|
+
end
|
40
|
+
|
41
|
+
def eql?( other )
|
42
|
+
klazz_associations_str == other.klazz_associations_str
|
43
|
+
end
|
44
|
+
|
45
|
+
def hash
|
46
|
+
klazz_associations_str.hash
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
def klazz_associations_str
|
51
|
+
" #{@base_class} => [#{@associations.map(&:inspect).join(', ')}]"
|
52
|
+
end
|
53
|
+
|
54
|
+
def associations_str
|
55
|
+
":include => #{@associations.map{|a| a.to_s.to_sym unless a.is_a? Hash}.inspect}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
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
|
@@ -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
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Bullet
|
2
|
+
module Presenter
|
3
|
+
autoload :Base, 'bullet/presenter/base'
|
4
|
+
autoload :JavascriptAlert, 'bullet/presenter/javascript_alert'
|
5
|
+
autoload :JavascriptConsole, 'bullet/presenter/javascript_console'
|
6
|
+
autoload :Growl, 'bullet/presenter/growl'
|
7
|
+
autoload :Xmpp, 'bullet/presenter/xmpp'
|
8
|
+
autoload :RailsLogger, 'bullet/presenter/rails_logger'
|
9
|
+
autoload :BulletLogger, 'bullet/presenter/bullet_logger'
|
10
|
+
|
11
|
+
autoload :JavascriptHelpers, 'bullet/presenter/javascript_helpers'
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Bullet
|
2
|
+
module Presenter
|
3
|
+
class BulletLogger < Base
|
4
|
+
@logger_file = nil
|
5
|
+
@logger = nil
|
6
|
+
|
7
|
+
def self.active?
|
8
|
+
@logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.out_of_channel( notice )
|
12
|
+
return unless active?
|
13
|
+
@logger.info notice.full_notice
|
14
|
+
@logger_file.flush
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.setup
|
18
|
+
@logger_file = File.open( Rails.root.join('log/bullet.log'), 'a+' )
|
19
|
+
@logger = Logger.new( @logger_file )
|
20
|
+
|
21
|
+
def @logger.format_message( severity, timestamp, progname, msg )
|
22
|
+
"#{timestamp.to_formatted_s(:db)}[#{severity}] #{msg}\n"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Bullet
|
2
|
+
module Presenter
|
3
|
+
class Growl < Base
|
4
|
+
@growl = nil
|
5
|
+
|
6
|
+
def self.active?
|
7
|
+
@growl
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.out_of_channel( notice )
|
11
|
+
return unless active?
|
12
|
+
notify( notice.standard_notice )
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.setup_connection( password )
|
16
|
+
require 'ruby-growl'
|
17
|
+
@password = password
|
18
|
+
@growl = connect
|
19
|
+
|
20
|
+
notify 'Bullet Growl notifications have been turned on'
|
21
|
+
rescue MissingSourceFile
|
22
|
+
@growl = nil
|
23
|
+
raise NotificationError.new( 'You must install the ruby-growl gem to use Growl notifications: `sudo gem install ruby-growl`' )
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def self.connect
|
28
|
+
::Growl.new 'localhost',
|
29
|
+
'ruby-growl',
|
30
|
+
[ 'Bullet Notification' ],
|
31
|
+
nil,
|
32
|
+
@password
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.notify( message )
|
36
|
+
@growl.notify( 'Bullet Notification', 'Bullet Notification', message )
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Bullet
|
2
|
+
module Presenter
|
3
|
+
class JavascriptAlert < Base
|
4
|
+
def self.active?
|
5
|
+
Bullet.alert
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.inline( notice )
|
9
|
+
return '' unless self.active?
|
10
|
+
|
11
|
+
JavascriptHelpers::wrap_js_association "alert( #{notice.standard_notice.inspect} ); "
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Bullet
|
2
|
+
module Presenter
|
3
|
+
class JavascriptConsole < Base
|
4
|
+
def self.active?
|
5
|
+
Bullet.console
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.inline( notice )
|
9
|
+
return '' unless active?
|
10
|
+
|
11
|
+
code = <<-CODE
|
12
|
+
if (typeof(console) !== 'undefined') {
|
13
|
+
if (console.groupCollapsed && console.groupEnd && console.log) {
|
14
|
+
console.groupCollapsed(#{notice.title.inspect});
|
15
|
+
console.log(#{notice.body_with_caller.inspect});
|
16
|
+
console.groupEnd();
|
17
|
+
|
18
|
+
} else if (console.log) {
|
19
|
+
console.log(#{notice.full_notice.inspect});
|
20
|
+
}
|
21
|
+
}
|
22
|
+
CODE
|
23
|
+
|
24
|
+
JavascriptHelpers::wrap_js_association code
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Bullet
|
2
|
+
module Presenter
|
3
|
+
class RailsLogger < Base
|
4
|
+
def self.active?
|
5
|
+
Bullet.rails_logger
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.out_of_channel( notice )
|
9
|
+
return unless active?
|
10
|
+
Rails.logger.warn ''
|
11
|
+
Rails.logger.warn notice.full_notice
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|