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 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 succesfully visit `/articles`.
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 overriden if you simply want to ignore failing cases (they will still be written to the log).
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. Iff a view fails to be `Speedy`, then the developer is forced to acknowledge it in code.
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 decortators][ruby-decorators] into Ruby.
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, `performace_promise` is applied only in `development` and `test` environments. You can choose to override this, but is strongly discouraged.
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 additonal checks like how long the most expensive query took, whether the action performed any table scans (available through an `EXPLAIN`) etc.
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
 
@@ -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
- if PerformancePromise.configuration.throw_exception
93
- bp = BrokenPromise.new("Broken promise: #{error_message}")
94
- bp.set_backtrace(backtrace)
95
- raise bp
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
- PerformanceValidations.report_promise_passed(method, db_queries, options) unless promise_broken
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
- explained = ActiveRecord::Base.connection.execute("EXPLAIN QUERY PLAN #{sql}", 'SQLR-EXPLAIN')
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].in?(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
- order << "1.query"
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
- 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
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
- error_message = "Promised table scans on #{promised_full_table_scans}, made: #{full_table_scans}"
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 = "promised #{makes}, made #{db_queries.length} (possibly #{guessed_order})"
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]}"
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'performance_promise'
3
- s.version = '1.0.1'
3
+ s.version = '1.0.2'
4
4
  s.date = '2016-01-11'
5
5
  s.summary = 'Validate your Rails actions\' performance'
6
6
  s.description = 'Validate your Rails actions\' performance'
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.1
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
- - ".gitignore"
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: 2.4.8
63
+ rubygems_version: 1.8.23
62
64
  signing_key:
63
- specification_version: 4
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