runtime_profiler 0.1.0 → 0.4.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 +5 -5
- data/.semaphore/semaphore.yml +18 -0
- data/README.md +112 -16
- data/lib/runtime_profiler.rb +13 -17
- data/lib/runtime_profiler/callbacks/action_controller.rb +1 -0
- data/lib/runtime_profiler/callbacks/active_record.rb +2 -2
- data/lib/runtime_profiler/cli.rb +17 -12
- data/lib/runtime_profiler/events/process_action_event.rb +1 -1
- data/lib/runtime_profiler/events/sql_event.rb +2 -0
- data/lib/runtime_profiler/instrumentation_data.rb +28 -25
- data/lib/runtime_profiler/profiler.rb +7 -7
- data/lib/runtime_profiler/text_report.rb +148 -41
- data/lib/runtime_profiler/version.rb +1 -1
- data/rp_view_only_methods_full_details.png +0 -0
- data/runtime_profiler.gemspec +8 -8
- metadata +36 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cfc4edf1ad5783057cfd0760d11e49a8d91665b71c33b6e809a0796a7d51cb83
|
4
|
+
data.tar.gz: cb19a035d46cae60fd4d16c626fac305a47fab4aaf28134feb47883a9f8a5d94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d85ca30cfa19fab9739ca87d6fb0fd1e4cddcf43ed0c5af8c1eab7d867ab24e5dfb11c4e0ad37cbac48a5bf5c149214e40899bd5e4e0a5555bef22d68efd60f
|
7
|
+
data.tar.gz: 6f3ab5d14e1cb231bd4c55156270190346d1de5f1aac3a4c3b09ca992ec577e105b35197a24022f63687034bb07df2f168249a6f72b5adb3ee8a97d4e79282d2
|
@@ -0,0 +1,18 @@
|
|
1
|
+
version: v1.0
|
2
|
+
name: Ruby
|
3
|
+
agent:
|
4
|
+
machine:
|
5
|
+
type: e1-standard-2
|
6
|
+
os_image: ubuntu1804
|
7
|
+
blocks:
|
8
|
+
- name: test
|
9
|
+
task:
|
10
|
+
jobs:
|
11
|
+
- name: test
|
12
|
+
commands:
|
13
|
+
- checkout
|
14
|
+
- sem-version ruby 2.6.5
|
15
|
+
- cache restore
|
16
|
+
- bundle install --path vendor/bundle
|
17
|
+
- cache store
|
18
|
+
- bundle exec rake
|
data/README.md
CHANGED
@@ -1,20 +1,36 @@
|
|
1
|
-
# runtime_profiler
|
1
|
+
# runtime_profiler
|
2
2
|
|
3
|
-
|
3
|
+
*A runtime profiler for Rails applications.*
|
4
4
|
|
5
|
-
|
5
|
+
Check which part of your Rails application is causing slow response time. **runtime_profiler** gives you an easy way to find performance problems by profiling an endpoint or a method in your Rails application.
|
6
6
|
|
7
|
-
|
7
|
+
[](https://wnuqui.semaphoreci.com/projects/runtime_profiler)
|
8
8
|
|
9
|
-
|
9
|
+
## Table of contents
|
10
10
|
|
11
|
-
|
11
|
+
- [Getting Started](#getting-started)
|
12
|
+
- [Installing](#installing)
|
13
|
+
- [Profiling](#profiling)
|
14
|
+
- [Structure](#structure)
|
15
|
+
- [Examples](#examples)
|
16
|
+
- [Viewing Profiling Result](#viewing-profiling-result)
|
17
|
+
- [view Options](#view-options)
|
18
|
+
- [Screenshot](#screenshot)
|
19
|
+
- [Configurations](#configurations)
|
20
|
+
- [Development](#development)
|
21
|
+
- [Contributing](#contributing)
|
22
|
+
- [Acknowledgement](#acknowledgement)
|
23
|
+
- [License](#license)
|
24
|
+
|
25
|
+
## Getting Started
|
26
|
+
|
27
|
+
### Installing
|
12
28
|
|
13
29
|
Add this line to your application's Gemfile:
|
14
30
|
|
15
31
|
```ruby
|
32
|
+
# In your Gemfile
|
16
33
|
group :development, :test do
|
17
|
-
... ...
|
18
34
|
gem 'runtime_profiler'
|
19
35
|
end
|
20
36
|
```
|
@@ -23,13 +39,33 @@ And then execute:
|
|
23
39
|
|
24
40
|
$ bundle
|
25
41
|
|
26
|
-
|
42
|
+
Or install it yourself as:
|
43
|
+
|
44
|
+
$ gem install runtime_profiler
|
45
|
+
|
46
|
+
### Profiling
|
47
|
+
|
48
|
+
#### Structure
|
49
|
+
|
50
|
+
To profile a specific class (model, controller, etc), all you need to do is to wrap a line where the target class (or instance) is calling a method (entry point of profiling).
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
# Profiles runtime of `ClassToProfile` class.
|
54
|
+
RuntimeProfiler.profile!('description', [ClassToProfile]) {
|
55
|
+
# one line where `ClassToProfile` (or its instance) is calling a method
|
56
|
+
}
|
57
|
+
```
|
58
|
+
|
59
|
+
Since the second argument of `.profile!` accepts array of classes, then you can provide all target classes that you want to profile.
|
60
|
+
|
61
|
+
#### Examples
|
27
62
|
|
28
|
-
|
63
|
+
You can make a test that targets a particular endpoint (or even just a method) and use `RuntimeProfiler.profile!` method in the test.
|
29
64
|
|
30
|
-
Example:
|
31
65
|
```ruby
|
32
66
|
it 'updates user' do
|
67
|
+
# Profiles runtime of PUT /users/:id endpoint and
|
68
|
+
# specifically interested with the methods of `User` model.
|
33
69
|
RuntimeProfiler.profile!('updates user', [User]) {
|
34
70
|
patch :update, { id: user.id, name: 'Joe' }
|
35
71
|
}
|
@@ -38,19 +74,79 @@ it 'updates user' do
|
|
38
74
|
end
|
39
75
|
```
|
40
76
|
|
41
|
-
Run
|
77
|
+
Run the test as usual and follow printed instructions after running.
|
78
|
+
|
79
|
+
Or if you prefer writing just code snippet, then just wrap the snippet with `RuntimeProfiler.profile!` method:
|
80
|
+
```ruby
|
81
|
+
# Profiles runtime of `UserMailer` mailer.
|
82
|
+
RuntimeProfiler.profile!('UserMailer', [UserMailer]) {
|
83
|
+
user = User.last
|
84
|
+
UserMailer.with(user: user).weekly_summary.deliver_now
|
85
|
+
}
|
86
|
+
```
|
87
|
+
|
88
|
+
**Note:** The code (test or not) where `RuntimeProfiler.profile!` is used must be **free from any mocking/stubbing** since the goal is to check performance bottlenecks.
|
89
|
+
|
90
|
+
### Viewing Profiling Result
|
91
|
+
|
92
|
+
To see profiling report, you can open the report in browser with JSON viewer report. Or you can run the following command:
|
93
|
+
|
94
|
+
```bash
|
95
|
+
bundle exec runtime_profiler view tmp/rp-124094-1608308786.json
|
96
|
+
```
|
42
97
|
|
43
|
-
|
98
|
+
#### view options
|
44
99
|
|
45
|
-
|
100
|
+
Here are the command line options for `runtime_profiler view` command.
|
46
101
|
|
47
102
|
```bash
|
48
|
-
bundle exec runtime_profiler view
|
103
|
+
$ bundle exec runtime_profiler view --help
|
104
|
+
|
105
|
+
NAME:
|
106
|
+
|
107
|
+
view
|
108
|
+
|
109
|
+
SYNOPSIS:
|
110
|
+
|
111
|
+
runtime_profiler view <profile.report.json> [options]
|
112
|
+
|
113
|
+
DESCRIPTION:
|
114
|
+
|
115
|
+
Display report in console given the JSON report file
|
116
|
+
|
117
|
+
OPTIONS:
|
118
|
+
|
119
|
+
--sort-by COLUMN
|
120
|
+
Sort by COLUMN. COLUMN can be "max_runtime", total_calls" or "total_runtime". Default is "max_runtime".
|
121
|
+
|
122
|
+
--details TYPE
|
123
|
+
TYPE can be "full" or "summary". Default is "summary"
|
124
|
+
|
125
|
+
--only-sqls
|
126
|
+
Show only SQL queries. Default is false.
|
127
|
+
|
128
|
+
--only-methods
|
129
|
+
Show only methods. Default is false.
|
130
|
+
|
131
|
+
--runtime-above RUNTIME
|
132
|
+
RUNTIME is integer or float value in ms.
|
133
|
+
|
134
|
+
--calls-above CALLS
|
135
|
+
CALLS is integer value.
|
136
|
+
|
137
|
+
--rounding ROUNDING
|
138
|
+
ROUNDING is integer value. Used in rounding runtimes. Default is 4.
|
49
139
|
```
|
50
140
|
|
51
|
-
|
141
|
+
### Screenshot
|
142
|
+
|
143
|
+
<p align="center">
|
144
|
+
<img src="rp_view_only_methods_full_details.png">
|
145
|
+
</p>
|
146
|
+
|
147
|
+
### Configurations
|
52
148
|
|
53
|
-
All the configurable variables and their defaults are listed below
|
149
|
+
All the configurable variables and their defaults are listed below. There is no one correct place where to put these configurations. It can be inside `config/initializers` folder of your Rails application. Or if you are using test to profile, it can be in the last part of `spec/spec_helper.rb`.
|
54
150
|
```ruby
|
55
151
|
RuntimeProfiler.output_path = File.join(Rails.root.to_s, 'tmp')
|
56
152
|
RuntimeProfiler.instrumented_constants = [User]
|
data/lib/runtime_profiler.rb
CHANGED
@@ -11,24 +11,32 @@ module RuntimeProfiler
|
|
11
11
|
end
|
12
12
|
|
13
13
|
config_accessor :instrumented_paths do
|
14
|
-
%w
|
14
|
+
%w[app lib]
|
15
15
|
end
|
16
16
|
|
17
17
|
config_accessor :instrumented_sql_commands do
|
18
|
-
%w
|
18
|
+
%w[SELECT INSERT UPDATE DELETE]
|
19
19
|
end
|
20
20
|
|
21
21
|
config_accessor :output_path do
|
22
|
-
if defined?(Rails) && Rails.respond_to?(:root)
|
22
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
23
23
|
File.join(Rails.root.to_s, 'tmp')
|
24
24
|
else
|
25
25
|
'tmp'
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
config_accessor :excepted_methods do
|
30
|
+
[]
|
31
|
+
end
|
32
|
+
|
29
33
|
class << self
|
30
34
|
def configure
|
31
|
-
|
35
|
+
begin
|
36
|
+
Rails.application.eager_load!
|
37
|
+
rescue StandardError
|
38
|
+
nil
|
39
|
+
end
|
32
40
|
yield self if block_given?
|
33
41
|
end
|
34
42
|
|
@@ -41,17 +49,5 @@ module RuntimeProfiler
|
|
41
49
|
|
42
50
|
profiler.save_instrumentation_data
|
43
51
|
end
|
44
|
-
|
45
|
-
def runtime(label='for the block')
|
46
|
-
result = nil
|
47
|
-
|
48
|
-
elapsed_time = Benchmark.realtime { result = yield }
|
49
|
-
|
50
|
-
puts
|
51
|
-
puts '~~~~> ELAPSED TIME (%s): %s' % [label, elapsed_time * 1000]
|
52
|
-
puts
|
53
|
-
|
54
|
-
result
|
55
|
-
end
|
56
52
|
end
|
57
|
-
end
|
53
|
+
end
|
@@ -26,12 +26,12 @@ module RuntimeProfiler
|
|
26
26
|
|
27
27
|
@data[key][:sql] = event.sanitized_sql
|
28
28
|
@data[key][:runtimes] = [
|
29
|
-
[
|
29
|
+
[event.total_runtime, event.trace.first]
|
30
30
|
]
|
31
31
|
end
|
32
32
|
|
33
33
|
def update(event)
|
34
|
-
@data[event.key][:runtimes] << [
|
34
|
+
@data[event.key][:runtimes] << [event.total_runtime, event.trace.first]
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
data/lib/runtime_profiler/cli.rb
CHANGED
@@ -16,32 +16,37 @@ module RuntimeProfiler
|
|
16
16
|
c.syntax = 'runtime_profiler view <profile.report.json> [options]'
|
17
17
|
c.description = 'Display report in console given the JSON report file'
|
18
18
|
|
19
|
-
c.option '--sort-by COLUMN', String,
|
20
|
-
c.option '--details TYPE', String,
|
21
|
-
c.option '--
|
22
|
-
c.option '--only-
|
23
|
-
c.option '--
|
24
|
-
c.option '--calls-above CALLS',
|
19
|
+
c.option '--sort-by COLUMN', String, 'Sort by COLUMN. COLUMN can be "max_runtime", total_calls" or "total_runtime". Default is "max_runtime".'
|
20
|
+
c.option '--details TYPE', String, 'TYPE can be "full" or "summary". Default is "summary"'
|
21
|
+
c.option '--only-sqls', String, 'Show only SQL queries. Default is false.'
|
22
|
+
c.option '--only-methods', String, 'Show only methods. Default is false.'
|
23
|
+
c.option '--runtime-above RUNTIME', Float, 'RUNTIME is integer or float value in ms.'
|
24
|
+
c.option '--calls-above CALLS', Integer, 'CALLS is integer value.'
|
25
|
+
c.option '--rounding ROUNDING', Integer, 'ROUNDING is integer value. Used in rounding runtimes. Default is 4.'
|
25
26
|
|
26
27
|
c.action do |args, options|
|
27
28
|
default_options = {
|
28
|
-
sort_by: '
|
29
|
+
sort_by: 'max_runtime',
|
29
30
|
details: 'summary',
|
30
31
|
runtime_above: 0,
|
31
32
|
only_sqls: false,
|
32
33
|
only_methods: false,
|
33
|
-
calls_above:
|
34
|
+
calls_above: 0,
|
35
|
+
rounding: 4
|
34
36
|
}
|
35
37
|
|
36
38
|
options.default default_options
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
-
|
40
|
+
if args.first.nil?
|
41
|
+
say 'You need to supply <profile.report.json> as first argument of view.'
|
42
|
+
else
|
43
|
+
report = RuntimeProfiler::TextReport.new(args.first, options)
|
44
|
+
report.print
|
45
|
+
end
|
41
46
|
end
|
42
47
|
end
|
43
48
|
|
44
49
|
run!
|
45
50
|
end
|
46
51
|
end
|
47
|
-
end
|
52
|
+
end
|
@@ -3,14 +3,39 @@ module RuntimeProfiler
|
|
3
3
|
attr_reader :controller_data, :sql_data
|
4
4
|
|
5
5
|
def initialize(controller_data: nil, sql_data: nil)
|
6
|
-
@controller_data = controller_data
|
6
|
+
@controller_data = controller_data || {}
|
7
7
|
@sql_data = sql_data
|
8
8
|
end
|
9
9
|
|
10
10
|
def persist!
|
11
|
-
|
11
|
+
File.open(output_file, 'w') do |f|
|
12
|
+
f.write JSON.dump(instrumentation_data)
|
13
|
+
end
|
12
14
|
|
13
|
-
|
15
|
+
puts "\n"
|
16
|
+
puts 'Profiling data written at ' + output_file.to_s
|
17
|
+
puts 'You can view profiling data via: bundle exec runtime_profiler view ' + output_file.to_s
|
18
|
+
puts "\n"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def output_file
|
24
|
+
FileUtils.mkdir_p(RuntimeProfiler.output_path)
|
25
|
+
filename = ['rp', Process.pid, Time.now.to_i].join('-') + '.json'
|
26
|
+
File.join(RuntimeProfiler.output_path, filename)
|
27
|
+
end
|
28
|
+
|
29
|
+
def instrumented_api
|
30
|
+
return unless controller_data[:payload]
|
31
|
+
@instrumented_api ||= [
|
32
|
+
controller_data[:payload][:controller],
|
33
|
+
controller_data[:payload][:action]
|
34
|
+
].join('#')
|
35
|
+
end
|
36
|
+
|
37
|
+
def instrumentation_data
|
38
|
+
@instrumentation_data ||= {
|
14
39
|
instrumentation: {
|
15
40
|
instrumented_api: instrumented_api,
|
16
41
|
summary: {
|
@@ -29,30 +54,8 @@ module RuntimeProfiler
|
|
29
54
|
instrumented_at: Time.now
|
30
55
|
}
|
31
56
|
}
|
32
|
-
|
33
|
-
FileUtils.mkdir_p(RuntimeProfiler.output_path)
|
34
|
-
filename = ['runtime-profiling', Process.pid, Time.now.to_i].join('-') << '.json'
|
35
|
-
output_file = File.join(RuntimeProfiler.output_path, filename)
|
36
|
-
|
37
|
-
File.open(output_file, 'w') do |f|
|
38
|
-
f.write JSON.dump(instrumentation_data)
|
39
|
-
end
|
40
|
-
|
41
|
-
puts '~~~~> [ Profiling RUNTIME ] Profiling now COMPLETE and JSON report written at ' + output_file.to_s
|
42
|
-
puts '~~~~> [ Profiling RUNTIME ]'
|
43
|
-
puts '~~~~> [ Profiling RUNTIME ] You can do the following to view the JSON report in console:'
|
44
|
-
puts '~~~~> [ Profiling RUNTIME ]'
|
45
|
-
puts '~~~~> [ Profiling RUNTIME ] bundle exec runtime_profiler view ' + output_file.to_s
|
46
|
-
puts '~~~~> [ Profiling RUNTIME ]'
|
47
|
-
puts '~~~~> [ Profiling RUNTIME ] Or'
|
48
|
-
puts '~~~~> [ Profiling RUNTIME ]'
|
49
|
-
puts '~~~~> [ Profiling RUNTIME ] bundle exec runtime_profiler view --help'
|
50
|
-
puts '~~~~> [ Profiling RUNTIME ]'
|
51
|
-
puts '~~~~> [ Profiling RUNTIME ] for more details.'
|
52
57
|
end
|
53
58
|
|
54
|
-
private
|
55
|
-
|
56
59
|
def method_calls_data
|
57
60
|
@method_calls_data ||= begin
|
58
61
|
instrumented_methods = {}
|
@@ -23,12 +23,12 @@ module RuntimeProfiler
|
|
23
23
|
@active_record_callback = Callback::ActiveRecord.new
|
24
24
|
|
25
25
|
@subscribers << ActiveSupport::Notifications
|
26
|
-
|
26
|
+
.subscribe('sql.active_record', @active_record_callback)
|
27
27
|
|
28
28
|
@action_controller_callback = Callback::ActionController.new
|
29
29
|
|
30
30
|
@subscribers << ActiveSupport::Notifications
|
31
|
-
|
31
|
+
.subscribe('process_action.action_controller', @action_controller_callback)
|
32
32
|
end
|
33
33
|
|
34
34
|
def unsubscribe_to_event_notifications
|
@@ -38,18 +38,18 @@ module RuntimeProfiler
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def prepare_methods_to_instrument
|
41
|
-
|
42
|
-
|
41
|
+
instrumented_constants.flatten
|
42
|
+
.each { |constant| MethodMeter.observe(constant, RuntimeProfiler.excepted_methods) }
|
43
43
|
end
|
44
44
|
|
45
45
|
def save_instrumentation_data
|
46
46
|
unsubscribe_to_event_notifications
|
47
47
|
|
48
48
|
instrumentation_data = RuntimeProfiler::InstrumentationData.new \
|
49
|
-
|
50
|
-
|
49
|
+
controller_data: @action_controller_callback.controller_data,
|
50
|
+
sql_data: @active_record_callback.data
|
51
51
|
|
52
52
|
instrumentation_data.persist!
|
53
53
|
end
|
54
54
|
end
|
55
|
-
end
|
55
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'hirb'
|
2
2
|
require 'terminal-table'
|
3
3
|
require 'active_support/core_ext/string'
|
4
|
+
require 'pry'
|
4
5
|
|
5
6
|
module RuntimeProfiler
|
6
7
|
class TextReport
|
@@ -8,29 +9,72 @@ module RuntimeProfiler
|
|
8
9
|
DURATION_WIDTH = 22
|
9
10
|
TOTAL_RUNTIME_WIDTH = 20
|
10
11
|
|
11
|
-
|
12
|
+
FULL_DETAILS_TEMPLATE = <<-EOT.strip_heredoc
|
12
13
|
|
13
14
|
\e[1mPROFILING REPORT\e[22m
|
14
15
|
----------------
|
15
16
|
|
16
|
-
\e[
|
17
|
+
\e[1mAPI RUNTIME\e[22m
|
17
18
|
Total Runtime : %s ms
|
18
19
|
Database Runtime : %s ms
|
19
20
|
View Runtime : %s ms
|
20
21
|
|
21
|
-
\e[
|
22
|
+
\e[1mMETHOD CALLS\e[22m
|
22
23
|
SLOWEST : %s (%s ms)
|
23
24
|
MOSTLY CALLED : %s (%s number of calls in %s ms)
|
24
25
|
|
25
26
|
\e[1mSQL CALLS\e[22m
|
26
27
|
Total : %s
|
27
28
|
Total Unique : %s
|
28
|
-
|
29
|
+
|
30
|
+
\e[1mSLOWEST\e[22m
|
31
|
+
Total Runtime : %s ms
|
32
|
+
SQL : %s
|
33
|
+
Source : %s
|
34
|
+
|
35
|
+
\e[1mMOSTLY CALLED\e[22m
|
36
|
+
Total Calls : %s
|
37
|
+
Total Runtime : %s ms
|
38
|
+
SQL : %s
|
39
|
+
Sources : %s
|
40
|
+
|
41
|
+
EOT
|
42
|
+
|
43
|
+
METHODS_DETAILS_TEMPLATE = <<-EOT.strip_heredoc
|
44
|
+
|
45
|
+
\e[1mPROFILING REPORT\e[22m
|
46
|
+
----------------
|
47
|
+
|
48
|
+
\e[1mAPI RUNTIME\e[22m
|
49
|
+
Total Runtime : %s ms
|
50
|
+
Database Runtime : %s ms
|
51
|
+
View Runtime : %s ms
|
52
|
+
|
53
|
+
\e[1mMETHOD CALLS\e[22m
|
54
|
+
SLOWEST : %s (%s ms)
|
55
|
+
MOSTLY CALLED : %s (%s number of calls in %s ms)
|
56
|
+
|
57
|
+
EOT
|
58
|
+
|
59
|
+
SQLS_DETAILS_TEMPLATE = <<-EOT.strip_heredoc
|
60
|
+
|
61
|
+
\e[1mPROFILING REPORT\e[22m
|
62
|
+
----------------
|
63
|
+
|
64
|
+
\e[1mAPI RUNTIME\e[22m
|
65
|
+
Total Runtime : %s ms
|
66
|
+
Database Runtime : %s ms
|
67
|
+
View Runtime : %s ms
|
68
|
+
|
69
|
+
\e[1mSQL CALLS\e[22m
|
70
|
+
Total : %s
|
71
|
+
Total Unique : %s
|
72
|
+
|
29
73
|
\e[1mSLOWEST\e[22m
|
30
74
|
Total Runtime : %s ms
|
31
75
|
SQL : %s
|
32
76
|
Source : %s
|
33
|
-
|
77
|
+
|
34
78
|
\e[1mMOSTLY CALLED\e[22m
|
35
79
|
Total Calls : %s
|
36
80
|
Total Runtime : %s ms
|
@@ -42,49 +86,78 @@ module RuntimeProfiler
|
|
42
86
|
attr_accessor :data, :options
|
43
87
|
|
44
88
|
def initialize(json_file, options)
|
45
|
-
self.data = JSON.parse(
|
46
|
-
self.options
|
89
|
+
self.data = JSON.parse(File.read(json_file))
|
90
|
+
self.options = options
|
47
91
|
end
|
48
92
|
|
49
93
|
def print
|
50
94
|
print_summary
|
51
95
|
|
52
|
-
if
|
53
|
-
|
54
|
-
|
96
|
+
if options.details == 'full'
|
97
|
+
if only_methods?
|
98
|
+
print_instrumented_methods
|
99
|
+
elsif only_sqls?
|
100
|
+
print_instrumented_sql_calls
|
101
|
+
else
|
102
|
+
print_instrumented_methods
|
103
|
+
print_instrumented_sql_calls
|
104
|
+
end
|
55
105
|
end
|
56
106
|
end
|
57
107
|
|
58
108
|
private
|
59
109
|
|
110
|
+
def only_methods?
|
111
|
+
options.only_methods.present? && options.only_sqls.blank?
|
112
|
+
end
|
113
|
+
|
114
|
+
def only_sqls?
|
115
|
+
options.only_sqls.present? && options.only_methods.blank?
|
116
|
+
end
|
117
|
+
|
118
|
+
def rounding
|
119
|
+
options.rounding
|
120
|
+
end
|
121
|
+
|
60
122
|
def print_summary
|
61
|
-
summary =
|
123
|
+
summary = if only_methods?
|
124
|
+
METHODS_DETAILS_TEMPLATE % details_template_data
|
125
|
+
elsif only_sqls?
|
126
|
+
SQLS_DETAILS_TEMPLATE % details_template_data
|
127
|
+
else
|
128
|
+
FULL_DETAILS_TEMPLATE % details_template_data
|
129
|
+
end
|
62
130
|
puts summary
|
63
131
|
end
|
64
132
|
|
65
133
|
def print_instrumented_methods
|
66
134
|
instrumented_methods = []
|
67
135
|
|
68
|
-
|
69
|
-
|
136
|
+
data['instrumentation']['instrumented_methods'].each do |profiled_object_name, methods|
|
137
|
+
_instrumented_methods = methods.map do |method|
|
138
|
+
method['method'] = [profiled_object_name, method['method']].join
|
139
|
+
method
|
140
|
+
end
|
141
|
+
instrumented_methods.concat(_instrumented_methods)
|
70
142
|
end
|
71
143
|
|
144
|
+
instrumented_methods = runtime_above(instrumented_methods) if options.runtime_above.presence > 0
|
145
|
+
instrumented_methods = calls_above(instrumented_methods) if options.calls_above.presence > 0
|
72
146
|
instrumented_methods = sort(instrumented_methods)
|
73
147
|
|
74
148
|
table = Terminal::Table.new do |t|
|
75
|
-
t.headings = ['Method', 'Total Runtime (ms)', 'Total Calls', 'Min', 'Max']
|
149
|
+
t.headings = ['Method', 'Total Runtime (ms)', 'Total Calls', 'Min (ms)', 'Max (ms)']
|
76
150
|
|
77
151
|
instrumented_methods.each_with_index do |row, index|
|
78
152
|
t.add_row [
|
79
153
|
row['method'],
|
80
|
-
row['total_runtime'],
|
154
|
+
row['total_runtime'].round(rounding),
|
81
155
|
row['total_calls'],
|
82
|
-
row['min'],
|
83
|
-
row['max']
|
156
|
+
row['min'].round(rounding),
|
157
|
+
row['max'].round(rounding)
|
84
158
|
]
|
85
159
|
t.add_separator if index < instrumented_methods.size - 1
|
86
160
|
end
|
87
|
-
|
88
161
|
end
|
89
162
|
|
90
163
|
puts
|
@@ -95,15 +168,20 @@ module RuntimeProfiler
|
|
95
168
|
end
|
96
169
|
|
97
170
|
def print_instrumented_sql_calls
|
98
|
-
instrumented_sql_calls =
|
171
|
+
instrumented_sql_calls = data['instrumentation']['instrumented_sql_calls']
|
172
|
+
|
173
|
+
instrumented_sql_calls = runtime_above(instrumented_sql_calls) if options.runtime_above.presence > 0
|
174
|
+
instrumented_sql_calls = calls_above(instrumented_sql_calls) if options.calls_above.presence > 0
|
175
|
+
instrumented_sql_calls = sort(instrumented_sql_calls, false)
|
99
176
|
|
100
177
|
table = Terminal::Table.new do |t|
|
101
|
-
t.headings = ['Count', 'Total Runtime (ms)', 'Average Runtime (ms)', '
|
178
|
+
t.headings = ['SQL Query', 'Count', 'Total Runtime (ms)', 'Average Runtime (ms)', 'Source']
|
102
179
|
|
103
180
|
instrumented_sql_calls.each_with_index do |row, index|
|
104
181
|
chopped_sql = wrap_text(row['sql'], sql_width)
|
105
182
|
source_list = wrap_list(row['runtimes'].map { |runtime| runtime[1] }.uniq, sql_width - 15)
|
106
|
-
average_runtime = row['average'].round(
|
183
|
+
average_runtime = row['average'].round(rounding)
|
184
|
+
total_runtime = row['total_runtime'].round(rounding)
|
107
185
|
total_lines = if chopped_sql.length >= source_list.length
|
108
186
|
chopped_sql.length
|
109
187
|
else
|
@@ -113,18 +191,17 @@ module RuntimeProfiler
|
|
113
191
|
(0...total_lines).each do |line|
|
114
192
|
count = line == 0 ? row['total_calls'] : ''
|
115
193
|
average = line == 0 ? average_runtime : ''
|
116
|
-
total_runtime = line == 0 ?
|
194
|
+
total_runtime = line == 0 ? total_runtime : ''
|
117
195
|
source = source_list.length > line ? source_list[line] : ''
|
118
196
|
query = row['sql'].length > line ? chopped_sql[line] : ''
|
119
197
|
|
120
198
|
t.add_row []
|
121
|
-
t.add_row [count, total_runtime, average,
|
199
|
+
t.add_row [query, count, total_runtime, average, source]
|
122
200
|
end
|
123
|
-
|
201
|
+
|
124
202
|
t.add_row []
|
125
203
|
t.add_separator if index < instrumented_sql_calls.size - 1
|
126
204
|
end
|
127
|
-
|
128
205
|
end
|
129
206
|
|
130
207
|
puts
|
@@ -136,6 +213,7 @@ module RuntimeProfiler
|
|
136
213
|
|
137
214
|
def wrap_text(text, width)
|
138
215
|
return [text] if text.length <= width
|
216
|
+
|
139
217
|
text.scan(/.{1,#{width}}/)
|
140
218
|
end
|
141
219
|
|
@@ -152,43 +230,72 @@ module RuntimeProfiler
|
|
152
230
|
end
|
153
231
|
end
|
154
232
|
|
155
|
-
def sort(data)
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
233
|
+
def sort(data, methods = true)
|
234
|
+
if methods
|
235
|
+
data.sort_by do |d|
|
236
|
+
if options.sort_by == 'max_runtime'
|
237
|
+
-d['max']
|
238
|
+
elsif options.sort_by == 'total_runtime'
|
239
|
+
-d['total_runtime']
|
240
|
+
elsif options.sort_by == 'total_calls'
|
241
|
+
-d['total_calls']
|
242
|
+
end
|
161
243
|
end
|
244
|
+
else
|
245
|
+
options.sort_by = 'total_runtime' if options.sort_by == 'max_runtime'
|
246
|
+
data.sort_by { |d| options.sort_by == 'total_runtime' ? -d['total_runtime'] : -d['total_calls'] }
|
162
247
|
end
|
163
248
|
end
|
164
249
|
|
165
|
-
def
|
166
|
-
|
250
|
+
def runtime_above(data)
|
251
|
+
data.select { |d| d['total_runtime'] > options.runtime_above }
|
252
|
+
end
|
253
|
+
|
254
|
+
def calls_above(data)
|
255
|
+
data.select { |d| d['total_calls'] > options.calls_above }
|
256
|
+
end
|
257
|
+
|
258
|
+
def details_template_data
|
259
|
+
summary = data['instrumentation']['summary']
|
167
260
|
|
168
|
-
[
|
169
|
-
summary['total_runtime'].round(
|
170
|
-
summary['db_runtime'].round(
|
171
|
-
summary['view_runtime'].round(
|
261
|
+
template_data = [
|
262
|
+
summary['total_runtime'] ? summary['total_runtime'].round(rounding) : 'n/a',
|
263
|
+
summary['db_runtime'] ? summary['db_runtime'].round(rounding) : 'n/a',
|
264
|
+
summary['view_runtime'] ? summary['view_runtime'].round(rounding) : 'n/a'
|
265
|
+
]
|
172
266
|
|
267
|
+
methods_data = [
|
173
268
|
summary['slowest_method']['method'],
|
174
|
-
summary['slowest_method']['total_runtime'].round(
|
269
|
+
summary['slowest_method']['total_runtime'].round(rounding),
|
175
270
|
|
176
271
|
summary['mostly_called_method']['method'],
|
177
272
|
summary['mostly_called_method']['total_calls'],
|
178
|
-
summary['mostly_called_method']['total_runtime'].round(
|
273
|
+
summary['mostly_called_method']['total_runtime'].round(rounding)
|
274
|
+
]
|
179
275
|
|
276
|
+
sqls_data = [
|
180
277
|
summary['total_sql_calls'],
|
181
278
|
summary['total_unique_sql_calls'],
|
182
279
|
|
183
|
-
summary['slowest_sql']['total_runtime'].round(
|
280
|
+
summary['slowest_sql']['total_runtime'].round(rounding),
|
184
281
|
summary['slowest_sql']['sql'],
|
185
282
|
summary['slowest_sql']['source'],
|
186
283
|
|
187
284
|
summary['mostly_called_sql']['total_calls'],
|
188
|
-
summary['mostly_called_sql']['total_runtime'].round(
|
285
|
+
summary['mostly_called_sql']['total_runtime'].round(rounding),
|
189
286
|
summary['mostly_called_sql']['sql'],
|
190
287
|
summary['mostly_called_sql']['runtimes'].map { |runtime| runtime[1] }.uniq
|
191
288
|
]
|
289
|
+
|
290
|
+
if only_methods?
|
291
|
+
template_data.concat(methods_data)
|
292
|
+
elsif only_sqls?
|
293
|
+
template_data.concat(sqls_data)
|
294
|
+
else
|
295
|
+
template_data
|
296
|
+
.concat(methods_data)
|
297
|
+
.concat(sqls_data)
|
298
|
+
end
|
192
299
|
end
|
193
300
|
end
|
194
301
|
end
|
Binary file
|
data/runtime_profiler.gemspec
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
3
|
require 'runtime_profiler/version'
|
5
4
|
|
@@ -19,15 +18,16 @@ Gem::Specification.new do |spec|
|
|
19
18
|
spec.executables = ['runtime_profiler']
|
20
19
|
spec.require_paths = ['lib']
|
21
20
|
|
22
|
-
spec.add_development_dependency 'bundler', '~> 1.12'
|
23
|
-
spec.add_development_dependency 'rake', '~> 10.0'
|
24
|
-
spec.add_development_dependency 'minitest', '~> 5.0'
|
25
21
|
spec.add_development_dependency 'activesupport', '>= 3.0.0'
|
26
|
-
spec.add_development_dependency '
|
22
|
+
spec.add_development_dependency 'bundler'
|
23
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
27
24
|
spec.add_development_dependency 'minitest-line'
|
28
|
-
spec.
|
25
|
+
spec.add_development_dependency 'pry'
|
26
|
+
spec.add_development_dependency 'rake', '>= 12.3.3'
|
27
|
+
spec.add_development_dependency 'rubocop'
|
29
28
|
spec.add_runtime_dependency 'commander'
|
29
|
+
spec.add_runtime_dependency 'defined_methods'
|
30
30
|
spec.add_runtime_dependency 'hirb'
|
31
31
|
spec.add_runtime_dependency 'method_meter'
|
32
|
-
spec.add_runtime_dependency '
|
32
|
+
spec.add_runtime_dependency 'terminal-table'
|
33
33
|
end
|
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: runtime_profiler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wilfrido T. Nuqui Jr.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: activesupport
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 3.0.0
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 3.0.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: minitest
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -53,19 +53,19 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '5.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: minitest-line
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: pry
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,7 +81,21 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 12.3.3
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 12.3.3
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
86
100
|
requirements:
|
87
101
|
- - ">="
|
@@ -95,7 +109,7 @@ dependencies:
|
|
95
109
|
- !ruby/object:Gem::Version
|
96
110
|
version: '0'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
112
|
+
name: commander
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
100
114
|
requirements:
|
101
115
|
- - ">="
|
@@ -109,7 +123,7 @@ dependencies:
|
|
109
123
|
- !ruby/object:Gem::Version
|
110
124
|
version: '0'
|
111
125
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
126
|
+
name: defined_methods
|
113
127
|
requirement: !ruby/object:Gem::Requirement
|
114
128
|
requirements:
|
115
129
|
- - ">="
|
@@ -151,7 +165,7 @@ dependencies:
|
|
151
165
|
- !ruby/object:Gem::Version
|
152
166
|
version: '0'
|
153
167
|
- !ruby/object:Gem::Dependency
|
154
|
-
name:
|
168
|
+
name: terminal-table
|
155
169
|
requirement: !ruby/object:Gem::Requirement
|
156
170
|
requirements:
|
157
171
|
- - ">="
|
@@ -173,6 +187,7 @@ extensions: []
|
|
173
187
|
extra_rdoc_files: []
|
174
188
|
files:
|
175
189
|
- ".gitignore"
|
190
|
+
- ".semaphore/semaphore.yml"
|
176
191
|
- Gemfile
|
177
192
|
- LICENSE.txt
|
178
193
|
- README.md
|
@@ -190,6 +205,7 @@ files:
|
|
190
205
|
- lib/runtime_profiler/profiler.rb
|
191
206
|
- lib/runtime_profiler/text_report.rb
|
192
207
|
- lib/runtime_profiler/version.rb
|
208
|
+
- rp_view_only_methods_full_details.png
|
193
209
|
- runtime_profiler.gemspec
|
194
210
|
homepage: http://www.github.com/wnuqui/runtime_profiler
|
195
211
|
licenses:
|
@@ -210,8 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
210
226
|
- !ruby/object:Gem::Version
|
211
227
|
version: '0'
|
212
228
|
requirements: []
|
213
|
-
|
214
|
-
rubygems_version: 2.6.13
|
229
|
+
rubygems_version: 3.0.3
|
215
230
|
signing_key:
|
216
231
|
specification_version: 4
|
217
232
|
summary: Runtime Profiler for Rails Applications
|