runtime_profiler 0.1.4 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e33ac72fec2841f6fb3c5b50ae46aece8bd6902ff177392dfc056e7f6ae336d6
4
- data.tar.gz: 1053411fdb7d59d1c54f9c89cd1f465dbf8cc1b280bf9d42a93ee5055fc223bf
3
+ metadata.gz: 10844b957c031772b37b68bfe5f2c83fc98c25c40c709eea8e6920badd400b04
4
+ data.tar.gz: 52b61fe38de36b0f97fa16bbe742e3baa5e4444f22db6fa64e577deddeba491a
5
5
  SHA512:
6
- metadata.gz: 430be7e054bd7748c36bf468bf28d134d35a43a441d5d3fff6730e29d15d173a74ce5e64b8e3711ff63291b10dc8c3977f58a3caecff6c649936f632d7b15be7
7
- data.tar.gz: b2d7f4f5019f489def998d8858bbec09c99be054289429482858836a0e88a2e49e22626e2f31461cedf4e9f5473eafb8baac4e0818528c2aec04a824d68683d7
6
+ metadata.gz: 289e5c785b16a2fb16ff17af9c1d7dbb02d765cf6990bac5ad1c7f4f6005042ff8589a80b79f54b7473705b5323adfb93b49cd31f4fa3e3934d9df15dbb31a6c
7
+ data.tar.gz: 5b783916251fafdf991f7a3cca98336bc329ce96ff4b1efcb4f215dc2924439c3dabe797f66502956b4169bb72bc60512f3a7b7b04740463a65cb5c60a803353
@@ -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 - Runtime Profiler for Rails Applications
1
+ # runtime_profiler
2
2
 
3
- `runtime_profiler` instruments api or a method of your Rails application using Rails' `ActiveSupport::Notifications`
3
+ *A runtime profiler for Rails applications.*
4
4
 
5
- It then aggregates and generates report to give you insights about specific calls in your Rails application.
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
- ## Note
7
+ [![Build Status](https://wnuqui.semaphoreci.com/badges/runtime_profiler/branches/master.svg?style=shields)](https://wnuqui.semaphoreci.com/projects/runtime_profiler)
8
8
 
9
- This is still a **work in progress**. However, this is a tool meant to be used in development so it is safe to use.
9
+ ## Table of contents
10
10
 
11
- ## Installation
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
- ## Profiling/Instrumenting
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
- To start profiling, make a test and use `RuntimeProfiler.profile!` method in the tests. The output of instrumentation will be generated under the `tmp` folder of your application.
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,24 +74,86 @@ it 'updates user' do
38
74
  end
39
75
  ```
40
76
 
41
- Run tests as usual and follow printed instructions after running tests.
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
- ## Reporting
98
+ #### view options
44
99
 
45
- To see profiling/instrumenting report, please open the report in browser with JSON viewer report. Or you can run the following command:
100
+ Here are the command line options for `runtime_profiler view` command.
46
101
 
47
102
  ```bash
48
- bundle exec runtime_profiler view ~/the-rails-app/tmp/runtime-profiling-51079-1521371428.json
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
- ## Configurations
141
+ ### Screenshot
142
+
143
+ <p align="center">
144
+ <img src="rp_view_command.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
- RuntimeProfiler.instrumented_constants = [User]
57
- RuntimeProfiler.instrumented_paths = %w(app lib)
58
- RuntimeProfiler.instrumented_sql_commands = %w(SELECT INSERT UPDATE DELETE)
152
+ RuntimeProfiler.profiled_constants = [User]
153
+ RuntimeProfiler.profiled_paths = %w(app lib)
154
+ RuntimeProfiler.profiled_sql_commands = %w(SELECT INSERT UPDATE DELETE)
155
+ # Useful when you want to exclude in profiling specific method(s) from framework/library being used
156
+ RuntimeProfiler.excepted_methods = [:attribute_type_decorations, :_validators, :defined_enums]
59
157
  ```
60
158
 
61
159
  ## Development
@@ -6,52 +6,48 @@ require 'method_meter'
6
6
  module RuntimeProfiler
7
7
  include ActiveSupport::Configurable
8
8
 
9
- config_accessor :instrumented_constants do
9
+ config_accessor :profiled_constants do
10
10
  []
11
11
  end
12
12
 
13
- config_accessor :instrumented_paths do
14
- %w(app lib)
13
+ config_accessor :profiled_paths do
14
+ %w[app lib]
15
15
  end
16
16
 
17
- config_accessor :instrumented_sql_commands do
18
- %w(SELECT INSERT UPDATE DELETE)
17
+ config_accessor :profiled_sql_commands do
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
- Rails.application.eager_load! rescue nil
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
 
35
43
  def profile!(key, konstants)
36
44
  konstants = konstants.is_a?(Array) ? konstants : [konstants]
37
45
  profiler = Profiler.new(konstants)
38
- profiler.prepare_for_instrumentation
46
+ profiler.prepare_for_profiling
39
47
 
40
48
  MethodMeter.measure!(key) { yield }
41
49
 
42
- profiler.save_instrumentation_data
43
- 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
50
+ profiler.save_profiling_data
55
51
  end
56
52
  end
57
- end
53
+ end
@@ -12,6 +12,7 @@ module RuntimeProfiler
12
12
  def call(*args)
13
13
  event = RuntimeProfiler::ProcessActionEvent.new(args: args)
14
14
  return unless event.recordable?
15
+
15
16
  add event
16
17
  end
17
18
 
@@ -26,12 +26,12 @@ module RuntimeProfiler
26
26
 
27
27
  @data[key][:sql] = event.sanitized_sql
28
28
  @data[key][:runtimes] = [
29
- [ event.total_runtime, event.trace.first ]
29
+ [event.total_runtime, event.trace.first]
30
30
  ]
31
31
  end
32
32
 
33
33
  def update(event)
34
- @data[event.key][:runtimes] << [ event.total_runtime, event.trace.first ]
34
+ @data[event.key][:runtimes] << [event.total_runtime, event.trace.first]
35
35
  end
36
36
  end
37
37
  end
@@ -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, 'Sort by COLUMN. COLUMN can either be "total_calls" or "total_runtime". Default is "total_calls".'
20
- c.option '--details TYPE', String, 'TYPE can be "full" or "summary".'
21
- c.option '--runtime-above RUNTIME', String, 'RUNTIME is numeric value in ms.'
22
- c.option '--only-sqls', String, 'Show only SQL(s).'
23
- c.option '--only-methods', String, 'Show only method(s).'
24
- c.option '--calls-above CALLS', String, 'CALLS is numeric value.'
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: 'total_calls',
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: 1
34
+ calls_above: 0,
35
+ rounding: 4
34
36
  }
35
37
 
36
38
  options.default default_options
37
39
 
38
- report = RuntimeProfiler::TextReport.new(args.first, options)
39
-
40
- report.print
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
@@ -1,18 +1,43 @@
1
1
  module RuntimeProfiler
2
- class InstrumentationData
2
+ class Data
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
- instrumented_api = [controller_data[:payload][:controller], controller_data[:payload][:action]].join('#')
11
+ File.open(output_file, 'w') do |f|
12
+ f.write JSON.dump(profiling_data)
13
+ end
14
+
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
12
22
 
13
- instrumentation_data = {
14
- instrumentation: {
15
- instrumented_api: instrumented_api,
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 profiled_api
30
+ return unless controller_data[:payload]
31
+ @profiled_api ||= [
32
+ controller_data[:payload][:controller],
33
+ controller_data[:payload][:action]
34
+ ].join('#')
35
+ end
36
+
37
+ def profiling_data
38
+ @profiling_data ||= {
39
+ profiling: {
40
+ profiled_api: profiled_api,
16
41
  summary: {
17
42
  db_runtime: controller_data[:db_runtime],
18
43
  view_runtime: controller_data[:view_runtime],
@@ -24,38 +49,16 @@ module RuntimeProfiler
24
49
  slowest_method: method_calls_data[:slowest_method],
25
50
  mostly_called_method: method_calls_data[:mostly_called_method]
26
51
  },
27
- instrumented_sql_calls: sql_calls_data[:instrumented_sql_calls],
28
- instrumented_methods: method_calls_data[:instrumented_methods],
29
- instrumented_at: Time.now
52
+ profiled_sql_calls: sql_calls_data[:profiled_sql_calls],
53
+ profiled_methods: method_calls_data[:profiled_methods],
54
+ profiled_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
- instrumented_methods = {}
61
+ profiled_methods = {}
59
62
 
60
63
  # TODO: Group methods under a key and under an object
61
64
  MethodMeter.measurement.each do |measurement|
@@ -66,16 +69,16 @@ module RuntimeProfiler
66
69
 
67
70
  d[:method] = separator + object.second
68
71
 
69
- if instrumented_methods[object.first]
70
- instrumented_methods[object.first] << d
72
+ if profiled_methods[object.first]
73
+ profiled_methods[object.first] << d
71
74
  else
72
- instrumented_methods[object.first] = [d]
75
+ profiled_methods[object.first] = [d]
73
76
  end
74
77
  end
75
78
  end
76
79
  end
77
80
 
78
- instrumented_methods = instrumented_methods.inject({}) do |hash, (key, value)|
81
+ profiled_methods = profiled_methods.inject({}) do |hash, (key, value)|
79
82
  val = value.sort { |a, b| b[:total_runtime] <=> a[:total_runtime] }
80
83
  hash[key] = val
81
84
  hash
@@ -84,7 +87,7 @@ module RuntimeProfiler
84
87
  slowest_method = {total_runtime: 0}
85
88
  mostly_called_method = {total_calls: 0}
86
89
 
87
- instrumented_methods.each do |profiled_object_name, methods|
90
+ profiled_methods.each do |profiled_object_name, methods|
88
91
  # sort using `total_runtime` in DESC order
89
92
  _methods = methods.sort { |a, b| b[:total_runtime] <=> a[:total_runtime] }
90
93
  slowest = _methods[0]
@@ -107,7 +110,7 @@ module RuntimeProfiler
107
110
  end
108
111
 
109
112
  {
110
- instrumented_methods: instrumented_methods,
113
+ profiled_methods: profiled_methods,
111
114
  slowest_method: slowest_method,
112
115
  mostly_called_method: mostly_called_method
113
116
  }
@@ -116,7 +119,7 @@ module RuntimeProfiler
116
119
 
117
120
  def sql_calls_data
118
121
  @sql_calls_data ||= begin
119
- instrumented_sql_calls = []
122
+ profiled_sql_calls = []
120
123
 
121
124
  slowest_sql = {total_runtime: 0}
122
125
  mostly_called_sql = {total_calls: 0}
@@ -137,7 +140,7 @@ module RuntimeProfiler
137
140
  slowest_sql[:source] = slowest[1]
138
141
  end
139
142
 
140
- instrumented_sql_calls << {
143
+ profiled_sql_calls << {
141
144
  sql: value[:sql],
142
145
  runtimes: runtimes,
143
146
  total_calls: total_calls,
@@ -159,9 +162,9 @@ module RuntimeProfiler
159
162
  end
160
163
 
161
164
  {
162
- instrumented_sql_calls: instrumented_sql_calls.sort { |a, b| b[:max] <=> a[:max] },
163
- total_sql_calls: instrumented_sql_calls.map { |sql_call| sql_call[:total_calls] }.reduce(:+),
164
- total_unique_sql_calls: instrumented_sql_calls.size,
165
+ profiled_sql_calls: profiled_sql_calls.sort { |a, b| b[:max] <=> a[:max] },
166
+ total_sql_calls: profiled_sql_calls.map { |sql_call| sql_call[:total_calls] }.reduce(:+),
167
+ total_unique_sql_calls: profiled_sql_calls.size,
165
168
  slowest_sql: slowest_sql,
166
169
  mostly_called_sql: mostly_called_sql
167
170
  }
@@ -30,4 +30,4 @@ module RuntimeProfiler
30
30
  @key ||= Digest::MD5.hexdigest(path.downcase)
31
31
  end
32
32
  end
33
- end
33
+ end
@@ -10,8 +10,8 @@ module RuntimeProfiler
10
10
  end
11
11
 
12
12
  def recordable?
13
- return true unless RuntimeProfiler.instrumented_sql_commands.respond_to?(:join)
14
- instrumented_sql_matcher =~ sql
13
+ return true unless RuntimeProfiler.profiled_sql_commands.respond_to?(:join)
14
+ profiled_sql_matcher =~ sql
15
15
  end
16
16
 
17
17
  def total_runtime
@@ -41,12 +41,12 @@ module RuntimeProfiler
41
41
  @sql ||= @payload[:sql].dup
42
42
  end
43
43
 
44
- def instrumented_sql_matcher
45
- @instrumented_sql_matcher ||= /\A#{RuntimeProfiler.instrumented_sql_commands.join('|')}/i
44
+ def profiled_sql_matcher
45
+ @profiled_sql_matcher ||= /\A#{RuntimeProfiler.profiled_sql_commands.join('|')}/i
46
46
  end
47
47
 
48
48
  def trace_path_matcher
49
- @trace_path_matcher ||= %r{^(#{RuntimeProfiler.instrumented_paths.join('|')})\/}
49
+ @trace_path_matcher ||= %r{^(#{RuntimeProfiler.profiled_paths.join('|')})\/}
50
50
  end
51
51
 
52
52
  def sanitize_trace!(trace)
@@ -59,7 +59,7 @@ module RuntimeProfiler
59
59
 
60
60
  Rails.backtrace_cleaner.remove_silencers!
61
61
 
62
- if RuntimeProfiler.instrumented_paths.respond_to?(:join)
62
+ if RuntimeProfiler.profiled_paths.respond_to?(:join)
63
63
  Rails.backtrace_cleaner.add_silencer do |line|
64
64
  line !~ trace_path_matcher
65
65
  end
@@ -2,19 +2,19 @@
2
2
 
3
3
  require 'runtime_profiler/callbacks/active_record'
4
4
  require 'runtime_profiler/callbacks/action_controller'
5
- require 'runtime_profiler/instrumentation_data'
5
+ require 'runtime_profiler/data'
6
6
 
7
7
  module RuntimeProfiler
8
8
  class Profiler
9
- attr_accessor :instrumented_constants
9
+ attr_accessor :profiled_constants
10
10
 
11
- def initialize(konstants)
12
- self.instrumented_constants = konstants
11
+ def initialize(constants)
12
+ self.profiled_constants = constants
13
13
  end
14
14
 
15
- def prepare_for_instrumentation
15
+ def prepare_for_profiling
16
16
  subscribe_to_event_notifications
17
- prepare_methods_to_instrument
17
+ prepare_methods_to_profile
18
18
  end
19
19
 
20
20
  def subscribe_to_event_notifications
@@ -23,12 +23,12 @@ module RuntimeProfiler
23
23
  @active_record_callback = Callback::ActiveRecord.new
24
24
 
25
25
  @subscribers << ActiveSupport::Notifications
26
- .subscribe('sql.active_record', @active_record_callback)
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
- .subscribe('process_action.action_controller', @action_controller_callback)
31
+ .subscribe('process_action.action_controller', @action_controller_callback)
32
32
  end
33
33
 
34
34
  def unsubscribe_to_event_notifications
@@ -37,19 +37,19 @@ module RuntimeProfiler
37
37
  end
38
38
  end
39
39
 
40
- def prepare_methods_to_instrument
41
- self.instrumented_constants.flatten
42
- .each { |constant| MethodMeter.observe(constant) }
40
+ def prepare_methods_to_profile
41
+ profiled_constants.flatten
42
+ .each { |constant| MethodMeter.observe(constant, RuntimeProfiler.excepted_methods) }
43
43
  end
44
44
 
45
- def save_instrumentation_data
45
+ def save_profiling_data
46
46
  unsubscribe_to_event_notifications
47
47
 
48
- instrumentation_data = RuntimeProfiler::InstrumentationData.new \
49
- controller_data: @action_controller_callback.controller_data,
50
- sql_data: @active_record_callback.data
48
+ profiling_data = RuntimeProfiler::Data.new \
49
+ controller_data: @action_controller_callback.controller_data,
50
+ sql_data: @active_record_callback.data
51
51
 
52
- instrumentation_data.persist!
52
+ profiling_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
- SUMMARY_TEMPLATE = <<-EOT.strip_heredoc
12
+ FULL_DETAILS_TEMPLATE = <<-EOT.strip_heredoc
12
13
 
13
14
  \e[1mPROFILING REPORT\e[22m
14
15
  ----------------
15
16
 
16
- \e[1mRUNTIME\e[22m
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[1mMETHODS\e[22m
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,68 +86,102 @@ module RuntimeProfiler
42
86
  attr_accessor :data, :options
43
87
 
44
88
  def initialize(json_file, options)
45
- self.data = JSON.parse( File.read(json_file) )
46
- self.options = 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 self.options.details == 'full'
53
- print_instrumented_methods
54
- print_instrumented_sql_calls
96
+ if options.details == 'full'
97
+ if only_methods?
98
+ print_profiled_methods
99
+ elsif only_sqls?
100
+ print_profiled_sql_calls
101
+ else
102
+ print_profiled_methods
103
+ print_profiled_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 = SUMMARY_TEMPLATE % summary_template_data
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
- def print_instrumented_methods
66
- instrumented_methods = []
133
+ def print_profiled_methods
134
+ profiled_methods = []
67
135
 
68
- self.data['instrumentation']['instrumented_methods'].each do |profiled_object_name, methods|
69
- instrumented_methods.concat methods.map { |method| method['method'] = [profiled_object_name, method['method']].join; method}
136
+ data['profiling']['profiled_methods'].each do |profiled_object_name, methods|
137
+ _profiled_methods = methods.map do |method|
138
+ method['method'] = [profiled_object_name, method['method']].join
139
+ method
140
+ end
141
+ profiled_methods.concat(_profiled_methods)
70
142
  end
71
143
 
72
- instrumented_methods = sort(instrumented_methods)
144
+ profiled_methods = runtime_above(profiled_methods) if options.runtime_above.presence > 0
145
+ profiled_methods = calls_above(profiled_methods) if options.calls_above.presence > 0
146
+ profiled_methods = sort(profiled_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
- instrumented_methods.each_with_index do |row, index|
151
+ profiled_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
- t.add_separator if index < instrumented_methods.size - 1
159
+ t.add_separator if index < profiled_methods.size - 1
86
160
  end
87
-
88
161
  end
89
162
 
90
163
  puts
91
164
  puts
92
- puts "\e[1mINSTRUMENTED METHOD(s)\e[22m"
165
+ puts "\e[1mPROFILED METHOD(s)\e[22m"
93
166
  puts
94
167
  puts table
95
168
  end
96
169
 
97
- def print_instrumented_sql_calls
98
- instrumented_sql_calls = sort(self.data['instrumentation']['instrumented_sql_calls'])
170
+ def print_profiled_sql_calls
171
+ profiled_sql_calls = data['profiling']['profiled_sql_calls']
172
+
173
+ profiled_sql_calls = runtime_above(profiled_sql_calls) if options.runtime_above.presence > 0
174
+ profiled_sql_calls = calls_above(profiled_sql_calls) if options.calls_above.presence > 0
175
+ profiled_sql_calls = sort(profiled_sql_calls, false)
99
176
 
100
177
  table = Terminal::Table.new do |t|
101
- t.headings = ['Count', 'Total Runtime (ms)', 'Average Runtime (ms)', 'SQL Query', 'Source']
178
+ t.headings = ['SQL Query', 'Count', 'Total Runtime (ms)', 'Average Runtime (ms)', 'Source']
102
179
 
103
- instrumented_sql_calls.each_with_index do |row, index|
180
+ profiled_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(2)
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,29 +191,29 @@ 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 ? row['total_runtime'] : ''
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, query, source]
199
+ t.add_row [query, count, total_runtime, average, source]
122
200
  end
123
-
201
+
124
202
  t.add_row []
125
- t.add_separator if index < instrumented_sql_calls.size - 1
203
+ t.add_separator if index < profiled_sql_calls.size - 1
126
204
  end
127
-
128
205
  end
129
206
 
130
207
  puts
131
208
  puts
132
- puts "\e[1mINSTRUMENTED SQL(s)\e[22m"
209
+ puts "\e[1mPROFILED SQL(s)\e[22m"
133
210
  puts
134
211
  puts table
135
212
  end
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
- data.sort_by do |d|
157
- if self.options.sort_by == 'total_runtime'
158
- -d['total_runtime']
159
- else
160
- -d['total_calls']
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 summary_template_data
166
- summary = self.data['instrumentation']['summary']
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['profiling']['summary']
167
260
 
168
- [
169
- summary['total_runtime'].round(2),
170
- summary['db_runtime'].round(2),
171
- summary['view_runtime'].round(2),
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(2),
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(2),
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(2),
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(2),
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
@@ -1,3 +1,3 @@
1
1
  module RuntimeProfiler
2
- VERSION = '0.1.4'.freeze
2
+ VERSION = '0.4.3'.freeze
3
3
  end
Binary file
@@ -1,5 +1,4 @@
1
- # coding: utf-8
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', '>= 12.3.3'
24
- spec.add_development_dependency 'minitest', '~> 5.0'
25
21
  spec.add_development_dependency 'activesupport', '>= 3.0.0'
26
- spec.add_development_dependency 'pry'
22
+ spec.add_development_dependency 'bundler'
23
+ spec.add_development_dependency 'minitest', '~> 5.0'
27
24
  spec.add_development_dependency 'minitest-line'
28
- spec.add_runtime_dependency 'terminal-table'
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 'defined_methods'
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
4
+ version: 0.4.3
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: 2020-11-14 00:00:00.000000000 Z
11
+ date: 2021-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.12'
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: '1.12'
26
+ version: 3.0.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 12.3.3
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: 12.3.3
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: activesupport
56
+ name: minitest-line
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 3.0.0
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: 3.0.0
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: minitest-line
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: terminal-table
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: commander
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: defined_methods
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
@@ -184,12 +199,13 @@ files:
184
199
  - lib/runtime_profiler/callbacks/action_controller.rb
185
200
  - lib/runtime_profiler/callbacks/active_record.rb
186
201
  - lib/runtime_profiler/cli.rb
202
+ - lib/runtime_profiler/data.rb
187
203
  - lib/runtime_profiler/events/process_action_event.rb
188
204
  - lib/runtime_profiler/events/sql_event.rb
189
- - lib/runtime_profiler/instrumentation_data.rb
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_command.png
193
209
  - runtime_profiler.gemspec
194
210
  homepage: http://www.github.com/wnuqui/runtime_profiler
195
211
  licenses: