bullet 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+