performance_promise 1.0.1 → 1.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.
- data/README.md +14 -7
- data/lib/performance_promise.rb +25 -19
- data/lib/performance_promise/sql_recorder.rb +20 -2
- data/lib/performance_promise/utils.rb +7 -1
- data/lib/performance_promise/validations/full_table_scans.rb +36 -7
- data/lib/performance_promise/validations/number_of_db_queries.rb +1 -1
- data/performance_promise.gemspec +1 -1
- metadata +9 -7
- checksums.yaml +0 -7
data/README.md
CHANGED
@@ -55,7 +55,7 @@ PerformancePromise.configure do |config|
|
|
55
55
|
# ]
|
56
56
|
# config.untagged_methods_are_speedy = true
|
57
57
|
# config.speedy_promise = {
|
58
|
-
# :makes => 2,
|
58
|
+
# :makes => 2.queries,
|
59
59
|
# }
|
60
60
|
# config.allowed_environments = [
|
61
61
|
# 'development',
|
@@ -67,6 +67,13 @@ end
|
|
67
67
|
PerformancePromise.start
|
68
68
|
```
|
69
69
|
|
70
|
+
Then, extend your `ApplicationController` in `app/controllers/application_controller.rb`:
|
71
|
+
```ruby
|
72
|
+
class ApplicationController < ActionController::Base
|
73
|
+
extend MethodDecorators
|
74
|
+
...
|
75
|
+
```
|
76
|
+
|
70
77
|
## Usage
|
71
78
|
To understand how to use `performance_promise`, let's use a simple [Blog App][rails-getting-started]. A `Blog` has `Article`s, each of which may have one or more `Comment`s.
|
72
79
|
|
@@ -78,7 +85,7 @@ class ArticlesController < ApplicationController
|
|
78
85
|
end
|
79
86
|
end
|
80
87
|
```
|
81
|
-
Assuming your routes and views are setup, you should be able to
|
88
|
+
Assuming your routes and views are setup, you should be able to successfully visit `/articles`.
|
82
89
|
|
83
90
|
You can annotate this action with a promise of how many database queries the action will make so:
|
84
91
|
```ruby
|
@@ -144,7 +151,7 @@ And now, we've successfully caught and averted a bad code commit!
|
|
144
151
|
By default, `performance_promise` runs only in `development` and `testing`. This ensures that you can identify issues when developing or running your test-suite. Be very careful about enabling this in `production` – you almost certainly don't want to.
|
145
152
|
|
146
153
|
#### `throw_exception: bool`
|
147
|
-
Tells `performance_promise` whether to throw an exception. Set to `true` by default, but can be
|
154
|
+
Tells `performance_promise` whether to throw an exception. Set to `true` by default, but can be overridden if you simply want to ignore failing cases (they will still be written to the log).
|
148
155
|
|
149
156
|
#### `speedy_promise: hash`
|
150
157
|
If you do not care to determine the _exact_ performance of your action, you can still simply mark it as `Speedy`:
|
@@ -157,7 +164,7 @@ If you do not care to determine the _exact_ performance of your action, you can
|
|
157
164
|
A `Speedy` action is supposed to be well behaved, making lesser than `x` database queries, and taking less than `y` to complete. You can set these defaults using this configuration parameter.
|
158
165
|
|
159
166
|
#### `untagged_methods_are_speedy: bool`
|
160
|
-
By default, actions that are not annotated aren't validated by `performance_promise`. If you'd like to force all actions to be validated, one option is to simply default them all to be `Speedy`. This allows developers to make _no_ change to their code, while still reaping the benefits of performance validation.
|
167
|
+
By default, actions that are not annotated aren't validated by `performance_promise`. If you'd like to force all actions to be validated, one option is to simply default them all to be `Speedy`. This allows developers to make _no_ change to their code, while still reaping the benefits of performance validation. If a view fails to be `Speedy`, then the developer is forced to acknowledge it in code.
|
161
168
|
|
162
169
|
|
163
170
|
## FAQ
|
@@ -165,11 +172,11 @@ By default, actions that are not annotated aren't validated by `performance_prom
|
|
165
172
|
|
166
173
|
We borrow the coding style from Python's `decorators`. This style allows for a function to be wrapped by another. This is a great use case for that style since it allows for us to express the annotation right above the function definition.
|
167
174
|
|
168
|
-
Credit goes to [Yehuda Katz][yehuda-katz] for the [port of
|
175
|
+
Credit goes to [Yehuda Katz][yehuda-katz] for the [port of decorators][ruby-decorators] into Ruby.
|
169
176
|
|
170
177
|
> **Will this affect my production service?**
|
171
178
|
|
172
|
-
By default, `
|
179
|
+
By default, `performance_promise` is applied only in `development` and `test` environments. You can choose to override this, but is strongly discouraged.
|
173
180
|
|
174
181
|
|
175
182
|
> **What are some other kinds of performance guarantees that I can make with `performance_promise`?**
|
@@ -192,7 +199,7 @@ If you come up with other validations that you think will be useful, please cons
|
|
192
199
|
|
193
200
|
`performance_promise` can be tuned to not only identify N + 1 queries, but can also alert whenever there's _any_ change in performance. It allows you to identify expensive actions irrespective of their database query profile.
|
194
201
|
|
195
|
-
`performance_promise` also has access to the entire database query object. In the future, `performance_promise` can be tuned to perform
|
202
|
+
`performance_promise` also has access to the entire database query object. In the future, `performance_promise` can be tuned to perform additional checks like how long the most expensive query took, whether the action performed any table scans (available through an `EXPLAIN`) etc.
|
196
203
|
|
197
204
|
Finally, the difference between `bullet` and `performance_promise` is akin to testing by refreshing your browser and testing by writing specs. `performance_promise` encourages you to specify your action's performance by declaring it in code itself. This allows both code-reviewers as well as automated tests to verify your code's performance.
|
198
205
|
|
data/lib/performance_promise.rb
CHANGED
@@ -81,7 +81,11 @@ module PerformancePromise
|
|
81
81
|
|
82
82
|
def self.validate_promise(method, db_queries, render_time, options)
|
83
83
|
return if options[:skip]
|
84
|
+
|
84
85
|
promise_broken = false
|
86
|
+
error_messages = []
|
87
|
+
backtraces = []
|
88
|
+
|
85
89
|
self.configuration.validations.each do |validation|
|
86
90
|
promised = options[validation]
|
87
91
|
if promised
|
@@ -89,28 +93,30 @@ module PerformancePromise
|
|
89
93
|
passed, error_message, backtrace =
|
90
94
|
PerformanceValidations.send(validation_method, db_queries, render_time, promised)
|
91
95
|
unless passed
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
else
|
97
|
-
PerformancePromise.configuration.logger.warn '-' * 80
|
98
|
-
PerformancePromise.configuration.logger.warn Utils.colored(:red, error_message)
|
99
|
-
backtrace.each do |trace|
|
100
|
-
PerformancePromise.configuration.logger.warn Utils.colored(:cyan, error_message)
|
101
|
-
end
|
102
|
-
PerformancePromise.configuration.logger.warn '-' * 80
|
103
|
-
end
|
96
|
+
error_messages << error_message
|
97
|
+
backtraces << '-'*80
|
98
|
+
backtraces << "#{validation.to_s.upcase}"
|
99
|
+
backtraces << backtrace
|
104
100
|
promise_broken = true
|
105
101
|
end
|
106
102
|
end
|
107
103
|
end
|
108
|
-
|
104
|
+
if promise_broken
|
105
|
+
combined_error_message = "#{method}: Try Performance #{error_messages.join(', ')}"
|
106
|
+
if PerformancePromise.configuration.throw_exception
|
107
|
+
bp = BrokenPromise.new(combined_error_message)
|
108
|
+
bp.set_backtrace(backtraces.flatten)
|
109
|
+
raise bp
|
110
|
+
else
|
111
|
+
PerformancePromise.configuration.logger.warn '-' * 80
|
112
|
+
PerformancePromise.configuration.logger.warn Utils.colored(:red, combined_error_message)
|
113
|
+
backtraces.flatten.each do |trace|
|
114
|
+
PerformancePromise.configuration.logger.warn Utils.colored(:cyan, trace)
|
115
|
+
end
|
116
|
+
PerformancePromise.configuration.logger.warn '-' * 80
|
117
|
+
end
|
118
|
+
else
|
119
|
+
PerformanceValidations.report_promise_passed(method, db_queries, options)
|
120
|
+
end
|
109
121
|
end
|
110
|
-
|
111
|
-
end
|
112
|
-
|
113
|
-
|
114
|
-
class ApplicationController < ActionController::Base
|
115
|
-
extend MethodDecorators
|
116
122
|
end
|
@@ -13,9 +13,27 @@ class SQLRecorder
|
|
13
13
|
|
14
14
|
def record(payload, duration)
|
15
15
|
return if invalid_payload?(payload)
|
16
|
+
|
17
|
+
# do not record/analyze explain queries used in dev
|
18
|
+
return if payload[:sql].include?('EXPLAIN')
|
19
|
+
|
16
20
|
sql = payload[:sql]
|
17
21
|
cleaned_trace = clean_trace(caller)
|
18
|
-
|
22
|
+
if sql.include?('SELECT')
|
23
|
+
connection = ActiveRecord::Base.connection
|
24
|
+
adapter_name = connection.adapter_name
|
25
|
+
if adapter_name == 'Mysql2'
|
26
|
+
explained = connection.explain(connection, sql).as_json
|
27
|
+
elsif adapter_name == 'SQLite'
|
28
|
+
explained = connection.execute("EXPLAIN QUERY PLAN #{sql}", 'SQLR-EXPLAIN')
|
29
|
+
elsif
|
30
|
+
PerformancePromise.configuration.logger.warn("Unkown database adapter {adapter_name}")
|
31
|
+
explained = connection.execute("EXPLAIN QUERY PLAN #{sql}", 'SQLR-EXPLAIN')
|
32
|
+
end
|
33
|
+
else
|
34
|
+
explained = nil
|
35
|
+
end
|
36
|
+
|
19
37
|
@db_queries << {
|
20
38
|
:sql => sql,
|
21
39
|
:duration => duration,
|
@@ -29,7 +47,7 @@ class SQLRecorder
|
|
29
47
|
'SCHEMA',
|
30
48
|
'SQLR-EXPLAIN',
|
31
49
|
]
|
32
|
-
payload[:name] && ignore_query_names.any? { |name| payload[:name].
|
50
|
+
payload[:name] && ignore_query_names.any? { |name| payload[:name].include?(name) }
|
33
51
|
end
|
34
52
|
|
35
53
|
def clean_trace(trace)
|
@@ -9,10 +9,11 @@ module Utils
|
|
9
9
|
|
10
10
|
def self.guess_order(db_queries)
|
11
11
|
order = []
|
12
|
+
single_queries = 0
|
12
13
|
queries_with_count = summarize_queries(db_queries)
|
13
14
|
queries_with_count.each do |query, count|
|
14
15
|
if count == 1
|
15
|
-
|
16
|
+
single_queries += 1
|
16
17
|
else
|
17
18
|
if (lookup_field = /WHERE .*"(.*?_id)" = \?/.match(query[:sql]))
|
18
19
|
klass = lookup_field[1].humanize
|
@@ -23,6 +24,11 @@ module Utils
|
|
23
24
|
end
|
24
25
|
end
|
25
26
|
|
27
|
+
if single_queries == 1
|
28
|
+
order << '1.query'
|
29
|
+
elsif single_queries > 1
|
30
|
+
order << "#{single_queries}.queries"
|
31
|
+
end
|
26
32
|
order.join(" + ")
|
27
33
|
end
|
28
34
|
|
@@ -1,15 +1,44 @@
|
|
1
1
|
module ValidateFullTableScans
|
2
2
|
def validate_full_table_scans(db_queries, render_time, promised)
|
3
3
|
full_table_scans = []
|
4
|
+
backtrace = []
|
4
5
|
|
5
6
|
# check the explained queries to see if there were any
|
6
7
|
# SCAN TABLEs
|
7
8
|
db_queries.each do |db_query|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
if db_query[:explained]
|
10
|
+
join_type = db_query[:explained].first[3]
|
11
|
+
if join_type
|
12
|
+
|
13
|
+
adapter_name = ActiveRecord::Base.connection.adapter_name
|
14
|
+
if adapter_name == 'Mysql2'
|
15
|
+
makes_full_table_scan = join_type.match(/ALL/)
|
16
|
+
table_name = db_query[:explained][0][2]
|
17
|
+
elsif adapter_name == 'SQLite'
|
18
|
+
makes_full_table_scan = join_type.match(/SCAN TABLE (.*)/)
|
19
|
+
table_name = makes_full_table_scan[1]
|
20
|
+
else
|
21
|
+
PerformancePromise.configuration.logger.warn("Unkown database adapter {adapter_name}")
|
22
|
+
makes_full_table_scan = join_type.match(/SCAN TABLE (.*)/)
|
23
|
+
table_name = makes_full_table_scan[1]
|
24
|
+
end
|
25
|
+
|
26
|
+
if makes_full_table_scan
|
27
|
+
full_table_scans << table_name
|
28
|
+
|
29
|
+
backtrace << db_query[:sql]
|
30
|
+
db_query[:trace].each do |trace|
|
31
|
+
if trace.starts_with?('app')
|
32
|
+
file, line_number = trace.split(':')
|
33
|
+
trace = ' |_' +
|
34
|
+
File.read(file).split("\n")[line_number.to_i - 1].strip +
|
35
|
+
' (' + trace + ')'
|
36
|
+
end
|
37
|
+
backtrace << trace
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
13
42
|
end
|
14
43
|
end
|
15
44
|
|
@@ -22,10 +51,10 @@ module ValidateFullTableScans
|
|
22
51
|
# check that the performed FTSs are a subset of the promised FTSs
|
23
52
|
passes = (full_table_scans & promised_full_table_scans == full_table_scans)
|
24
53
|
error_message = ''
|
25
|
-
backtrace = []
|
26
54
|
|
27
55
|
unless passes
|
28
|
-
|
56
|
+
model_names = full_table_scans.map { |table_name| table_name.classify }
|
57
|
+
error_message = ":full_table_scans => [#{model_names.join(', ')}]"
|
29
58
|
end
|
30
59
|
|
31
60
|
return passes, error_message, backtrace
|
@@ -11,7 +11,7 @@ module ValidateNumberOfQueries
|
|
11
11
|
end
|
12
12
|
|
13
13
|
guessed_order = Utils.guess_order(db_queries)
|
14
|
-
error_message = "
|
14
|
+
error_message = ":makes => #{guessed_order}"
|
15
15
|
backtrace = []
|
16
16
|
Utils.summarize_queries(db_queries).each do |db_query, count|
|
17
17
|
statement = "#{count} x #{db_query[:sql]}"
|
data/performance_promise.gemspec
CHANGED
metadata
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: performance_promise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
|
+
prerelease:
|
5
6
|
platform: ruby
|
6
7
|
authors:
|
7
8
|
- Bipin Suresh
|
@@ -16,7 +17,7 @@ executables: []
|
|
16
17
|
extensions: []
|
17
18
|
extra_rdoc_files: []
|
18
19
|
files:
|
19
|
-
-
|
20
|
+
- .gitignore
|
20
21
|
- Gemfile
|
21
22
|
- LICENSE
|
22
23
|
- README.md
|
@@ -41,26 +42,27 @@ files:
|
|
41
42
|
homepage: https://github.com/bipsandbytes/performance_promise
|
42
43
|
licenses:
|
43
44
|
- MIT
|
44
|
-
metadata: {}
|
45
45
|
post_install_message:
|
46
46
|
rdoc_options: []
|
47
47
|
require_paths:
|
48
48
|
- lib
|
49
49
|
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
50
51
|
requirements:
|
51
|
-
- -
|
52
|
+
- - ! '>='
|
52
53
|
- !ruby/object:Gem::Version
|
53
54
|
version: '0'
|
54
55
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
55
57
|
requirements:
|
56
|
-
- -
|
58
|
+
- - ! '>='
|
57
59
|
- !ruby/object:Gem::Version
|
58
60
|
version: '0'
|
59
61
|
requirements: []
|
60
62
|
rubyforge_project:
|
61
|
-
rubygems_version:
|
63
|
+
rubygems_version: 1.8.23
|
62
64
|
signing_key:
|
63
|
-
specification_version:
|
65
|
+
specification_version: 3
|
64
66
|
summary: Validate your Rails actions' performance
|
65
67
|
test_files:
|
66
68
|
- spec/performance_promise_spec.rb
|
checksums.yaml
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
---
|
2
|
-
SHA1:
|
3
|
-
metadata.gz: 08746655e263efe79e8f620979c2035a5b9b18ee
|
4
|
-
data.tar.gz: a1d134f39f8a458779282c5c8dbddfd5a93d412a
|
5
|
-
SHA512:
|
6
|
-
metadata.gz: 7d459fb634862cbc63727408f3ce899366e49ef422310e64801ee509fc513da694a5e33c7e45225e7e994fbdf408f122f8420a81bc59612b0c9157468533bb4f
|
7
|
-
data.tar.gz: a123885620753e8a00eec917765d709dc2969b14dfb1e23cac40c2061bef9bf19129ce038329fcde77be927c1bac376aa40cf785347974e1a635006aca563fa1
|