bullet 2.0.1 → 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.
@@ -0,0 +1,45 @@
1
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
2
+ require "bundler"
3
+ Bundler.setup
4
+
5
+ require "rake"
6
+ require "rdoc/task"
7
+ require "rspec"
8
+ require "rspec/core/rake_task"
9
+
10
+ require "bullet/version"
11
+
12
+ task :build do
13
+ system "gem build bullet.gemspec"
14
+ end
15
+
16
+ task :install => :build do
17
+ system "sudo gem install bullet-#{Bullet::VERSION}.gem"
18
+ end
19
+
20
+ task :release => :build do
21
+ puts "Tagging #{Bullet::VERSION}..."
22
+ system "git tag -a #{Bullet::VERSION} -m 'Tagging #{Bullet::VERSION}'"
23
+ puts "Pushing to Github..."
24
+ system "git push --tags"
25
+ puts "Pushing to rubygems.org..."
26
+ system "gem push bullet-#{Bullet::VERSION}.gem"
27
+ end
28
+
29
+ RSpec::Core::RakeTask.new(:spec) do |spec|
30
+ spec.pattern = "spec/**/*_spec.rb"
31
+ end
32
+
33
+ RSpec::Core::RakeTask.new('spec:progress') do |spec|
34
+ spec.rspec_opts = %w(--format progress)
35
+ spec.pattern = "spec/**/*_spec.rb"
36
+ end
37
+
38
+ Rake::RDocTask.new do |rdoc|
39
+ rdoc.rdoc_dir = "rdoc"
40
+ rdoc.title = "bullet #{Bullet::VERSION}"
41
+ rdoc.rdoc_files.include("README*")
42
+ rdoc.rdoc_files.include("lib/**/*.rb")
43
+ end
44
+
45
+ task :default => :spec
@@ -0,0 +1,24 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require "bullet/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "bullet"
8
+ s.version = Bullet::VERSION
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = ["Richard Huang"]
11
+ s.email = ["flyerhzm@gmail.com"]
12
+ s.homepage = "http://github.com/flyerhzm/bullet"
13
+ s.summary = "A rails plugin to kill N+1 queries and unused eager loading."
14
+ s.description = "A rails plugin to kill N+1 queries and unused eager loading."
15
+
16
+ s.required_rubygems_version = ">= 1.3.6"
17
+
18
+ s.add_dependency "uniform_notifier", "~> 1.0.0"
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.require_paths = ["lib"]
23
+ end
24
+
@@ -2,10 +2,10 @@ require 'set'
2
2
  require 'uniform_notifier'
3
3
 
4
4
  module Bullet
5
- class NotificationError < StandardError; end
6
-
7
- if Rails.version =~ /^3.0/
5
+ if Rails.version =~ /^3\.0/
8
6
  autoload :ActiveRecord, 'bullet/active_record3'
7
+ elsif Rails.version =~ /^3\.1/
8
+ autoload :ActiveRecord, 'bullet/active_record31'
9
9
  else
10
10
  autoload :ActiveRecord, 'bullet/active_record2'
11
11
  autoload :ActionController, 'bullet/action_controller2'
@@ -18,7 +18,6 @@ module Bullet
18
18
  autoload :NotificationCollector, 'bullet/notification_collector'
19
19
 
20
20
  if defined? Rails::Railtie
21
- # compatible with rails 3.0.0.beta4
22
21
  class BulletRailtie < Rails::Railtie
23
22
  initializer "bullet.configure_rails_initialization" do |app|
24
23
  app.middleware.use Bullet::Rack
@@ -88,8 +87,9 @@ module Bullet
88
87
  responses.join( "\n" )
89
88
  end
90
89
 
91
- def perform_out_of_channel_notifications
90
+ def perform_out_of_channel_notifications(env = {})
92
91
  for_each_active_notifier_with_notification do |notification|
92
+ notification.url = [env['HTTP_HOST'], env['REQUEST_URI']].compact.join
93
93
  notification.notify_out_of_channel
94
94
  end
95
95
  end
@@ -3,7 +3,7 @@ module Bullet
3
3
  def self.enable
4
4
  require 'action_controller'
5
5
  case Rails.version
6
- when /^2.3/
6
+ when /^2.3/
7
7
  ::ActionController::Dispatcher.middleware.use Bullet::Rack
8
8
  ::ActionController::Dispatcher.class_eval do
9
9
  class <<self
@@ -22,20 +22,20 @@ module Bullet
22
22
  Bullet.clear
23
23
  end
24
24
  end
25
-
25
+
26
26
  ::ActionController::Base.class_eval do
27
27
  alias_method :origin_process, :process
28
28
  def process(request, response, method = :perform_action, *arguments)
29
29
  Bullet.start_request
30
30
  response = origin_process(request, response, method = :perform_action, *arguments)
31
-
31
+
32
32
  if Bullet.notification?
33
33
  if response.headers["type"] and response.headers["type"].include? 'text/html' and response.body =~ %r{<html.*</html>}m
34
34
  response.body <<= Bullet.gather_inline_notifications
35
35
  response.headers["Content-Length"] = response.body.length.to_s
36
36
  end
37
-
38
- Bullet.perform_bullet_out_of_channel_notifications
37
+
38
+ Bullet.perform_out_of_channel_notifications
39
39
  end
40
40
  Bullet.end_request
41
41
  response
@@ -1,4 +1,5 @@
1
1
  module Bullet
2
+ LOAD_TARGET_RGX = /load_target/
2
3
  module ActiveRecord
3
4
  def self.enable
4
5
  require 'active_record'
@@ -81,7 +82,7 @@ module Bullet
81
82
  def load_target
82
83
  # avoid stack level too deep
83
84
  result = origin_load_target
84
- Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
85
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless caller.find {|c| c =~ LOAD_TARGET_RGX }
85
86
  Bullet::Detector::Association.add_possible_objects(result)
86
87
  result
87
88
  end
@@ -1,4 +1,5 @@
1
1
  module Bullet
2
+ LOAD_TARGET_RGX = /load_target/
2
3
  module ActiveRecord
3
4
  def self.enable
4
5
  require 'active_record'
@@ -75,7 +76,7 @@ module Bullet
75
76
  def load_target
76
77
  # avoid stack level too deep
77
78
  result = origin_load_target
78
- Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
79
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless caller.find {|c| c =~ LOAD_TARGET_RGX }
79
80
  Bullet::Detector::Association.add_possible_objects(result)
80
81
  result
81
82
  end
@@ -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
@@ -39,25 +39,35 @@ module Bullet
39
39
  def add_eager_loadings(objects, associations)
40
40
  objects = Array(objects)
41
41
 
42
+ to_add = nil
43
+ to_merge, to_delete = [], []
42
44
  eager_loadings.each do |k, v|
43
45
  key_objects_overlap = k & objects
44
46
 
45
47
  next if key_objects_overlap.empty?
46
48
 
47
49
  if key_objects_overlap == k
48
- eager_loadings.add k, associations
50
+ to_add = [k, associations]
49
51
  break
50
52
 
51
53
  else
52
- eager_loadings.merge key_objects_overlap, ( eager_loadings[k].dup << associations )
54
+ to_merge << [key_objects_overlap, ( eager_loadings[k].dup << associations )]
53
55
 
54
56
  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)
57
+ to_merge << [keys_without_objects, eager_loadings[k]]
58
+ to_delete << k
58
59
  objects = objects - k
59
60
  end
60
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
61
71
 
62
72
  eager_loadings.add objects, associations unless objects.empty?
63
73
  end
@@ -73,15 +83,14 @@ module Bullet
73
83
 
74
84
  # check if object => associations already exists in object_associations.
75
85
  def association?(object, associations)
76
- object_associations.each do |key, value|
77
- next unless key == object
78
-
86
+ value = object_associations[object]
87
+ if value
79
88
  value.each do |v|
80
89
  result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
81
90
  return true if result
82
91
  end
83
-
84
92
  end
93
+
85
94
  return false
86
95
  end
87
96
 
@@ -7,13 +7,6 @@ module Bullet
7
7
  def self.end_request
8
8
  clear
9
9
  end
10
-
11
- protected
12
- def self.unique( array )
13
- array.flatten!
14
- array.uniq!
15
- end
16
-
17
10
  end
18
11
  end
19
12
  end
@@ -23,11 +23,13 @@ module Bullet
23
23
  end
24
24
 
25
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
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
31
33
  end
32
34
 
33
35
  def self.diff_object_association( object, association )
@@ -1,7 +1,7 @@
1
1
  module Bullet
2
2
  module Notification
3
3
  class Base
4
- attr_accessor :notifier
4
+ attr_accessor :notifier, :url
5
5
  attr_reader :base_class, :associations, :path
6
6
 
7
7
  def initialize( base_class, associations, path = nil )
@@ -15,6 +15,10 @@ module Bullet
15
15
 
16
16
  def body
17
17
  end
18
+
19
+ def whoami
20
+ "user: " << `whoami`.chomp
21
+ end
18
22
 
19
23
  def body_with_caller
20
24
  body
@@ -25,7 +29,7 @@ module Bullet
25
29
  end
26
30
 
27
31
  def full_notice
28
- @full_notice ||= title + "\n" + body_with_caller
32
+ [whoami, url, title, body_with_caller].compact.join("\n")
29
33
  end
30
34
 
31
35
  def notify_inline
@@ -16,7 +16,7 @@ module Bullet
16
16
  response_body = response.body << Bullet.gather_inline_notifications
17
17
  headers['Content-Length'] = response_body.length.to_s
18
18
  end
19
- Bullet.perform_out_of_channel_notifications
19
+ Bullet.perform_out_of_channel_notifications(env)
20
20
  end
21
21
  response_body ||= response.body
22
22
  Bullet.end_request
@@ -3,13 +3,12 @@ module Bullet
3
3
  class Association < Base
4
4
  def merge( base, associations )
5
5
  @registry.merge!( { base => associations } )
6
- unique( @registry[base] )
7
6
  end
8
7
 
9
8
  def similarly_associated( base, associations )
10
9
  @registry.select do |key, value|
11
10
  key.include?( base ) and value == associations
12
- end.collect( &:first ).flatten!
11
+ end.collect( &:first ).flatten
13
12
  end
14
13
  end
15
14
  end
@@ -24,16 +24,14 @@ module Bullet
24
24
  end
25
25
 
26
26
  def add( key, value )
27
- @registry[key] ||= []
28
- @registry[key] << value
29
- unique( @registry[key] )
27
+ @registry[key] ||= Set.new
28
+ if value.is_a? Array
29
+ @registry[key] += value
30
+ else
31
+ @registry[key] << value
32
+ end
30
33
  end
31
34
 
32
- private
33
- def unique( array )
34
- array.flatten!
35
- array.uniq!
36
- end
37
35
  end
38
36
  end
39
37
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
  module Bullet
3
- VERSION = "2.0.1"
3
+ VERSION = "2.1.0"
4
4
  end
5
5
 
@@ -0,0 +1,106 @@
1
+ $: << 'lib'
2
+ require 'benchmark'
3
+ require 'rails'
4
+ require 'active_record'
5
+ require 'activerecord-import'
6
+ require 'bullet'
7
+
8
+ begin
9
+ require 'perftools'
10
+ rescue LoadError
11
+ puts "Could not load perftools.rb, profiling won't be possible"
12
+ end
13
+
14
+ class Post < ActiveRecord::Base
15
+ belongs_to :user
16
+ has_many :comments
17
+ end
18
+
19
+ class Comment < ActiveRecord::Base
20
+ belongs_to :user
21
+ belongs_to :post
22
+ end
23
+
24
+ class User < ActiveRecord::Base
25
+ has_many :posts
26
+ has_many :comments
27
+ end
28
+
29
+ # create database bullet_benchmark;
30
+ ActiveRecord::Base.establish_connection(:adapter => 'mysql', :database => 'bullet_benchmark', :server => '/tmp/mysql.socket', :username => 'root')
31
+
32
+ ActiveRecord::Base.connection.tables.each do |table|
33
+ ActiveRecord::Base.connection.drop_table(table)
34
+ end
35
+
36
+ ActiveRecord::Schema.define(:version => 1) do
37
+ create_table :posts do |t|
38
+ t.column :title, :string
39
+ t.column :body, :string
40
+ t.column :user_id, :integer
41
+ end
42
+
43
+ create_table :comments do |t|
44
+ t.column :body, :string
45
+ t.column :post_id, :integer
46
+ t.column :user_id, :integer
47
+ end
48
+
49
+ create_table :users do |t|
50
+ t.column :name, :string
51
+ end
52
+ end
53
+
54
+ users_size = 100
55
+ posts_size = 1000
56
+ comments_size = 10000
57
+ users = []
58
+ users_size.times do |i|
59
+ users << User.new(:name => "user#{i}")
60
+ end
61
+ User.import users
62
+ users = User.all
63
+
64
+ posts = []
65
+ posts_size.times do |i|
66
+ posts << Post.new(:title => "Title #{i}", :body => "Body #{i}", :user => users[i%100])
67
+ end
68
+ Post.import posts
69
+ posts = Post.all
70
+
71
+ comments = []
72
+ comments_size.times do |i|
73
+ comments << Comment.new(:body => "Comment #{i}", :post => posts[i%1000], :user => users[i%100])
74
+ end
75
+ Comment.import comments
76
+
77
+ puts "Start benchmarking..."
78
+
79
+
80
+ Bullet.enable = true
81
+
82
+ PerfTools::CpuProfiler.start(ARGV[0]|| "benchmark_profile") if defined? PerfTools
83
+
84
+ Benchmark.bm(70) do |bm|
85
+ bm.report("Querying & Iterating #{posts_size} Posts with #{comments_size} Comments and #{users_size} Users") do
86
+ Bullet.start_request
87
+ Post.select("SQL_NO_CACHE *").includes(:user, :comments => :user).each do |p|
88
+ p.title
89
+ p.user.name
90
+ p.comments.each do |c|
91
+ c.body
92
+ c.user.name
93
+ end
94
+ end
95
+ Bullet.end_request
96
+ end
97
+ end
98
+
99
+ PerfTools::CpuProfiler.stop if defined? PerfTools
100
+ puts "End benchmarking..."
101
+
102
+
103
+ # 2.0.1
104
+ # user system total real
105
+ # Querying & Iterating 100 Posts with 10000 Comments and 100 Users 2.290000 0.050000 2.340000 ( 2.366174)
106
+