performance_promise 0.0.1
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 +41 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +202 -0
- data/lib/performance_promise/decorators.rb +86 -0
- data/lib/performance_promise/lazily_evaluated.rb +78 -0
- data/lib/performance_promise/performance.rb +13 -0
- data/lib/performance_promise/performance_validations.rb +16 -0
- data/lib/performance_promise/speedy.rb +8 -0
- data/lib/performance_promise/sql_recorder.rb +41 -0
- data/lib/performance_promise/utils.rb +42 -0
- data/lib/performance_promise/validations/README.md +56 -0
- data/lib/performance_promise/validations/full_table_scans.rb +33 -0
- data/lib/performance_promise/validations/number_of_db_queries.rb +30 -0
- data/lib/performance_promise/validations/time_taken_for_render.rb +13 -0
- data/lib/performance_promise.rb +116 -0
- data/performance_promise.gemspec +13 -0
- data/spec/performance_promise_spec/lazily_evaluated.rb +53 -0
- data/spec/performance_promise_spec/performance.rb +31 -0
- data/spec/performance_promise_spec/speedy.rb +14 -0
- data/spec/performance_promise_spec.rb +230 -0
- data/spec/spec_helper.rb +121 -0
- metadata +70 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0fa56d47d2c2db583f926cd79f5094485853dff9
|
4
|
+
data.tar.gz: 3508721f4c377fe470c1a615d3c84a8e0eeaaaa9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: df5209a3ee5fd6236ed95e65d8b11030376d91ce905c08c081bb4b4bbd4afe405d35b979d69b99279d5f778fb425245d20e1f41c00221f95564b08da89e906f0
|
7
|
+
data.tar.gz: 7865ac4b28c63efc00c279420d6a988eb34b2da69204def8fc36b9ce8cfbd77728449efbe7723d23e3fd8f151f526a34c8d673751362dd92dcb984b1743e49c1
|
data/.gitignore
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
*.swp
|
2
|
+
*.rbc
|
3
|
+
capybara-*.html
|
4
|
+
.rspec
|
5
|
+
/log
|
6
|
+
/tmp
|
7
|
+
/db/*.sqlite3
|
8
|
+
/db/*.sqlite3-journal
|
9
|
+
/public/system
|
10
|
+
/coverage/
|
11
|
+
/spec/tmp
|
12
|
+
**.orig
|
13
|
+
rerun.txt
|
14
|
+
pickle-email-*.html
|
15
|
+
|
16
|
+
# TODO Comment out these rules if you are OK with secrets being uploaded to the repo
|
17
|
+
config/initializers/secret_token.rb
|
18
|
+
config/secrets.yml
|
19
|
+
|
20
|
+
## Environment normalisation:
|
21
|
+
/.bundle
|
22
|
+
/vendor/bundle
|
23
|
+
|
24
|
+
Gemfile.lock
|
25
|
+
|
26
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
27
|
+
.rvmrc
|
28
|
+
|
29
|
+
# if using bower-rails ignore default bower_components path bower.json files
|
30
|
+
/vendor/assets/bower_components
|
31
|
+
*.bowerrc
|
32
|
+
bower.json
|
33
|
+
|
34
|
+
# Ignore pow environment settings
|
35
|
+
.powenv
|
36
|
+
|
37
|
+
# Ignore rbenv
|
38
|
+
.ruby-version
|
39
|
+
|
40
|
+
# Ignore any created gem files
|
41
|
+
*.gem
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Bipin Suresh
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
# Performance Promise
|
2
|
+
The `performance_promise` gem enables you to annotate and validate the performance of your Rails actions.
|
3
|
+
|
4
|
+
You can declare the performance characteristics of your Rails actions in code (right next to the action definition itself), and `performance_promise` will monitor and validate the promise. If the action breaks the promise, the `performance_promise` gem will alert you and provide a helpful suggestion with a passing performance annotation.
|
5
|
+
|
6
|
+
Example syntax:
|
7
|
+
```ruby
|
8
|
+
class ArticlesController < ApplicationController
|
9
|
+
|
10
|
+
Performance :makes => 1.query,
|
11
|
+
:full_table_scans => [Article]
|
12
|
+
def index
|
13
|
+
# ...
|
14
|
+
end
|
15
|
+
|
16
|
+
Performance :makes => 1.query + Article.N.queries,
|
17
|
+
:full_table_scans => [Article, Comment]
|
18
|
+
def expensive_action
|
19
|
+
# ...
|
20
|
+
end
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
You may also choose to enable a default/minimum performance promise for _all_ actions by turning on the `untagged_methods_are_speedy` config parameter ([see here](https://github.com/bipsandbytes/performance_promise#untagged_methods_are_speedy-bool)). **You can reap all the benefits of performance validation without having to make any changes to code except tagging expensive actions**.
|
25
|
+
|
26
|
+
|
27
|
+
## Installation
|
28
|
+
You can install it as a gem:
|
29
|
+
```sh
|
30
|
+
gem install performance_promise
|
31
|
+
```
|
32
|
+
|
33
|
+
or add it to a Gemfile (Bundler):
|
34
|
+
```ruby
|
35
|
+
group :development, :test do
|
36
|
+
gem 'performance_promise'
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
## Building
|
41
|
+
You can build the gem yourself:
|
42
|
+
```sh
|
43
|
+
gem build performance_promise.gemspec
|
44
|
+
```
|
45
|
+
|
46
|
+
## Configuration
|
47
|
+
For safety, `performance_promise` is disabled by default. To enable it, create a new file `config/initializers/performance_promise.rb` with the following code:
|
48
|
+
```ruby
|
49
|
+
require 'performance_promise/performance.rb'
|
50
|
+
|
51
|
+
PerformancePromise.configure do |config|
|
52
|
+
config.enable = true
|
53
|
+
# config.validations = [
|
54
|
+
# :makes, # validate the number of DB queries made
|
55
|
+
# ]
|
56
|
+
# config.untagged_methods_are_speedy = true
|
57
|
+
# config.speedy_promise = {
|
58
|
+
# :makes => 2,
|
59
|
+
# }
|
60
|
+
# config.allowed_environments = [
|
61
|
+
# 'development',
|
62
|
+
# 'test',
|
63
|
+
# ]
|
64
|
+
# config.logger = Rails.logger
|
65
|
+
# config.throw_exception = true
|
66
|
+
end
|
67
|
+
PerformancePromise.start
|
68
|
+
```
|
69
|
+
|
70
|
+
## Usage
|
71
|
+
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
|
+
|
73
|
+
Here is a simple controller:
|
74
|
+
```ruby
|
75
|
+
class ArticlesController < ApplicationController
|
76
|
+
def index
|
77
|
+
@articles = Article.all
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
Assuming your routes and views are setup, you should be able to succesfully visit `/articles`.
|
82
|
+
|
83
|
+
You can annotate this action with a promise of how many database queries the action will make so:
|
84
|
+
```ruby
|
85
|
+
class ArticlesController < ApplicationController
|
86
|
+
|
87
|
+
Performance :makes => 1.query
|
88
|
+
def index
|
89
|
+
@articles = Article.all
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
```
|
94
|
+
Visit `/articles` to confirm that the view is rendered successfully again.
|
95
|
+
|
96
|
+
Now suppose, you make the view more complex, causing it to execute more database queries
|
97
|
+
```ruby
|
98
|
+
Performance :makes => 1.query
|
99
|
+
def index
|
100
|
+
@articles = Article.all
|
101
|
+
@total_comments = 0
|
102
|
+
@articles.each do |article|
|
103
|
+
@total_comments += article.comments.length
|
104
|
+
end
|
105
|
+
puts @total_comments
|
106
|
+
end
|
107
|
+
```
|
108
|
+
Since the performance annotation has not been updated, visiting `/articles` now will throw an exception. The exception tells you that the performance of your view does not respect the annotation promise.
|
109
|
+
|
110
|
+

|
111
|
+
|
112
|
+
Let's update the annotation:
|
113
|
+
```ruby
|
114
|
+
Performance :makes => 1.query + Article.N.queries
|
115
|
+
def index
|
116
|
+
@articles = Article.all
|
117
|
+
@total_comments = 0
|
118
|
+
@articles.each do |article|
|
119
|
+
@total_comments += article.comments.length
|
120
|
+
end
|
121
|
+
puts @total_comments
|
122
|
+
end
|
123
|
+
```
|
124
|
+
Now that you have annotated the action correctly, visiting `/articles` renders successfully.
|
125
|
+
|
126
|
+
The experienced code-reviewer however might ask the author to get rid of the `N + 1` query here, and use `.includes` instead:
|
127
|
+
```ruby
|
128
|
+
Performance :makes => 2.queries
|
129
|
+
def index
|
130
|
+
@articles = Article.all.includes(:comments)
|
131
|
+
@total_comments = 0
|
132
|
+
@articles.each do |article|
|
133
|
+
@total_comments += article.comments.length
|
134
|
+
end
|
135
|
+
puts @total_comments
|
136
|
+
end
|
137
|
+
```
|
138
|
+
And now, we've successfully caught and averted a bad code commit!
|
139
|
+
|
140
|
+
## Advanced configuration
|
141
|
+
`performance_promise` opens up more functionality through configuration variables:
|
142
|
+
|
143
|
+
#### `allowed_environments: array`
|
144
|
+
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
|
+
|
146
|
+
#### `throw_exception: bool`
|
147
|
+
Tells `performance_promise` whether to throw an exception. Set to `true` by default, but can be overriden if you simply want to ignore failing cases (they will still be written to the log).
|
148
|
+
|
149
|
+
#### `speedy_promise: hash`
|
150
|
+
If you do not care to determine the _exact_ performance of your action, you can still simply mark it as `Speedy`:
|
151
|
+
```ruby
|
152
|
+
Speedy()
|
153
|
+
def index
|
154
|
+
...
|
155
|
+
end
|
156
|
+
```
|
157
|
+
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
|
+
|
159
|
+
#### `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. Iff a view fails to be `Speedy`, then the developer is forced to acknowledge it in code.
|
161
|
+
|
162
|
+
|
163
|
+
## FAQ
|
164
|
+
> **What is the strange syntax? Is it a function call? Is it a method?**
|
165
|
+
|
166
|
+
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
|
+
|
168
|
+
Credit goes to [Yehuda Katz][yehuda-katz] for the [port of decortators][ruby-decorators] into Ruby.
|
169
|
+
|
170
|
+
> **Will this affect my production service?**
|
171
|
+
|
172
|
+
By default, `performace_promise` is applied only in `development` and `test` environments. You can choose to override this, but is strongly discouraged.
|
173
|
+
|
174
|
+
|
175
|
+
> **What are some other kinds of performance guarantees that I can make with `performance_promise`?**
|
176
|
+
|
177
|
+
In addition to promises about the number of database queries, you can also make promises on how long the entire view will take to render, and whether it performs any table scans.
|
178
|
+
```ruby
|
179
|
+
Performance :makes => 1.query + Article.N.queries,
|
180
|
+
:takes => 1.second,
|
181
|
+
:full_tables_scans => [Article]
|
182
|
+
def index
|
183
|
+
...
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
If you come up with other validations that you think will be useful, please consider sharing it with the community by [writing your own plugin here](https://github.com/bipsandbytes/performance_promise/tree/master/lib/performance_promise/validations), and raising a Pull Request.
|
188
|
+
|
189
|
+
> **Is this the same as [Bullet][bullet] gem?**
|
190
|
+
|
191
|
+
[Bullet][bullet] is a great piece of software that allows developers to help identify N + 1 queries and unused eager loading. It does this by watching your application in development mode, and alerting you when it does either of those things.
|
192
|
+
|
193
|
+
`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
|
+
|
195
|
+
`performance_promise` also has access to the entire database query object. In the future, `performance_promise` can be tuned to perform additonal checks like how long the most expensive query took, whether the action performed any table scans (available through an `EXPLAIN`) etc.
|
196
|
+
|
197
|
+
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
|
+
|
199
|
+
[rails-getting-started]: <http://guides.rubyonrails.org/getting_started.html>
|
200
|
+
[bullet]: <https://github.com/flyerhzm/bullet>
|
201
|
+
[yehuda-katz]: <http://yehudakatz.com/>
|
202
|
+
[ruby-decorators]: <http://yehudakatz.com/2009/07/11/python-decorators-in-ruby/>
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# An implementation of pythonish decorators in Ruby
|
2
|
+
# Credits: Yehuda Katz (http://yehudakatz.com/)
|
3
|
+
# https://github.com/wycats/ruby_decorators
|
4
|
+
|
5
|
+
|
6
|
+
module MethodDecorators
|
7
|
+
def self.extended(klass)
|
8
|
+
class << klass
|
9
|
+
attr_accessor :decorated_methods
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(name, *args, &blk)
|
14
|
+
if Object.const_defined?(name)
|
15
|
+
const = Object.const_get(name)
|
16
|
+
elsif Decorator.decorators.key?(name)
|
17
|
+
const = Decorator.decorators[name]
|
18
|
+
else
|
19
|
+
return super
|
20
|
+
end
|
21
|
+
|
22
|
+
instance_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
23
|
+
def #{name}(*args, &blk)
|
24
|
+
decorate(#{const.name}, *args, &blk)
|
25
|
+
end
|
26
|
+
ruby_eval
|
27
|
+
|
28
|
+
send(name, *args, &blk)
|
29
|
+
end
|
30
|
+
|
31
|
+
def method_added(name)
|
32
|
+
return unless @decorators
|
33
|
+
|
34
|
+
decorators = @decorators.dup
|
35
|
+
@decorators = nil
|
36
|
+
@decorated_methods ||= Hash.new {|h,k| h[k] = []}
|
37
|
+
|
38
|
+
class << self; attr_accessor :decorated_methods; end
|
39
|
+
|
40
|
+
decorators.each do |klass, args|
|
41
|
+
decorator = klass.respond_to?(:new) ? klass.new(self, instance_method(name), *args) : klass
|
42
|
+
@decorated_methods[name] << decorator
|
43
|
+
end
|
44
|
+
|
45
|
+
class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
46
|
+
def #{name}(*args, &blk)
|
47
|
+
ret = nil
|
48
|
+
self.class.decorated_methods[#{name.inspect}].each do |decorator|
|
49
|
+
ret = decorator.call(self, *args, &blk)
|
50
|
+
end
|
51
|
+
ret
|
52
|
+
end
|
53
|
+
ruby_eval
|
54
|
+
end
|
55
|
+
|
56
|
+
def decorate(klass, *args)
|
57
|
+
@decorators ||= []
|
58
|
+
@decorators << [klass, args]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Decorator
|
63
|
+
class << self
|
64
|
+
attr_accessor :decorators
|
65
|
+
def decorator_name(name)
|
66
|
+
Decorator.decorators ||= {}
|
67
|
+
Decorator.decorators[name] = self
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.inherited(klass)
|
72
|
+
name = klass.name.gsub(/^./) {|m| m.downcase}
|
73
|
+
|
74
|
+
return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
|
75
|
+
|
76
|
+
MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
77
|
+
def #{klass}(*args, &blk)
|
78
|
+
decorate(#{klass}, *args, &blk)
|
79
|
+
end
|
80
|
+
ruby_eval
|
81
|
+
end
|
82
|
+
|
83
|
+
def initialize(klass, method)
|
84
|
+
@method = method
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'performance_promise.rb'
|
2
|
+
|
3
|
+
|
4
|
+
class LazilyEvaluated
|
5
|
+
def initialize(c)
|
6
|
+
if c.is_a?(Fixnum)
|
7
|
+
@operand1 = c
|
8
|
+
@operator = 'constant'
|
9
|
+
elsif c.is_a?(Class)
|
10
|
+
@operand1 = c
|
11
|
+
@operator = 'model'
|
12
|
+
elsif c.is_a?(Array)
|
13
|
+
@operand1, @operand2, @operator = c
|
14
|
+
else
|
15
|
+
raise
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def +(other)
|
20
|
+
return LazilyEvaluated.new([self, other, '+'])
|
21
|
+
end
|
22
|
+
|
23
|
+
def -(other)
|
24
|
+
return LazilyEvaluated.new([self, other, '-'])
|
25
|
+
end
|
26
|
+
|
27
|
+
def *(other)
|
28
|
+
return LazilyEvaluated.new([self, other, '*'])
|
29
|
+
end
|
30
|
+
|
31
|
+
def /(other)
|
32
|
+
return LazilyEvaluated.new([self, other, '/'])
|
33
|
+
end
|
34
|
+
|
35
|
+
def evaluate
|
36
|
+
# don't do anything in prod-like environments
|
37
|
+
return 0 unless PerformancePromise.configuration.allowed_environments.include?(Rails.env)
|
38
|
+
|
39
|
+
case @operator
|
40
|
+
when 'constant'
|
41
|
+
return @operand1
|
42
|
+
when 'model'
|
43
|
+
return @operand1.count
|
44
|
+
when '+'
|
45
|
+
return @operand1.evaluate + @operand2.evaluate
|
46
|
+
when '-'
|
47
|
+
return @operand1.evaluate - @operand2.evaluate
|
48
|
+
when '*'
|
49
|
+
return @operand1.evaluate * @operand2.evaluate
|
50
|
+
when '/'
|
51
|
+
return @operand1.evaluate / @operand2.evaluate
|
52
|
+
else
|
53
|
+
raise
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def queries
|
58
|
+
# syntactic sugar
|
59
|
+
self
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
class Fixnum
|
65
|
+
def queries
|
66
|
+
LazilyEvaluated.new(self)
|
67
|
+
end
|
68
|
+
alias query queries
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
module ActiveRecord
|
73
|
+
class Base
|
74
|
+
def self.N
|
75
|
+
LazilyEvaluated.new(self)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'performance_promise.rb'
|
2
|
+
|
3
|
+
|
4
|
+
class Performance < Decorator
|
5
|
+
def initialize(klass, method, options)
|
6
|
+
@klass, @method = klass, method
|
7
|
+
PerformancePromise.promises["#{klass}\##{method.name.to_s}"] = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(this, *args)
|
11
|
+
@method.bind(this).call(*args)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'performance_promise/validations/number_of_db_queries.rb'
|
2
|
+
require 'performance_promise/validations/time_taken_for_render.rb'
|
3
|
+
require 'performance_promise/validations/full_table_scans.rb'
|
4
|
+
|
5
|
+
|
6
|
+
module PerformanceValidations
|
7
|
+
extend ValidateNumberOfQueries
|
8
|
+
extend ValidateTimeTakenForRender
|
9
|
+
extend ValidateFullTableScans
|
10
|
+
|
11
|
+
def self.report_promise_passed(method, db_queries, options)
|
12
|
+
PerformancePromise.configuration.logger.warn '-' * 80
|
13
|
+
PerformancePromise.configuration.logger.warn Utils.colored(:green, "Passed promise on #{method}")
|
14
|
+
PerformancePromise.configuration.logger.warn '-' * 80
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
|
4
|
+
class SQLRecorder
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
@db_queries = []
|
8
|
+
def flush
|
9
|
+
captured_queries = @db_queries
|
10
|
+
@db_queries = []
|
11
|
+
return captured_queries
|
12
|
+
end
|
13
|
+
|
14
|
+
def record(payload, duration)
|
15
|
+
return if invalid_payload?(payload)
|
16
|
+
sql = payload[:sql]
|
17
|
+
cleaned_trace = clean_trace(caller)
|
18
|
+
explained = ActiveRecord::Base.connection.execute("EXPLAIN QUERY PLAN #{sql}", 'SQLR-EXPLAIN')
|
19
|
+
@db_queries << {
|
20
|
+
:sql => sql,
|
21
|
+
:duration => duration,
|
22
|
+
:trace => cleaned_trace,
|
23
|
+
:explained => explained,
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def invalid_payload?(payload)
|
28
|
+
ignore_query_names = [
|
29
|
+
'SCHEMA',
|
30
|
+
'SQLR-EXPLAIN',
|
31
|
+
]
|
32
|
+
payload[:name] && ignore_query_names.any? { |name| payload[:name].in?(name) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def clean_trace(trace)
|
36
|
+
Rails.backtrace_cleaner.remove_silencers!
|
37
|
+
Rails.backtrace_cleaner.add_silencer { |line| not line =~ /^(app)\// }
|
38
|
+
Rails.backtrace_cleaner.clean(trace)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
SQLRecorder.instance.flush
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Utils
|
2
|
+
def self.summarize_queries(db_queries)
|
3
|
+
summary = Hash.new(0)
|
4
|
+
db_queries.each do |query|
|
5
|
+
summary[query.except(:duration)] += 1
|
6
|
+
end
|
7
|
+
summary
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.guess_order(db_queries)
|
11
|
+
order = []
|
12
|
+
queries_with_count = summarize_queries(db_queries)
|
13
|
+
queries_with_count.each do |query, count|
|
14
|
+
if count == 1
|
15
|
+
order << "1.query"
|
16
|
+
else
|
17
|
+
if (lookup_field = /WHERE .*"(.*?_id)" = \?/.match(query[:sql]))
|
18
|
+
klass = lookup_field[1].humanize
|
19
|
+
order << "#{klass}.N.queries"
|
20
|
+
else
|
21
|
+
order << "n(???)"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
order.join(" + ")
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.colored(color, string)
|
30
|
+
color =
|
31
|
+
case color
|
32
|
+
when :red
|
33
|
+
"\e[31m"
|
34
|
+
when :green
|
35
|
+
"\e[32m"
|
36
|
+
when :cyan
|
37
|
+
"\e[36m"
|
38
|
+
end
|
39
|
+
end_color = "\e[0m"
|
40
|
+
"#{color}#{string}#{end_color}"
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Creating your own validations
|
2
|
+
|
3
|
+
You can easily extend `performance_promise` by writing your own validations. You do it in 3 steps:
|
4
|
+
|
5
|
+
1. Write your validation
|
6
|
+
2. Register your validation
|
7
|
+
3. Enable your validation in configuration
|
8
|
+
|
9
|
+
## Write your validation
|
10
|
+
Create a new file in this directory, with a single function:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
module MODULE_NAME
|
14
|
+
def validate_NAME(db_queries, render_time, promised)
|
15
|
+
...
|
16
|
+
end
|
17
|
+
end
|
18
|
+
```
|
19
|
+
|
20
|
+
* `MODULE_NAME`: A name for the module.
|
21
|
+
* `NAME`: The symbol that will be used in the `Performance` promise. For example, if the `NAME` is `makes`, then the function function name will be `validates_makes`, and the `Promise` will take an option `:makes`.
|
22
|
+
* `validates_NAME`: A function that is called if the a function makes a promise with that `NAME`.
|
23
|
+
|
24
|
+
The function takes 3 parameters:
|
25
|
+
* `db_queries`: An `array` of database queries which can be inspected.
|
26
|
+
* `render_time`: Time it took to render the view.
|
27
|
+
* `promised`: The performance guarantee made by the author.
|
28
|
+
|
29
|
+
And returns 3 parameters:
|
30
|
+
* `passes`: Whether the promised made by the author in `promised` is upheld.
|
31
|
+
* `error_message`: An error message to show to the user explaining why the promise failed, and a best guess of how to fix it, if possible.
|
32
|
+
* `backtrace`: If possible, an `array` showing the codepath that caused the promise to be broken.
|
33
|
+
|
34
|
+
See [time_taken_for_render](https://github.com/bipsandbytes/performance_promise/blob/master/lib/performance_promise/validations/time_taken_for_render.rb) for a simple example.
|
35
|
+
|
36
|
+
## Register your validation
|
37
|
+
You are now ready to register this plugin. Simply add this plugin to the list of validations in [performace_validations](https://github.com/bipsandbytes/performance_promise/blob/master/lib/performance_promise/performance_validations.rb):
|
38
|
+
```ruby
|
39
|
+
module PerformanceValidations
|
40
|
+
...
|
41
|
+
extend MODULE_NAME
|
42
|
+
...
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
## Enable your validation in configuration
|
47
|
+
You are now ready to include this validation in your configuration file:
|
48
|
+
```ruby
|
49
|
+
PerformancePromise.configure do |config|
|
50
|
+
config.enable = true
|
51
|
+
config.validations = [
|
52
|
+
...
|
53
|
+
:NAME,
|
54
|
+
]
|
55
|
+
end
|
56
|
+
```
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ValidateFullTableScans
|
2
|
+
def validate_full_table_scans(db_queries, render_time, promised)
|
3
|
+
full_table_scans = []
|
4
|
+
|
5
|
+
# check the explained queries to see if there were any
|
6
|
+
# SCAN TABLEs
|
7
|
+
db_queries.each do |db_query|
|
8
|
+
detail = db_query[:explained][0]['detail']
|
9
|
+
makes_full_table_scan = detail.match(/SCAN TABLE (.*)/)
|
10
|
+
if makes_full_table_scan
|
11
|
+
table_name = makes_full_table_scan[1]
|
12
|
+
full_table_scans << table_name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# we do not care about duplicates
|
17
|
+
full_table_scans = full_table_scans.uniq
|
18
|
+
|
19
|
+
# map the models in the promise to their corresponding table names
|
20
|
+
promised_full_table_scans = promised.map { |model| model.table_name }
|
21
|
+
|
22
|
+
# check that the performed FTSs are a subset of the promised FTSs
|
23
|
+
passes = (full_table_scans & promised_full_table_scans == full_table_scans)
|
24
|
+
error_message = ''
|
25
|
+
backtrace = []
|
26
|
+
|
27
|
+
unless passes
|
28
|
+
error_message = "Promised table scans on #{promised_full_table_scans}, made: #{full_table_scans}"
|
29
|
+
end
|
30
|
+
|
31
|
+
return passes, error_message, backtrace
|
32
|
+
end
|
33
|
+
end
|