flyerhzm-bullet 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -1,57 +1,59 @@
1
1
  h1. Bullet
2
2
 
3
- This plugin is aimed to give you some performance suggestion about ActiveRecord usage, what should use but not use, such as eager loading, counter cache and so on, what should not use but use, such as unused eager loading.
3
+ The Bullet plugin is designed to help you increase your application's performance by reducing the number of queries it makes. It will watch your queries while you develop your application and notify you when you should add eager loading (N+1 queries) or when you're using eager loading that isn't necessary.
4
4
 
5
- Now it provides you the suggestion of eager loading and unused eager loading.
6
-
7
- The others are todo, next may be couter cache.
5
+ Best practice is to use Bullet in development mode or custom mode (staging, profile, etc.). The last thing you want is your clients getting alerts about how lazy you are.
8
6
 
9
7
  ****************************************************************************
10
8
 
11
- h2. Install
9
+ h2. Thanks
12
10
 
13
- install as gem:
14
- <pre><code>
15
- gem sources -a http://gems.github.com
16
- gem install flyerhzm-bullet
17
- </code></pre>
18
-
19
- install as plugin:
20
- <pre><code>script/plugin install git://github.com/flyerhzm/bullet.git</code></pre>
11
+ flipsasser added Growl, console.log and Rails.log support, very awesome. And he improved README.
21
12
 
22
13
  ****************************************************************************
23
14
 
24
- h2. Usage
25
-
26
- * "Eager Loading, protect N+1 query and protect from unused eager loading":#EagerLoading
27
-
28
- ****************************************************************************
15
+ h2. Install
29
16
 
30
- h2. Step by step example
17
+ You can add Bullet to your Rails gem requirements:
18
+ <pre><code>config.gem 'flyerhzm-bullet', :lib => 'bullet', :source => 'http://gems.github.com'</code></pre>
31
19
 
32
- * "Eager Loading, protect N+1 query and protect from unused eager loading":#EagerLoadingExample
20
+ You can install it as a gem:
21
+ <pre><code>sudo gem install flyerhzm-bullet --source http://gems.github.com</code></pre>
33
22
 
23
+ Or you can install it as a rails plugin:
24
+ <pre><code>script/plugin install git://github.com/flyerhzm/bullet.git</code></pre>
34
25
 
35
26
  ****************************************************************************
36
27
 
37
- h3. Eager Loading, protect N+1 query and protect from unused eager loading, usage
38
- <a id="EagerLoading"/></a>
39
-
40
- *important*: It is strongly recommended to disable cache in browser.
28
+ h2. Configuration
41
29
 
42
- * add configuration to environment
30
+ Bullet won't do ANYTHING unless you tell it to explicitly. Append to <code>config/environments/development.rb</code> initializer with the following code:
43
31
  <pre><code>
44
- Bullet.enable = true
45
- Bullet::Association.logger = true
46
- Bullet::Association.alert = true
32
+ config.after_initialize do
33
+ Bullet.enable = true
34
+ Bullet::Association.alert = true
35
+ Bullet::Association.bullet_logger = true
36
+ Bullet::Association.console = true
37
+ Bullet::Association.growl = true
38
+ Bullet::Association.rails_logger = true
39
+ end
47
40
  </code></pre>
48
- ** Bullet.enable (required), if enable is true (default is false), Bullet plugin is enabled. Otherwise, Bullet plugin is disabled.
49
- ** Bullet::Association.logger (optional), if logger is true (default is true), the N+1 query hints will be appended to <code>log/bullet.log</code> with N+1 query method call stack. Otherwise, no hint to log/bullet.log.
50
- ** Bullet::Association.alert (optional), if alert is true (default value), alert box will popup if there is N+1 query when browsing web page. Otherwise, no alert box.
51
41
 
52
- * browse the webpage, if there are N+1 queries or unused eager loading, alert box and bullet log will generate according to configurations. Alert box will only popup when the request's Content-Type is text/html, and <code>log/bullet.log</code> will produce whatever the request is.
42
+ The code above will enable all five of the Bullet notification systems:
43
+ * <code>Bullet.enable</code>: enable Bullet plugin, otherwise do nothing
44
+ * <code>Bullet::Association.alert</code>: pop up a JavaScript alert in the browser
45
+ * <code>Bullet::Association.bullet_logger</code>: log to the Bullet log file (RAILS_ROOT/log/bullet.log)
46
+ * <code>Bullet::Association.rails_logger</code>: add warnings directly to the Rails log
47
+ * <code>Bullet::Association.console</code>: log warnings to your browser's console.log (Safari/Webkit browsers or Firefox w/Firebug installed)
48
+ * <code>Bullet::Association.growl</code>: pop up Growl warnings if your system has Growl installed. Requires a little bit of configuration
49
+
50
+ ****************************************************************************
51
+
52
+ h2. Log
53
53
 
54
- * example of <code>log/bullet.log</code>
54
+ The Bullet log <code>log/bullet.log</code> will look something like this:
55
+
56
+ * N+1 Query:
55
57
  <pre><code>
56
58
  2009-08-25 20:40:17[INFO] N+1 Query: PATH_INFO: /posts; model: Post => associations: [comments]·
57
59
  Add to your finder: :include => [:comments]
@@ -61,22 +63,36 @@ Add to your finder: :include => [:comments]
61
63
  /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb'
62
64
  /Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
63
65
  </code></pre>
64
- It represents that in request '/posts', there is a N+1 query from Post to comments. It means you may have a logic code in controller <code>@posts = Post.find(:all)</code> should be changed to <code>@posts = Post.find(:all, :include => :comments)</code>
66
+
67
+ The first two lines are notifications that N+1 queries have been encountered. The remaining lines are stack traces so you can find exactly where the queries were invoked in your code, and fix them.
68
+
69
+ * Unsed eager loading:
65
70
  <pre><code>
66
- 2009-08-25 20:53:56[INFO] Unused preload associations: PATH_INFO: /posts; model: Post => associations: [comments]·
71
+ 2009-08-25 20:53:56[INFO] Unused eager loadings: PATH_INFO: /posts; model: Post => associations: [comments]·
67
72
  Remove from your finder: :include => [:comments]
68
73
  </code></pre>
69
- It represents that in request '/posts', there is a used eager loading from Post to comments. It means you may have a logic code in controller <code>@posts = Post.find(:all, :include => :comments)</code> should be changed to <code>@posts = Post.find(:all)</code>
70
74
 
71
- * To see what causes N+1 queries, check the <code>spec/bullet_association_spec.rb</code>
75
+ These two lines are notifications that unused eager loadings have been encountered.
72
76
 
73
- * Rake tasks
74
- <code>rake bullet:log:clear</code>, clear the <code>log/bullet.log</code>
77
+ ****************************************************************************
78
+
79
+ h2. Growl Support
80
+
81
+ To get Growl support up-and-running for Bullet, follow the steps below:
82
+ * Install the ruby-growl gem: <code>sudo gem install ruby-growl</code>
83
+ * Open the Growl preference pane in Systems Preferences
84
+ * Click the "Network" tab
85
+ * Make sure both "Listen for incoming notifications" and "Allow remote application registration" are checked. *Note*: If you set a password, you will need to set <code>Bullet::Association.growl_password = 'your_growl_password</code>' in the config file.
86
+ * Restart Growl ("General" tab -> Stop Growl -> Start Growl)
87
+ * Boot up your application. Bullet will automatically send a Growl notification when Growl is turned on. If you do not see it when your application loads, make sure it is enabled in your initializer and double-check the steps above.
75
88
 
76
89
  ****************************************************************************
77
90
 
78
- h3. Eager Loading, protect N+1 query and protect from unused eager loading, step by step example
79
- <a id="EagerLoadingExample"/></a>
91
+ h2. Step by step example
92
+
93
+ Bullet is designed to function as you browse through your application in development. It will alert you whenever it encounters N+1 queries or unused eager loading.
94
+
95
+ *Important*: It is strongly recommended you disable your browser's cache.
80
96
 
81
97
  1. setup test environment
82
98
 
data/Rakefile CHANGED
@@ -23,10 +23,10 @@ end
23
23
  Jeweler::Tasks.new do |gemspec|
24
24
  gemspec.name = "bullet"
25
25
  gemspec.summary = "A plugin to kill N+1 queries and unused eager loading"
26
- gemspec.description = "This plugin is aimed to give you some performance suggestion about ActiveRecord usage, what should use but not use, such as eager loading, counter cache and so on, what should not use but use, such as unused eager loading. Now it provides you the suggestion of eager loading and unused eager loading. The others are todo, next may be couter cache."
26
+ gemspec.description = "The Bullet plugin is designed to help you increase your application's performance by reducing the number of queries it makes. It will watch your queries while you develop your application and notify you when you should add eager loading (N+1 queries) or when you're using eager loading that isn't necessary."
27
27
  gemspec.email = "flyerhzm@gmail.com"
28
- gemspec.homepage = "http://www.huangzhimin.com/projects/4-bullet"
28
+ gemspec.homepage = "http://github.com/flyerhzm/bullet"
29
29
  gemspec.authors = ["Richard Huang"]
30
30
  gemspec.files.exclude '.gitignore'
31
- gemspec.files.exclude 'log/'
31
+ gemspec.files.exclude 'log/*'
32
32
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.0
1
+ 1.2.0
data/bullet.gemspec CHANGED
@@ -5,12 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{bullet}
8
- s.version = "1.1.0"
8
+ s.version = "1.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Richard Huang"]
12
- s.date = %q{2009-08-27}
13
- s.description = %q{This plugin is aimed to give you some performance suggestion about ActiveRecord usage, what should use but not use, such as eager loading, counter cache and so on, what should not use but use, such as unused eager loading. Now it provides you the suggestion of eager loading and unused eager loading. The others are todo, next may be couter cache.}
12
+ s.date = %q{2009-08-29}
13
+ s.description = %q{The Bullet plugin is designed to help you increase your application's performance by reducing the number of queries it makes. It will watch your queries while you develop your application and notify you when you should add eager loading (N+1 queries) or when you're using eager loading that isn't necessary.}
14
14
  s.email = %q{flyerhzm@gmail.com}
15
15
  s.extra_rdoc_files = [
16
16
  "README.textile"
@@ -22,24 +22,24 @@ Gem::Specification.new do |s|
22
22
  "VERSION",
23
23
  "bullet.gemspec",
24
24
  "lib/bullet.rb",
25
+ "lib/bullet/active_record.rb",
25
26
  "lib/bullet/association.rb",
26
27
  "lib/bullet/logger.rb",
27
28
  "lib/bulletware.rb",
28
- "lib/hack/active_record.rb",
29
29
  "rails/init.rb",
30
30
  "spec/bullet_association_spec.rb",
31
31
  "spec/spec.opts",
32
32
  "spec/spec_helper.rb",
33
33
  "tasks/bullet_tasks.rake"
34
34
  ]
35
- s.homepage = %q{http://www.huangzhimin.com/projects/4-bullet}
35
+ s.homepage = %q{http://github.com/flyerhzm/bullet}
36
36
  s.rdoc_options = ["--charset=UTF-8"]
37
37
  s.require_paths = ["lib"]
38
38
  s.rubygems_version = %q{1.3.5}
39
39
  s.summary = %q{A plugin to kill N+1 queries and unused eager loading}
40
40
  s.test_files = [
41
- "spec/bullet_association_spec.rb",
42
- "spec/spec_helper.rb"
41
+ "spec/spec_helper.rb",
42
+ "spec/bullet_association_spec.rb"
43
43
  ]
44
44
 
45
45
  if s.respond_to? :specification_version then
data/lib/bullet.rb CHANGED
@@ -1,14 +1,21 @@
1
1
  module Bullet
2
+ @@enable = nil
3
+
2
4
  class <<self
3
5
  def enable=(enable)
4
6
  @@enable = enable
7
+ if enable?
8
+ Bullet::ActiveRecord.enable
9
+ ActionController::Dispatcher.middleware.use Bulletware
10
+ end
5
11
  end
6
12
 
7
13
  def enable?
8
- class_variables.include?('@@enable') and @@enable == true
14
+ @@enable == true
9
15
  end
10
16
  end
11
17
 
18
+ autoload :ActiveRecord, 'bullet/active_record'
12
19
  autoload :Association, 'bullet/association'
13
20
  autoload :BulletLogger, 'bullet/logger'
14
21
  end
@@ -0,0 +1,66 @@
1
+ module Bullet
2
+ module ActiveRecord
3
+ def self.enable
4
+ ::ActiveRecord::Base.class_eval do
5
+ class << self
6
+ alias_method :bullet_find_every, :find_every
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 find_every(options)
10
+ records = bullet_find_every(options)
11
+
12
+ if records
13
+ if records.size > 1
14
+ Bullet::Association.add_possible_objects(records)
15
+ elsif records.size == 1
16
+ Bullet::Association.add_impossible_object(records.first)
17
+ end
18
+ end
19
+
20
+ records
21
+ end
22
+ end
23
+ end
24
+
25
+ ::ActiveRecord::AssociationPreload::ClassMethods.class_eval do
26
+ alias_method :bullet_preload_associations, :preload_associations
27
+ # add include for one to many associations query
28
+ def preload_associations(records, associations, preload_options={})
29
+ records = [records].flatten.compact.uniq
30
+ return if records.empty?
31
+ records.each do |record|
32
+ Bullet::Association.add_association(record, associations)
33
+ end
34
+ bullet_preload_associations(records, associations, preload_options={})
35
+ end
36
+ end
37
+
38
+ ::ActiveRecord::Associations::ClassMethods.class_eval do
39
+ # define one to many associations
40
+ alias_method :bullet_collection_reader_method, :collection_reader_method
41
+ def collection_reader_method(reflection, association_proxy_class)
42
+ Bullet::Association.define_association(self, reflection.name)
43
+ bullet_collection_reader_method(reflection, association_proxy_class)
44
+ end
45
+ end
46
+
47
+ ::ActiveRecord::Associations::AssociationCollection.class_eval do
48
+ # call one to many associations
49
+ alias_method :bullet_load_target, :load_target
50
+ def load_target
51
+ Bullet::Association.call_association(@owner, @reflection.name)
52
+ bullet_load_target
53
+ end
54
+ end
55
+
56
+ ::ActiveRecord::Associations::AssociationProxy.class_eval do
57
+ # call has_one association
58
+ alias_method :bullet_load_target, :load_target
59
+ def load_target
60
+ Bullet::Association.call_association(@owner, @reflection.name)
61
+ bullet_load_target
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,10 +1,16 @@
1
1
  module Bullet
2
+ class BulletAssociationError < StandardError
3
+ end
4
+
2
5
  class Association
3
6
  class <<self
4
- @@logger_file = File.open(Bullet::BulletLogger::LOG_FILE, 'a+')
5
- @@logger = Bullet::BulletLogger.new(@@logger_file)
6
- @@alert = true
7
-
7
+ @@alert = nil
8
+ @@bullet_logger = nil
9
+ @@console = nil
10
+ @@growl = nil
11
+ @@growl_password = nil
12
+ @@rails_logger = nil
13
+
8
14
  def start_request
9
15
  # puts "start request"
10
16
  end
@@ -24,19 +30,42 @@ module Bullet
24
30
  @@alert = alert
25
31
  end
26
32
 
27
- def logger=(logger)
28
- if logger == false
29
- @@logger = nil
33
+ def bullet_logger=(bullet_logger)
34
+ if @@bullet_logger = bullet_logger
35
+ @@logger_file = File.open(Bullet::BulletLogger::LOG_FILE, 'a+')
36
+ @@logger = Bullet::BulletLogger.new(@@logger_file)
30
37
  end
31
38
  end
32
-
39
+
40
+ def console=(console)
41
+ @@console = console
42
+ end
43
+
44
+ def growl=(growl)
45
+ if growl
46
+ begin
47
+ require 'ruby-growl'
48
+ growl = Growl.new('localhost', 'ruby-growl', ['Bullet Notification'], nil, @@growl_password)
49
+ growl.notify('Bullet Notification', 'Bullet Notification', 'Bullet Growl notifications have been turned on')
50
+ rescue MissingSourceFile
51
+ raise BulletAssociationError.new('You must install the ruby-growl gem to use Growl notifications: `sudo gem install ruby-growl`')
52
+ end
53
+ end
54
+ @@growl = growl
55
+ end
56
+
57
+ def growl_password=(growl_password)
58
+ @@growl_password = growl_password
59
+ end
60
+
61
+ def rails_logger=(rails_logger)
62
+ @@rails_logger = rails_logger
63
+ end
64
+
33
65
  def check_unused_preload_associations
34
66
  object_associations.each do |object, association|
35
- call_association = call_object_associations[object] || []
36
- association.uniq! unless association.flatten!.nil?
37
- call_association.uniq! unless call_association.flatten!.nil?
38
-
39
- add_unused_preload_associations(object.class, association - call_association) unless (association - call_association).empty?
67
+ call_object_association = call_object_associations[object] || []
68
+ add_unused_preload_associations(object.class, association - call_object_association) unless (association - call_object_association).empty?
40
69
  end
41
70
  end
42
71
 
@@ -55,60 +84,90 @@ module Bullet
55
84
 
56
85
  def bad_associations_alert
57
86
  str = ''
87
+ if @@alert || @@console || @@growl
88
+ response = []
89
+ if has_unused_preload_associations?
90
+ response.push("Unused eager loadings detected:\n")
91
+ response.push(*@@unused_preload_associations.to_a.collect{|klazz, associations| klazz_associations_str(klazz, associations)}.join('\n'))
92
+ end
93
+ if has_unpreload_associations?
94
+ response.push("#{"\n" unless response.empty?}N+1 queries detected:\n")
95
+ response.push(*@@unpreload_associations.to_a.collect{|klazz, associations| " #{klazz} => [#{associations.map(&:inspect).join(', ')}]"}.join('\n'))
96
+ end
97
+ end
58
98
  if @@alert
59
- str = "<script type='text/javascript'>"
60
- str << "alert('The request has unused preload assocations as follows:\\n"
61
- str << (has_unused_preload_associations? ? bad_associations_str(unused_preload_associations) : "None")
62
- str << "\\nThe request has N+1 queries as follows:\\n"
63
- str << (has_unpreload_associations? ? bad_associations_str(unpreload_associations) : "None")
64
- str << "')"
65
- str << "</script>\n"
99
+ str << wrap_js_association("alert(#{response.join("\n").inspect});")
100
+ end
101
+ if @@console
102
+ str << wrap_js_association("if (typeof(console) != 'undefined' && console.log) console.log(#{response.join("\n").inspect});")
103
+ end
104
+ if @@growl
105
+ begin
106
+ growl = Growl.new('localhost', 'ruby-growl', ['Bullet Notification'], nil, @@growl_password)
107
+ growl.notify('Bullet Notification', 'Bullet Notification', response.join("\n"))
108
+ rescue
109
+ end
110
+ str << '<!-- Sent Growl notification -->'
66
111
  end
67
112
  str
68
113
  end
114
+
115
+ def wrap_js_association(message)
116
+ str = ''
117
+ str << "<script type=\"text/javascript\">/*<![CDATA[*/"
118
+ str << message
119
+ str << "/*]]>*/</script>\n"
120
+ end
121
+
122
+ def log_bad_associations(path)
123
+ if (@@bullet_logger || @@rails_logger) && (!unpreload_associations.empty? || !unused_preload_associations.empty?)
124
+ Rails.logger.warn '' if @@rails_logger
125
+ unused_preload_associations.each do |klazz, associations|
126
+ log = ["Unused eager loadings: #{path}", klazz_associations_str(klazz, associations), " Remove from your finder: #{associations_str(associations)}"].join("\n")
127
+ @@logger.info(log) if @@bullet_logger
128
+ Rails.logger.warn(log) if @@rails_logger
129
+ end
130
+ unpreload_associations.each do |klazz, associations|
131
+ log = ["N+1 Query in #{path}", klazz_associations_str(klazz, associations), " Add to your finder: #{associations_str(associations)}"].join("\n")
132
+ @@logger.info(log) if @@bullet_logger
133
+ Rails.logger.warn(log) if @@rails_logger
134
+ end
135
+ callers.each do |c|
136
+ log = ["N+1 Query method call stack", c.map{|line| " #{line}"}].flatten.join("\n")
137
+ @@logger.info(log) if @@bullet_logger
138
+ Rails.logger.warn(log) if @@rails_logger
139
+ end
140
+ @@logger_file.flush if @@bullet_logger
141
+ end
142
+ end
69
143
 
70
144
  def bad_associations_str(bad_associations)
71
- puts bad_associations.inspect
145
+ # puts bad_associations.inspect
72
146
  bad_associations.to_a.collect{|klazz, associations| klazz_associations_str(klazz, associations)}.join('\\n')
73
147
  end
74
148
 
75
149
  def klazz_associations_str(klazz, associations)
76
- "model: #{klazz} => associations: [#{associations.join(', ')}]"
150
+ " #{klazz} => [#{associations.map(&:inspect).join(', ')}]"
77
151
  end
78
152
 
79
153
  def associations_str(associations)
80
154
  ":include => #{associations.map{|a| a.to_sym unless a.is_a? Hash}.inspect}"
81
155
  end
82
156
 
83
- def log_bad_associations(path)
84
- if @@logger
85
- unused_preload_associations.each do |klazz, associations|
86
- @@logger.info "Unused preload associations: PATH_INFO: #{path}; " + klazz_associations_str(klazz, associations) + "\n Remove from your finder: " + associations_str(associations)
87
- end
88
- unpreload_associations.each do |klazz, associations|
89
- @@logger.info "N+1 Query: PATH_INFO: #{path}; " + klazz_associations_str(klazz, associations) + "\n Add to your finder: " + associations_str(associations)
90
- end
91
- callers.each do |c|
92
- @@logger.info "N+1 Query: method call stack: \n" + c.join("\n")
93
- end
94
- @@logger_file.flush
95
- end
96
- end
97
-
98
157
  def has_klazz_association(klazz)
99
158
  !klazz_associations[klazz].nil? and klazz_associations.keys.include?(klazz)
100
159
  end
101
160
 
102
161
  def define_association(klazz, associations)
103
- # puts "define association, #{klazz} => #{associations}"
162
+ # puts "define association, #{klazz} => #{associations.inspect}"
104
163
  add_klazz_associations(klazz, associations)
105
164
  end
106
165
 
107
166
  def call_association(object, associations)
108
- # puts "call association, #{object} => #{associations}"
167
+ # puts "call association, #{object} => #{associations.inspect}"
168
+ add_call_object_associations(object, associations)
109
169
  if unpreload_associations?(object, associations)
110
170
  add_unpreload_associations(object.class, associations)
111
- add_call_object_associations(object, associations)
112
171
  caller_in_project
113
172
  end
114
173
  end
@@ -124,26 +183,28 @@ module Bullet
124
183
  # puts "add unpreload associations, #{klazz} => #{associations.inspect}"
125
184
  unpreload_associations[klazz] ||= []
126
185
  unpreload_associations[klazz] << associations
127
- unpreload_associations[klazz].uniq!
186
+ unique(unpreload_associations[klazz])
128
187
  end
129
188
 
130
189
  def add_unused_preload_associations(klazz, associations)
131
- # puts "add unused preload associations, #{object} => #{associations.inspect}"
190
+ # puts "add unused preload associations, #{klazz} => #{associations.inspect}"
132
191
  unused_preload_associations[klazz] ||= []
133
192
  unused_preload_associations[klazz] << associations
134
- unused_preload_associations[klazz].flatten!.uniq!
193
+ unique(unused_preload_associations[klazz])
135
194
  end
136
195
 
137
196
  def add_association(object, associations)
138
197
  # puts "add associations, #{object} => #{associations.inspect}"
139
198
  object_associations[object] ||= []
140
199
  object_associations[object] << associations
200
+ unique(object_associations[object])
141
201
  end
142
202
 
143
203
  def add_call_object_associations(object, associations)
144
204
  # puts "add call object associations, #{object} => #{associations.inspect}"
145
205
  call_object_associations[object] ||= []
146
206
  call_object_associations[object] << associations
207
+ unique(call_object_associations[object])
147
208
  end
148
209
 
149
210
  def add_possible_objects(objects)
@@ -151,7 +212,7 @@ module Bullet
151
212
  klazz= objects.first.class
152
213
  possible_objects[klazz] ||= []
153
214
  possible_objects[klazz] << objects
154
- possible_objects[klazz].flatten!.uniq!
215
+ unique(possible_objects[klazz])
155
216
  end
156
217
 
157
218
  def add_impossible_object(object)
@@ -159,12 +220,19 @@ module Bullet
159
220
  klazz = object.class
160
221
  impossible_objects[klazz] ||= []
161
222
  impossible_objects[klazz] << object
223
+ impossible_objects[klazz].uniq!
162
224
  end
163
225
 
164
226
  def add_klazz_associations(klazz, associations)
165
227
  # puts "define associations, #{klazz} => #{associations.inspect}"
166
228
  klazz_associations[klazz] ||= []
167
229
  klazz_associations[klazz] << associations
230
+ unique(klazz_associations[klazz])
231
+ end
232
+
233
+ def unique(array)
234
+ array.flatten!
235
+ array.uniq!
168
236
  end
169
237
 
170
238
  def unpreload_associations
data/lib/bulletware.rb CHANGED
@@ -12,7 +12,7 @@ class Bulletware
12
12
 
13
13
  if Bullet::Association.has_bad_assocations?
14
14
  if !headers['Content-Type'].nil? and headers['Content-Type'].include? 'text/html'
15
- response_body = response.body.insert(-17, Bullet::Association.bad_associations_alert)
15
+ response_body = response.body << Bullet::Association.bad_associations_alert
16
16
  headers['Content-Length'] = response_body.length.to_s
17
17
  end
18
18
 
data/rails/init.rb CHANGED
@@ -1,4 +1 @@
1
- # Include hook code here
2
1
  require 'bullet'
3
- require 'hack/active_record'
4
- ActionController::Dispatcher.middleware.use Bulletware
@@ -95,17 +95,34 @@ describe Bullet::Association, 'has_many' do
95
95
  Bullet::Association.should be_has_unpreload_associations
96
96
  end
97
97
 
98
- it "should detect unused preload post => comments" do
98
+ it "should detect unused preload post => comments for post" do
99
99
  Post.find(:all, :include => :comments).collect(&:name)
100
100
  Bullet::Association.check_unused_preload_associations
101
101
  Bullet::Association.should be_has_unused_preload_associations
102
102
  end
103
103
 
104
- it "should no detect unused preload post => comments" do
104
+ it "should detect no unused preload post => comments for post" do
105
105
  Post.find(:all).collect(&:name)
106
106
  Bullet::Association.check_unused_preload_associations
107
107
  Bullet::Association.should_not be_has_unused_preload_associations
108
108
  end
109
+
110
+ it "should detect no unused preload post => comments for comment" do
111
+ Post.find(:all).each do |post|
112
+ post.comments.collect(&:name)
113
+ end
114
+ Bullet::Association.check_unused_preload_associations
115
+ Bullet::Association.should_not be_has_unused_preload_associations
116
+
117
+ Bullet::Association.end_request
118
+ Bullet::Association.start_request
119
+
120
+ Post.find(:all).each do |post|
121
+ post.comments.collect(&:name)
122
+ end
123
+ Bullet::Association.check_unused_preload_associations
124
+ Bullet::Association.should_not be_has_unused_preload_associations
125
+ end
109
126
  end
110
127
 
111
128
  context "category => posts => comments" do
@@ -224,7 +241,7 @@ describe Bullet::Association, 'has_many' do
224
241
  end
225
242
  Bullet::Association.should be_has_unpreload_associations
226
243
  end
227
-
244
+
228
245
  it "should no preload comment => post" do
229
246
  Comment.first.post.name
230
247
  Bullet::Association.should_not be_has_unpreload_associations
@@ -236,7 +253,7 @@ describe Bullet::Association, 'has_many' do
236
253
  end
237
254
  Bullet::Association.should_not be_has_unpreload_associations
238
255
  end
239
-
256
+
240
257
  it "should detect no unused preload comments => post" do
241
258
  Comment.find(:all).collect(&:name)
242
259
  Bullet::Association.check_unused_preload_associations
@@ -248,6 +265,22 @@ describe Bullet::Association, 'has_many' do
248
265
  Bullet::Association.check_unused_preload_associations
249
266
  Bullet::Association.should be_has_unused_preload_associations
250
267
  end
268
+
269
+ it "should dectect no unused preload comments => post" do
270
+ Comment.find(:all).each do |comment|
271
+ comment.post.name
272
+ end
273
+ Bullet::Association.check_unused_preload_associations
274
+ Bullet::Association.should_not be_has_unused_preload_associations
275
+ end
276
+
277
+ it "should dectect no unused preload comments => post" do
278
+ Comment.find(:all, :include => :post).each do |comment|
279
+ comment.post.name
280
+ end
281
+ Bullet::Association.check_unused_preload_associations
282
+ Bullet::Association.should_not be_has_unused_preload_associations
283
+ end
251
284
  end
252
285
  end
253
286
 
data/spec/spec_helper.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  require 'rubygems'
2
2
  require 'spec/autorun'
3
3
  require 'active_record'
4
+ require 'action_controller'
4
5
 
5
- RAILS_ROOT = File.expand_path(__FILE__).split('/')[0..-3].join('/')
6
+ RAILS_ROOT = File.expand_path(__FILE__).split('/')[0..-3].join('/') unless defined? RAILS_ROOT
6
7
  require File.expand_path(File.join(File.dirname(__FILE__), '../lib/bullet/logger'))
8
+ require File.expand_path(File.join(File.dirname(__FILE__), '../lib/bullet/active_record'))
7
9
  require File.expand_path(File.join(File.dirname(__FILE__), '../lib/bullet/association'))
8
10
  require File.expand_path(File.join(File.dirname(__FILE__), '../lib/bullet'))
11
+ require File.expand_path(File.join(File.dirname(__FILE__), '../lib/bulletware'))
9
12
  Bullet.enable = true
10
- require File.expand_path(File.join(File.dirname(__FILE__), '../lib/hack/active_record'))
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flyerhzm-bullet
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Huang
@@ -9,11 +9,11 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-08-27 00:00:00 -07:00
12
+ date: 2009-08-29 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
16
- description: This plugin is aimed to give you some performance suggestion about ActiveRecord usage, what should use but not use, such as eager loading, counter cache and so on, what should not use but use, such as unused eager loading. Now it provides you the suggestion of eager loading and unused eager loading. The others are todo, next may be couter cache.
16
+ description: The Bullet plugin is designed to help you increase your application's performance by reducing the number of queries it makes. It will watch your queries while you develop your application and notify you when you should add eager loading (N+1 queries) or when you're using eager loading that isn't necessary.
17
17
  email: flyerhzm@gmail.com
18
18
  executables: []
19
19
 
@@ -28,17 +28,18 @@ files:
28
28
  - VERSION
29
29
  - bullet.gemspec
30
30
  - lib/bullet.rb
31
+ - lib/bullet/active_record.rb
31
32
  - lib/bullet/association.rb
32
33
  - lib/bullet/logger.rb
33
34
  - lib/bulletware.rb
34
- - lib/hack/active_record.rb
35
35
  - rails/init.rb
36
36
  - spec/bullet_association_spec.rb
37
37
  - spec/spec.opts
38
38
  - spec/spec_helper.rb
39
39
  - tasks/bullet_tasks.rake
40
40
  has_rdoc: false
41
- homepage: http://www.huangzhimin.com/projects/4-bullet
41
+ homepage: http://github.com/flyerhzm/bullet
42
+ licenses:
42
43
  post_install_message:
43
44
  rdoc_options:
44
45
  - --charset=UTF-8
@@ -59,10 +60,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
60
  requirements: []
60
61
 
61
62
  rubyforge_project:
62
- rubygems_version: 1.2.0
63
+ rubygems_version: 1.3.5
63
64
  signing_key:
64
65
  specification_version: 3
65
66
  summary: A plugin to kill N+1 queries and unused eager loading
66
67
  test_files:
67
- - spec/bullet_association_spec.rb
68
68
  - spec/spec_helper.rb
69
+ - spec/bullet_association_spec.rb
@@ -1,95 +0,0 @@
1
- if Bullet.enable?
2
- ActiveRecord::ActiveRecordError # An ActiveRecord bug
3
-
4
- module ActiveRecord
5
- class Base
6
- class <<self
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
- alias_method :origin_find_every, :find_every
10
-
11
- def find_every(options)
12
- records = origin_find_every(options)
13
-
14
- if records
15
- if records.size > 1
16
- Bullet::Association.add_possible_objects(records)
17
- elsif records.size == 1
18
- Bullet::Association.add_impossible_object(records.first)
19
- end
20
- end
21
-
22
- records
23
- end
24
- end
25
- end
26
-
27
- module AssociationPreload
28
- module ClassMethods
29
- # add include for one to many associations query
30
- alias_method :origin_preload_associations, :preload_associations
31
-
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::Association.add_association(record, associations)
37
- end
38
- origin_preload_associations(records, associations, preload_options={})
39
- end
40
- end
41
- end
42
-
43
- module Associations
44
- module ClassMethods
45
- # define one to many associations
46
- alias_method :origin_collection_reader_method, :collection_reader_method
47
-
48
- def collection_reader_method(reflection, association_proxy_class)
49
- Bullet::Association.define_association(self, reflection.name)
50
- origin_collection_reader_method(reflection, association_proxy_class)
51
- end
52
- end
53
-
54
- class AssociationCollection
55
- # call one to many associations
56
- alias_method :origin_load_target, :load_target
57
-
58
- def load_target
59
- Bullet::Association.call_association(@owner, @reflection.name)
60
- origin_load_target
61
- end
62
- end
63
-
64
- class HasOneAssociation
65
- # call has_one association
66
- alias_method :origin_find_target, :find_target
67
-
68
- def find_target
69
- Bullet::Association.call_association(@owner, @reflection.name)
70
- origin_find_target
71
- end
72
- end
73
-
74
- class BelongsToAssociation
75
- # call belongs_to association
76
- alias_method :origin_find_target, :find_target
77
-
78
- def find_target
79
- Bullet::Association.call_association(@owner, @reflection.name)
80
- origin_find_target
81
- end
82
- end
83
-
84
- class BelongsToPolymorphicAssociation
85
- # call belongs_to association
86
- alias_method :origin_find_target, :find_target
87
-
88
- def find_target
89
- Bullet::Association.call_association(@owner, @reflection.name)
90
- origin_find_target
91
- end
92
- end
93
- end
94
- end
95
- end