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.
Files changed (118) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +20 -0
  6. data/CHANGELOG.md +75 -0
  7. data/Gemfile +19 -0
  8. data/Gemfile.mongoid +14 -0
  9. data/Gemfile.mongoid-2.4 +19 -0
  10. data/Gemfile.mongoid-2.5 +19 -0
  11. data/Gemfile.mongoid-2.6 +19 -0
  12. data/Gemfile.mongoid-2.7 +19 -0
  13. data/Gemfile.mongoid-2.8 +19 -0
  14. data/Gemfile.mongoid-3.0 +19 -0
  15. data/Gemfile.mongoid-3.1 +19 -0
  16. data/Gemfile.mongoid-4.0 +19 -0
  17. data/Gemfile.rails-3.0 +19 -0
  18. data/Gemfile.rails-3.1 +19 -0
  19. data/Gemfile.rails-3.2 +19 -0
  20. data/Gemfile.rails-4.0 +19 -0
  21. data/Gemfile.rails-4.1 +19 -0
  22. data/Guardfile +8 -0
  23. data/Hacking.md +74 -0
  24. data/MIT-LICENSE +20 -0
  25. data/README.md +428 -0
  26. data/Rakefile +52 -0
  27. data/bullet_instructure.gemspec +27 -0
  28. data/lib/bullet.rb +196 -0
  29. data/lib/bullet/active_record3.rb +148 -0
  30. data/lib/bullet/active_record3x.rb +128 -0
  31. data/lib/bullet/active_record4.rb +128 -0
  32. data/lib/bullet/active_record41.rb +121 -0
  33. data/lib/bullet/dependency.rb +81 -0
  34. data/lib/bullet/detector.rb +9 -0
  35. data/lib/bullet/detector/association.rb +67 -0
  36. data/lib/bullet/detector/base.rb +6 -0
  37. data/lib/bullet/detector/counter_cache.rb +59 -0
  38. data/lib/bullet/detector/n_plus_one_query.rb +89 -0
  39. data/lib/bullet/detector/unused_eager_loading.rb +84 -0
  40. data/lib/bullet/ext/object.rb +9 -0
  41. data/lib/bullet/ext/string.rb +5 -0
  42. data/lib/bullet/mongoid2x.rb +56 -0
  43. data/lib/bullet/mongoid3x.rb +56 -0
  44. data/lib/bullet/mongoid4x.rb +56 -0
  45. data/lib/bullet/notification.rb +10 -0
  46. data/lib/bullet/notification/base.rb +97 -0
  47. data/lib/bullet/notification/counter_cache.rb +13 -0
  48. data/lib/bullet/notification/n_plus_one_query.rb +28 -0
  49. data/lib/bullet/notification/unused_eager_loading.rb +13 -0
  50. data/lib/bullet/notification_collector.rb +24 -0
  51. data/lib/bullet/rack.rb +81 -0
  52. data/lib/bullet/registry.rb +7 -0
  53. data/lib/bullet/registry/association.rb +13 -0
  54. data/lib/bullet/registry/base.rb +40 -0
  55. data/lib/bullet/registry/object.rb +13 -0
  56. data/lib/bullet/version.rb +4 -0
  57. data/perf/benchmark.rb +121 -0
  58. data/rails/init.rb +1 -0
  59. data/spec/bullet/detector/association_spec.rb +26 -0
  60. data/spec/bullet/detector/base_spec.rb +8 -0
  61. data/spec/bullet/detector/counter_cache_spec.rb +56 -0
  62. data/spec/bullet/detector/n_plus_one_query_spec.rb +138 -0
  63. data/spec/bullet/detector/unused_eager_loading_spec.rb +88 -0
  64. data/spec/bullet/ext/object_spec.rb +17 -0
  65. data/spec/bullet/ext/string_spec.rb +13 -0
  66. data/spec/bullet/notification/base_spec.rb +83 -0
  67. data/spec/bullet/notification/counter_cache_spec.rb +12 -0
  68. data/spec/bullet/notification/n_plus_one_query_spec.rb +14 -0
  69. data/spec/bullet/notification/unused_eager_loading_spec.rb +12 -0
  70. data/spec/bullet/notification_collector_spec.rb +32 -0
  71. data/spec/bullet/rack_spec.rb +97 -0
  72. data/spec/bullet/registry/association_spec.rb +26 -0
  73. data/spec/bullet/registry/base_spec.rb +44 -0
  74. data/spec/bullet/registry/object_spec.rb +24 -0
  75. data/spec/bullet_spec.rb +41 -0
  76. data/spec/integration/active_record3/association_spec.rb +651 -0
  77. data/spec/integration/active_record4/association_spec.rb +649 -0
  78. data/spec/integration/counter_cache_spec.rb +63 -0
  79. data/spec/integration/mongoid/association_spec.rb +258 -0
  80. data/spec/models/address.rb +3 -0
  81. data/spec/models/author.rb +3 -0
  82. data/spec/models/base_user.rb +5 -0
  83. data/spec/models/category.rb +7 -0
  84. data/spec/models/city.rb +3 -0
  85. data/spec/models/client.rb +4 -0
  86. data/spec/models/comment.rb +4 -0
  87. data/spec/models/company.rb +3 -0
  88. data/spec/models/country.rb +3 -0
  89. data/spec/models/document.rb +5 -0
  90. data/spec/models/entry.rb +3 -0
  91. data/spec/models/firm.rb +4 -0
  92. data/spec/models/folder.rb +2 -0
  93. data/spec/models/mongoid/address.rb +7 -0
  94. data/spec/models/mongoid/category.rb +8 -0
  95. data/spec/models/mongoid/comment.rb +7 -0
  96. data/spec/models/mongoid/company.rb +7 -0
  97. data/spec/models/mongoid/entry.rb +7 -0
  98. data/spec/models/mongoid/post.rb +12 -0
  99. data/spec/models/mongoid/user.rb +5 -0
  100. data/spec/models/newspaper.rb +3 -0
  101. data/spec/models/page.rb +2 -0
  102. data/spec/models/person.rb +3 -0
  103. data/spec/models/pet.rb +3 -0
  104. data/spec/models/post.rb +10 -0
  105. data/spec/models/relationship.rb +4 -0
  106. data/spec/models/student.rb +3 -0
  107. data/spec/models/submission.rb +4 -0
  108. data/spec/models/teacher.rb +3 -0
  109. data/spec/models/user.rb +4 -0
  110. data/spec/models/writer.rb +2 -0
  111. data/spec/spec_helper.rb +103 -0
  112. data/spec/support/bullet_ext.rb +55 -0
  113. data/spec/support/mongo_seed.rb +65 -0
  114. data/spec/support/rack_double.rb +55 -0
  115. data/spec/support/sqlite_seed.rb +229 -0
  116. data/tasks/bullet_tasks.rake +9 -0
  117. data/test.sh +15 -0
  118. metadata +246 -0
@@ -0,0 +1,89 @@
1
+ module Bullet
2
+ module Detector
3
+ class NPlusOneQuery < Association
4
+ extend Dependency
5
+
6
+ class <<self
7
+ # executed when object.assocations is called.
8
+ # first, it keeps this method call for object.association.
9
+ # then, it checks if this associations call is unpreload.
10
+ # if it is, keeps this unpreload associations and caller.
11
+ def call_association(object, associations)
12
+ return unless Bullet.start?
13
+ return unless object.id
14
+ add_call_object_associations(object, associations)
15
+
16
+ Bullet.debug("Detector::NPlusOneQuery#call_association", "object: #{object.bullet_key}, associations: #{associations}")
17
+ if conditions_met?(object.bullet_key, associations)
18
+ Bullet.debug("detect n + 1 query", "object: #{object.bullet_key}, associations: #{associations}")
19
+ create_notification caller_in_project, object.class.to_s, associations
20
+ end
21
+ end
22
+
23
+ def add_possible_objects(object_or_objects)
24
+ return unless Bullet.start?
25
+ return unless Bullet.n_plus_one_query_enable?
26
+ objects = Array(object_or_objects)
27
+ return if objects.map(&:id).compact.empty?
28
+
29
+ Bullet.debug("Detector::NPlusOneQuery#add_possible_objects", "objects: #{objects.map(&:bullet_key).join(', ')}")
30
+ objects.each { |object| possible_objects.add object.bullet_key }
31
+ end
32
+
33
+ def add_impossible_object(object)
34
+ return unless Bullet.start?
35
+ return unless Bullet.n_plus_one_query_enable?
36
+ return unless object.id
37
+
38
+ Bullet.debug("Detector::NPlusOneQuery#add_impossible_object", "object: #{object.bullet_key}")
39
+ impossible_objects.add object.bullet_key
40
+ end
41
+
42
+ private
43
+ def create_notification(callers, klazz, associations)
44
+ notify_associations = Array(associations) - Bullet.get_whitelist_associations(:n_plus_one_query, klazz)
45
+
46
+ if notify_associations.present?
47
+ notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, notify_associations)
48
+ Bullet.notification_collector.add(notice)
49
+ end
50
+ end
51
+
52
+ # decide whether the object.associations is unpreloaded or not.
53
+ def conditions_met?(bullet_key, associations)
54
+ possible?(bullet_key) && !impossible?(bullet_key) && !association?(bullet_key, associations)
55
+ end
56
+
57
+ def caller_in_project
58
+ app_root = rails? ? Rails.root.to_s : Dir.pwd
59
+ vendor_root = app_root + "/vendor"
60
+ caller.select do |c|
61
+ c.include?(app_root) && !c.include?(vendor_root) ||
62
+ Bullet.stacktrace_includes.any? { |include| c.include?(include) }
63
+ end
64
+ end
65
+
66
+ def possible?(bullet_key)
67
+ possible_objects.include? bullet_key
68
+ end
69
+
70
+ def impossible?(bullet_key)
71
+ impossible_objects.include? bullet_key
72
+ end
73
+
74
+ # check if object => associations already exists in object_associations.
75
+ def association?(bullet_key, associations)
76
+ value = object_associations[bullet_key]
77
+ if value
78
+ value.each do |v|
79
+ result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
80
+ return true if result
81
+ end
82
+ end
83
+
84
+ return false
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,84 @@
1
+ module Bullet
2
+ module Detector
3
+ class UnusedEagerLoading < Association
4
+ class <<self
5
+ # check if there are unused preload associations.
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 check_unused_preload_associations
10
+ return unless Bullet.start?
11
+ return unless Bullet.unused_eager_loading_enable?
12
+
13
+ object_associations.each do |bullet_key, associations|
14
+ object_association_diff = diff_object_associations bullet_key, associations
15
+ next if object_association_diff.empty?
16
+
17
+ Bullet.debug("detect unused preload", "object: #{bullet_key}, associations: #{object_association_diff}")
18
+ create_notification bullet_key.bullet_class_name, object_association_diff
19
+ end
20
+ end
21
+
22
+ def add_eager_loadings(objects, associations)
23
+ return unless Bullet.start?
24
+ return unless Bullet.unused_eager_loading_enable?
25
+ return if objects.map(&:id).compact.empty?
26
+
27
+ Bullet.debug("Detector::UnusedEagerLoading#add_eager_loadings", "objects: #{objects.map(&:bullet_key).join(', ')}, associations: #{associations}")
28
+ bullet_keys = objects.map(&:bullet_key)
29
+
30
+ to_add = nil
31
+ to_merge, to_delete = [], []
32
+ eager_loadings.each do |k, v|
33
+ key_objects_overlap = k & bullet_keys
34
+
35
+ next if key_objects_overlap.empty?
36
+
37
+ if key_objects_overlap == k
38
+ to_add = [k, associations]
39
+ break
40
+ else
41
+ to_merge << [key_objects_overlap, ( eager_loadings[k].dup << associations )]
42
+
43
+ keys_without_objects = k - bullet_keys
44
+ to_merge << [keys_without_objects, eager_loadings[k]]
45
+ to_delete << k
46
+ bullet_keys = bullet_keys - k
47
+ end
48
+ end
49
+
50
+ eager_loadings.add *to_add if to_add
51
+ to_merge.each { |k,val| eager_loadings.merge k, val }
52
+ to_delete.each { |k| eager_loadings.delete k }
53
+
54
+ eager_loadings.add bullet_keys, associations unless bullet_keys.empty?
55
+ end
56
+
57
+ private
58
+ def create_notification(klazz, associations)
59
+ notify_associations = Array(associations) - Bullet.get_whitelist_associations(:unused_eager_loading, klazz)
60
+
61
+ if notify_associations.present?
62
+ notice = Bullet::Notification::UnusedEagerLoading.new(klazz, notify_associations)
63
+ Bullet.notification_collector.add(notice)
64
+ end
65
+ end
66
+
67
+ def call_associations(bullet_key, associations)
68
+ all = Set.new
69
+ eager_loadings.similarly_associated(bullet_key, associations).each do |related_bullet_key|
70
+ coa = call_object_associations[related_bullet_key]
71
+ next if coa.nil?
72
+ all.merge coa
73
+ end
74
+ all.to_a
75
+ end
76
+
77
+ def diff_object_associations(bullet_key, associations)
78
+ potential_associations = associations - call_associations(bullet_key, associations)
79
+ potential_associations.reject { |a| a.is_a?(Hash) }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,9 @@
1
+ class Object
2
+ def bullet_key
3
+ if self.class.respond_to?(:primary_key) && self.class.primary_key
4
+ "#{self.class}:#{self.send self.class.primary_key}"
5
+ else
6
+ "#{self.class}:#{self.id}"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def bullet_class_name
3
+ self.sub(/:[^:]*?$/, "")
4
+ end
5
+ end
@@ -0,0 +1,56 @@
1
+ module Bullet
2
+ module Mongoid
3
+ def self.enable
4
+ require 'mongoid'
5
+
6
+ ::Mongoid::Contexts::Mongo.class_eval do
7
+ alias_method :origin_first, :first
8
+ alias_method :origin_last, :last
9
+ alias_method :origin_iterate, :iterate
10
+ alias_method :origin_eager_load, :eager_load
11
+
12
+ def first
13
+ result = origin_first
14
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
15
+ result
16
+ end
17
+
18
+ def last
19
+ result = origin_last
20
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
21
+ result
22
+ end
23
+
24
+ def iterate(&block)
25
+ records = execute.to_a
26
+ if records.size > 1
27
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
28
+ elsif records.size == 1
29
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
30
+ end
31
+ origin_iterate(&block)
32
+ end
33
+
34
+ def eager_load(docs)
35
+ associations = criteria.inclusions.map(&:name)
36
+ docs.each do |doc|
37
+ Bullet::Detector::Association.add_object_associations(doc, associations)
38
+ end
39
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
40
+ origin_eager_load(docs)
41
+ end
42
+ end
43
+
44
+ ::Mongoid::Relations::Accessors.class_eval do
45
+ alias_method :origin_set_relation, :set_relation
46
+
47
+ def set_relation(name, relation)
48
+ if relation && relation.metadata.macro !~ /embed/
49
+ Bullet::Detector::NPlusOneQuery.call_association(self, name)
50
+ end
51
+ origin_set_relation(name, relation)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ module Bullet
2
+ module Mongoid
3
+ def self.enable
4
+ require 'mongoid'
5
+ ::Mongoid::Contextual::Mongo.class_eval do
6
+ alias_method :origin_first, :first
7
+ alias_method :origin_last, :last
8
+ alias_method :origin_each, :each
9
+ alias_method :origin_eager_load, :eager_load
10
+
11
+ def first
12
+ result = origin_first
13
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
14
+ result
15
+ end
16
+
17
+ def last
18
+ result = origin_last
19
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
20
+ result
21
+ end
22
+
23
+ def each(&block)
24
+ records = query.map{ |doc| ::Mongoid::Factory.from_db(klass, doc) }
25
+ if records.length > 1
26
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
27
+ elsif records.size == 1
28
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
29
+ end
30
+ origin_each(&block)
31
+ end
32
+
33
+ def eager_load(docs)
34
+ associations = criteria.inclusions.map(&:name)
35
+ docs.each do |doc|
36
+ Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
37
+ end
38
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
39
+ origin_eager_load(docs)
40
+ end
41
+ end
42
+
43
+ ::Mongoid::Relations::Accessors.class_eval do
44
+ alias_method :origin_get_relation, :get_relation
45
+
46
+ def get_relation(name, metadata, reload = false)
47
+ result = origin_get_relation(name, metadata, reload)
48
+ if metadata.macro !~ /embed/
49
+ Bullet::Detector::NPlusOneQuery.call_association(self, name)
50
+ end
51
+ result
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ module Bullet
2
+ module Mongoid
3
+ def self.enable
4
+ require 'mongoid'
5
+ ::Mongoid::Contextual::Mongo.class_eval do
6
+ alias_method :origin_first, :first
7
+ alias_method :origin_last, :last
8
+ alias_method :origin_each, :each
9
+ alias_method :origin_eager_load, :eager_load
10
+
11
+ def first
12
+ result = origin_first
13
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
14
+ result
15
+ end
16
+
17
+ def last
18
+ result = origin_last
19
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
20
+ result
21
+ end
22
+
23
+ def each(&block)
24
+ records = query.map{ |doc| ::Mongoid::Factory.from_db(klass, doc) }
25
+ if records.length > 1
26
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
27
+ elsif records.size == 1
28
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
29
+ end
30
+ origin_each(&block)
31
+ end
32
+
33
+ def eager_load(docs)
34
+ associations = criteria.inclusions.map(&:name)
35
+ docs.each do |doc|
36
+ Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
37
+ end
38
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
39
+ origin_eager_load(docs)
40
+ end
41
+ end
42
+
43
+ ::Mongoid::Relations::Accessors.class_eval do
44
+ alias_method :origin_get_relation, :get_relation
45
+
46
+ def get_relation(name, metadata, object, reload = false)
47
+ result = origin_get_relation(name, metadata, object, reload)
48
+ if metadata.macro !~ /embed/
49
+ Bullet::Detector::NPlusOneQuery.call_association(self, name)
50
+ end
51
+ result
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,10 @@
1
+ module Bullet
2
+ module Notification
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'
7
+
8
+ class UnoptimizedQueryError < StandardError; end
9
+ end
10
+ end
@@ -0,0 +1,97 @@
1
+ module Bullet
2
+ module Notification
3
+ class Base
4
+ attr_accessor :notifier, :url
5
+ attr_reader :base_class, :associations, :path
6
+
7
+ def initialize(base_class, association_or_associations, path = nil)
8
+ @base_class = base_class
9
+ @associations = association_or_associations.is_a?(Array) ? association_or_associations : [association_or_associations]
10
+ @path = path
11
+ end
12
+
13
+ def title
14
+ raise NoMethodError.new("no method title defined")
15
+ end
16
+
17
+ def body
18
+ raise NoMethodError.new("no method body defined")
19
+ end
20
+
21
+ def whoami
22
+ user = `whoami`
23
+ if user
24
+ "user: #{user.chomp}"
25
+ else
26
+ ""
27
+ end
28
+ end
29
+
30
+ def body_with_caller
31
+ body
32
+ end
33
+
34
+ def standard_notice
35
+ @standard_notifice ||= title + "\n" + body
36
+ end
37
+
38
+ def full_notice
39
+ [url, title, body_with_caller].compact.join("\n")
40
+ end
41
+
42
+ def notify_inline
43
+ self.notifier.inline_notify(notification_data)
44
+ end
45
+
46
+ def notify_out_of_channel
47
+ self.notifier.out_of_channel_notify(notification_data)
48
+ end
49
+
50
+ def short_notice
51
+ [url, title, body].compact.join("\n")
52
+ end
53
+
54
+ def notification_data
55
+ {
56
+ :url => url,
57
+ :title => title,
58
+ :body => body_with_caller,
59
+ }
60
+ end
61
+
62
+ def eql?(other)
63
+ klazz_associations_str == other.klazz_associations_str
64
+ end
65
+
66
+ def hash
67
+ klazz_associations_str.hash
68
+ end
69
+
70
+ class HTMLwithPygments < Redcarpet::Render::HTML
71
+ def block_code(code, language)
72
+ Pygments.highlight(code, :lexer => language)
73
+ end
74
+ end
75
+
76
+ def markdown(text)
77
+ renderer = HTMLwithPygments.new(hard_wrap: true)
78
+ options = {
79
+ :no_intra_emphasis => true,
80
+ :fenced_code_blocks => true
81
+ }
82
+ # markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, options)
83
+ # markdown.render(text).html_safe
84
+ Redcarpet::Markdown.new(renderer, options).render(text).html_safe
85
+ end
86
+
87
+ protected
88
+ def klazz_associations_str
89
+ " #{@base_class} => [#{@associations.map(&:inspect).join(', ')}]"
90
+ end
91
+
92
+ def associations_str
93
+ ":include => #{@associations.map{ |a| a.to_s.to_sym unless a.is_a? Hash }.inspect}"
94
+ end
95
+ end
96
+ end
97
+ end