bullet_instructure 4.0.2
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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/CHANGELOG.md +75 -0
- data/Gemfile +19 -0
- data/Gemfile.mongoid +14 -0
- data/Gemfile.mongoid-2.4 +19 -0
- data/Gemfile.mongoid-2.5 +19 -0
- data/Gemfile.mongoid-2.6 +19 -0
- data/Gemfile.mongoid-2.7 +19 -0
- data/Gemfile.mongoid-2.8 +19 -0
- data/Gemfile.mongoid-3.0 +19 -0
- data/Gemfile.mongoid-3.1 +19 -0
- data/Gemfile.mongoid-4.0 +19 -0
- data/Gemfile.rails-3.0 +19 -0
- data/Gemfile.rails-3.1 +19 -0
- data/Gemfile.rails-3.2 +19 -0
- data/Gemfile.rails-4.0 +19 -0
- data/Gemfile.rails-4.1 +19 -0
- data/Guardfile +8 -0
- data/Hacking.md +74 -0
- data/MIT-LICENSE +20 -0
- data/README.md +428 -0
- data/Rakefile +52 -0
- data/bullet_instructure.gemspec +27 -0
- data/lib/bullet.rb +196 -0
- data/lib/bullet/active_record3.rb +148 -0
- data/lib/bullet/active_record3x.rb +128 -0
- data/lib/bullet/active_record4.rb +128 -0
- data/lib/bullet/active_record41.rb +121 -0
- data/lib/bullet/dependency.rb +81 -0
- data/lib/bullet/detector.rb +9 -0
- data/lib/bullet/detector/association.rb +67 -0
- data/lib/bullet/detector/base.rb +6 -0
- data/lib/bullet/detector/counter_cache.rb +59 -0
- data/lib/bullet/detector/n_plus_one_query.rb +89 -0
- data/lib/bullet/detector/unused_eager_loading.rb +84 -0
- data/lib/bullet/ext/object.rb +9 -0
- data/lib/bullet/ext/string.rb +5 -0
- data/lib/bullet/mongoid2x.rb +56 -0
- data/lib/bullet/mongoid3x.rb +56 -0
- data/lib/bullet/mongoid4x.rb +56 -0
- data/lib/bullet/notification.rb +10 -0
- data/lib/bullet/notification/base.rb +97 -0
- data/lib/bullet/notification/counter_cache.rb +13 -0
- data/lib/bullet/notification/n_plus_one_query.rb +28 -0
- data/lib/bullet/notification/unused_eager_loading.rb +13 -0
- data/lib/bullet/notification_collector.rb +24 -0
- data/lib/bullet/rack.rb +81 -0
- data/lib/bullet/registry.rb +7 -0
- data/lib/bullet/registry/association.rb +13 -0
- data/lib/bullet/registry/base.rb +40 -0
- data/lib/bullet/registry/object.rb +13 -0
- data/lib/bullet/version.rb +4 -0
- data/perf/benchmark.rb +121 -0
- data/rails/init.rb +1 -0
- data/spec/bullet/detector/association_spec.rb +26 -0
- data/spec/bullet/detector/base_spec.rb +8 -0
- data/spec/bullet/detector/counter_cache_spec.rb +56 -0
- data/spec/bullet/detector/n_plus_one_query_spec.rb +138 -0
- data/spec/bullet/detector/unused_eager_loading_spec.rb +88 -0
- data/spec/bullet/ext/object_spec.rb +17 -0
- data/spec/bullet/ext/string_spec.rb +13 -0
- data/spec/bullet/notification/base_spec.rb +83 -0
- data/spec/bullet/notification/counter_cache_spec.rb +12 -0
- data/spec/bullet/notification/n_plus_one_query_spec.rb +14 -0
- data/spec/bullet/notification/unused_eager_loading_spec.rb +12 -0
- data/spec/bullet/notification_collector_spec.rb +32 -0
- data/spec/bullet/rack_spec.rb +97 -0
- data/spec/bullet/registry/association_spec.rb +26 -0
- data/spec/bullet/registry/base_spec.rb +44 -0
- data/spec/bullet/registry/object_spec.rb +24 -0
- data/spec/bullet_spec.rb +41 -0
- data/spec/integration/active_record3/association_spec.rb +651 -0
- data/spec/integration/active_record4/association_spec.rb +649 -0
- data/spec/integration/counter_cache_spec.rb +63 -0
- data/spec/integration/mongoid/association_spec.rb +258 -0
- data/spec/models/address.rb +3 -0
- data/spec/models/author.rb +3 -0
- data/spec/models/base_user.rb +5 -0
- data/spec/models/category.rb +7 -0
- data/spec/models/city.rb +3 -0
- data/spec/models/client.rb +4 -0
- data/spec/models/comment.rb +4 -0
- data/spec/models/company.rb +3 -0
- data/spec/models/country.rb +3 -0
- data/spec/models/document.rb +5 -0
- data/spec/models/entry.rb +3 -0
- data/spec/models/firm.rb +4 -0
- data/spec/models/folder.rb +2 -0
- data/spec/models/mongoid/address.rb +7 -0
- data/spec/models/mongoid/category.rb +8 -0
- data/spec/models/mongoid/comment.rb +7 -0
- data/spec/models/mongoid/company.rb +7 -0
- data/spec/models/mongoid/entry.rb +7 -0
- data/spec/models/mongoid/post.rb +12 -0
- data/spec/models/mongoid/user.rb +5 -0
- data/spec/models/newspaper.rb +3 -0
- data/spec/models/page.rb +2 -0
- data/spec/models/person.rb +3 -0
- data/spec/models/pet.rb +3 -0
- data/spec/models/post.rb +10 -0
- data/spec/models/relationship.rb +4 -0
- data/spec/models/student.rb +3 -0
- data/spec/models/submission.rb +4 -0
- data/spec/models/teacher.rb +3 -0
- data/spec/models/user.rb +4 -0
- data/spec/models/writer.rb +2 -0
- data/spec/spec_helper.rb +103 -0
- data/spec/support/bullet_ext.rb +55 -0
- data/spec/support/mongo_seed.rb +65 -0
- data/spec/support/rack_double.rb +55 -0
- data/spec/support/sqlite_seed.rb +229 -0
- data/tasks/bullet_tasks.rake +9 -0
- data/test.sh +15 -0
- metadata +246 -0
@@ -0,0 +1,28 @@
|
|
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
|
+
markdown("#{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
|
+
markdown(("##")+"N+1 Query #{@path ? "in #{@path}" : 'detected'}")
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
def call_stack_messages
|
24
|
+
markdown((['N+1 Query method call stack'] + @callers).join( "\n " ))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Bullet
|
2
|
+
module Notification
|
3
|
+
class UnusedEagerLoading < Base
|
4
|
+
def body
|
5
|
+
markdown("#{klazz_associations_str}\n Remove from your finder: #{associations_str}\n")
|
6
|
+
end
|
7
|
+
|
8
|
+
def title
|
9
|
+
markdown(("##")+("Unused Eager Loading #{@path ? "in #{@path}" : 'detected'}"))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,24 @@
|
|
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
|
+
end
|
23
|
+
end
|
24
|
+
|
data/lib/bullet/rack.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module Bullet
|
2
|
+
class Rack
|
3
|
+
include Dependency
|
4
|
+
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
return @app.call(env) unless Bullet.enable?
|
11
|
+
Bullet.start_request
|
12
|
+
status, headers, response = @app.call(env)
|
13
|
+
return [status, headers, response] if file?(headers) || sse?(response) || empty?(response)
|
14
|
+
|
15
|
+
response_body = nil
|
16
|
+
if Bullet.notification?
|
17
|
+
if status == 200 && !response_body(response).frozen? && html_request?(headers, response)
|
18
|
+
response_body = response_body(response) << Bullet.gather_inline_notifications
|
19
|
+
add_footer_note(response_body) if Bullet.add_footer
|
20
|
+
headers['Content-Length'] = response_body.bytesize.to_s
|
21
|
+
end
|
22
|
+
end
|
23
|
+
[status, headers, response_body ? [response_body] : response]
|
24
|
+
ensure
|
25
|
+
if Bullet.enable? && Bullet.notification?
|
26
|
+
Bullet.perform_out_of_channel_notifications(env)
|
27
|
+
end
|
28
|
+
Bullet.end_request
|
29
|
+
end
|
30
|
+
|
31
|
+
# fix issue if response's body is a Proc
|
32
|
+
def empty?(response)
|
33
|
+
# response may be ["Not Found"], ["Move Permanently"], etc.
|
34
|
+
if rails?
|
35
|
+
(response.is_a?(Array) && response.size <= 1) ||
|
36
|
+
!response.respond_to?(:body) ||
|
37
|
+
!response_body(response).respond_to?(:empty?) ||
|
38
|
+
response_body(response).empty?
|
39
|
+
else
|
40
|
+
body = response_body(response)
|
41
|
+
body.nil? || body.empty?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_footer_note(response_body)
|
46
|
+
response_body << "<div #{footer_div_style}>" + Bullet.footer_info.uniq.join("<br>") + "</div>"
|
47
|
+
end
|
48
|
+
|
49
|
+
def file?(headers)
|
50
|
+
headers["Content-Transfer-Encoding"] == "binary"
|
51
|
+
end
|
52
|
+
|
53
|
+
def sse?(response)
|
54
|
+
response.respond_to?(:stream) && response.stream.is_a?(ActionController::Live::Buffer)
|
55
|
+
end
|
56
|
+
|
57
|
+
def html_request?(headers, response)
|
58
|
+
headers['Content-Type'] && headers['Content-Type'].include?('text/html') && response_body(response).include?("<html")
|
59
|
+
end
|
60
|
+
|
61
|
+
def response_body(response)
|
62
|
+
if rails?
|
63
|
+
Array === response.body ? response.body.first : response.body
|
64
|
+
else
|
65
|
+
response.first
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def footer_div_style
|
71
|
+
<<EOF
|
72
|
+
style="position: fixed; bottom: 0pt; left: 0pt; cursor: pointer; border-style: solid; border-color: rgb(153, 153, 153);
|
73
|
+
-moz-border-top-colors: none; -moz-border-right-colors: none; -moz-border-bottom-colors: none;
|
74
|
+
-moz-border-left-colors: none; -moz-border-image: none; border-width: 2pt 2pt 0px 0px;
|
75
|
+
padding: 5px; border-radius: 0pt 10pt 0pt 0px; background: none repeat scroll 0% 0% rgba(200, 200, 200, 0.8);
|
76
|
+
color: rgb(119, 119, 119); font-size: 18px; font-family: 'Arial', sans-serif; z-index:9999;"
|
77
|
+
EOF
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Bullet
|
2
|
+
module Registry
|
3
|
+
class Association < Base
|
4
|
+
def merge(base, associations)
|
5
|
+
@registry.merge!(base => associations)
|
6
|
+
end
|
7
|
+
|
8
|
+
def similarly_associated(base, associations)
|
9
|
+
@registry.select { |key, value| key.include?(base) && value == associations }.collect(&:first).flatten
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Bullet
|
2
|
+
module Registry
|
3
|
+
class Base
|
4
|
+
attr_reader :registry
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@registry = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](key)
|
11
|
+
@registry[key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(&block)
|
15
|
+
@registry.each(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete(base)
|
19
|
+
@registry.delete(base)
|
20
|
+
end
|
21
|
+
|
22
|
+
def select(*args, &block)
|
23
|
+
@registry.select(*args, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add(key, value)
|
27
|
+
@registry[key] ||= Set.new
|
28
|
+
if value.is_a? Array
|
29
|
+
@registry[key] += value
|
30
|
+
else
|
31
|
+
@registry[key] << value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def include?(key, value)
|
36
|
+
!@registry[key].nil? && @registry[key].include?(value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/perf/benchmark.rb
ADDED
@@ -0,0 +1,121 @@
|
|
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 => 'mysql2', :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
|
+
Benchmark.bm(70) do |bm|
|
83
|
+
bm.report("Querying & Iterating #{posts_size} Posts with #{comments_size} Comments and #{users_size} Users") do
|
84
|
+
10.times do
|
85
|
+
Bullet.start_request
|
86
|
+
Post.select("SQL_NO_CACHE *").includes(:user, :comments => :user).each do |p|
|
87
|
+
p.title
|
88
|
+
p.user.name
|
89
|
+
p.comments.each do |c|
|
90
|
+
c.body
|
91
|
+
c.user.name
|
92
|
+
end
|
93
|
+
end
|
94
|
+
Bullet.end_request
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
puts "End benchmarking..."
|
100
|
+
|
101
|
+
|
102
|
+
# Run benchmark with bundler
|
103
|
+
#
|
104
|
+
# bundle exec ruby perf/benchmark.rb
|
105
|
+
#
|
106
|
+
# bullet 2.3.0 with rails 3.2.2
|
107
|
+
# user system total real
|
108
|
+
# Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 16.460000 0.190000 16.650000 ( 16.968246)
|
109
|
+
#
|
110
|
+
# bullet 2.3.0 with rails 3.1.4
|
111
|
+
# user system total real
|
112
|
+
# Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 14.600000 0.130000 14.730000 ( 14.937590)
|
113
|
+
#
|
114
|
+
# bullet 2.3.0 with rails 3.0.12
|
115
|
+
# user system total real
|
116
|
+
# Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 26.120000 0.430000 26.550000 ( 27.179304)
|
117
|
+
#
|
118
|
+
#
|
119
|
+
# bullet 2.2.1 with rails 3.0.12
|
120
|
+
# user system total real
|
121
|
+
# Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 29.970000 0.270000 30.240000 ( 30.452083)
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bullet'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Bullet
|
4
|
+
module Detector
|
5
|
+
describe Association do
|
6
|
+
before :all do
|
7
|
+
@post1 = Post.first
|
8
|
+
@post2 = Post.last
|
9
|
+
end
|
10
|
+
|
11
|
+
context ".add_object_association" do
|
12
|
+
it "should add object, associations pair" do
|
13
|
+
Association.add_object_associations(@post1, :associations)
|
14
|
+
expect(Association.send(:object_associations)).to be_include(@post1.bullet_key, :associations)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context ".add_call_object_associations" do
|
19
|
+
it "should add call object, associations pair" do
|
20
|
+
Association.add_call_object_associations(@post1, :associations)
|
21
|
+
expect(Association.send(:call_object_associations)).to be_include(@post1.bullet_key, :associations)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Bullet
|
4
|
+
module Detector
|
5
|
+
describe CounterCache do
|
6
|
+
before :all do
|
7
|
+
@post1 = Post.first
|
8
|
+
@post2 = Post.last
|
9
|
+
end
|
10
|
+
|
11
|
+
context ".add_counter_cache" do
|
12
|
+
it "should create notification if conditions met" do
|
13
|
+
expect(CounterCache).to receive(:conditions_met?).with(@post1.bullet_key, [:comments]).and_return(true)
|
14
|
+
expect(CounterCache).to receive(:create_notification).with("Post", [:comments])
|
15
|
+
CounterCache.add_counter_cache(@post1, [:comments])
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should not create notification if conditions not met" do
|
19
|
+
expect(CounterCache).to receive(:conditions_met?).with(@post1.bullet_key, [:comments]).and_return(false)
|
20
|
+
expect(CounterCache).to receive(:create_notification).never
|
21
|
+
CounterCache.add_counter_cache(@post1, [:comments])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context ".add_possible_objects" do
|
26
|
+
it "should add possible objects" do
|
27
|
+
CounterCache.add_possible_objects([@post1, @post2])
|
28
|
+
expect(CounterCache.send(:possible_objects)).to be_include(@post1.bullet_key)
|
29
|
+
expect(CounterCache.send(:possible_objects)).to be_include(@post2.bullet_key)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should add impossible object" do
|
33
|
+
CounterCache.add_impossible_object(@post1)
|
34
|
+
expect(CounterCache.send(:impossible_objects)).to be_include(@post1.bullet_key)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context ".conditions_met?" do
|
39
|
+
it "should be true when object is possible, not impossible" do
|
40
|
+
CounterCache.add_possible_objects(@post1)
|
41
|
+
expect(CounterCache.send(:conditions_met?, @post1.bullet_key, :associations)).to eq true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should be false when object is not possible" do
|
45
|
+
expect(CounterCache.send(:conditions_met?, @post1.bullet_key, :associations)).to eq false
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should be true when object is possible, and impossible" do
|
49
|
+
CounterCache.add_possible_objects(@post1)
|
50
|
+
CounterCache.add_impossible_object(@post1)
|
51
|
+
expect(CounterCache.send(:conditions_met?, @post1.bullet_key, :associations)).to eq false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|