predictive_load 0.4.2 → 0.5.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.
- checksums.yaml +4 -4
- data/README.md +0 -34
- data/lib/predictive_load/active_record_collection_observation.rb +28 -19
- metadata +7 -120
- data/lib/predictive_load/watcher.rb +0 -84
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54bffe32fe50393ce8bb75bd01ae397084d3c2924ea4dafa9bccddb860baedf3
|
4
|
+
data.tar.gz: 1b37f4afd9874034fe0b7adc81317687d734cc51539665a5df005deefc0553c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8af3089f4ab81c3470d2811a335d40e36bede5ca38c59cf739135ebf4fdb18d7181338d9065a7fff06ff436a54c73e4ad618c61a890d8ee610c935855542e1da
|
7
|
+
data.tar.gz: de072a8eeaf0080dcf1bd2c1183b9b04f305c1206ed170737a6cbf97b62dee6945e876fb739b2495d715f9e1e373605383db35b13ed8ccb6b5a0f1bc71528e4c
|
data/README.md
CHANGED
@@ -40,40 +40,6 @@ Some things cannot be preloaded, use `predictive_load: false`
|
|
40
40
|
has_many :foos, predictive_load: false
|
41
41
|
```
|
42
42
|
|
43
|
-
### N+1 detection logging
|
44
|
-
|
45
|
-
There is also a log-only version:
|
46
|
-
```ruby
|
47
|
-
require 'predictive_load'
|
48
|
-
require 'predictive_load/active_record_collection_observation'
|
49
|
-
ActiveRecord::Base.send(:include, PredictiveLoad::ActiveRecordCollectionObservation)
|
50
|
-
|
51
|
-
require 'predictive_load/watcher'
|
52
|
-
|
53
|
-
ActiveRecord::Relation.collection_observer = PredictiveLoad::Watcher
|
54
|
-
|
55
|
-
Comment.all.each do |comment|
|
56
|
-
comment.account
|
57
|
-
end
|
58
|
-
|
59
|
-
```
|
60
|
-
|
61
|
-
Produces:
|
62
|
-
|
63
|
-
```
|
64
|
-
detected n1 call on Comment#account
|
65
|
-
expect to prevent 10 queries
|
66
|
-
would preload with: SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`id` IN (...)
|
67
|
-
+----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
|
68
|
-
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|
69
|
-
+----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
|
70
|
-
| 1 | SIMPLE | accounts | const | PRIMARY | PRIMARY | 4 | const | 10 | |
|
71
|
-
+----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
|
72
|
-
1 row in set (0.00 sec)
|
73
|
-
would have prevented all 10 queries
|
74
|
-
|
75
|
-
```
|
76
|
-
|
77
43
|
#### Known limitations:
|
78
44
|
|
79
45
|
* Calling association#size will trigger an N+1 on SELECT COUNT(*). Work around by calling #length, loading all records.
|
@@ -1,31 +1,40 @@
|
|
1
1
|
module PredictiveLoad::ActiveRecordCollectionObservation
|
2
2
|
|
3
3
|
def self.included(base)
|
4
|
-
ActiveRecord::Relation.
|
5
|
-
ActiveRecord::
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
ActiveRecord::Relation.class_attribute :collection_observer
|
5
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
6
|
+
ActiveRecord::Relation.prepend Rails5RelationObservation
|
7
|
+
else
|
8
|
+
ActiveRecord::Relation.prepend Rails4RelationObservation
|
9
|
+
end
|
10
|
+
ActiveRecord::Base.include CollectionMember
|
11
|
+
ActiveRecord::Base.extend UnscopedTracker
|
12
|
+
ActiveRecord::Associations::Association.include AssociationNotification
|
13
|
+
ActiveRecord::Associations::CollectionAssociation.include CollectionAssociationNotification
|
9
14
|
end
|
10
15
|
|
11
|
-
module
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
16
|
+
module Rails5RelationObservation
|
17
|
+
# this essentially intercepts the enumerable methods that would result in n+1s since most of
|
18
|
+
# those are delegated to :records in Rails 5+ in the ActiveRecord::Relation::Delegation module
|
19
|
+
def records
|
20
|
+
record_array = super
|
21
|
+
if record_array.size > 1 && collection_observer
|
22
|
+
collection_observer.observe(record_array.dup)
|
23
|
+
end
|
24
|
+
record_array
|
17
25
|
end
|
26
|
+
end
|
18
27
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
28
|
+
module Rails4RelationObservation
|
29
|
+
# this essentially intercepts the enumerable methods that would result in n+1s since most of
|
30
|
+
# those are delegated to :to_a in Rails 5+ in the ActiveRecord::Relation::Delegation module
|
31
|
+
def to_a
|
32
|
+
record_array = super
|
33
|
+
if record_array.size > 1 && collection_observer
|
34
|
+
collection_observer.observe(record_array.dup)
|
24
35
|
end
|
25
|
-
|
26
|
-
records
|
36
|
+
record_array
|
27
37
|
end
|
28
|
-
|
29
38
|
end
|
30
39
|
|
31
40
|
module CollectionMember
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: predictive_load
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Chapweske
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-02-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,132 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 4.2.0
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '5.
|
22
|
+
version: '5.3'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version:
|
29
|
+
version: 4.2.0
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '5.
|
33
|
-
- !ruby/object:Gem::Dependency
|
34
|
-
name: minitest
|
35
|
-
requirement: !ruby/object:Gem::Requirement
|
36
|
-
requirements:
|
37
|
-
- - ">="
|
38
|
-
- !ruby/object:Gem::Version
|
39
|
-
version: '0'
|
40
|
-
type: :development
|
41
|
-
prerelease: false
|
42
|
-
version_requirements: !ruby/object:Gem::Requirement
|
43
|
-
requirements:
|
44
|
-
- - ">="
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: '0'
|
47
|
-
- !ruby/object:Gem::Dependency
|
48
|
-
name: minitest-rg
|
49
|
-
requirement: !ruby/object:Gem::Requirement
|
50
|
-
requirements:
|
51
|
-
- - ">="
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '0'
|
54
|
-
type: :development
|
55
|
-
prerelease: false
|
56
|
-
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
requirements:
|
58
|
-
- - ">="
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
version: '0'
|
61
|
-
- !ruby/object:Gem::Dependency
|
62
|
-
name: sqlite3
|
63
|
-
requirement: !ruby/object:Gem::Requirement
|
64
|
-
requirements:
|
65
|
-
- - ">="
|
66
|
-
- !ruby/object:Gem::Version
|
67
|
-
version: '0'
|
68
|
-
type: :development
|
69
|
-
prerelease: false
|
70
|
-
version_requirements: !ruby/object:Gem::Requirement
|
71
|
-
requirements:
|
72
|
-
- - ">="
|
73
|
-
- !ruby/object:Gem::Version
|
74
|
-
version: '0'
|
75
|
-
- !ruby/object:Gem::Dependency
|
76
|
-
name: rake
|
77
|
-
requirement: !ruby/object:Gem::Requirement
|
78
|
-
requirements:
|
79
|
-
- - ">="
|
80
|
-
- !ruby/object:Gem::Version
|
81
|
-
version: '0'
|
82
|
-
type: :development
|
83
|
-
prerelease: false
|
84
|
-
version_requirements: !ruby/object:Gem::Requirement
|
85
|
-
requirements:
|
86
|
-
- - ">="
|
87
|
-
- !ruby/object:Gem::Version
|
88
|
-
version: '0'
|
89
|
-
- !ruby/object:Gem::Dependency
|
90
|
-
name: bump
|
91
|
-
requirement: !ruby/object:Gem::Requirement
|
92
|
-
requirements:
|
93
|
-
- - ">="
|
94
|
-
- !ruby/object:Gem::Version
|
95
|
-
version: '0'
|
96
|
-
type: :development
|
97
|
-
prerelease: false
|
98
|
-
version_requirements: !ruby/object:Gem::Requirement
|
99
|
-
requirements:
|
100
|
-
- - ">="
|
101
|
-
- !ruby/object:Gem::Version
|
102
|
-
version: '0'
|
103
|
-
- !ruby/object:Gem::Dependency
|
104
|
-
name: wwtd
|
105
|
-
requirement: !ruby/object:Gem::Requirement
|
106
|
-
requirements:
|
107
|
-
- - ">="
|
108
|
-
- !ruby/object:Gem::Version
|
109
|
-
version: '0'
|
110
|
-
type: :development
|
111
|
-
prerelease: false
|
112
|
-
version_requirements: !ruby/object:Gem::Requirement
|
113
|
-
requirements:
|
114
|
-
- - ">="
|
115
|
-
- !ruby/object:Gem::Version
|
116
|
-
version: '0'
|
117
|
-
- !ruby/object:Gem::Dependency
|
118
|
-
name: query_diet
|
119
|
-
requirement: !ruby/object:Gem::Requirement
|
120
|
-
requirements:
|
121
|
-
- - ">="
|
122
|
-
- !ruby/object:Gem::Version
|
123
|
-
version: '0'
|
124
|
-
type: :development
|
125
|
-
prerelease: false
|
126
|
-
version_requirements: !ruby/object:Gem::Requirement
|
127
|
-
requirements:
|
128
|
-
- - ">="
|
129
|
-
- !ruby/object:Gem::Version
|
130
|
-
version: '0'
|
131
|
-
- !ruby/object:Gem::Dependency
|
132
|
-
name: byebug
|
133
|
-
requirement: !ruby/object:Gem::Requirement
|
134
|
-
requirements:
|
135
|
-
- - ">="
|
136
|
-
- !ruby/object:Gem::Version
|
137
|
-
version: '0'
|
138
|
-
type: :development
|
139
|
-
prerelease: false
|
140
|
-
version_requirements: !ruby/object:Gem::Requirement
|
141
|
-
requirements:
|
142
|
-
- - ">="
|
143
|
-
- !ruby/object:Gem::Version
|
144
|
-
version: '0'
|
32
|
+
version: '5.3'
|
145
33
|
description: Predictive loader
|
146
34
|
email:
|
147
35
|
- eac@zendesk.com
|
@@ -155,7 +43,6 @@ files:
|
|
155
43
|
- lib/predictive_load/active_record_collection_observation.rb
|
156
44
|
- lib/predictive_load/loader.rb
|
157
45
|
- lib/predictive_load/preload_log.rb
|
158
|
-
- lib/predictive_load/watcher.rb
|
159
46
|
homepage: https://github.com/zendesk/predictive_load
|
160
47
|
licenses:
|
161
48
|
- Apache License Version 2.0
|
@@ -168,7 +55,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
168
55
|
requirements:
|
169
56
|
- - ">="
|
170
57
|
- !ruby/object:Gem::Version
|
171
|
-
version: '
|
58
|
+
version: '2.4'
|
172
59
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
173
60
|
requirements:
|
174
61
|
- - ">="
|
@@ -1,84 +0,0 @@
|
|
1
|
-
raise "Not supported on rails 4.1+" if ActiveRecord::VERSION::STRING >= "4.1.0"
|
2
|
-
|
3
|
-
require 'predictive_load/loader'
|
4
|
-
require 'predictive_load/preload_log'
|
5
|
-
|
6
|
-
module PredictiveLoad
|
7
|
-
# Provides N+1 detection / log mode.
|
8
|
-
#
|
9
|
-
# Usage:
|
10
|
-
# ActiveRecord::Relation.collection_observer = PredictiveLoad::Watcher
|
11
|
-
#
|
12
|
-
# Example output:
|
13
|
-
# predictive_load: detected n1 call on Comment#account
|
14
|
-
# predictive_load: expect to prevent 1 queries
|
15
|
-
# predictive_load: would preload with: SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`id` IN (...)
|
16
|
-
# predictive_load: +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
|
17
|
-
# predictive_load: | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|
18
|
-
# predictive_load: +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
|
19
|
-
# predictive_load: | 1 | SIMPLE | accounts | const | PRIMARY | PRIMARY | 4 | const | 1 | |
|
20
|
-
# predictive_load: +----+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
|
21
|
-
# predictive_load: 1 row in set (0.00 sec)
|
22
|
-
# predictive_load: would have prevented all 1 queries
|
23
|
-
class Watcher < Loader
|
24
|
-
|
25
|
-
attr_reader :loaded_associations
|
26
|
-
|
27
|
-
def initialize(records)
|
28
|
-
super
|
29
|
-
@loaded_associations = {}
|
30
|
-
end
|
31
|
-
|
32
|
-
def loading_association(record, association)
|
33
|
-
association_name = association.reflection.name
|
34
|
-
return if !all_records_will_likely_load_association?(association_name)
|
35
|
-
return if !supports_preload?(association)
|
36
|
-
|
37
|
-
if loaded_associations.key?(association_name)
|
38
|
-
log_query_plan(association_name)
|
39
|
-
end
|
40
|
-
|
41
|
-
increment_query_count(association_name)
|
42
|
-
end
|
43
|
-
|
44
|
-
protected
|
45
|
-
|
46
|
-
def log_query_plan(association_name)
|
47
|
-
log("detected n+1 call on #{records.first.class.name}##{association_name}")
|
48
|
-
|
49
|
-
# Detailed logging for first query
|
50
|
-
if query_count(association_name) == 1
|
51
|
-
log("expect to prevent #{expected_query_count} queries")
|
52
|
-
log_preload(association_name)
|
53
|
-
end
|
54
|
-
|
55
|
-
# All records loaded association
|
56
|
-
if query_count(association_name) == expected_query_count
|
57
|
-
log("would have prevented all #{expected_query_count} queries")
|
58
|
-
end
|
59
|
-
|
60
|
-
end
|
61
|
-
|
62
|
-
def query_count(association_name)
|
63
|
-
loaded_associations[association_name] || 0
|
64
|
-
end
|
65
|
-
|
66
|
-
def increment_query_count(association_name)
|
67
|
-
loaded_associations[association_name] ||= 0
|
68
|
-
loaded_associations[association_name] += 1
|
69
|
-
end
|
70
|
-
|
71
|
-
def expected_query_count
|
72
|
-
records.size - 1
|
73
|
-
end
|
74
|
-
|
75
|
-
def log_preload(association_name)
|
76
|
-
PreloadLog.new(records_with_association(association_name), [ association_name ]).run
|
77
|
-
end
|
78
|
-
|
79
|
-
def log(message)
|
80
|
-
ActiveRecord::Base.logger.info("predictive_load: #{message}")
|
81
|
-
end
|
82
|
-
|
83
|
-
end
|
84
|
-
end
|