perfm 1.0.0
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/LICENSE +21 -0
- data/README.md +208 -0
- data/Rakefile +16 -0
- data/app/models/perfm/application_record.rb +5 -0
- data/app/models/perfm/gvl_metric.rb +14 -0
- data/lib/generators/perfm/install/install_generator.rb +23 -0
- data/lib/generators/perfm/install/templates/create_perfm_gvl_metrics.rb.erb +19 -0
- data/lib/generators/perfm/uninstall/templates/drop_perfm_gvl_metrics.rb.erb +22 -0
- data/lib/generators/perfm/uninstall/uninstall_generator.rb +23 -0
- data/lib/perfm/agent.rb +14 -0
- data/lib/perfm/client.rb +78 -0
- data/lib/perfm/configuration.rb +27 -0
- data/lib/perfm/engine.rb +11 -0
- data/lib/perfm/errors/latency_exceeded_error.rb +17 -0
- data/lib/perfm/gvl_metrics_analyzer.rb +169 -0
- data/lib/perfm/heap_dumper.rb +80 -0
- data/lib/perfm/metrics/sidekiq.rb +69 -0
- data/lib/perfm/middleware/gvl_instrumentation.rb +60 -0
- data/lib/perfm/pid_store.rb +26 -0
- data/lib/perfm/queue.rb +56 -0
- data/lib/perfm/queue_latency.rb +23 -0
- data/lib/perfm/storage/api.rb +13 -0
- data/lib/perfm/storage/base.rb +9 -0
- data/lib/perfm/storage/local.rb +11 -0
- data/lib/perfm/version.rb +3 -0
- data/lib/perfm.rb +67 -0
- metadata +176 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6e1cff83c55f6245ed510d9f41fe5c169a662173e243ba0e8fb58c10d3274933
|
4
|
+
data.tar.gz: 3084b6db3933db8c23045b49374ac1474ad070b586a716452c095075be135a71
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a511d3fbefdb4554f5f87fa27f09e3f1c64d590dcac763e9cc0ca8b9e53c867ccf80337ff465edd0bcd2b6a63f88b27277d26121cc0dd87abec6411bab1e7e3e
|
7
|
+
data.tar.gz: cb3838701d25771a72c52866b6aaa7bddf215452f1980495c36854655da73ec254088a2e6d38d660e9b59242f5b6cd8f7db3001e74d7d89a2cf68eb76cee50f2
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 BigBinary
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
Perfm
|
2
|
+
==============
|
3
|
+
Perfm aims to be a performance monitoring tool for Ruby on Rails applications. Currently, it has support for GVL instrumentation and provides analytics to help optimize Puma thread concurrency settings based on the collected GVL data.
|
4
|
+
|
5
|
+
Requirements
|
6
|
+
-----------------
|
7
|
+
- Ruby: MRI 3.2+
|
8
|
+
|
9
|
+
This is because the GVL instrumentation API was [added](https://bugs.ruby-lang.org/issues/18339) in 3.2.0. Perfm makes use of the [gvl_timing](https://github.com/jhawthorn/gvl_timing) gem to capture per-thread timings for each GVL state.
|
10
|
+
|
11
|
+
Installation
|
12
|
+
-----------------
|
13
|
+
Add perfm to your Gemfile.
|
14
|
+
```ruby
|
15
|
+
gem 'perfm'
|
16
|
+
```
|
17
|
+
|
18
|
+
To set up GVL instrumentation run the following command:
|
19
|
+
|
20
|
+
```bash
|
21
|
+
bin/rails generate perfm:install
|
22
|
+
```
|
23
|
+
|
24
|
+
This will create a migration file with a table to store the GVL metrics. Run the migration and configure the gem as described below.
|
25
|
+
|
26
|
+
Configuration
|
27
|
+
-----------------
|
28
|
+
Configure Perfm in an initializer:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
Perfm.configure do |config|
|
32
|
+
config.enabled = true
|
33
|
+
config.monitor_gvl = true
|
34
|
+
config.storage = :local
|
35
|
+
end
|
36
|
+
|
37
|
+
Perfm.setup!
|
38
|
+
|
39
|
+
```
|
40
|
+
|
41
|
+
When `monitor_gvl` is enabled, perfm adds a Rack middleware to log GVL metrics for each request. The metrics are stored in the database.
|
42
|
+
|
43
|
+
We just need around `20000` datapoints(i.e requests) to get an idea of the app's workload. So the `monitor_gvl` config can be disabled after that. You can control the value via an ENV variable if you prefer.
|
44
|
+
|
45
|
+
## Analysis
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
gvl_metrics_analyzer = Perfm::GvlMetricsAnalyzer.new(
|
49
|
+
start_time: 5.days.ago,
|
50
|
+
end_time: Time.current
|
51
|
+
)
|
52
|
+
|
53
|
+
gvl_metrics_analyzer.analyze
|
54
|
+
|
55
|
+
# Write to file
|
56
|
+
File.write(
|
57
|
+
"tmp/perfm/gvl_analysis_#{Time.current.strftime('%Y%m%d_%H%M%S')}.json",
|
58
|
+
JSON.pretty_generate(gvl_metrics_analyzer.analyze)
|
59
|
+
)
|
60
|
+
```
|
61
|
+
|
62
|
+
This will print the following metrics:
|
63
|
+
|
64
|
+
- `total_io_percentage`: Percentage of time spent doing I/O operations
|
65
|
+
- `total_io_and_stall_percentage`: Percentage of time spent in I/O operations(idle time) and GVL stalls combined
|
66
|
+
- `average_response_time_ms`: Average response time in milliseconds per request
|
67
|
+
- `average_stall_ms`: Average GVL stall time in milliseconds per request
|
68
|
+
- `request_count`: Total number of requests analyzed
|
69
|
+
- `time_range`: Details about the analysis period including:
|
70
|
+
- `start_time`
|
71
|
+
- `end_time`
|
72
|
+
- `duration_seconds`
|
73
|
+
|
74
|
+
After analysis, you can drop the table to save space. The following command generates a migration to drop the table.
|
75
|
+
|
76
|
+
```bash
|
77
|
+
bin/rails generate perfm:uninstall
|
78
|
+
```
|
79
|
+
|
80
|
+
## Beta Features
|
81
|
+
|
82
|
+
The following features are currently in beta and may have limited functionality or be subject to change.
|
83
|
+
|
84
|
+
### Perfm queue latency monitor
|
85
|
+
|
86
|
+
|
87
|
+
The queue latency monitor tracks Sidekiq queue times and raises alerts when the queue latency exceed their thresholds. To enable this feature, set `config.monitor_sidekiq_queues = true` in your Perfm configuration.
|
88
|
+
|
89
|
+
ruby
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
Perfm.configure do |config|
|
93
|
+
# Other configurations...
|
94
|
+
config.monitor_sidekiq_queues = true
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
When enabled, Perfm will monitor your Sidekiq queues and raise a `Perfm::Errors::LatencyExceededError` when the queue latency exceeds the threshold.
|
99
|
+
|
100
|
+
#### Queue Naming Convention
|
101
|
+
|
102
|
+
Perfm expects queues that need latency monitoring to follow this naming pattern:
|
103
|
+
|
104
|
+
- `within_X_seconds` (e.g., within_5_seconds)
|
105
|
+
- `within_X_minutes` (e.g., within_2_minutes)
|
106
|
+
- `within_X_hours` (e.g., within_1_hours)
|
107
|
+
|
108
|
+
### Heap analyzer
|
109
|
+
|
110
|
+
### Generate and Store Heap Dumps via ActiveStorage
|
111
|
+
|
112
|
+
Perfm has a heap dump generator which can be used to generate heap dumps from running Puma worker processes and storing them via ActiveStorage. This can be useful for debugging memory leaks. We can generate three dumps separate by a time period of lets say 15 minutes and analyze it via heapy or sheap.
|
113
|
+
|
114
|
+
_Note: The process of heap dump generation can increase the memory usage._
|
115
|
+
|
116
|
+
#### Puma configuration changes:
|
117
|
+
|
118
|
+
Add the following to your `config/puma.rb`:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
on_worker_boot do
|
122
|
+
Perfm::PidStore.instance.add_worker_pid(Process.pid)
|
123
|
+
end
|
124
|
+
|
125
|
+
on_worker_shutdown do
|
126
|
+
Perfm::PidStore.instance.clear
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
We need to keep track of pid of each worker process so that we inject code to generate heap dump in each worker process using [rbtrace](https://github.com/tmm1/rbtrace)
|
131
|
+
|
132
|
+
#### Route setup
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# config/routes.rb
|
136
|
+
Rails.application.routes.draw do
|
137
|
+
namespace :perfm do
|
138
|
+
namespace :admin do
|
139
|
+
resources :heap_dumps, only: :create
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
#### Controller to generate heap dumps
|
146
|
+
|
147
|
+
As we need to invoke rbtrace from the same process, we'll use a controller itself to invoke the `HeapDumper`.
|
148
|
+
```ruby
|
149
|
+
class Perfm::Admin::HeapDumpsController < ActionController::Base
|
150
|
+
skip_forgery_protection
|
151
|
+
before_action :authenticate_admin
|
152
|
+
|
153
|
+
def create
|
154
|
+
blob = Perfm::HeapDumper.generate
|
155
|
+
|
156
|
+
render json: {
|
157
|
+
status: "success",
|
158
|
+
message: "Heap dump generated successfully",
|
159
|
+
blob_id: blob.id,
|
160
|
+
filename: blob.filename.to_s
|
161
|
+
}
|
162
|
+
rescue Perfm::HeapDumper::Error => e
|
163
|
+
render json: { status: "error", message: e.message }, status: :unprocessable_entity
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def authenticate_admin
|
169
|
+
return if Rails.env.development?
|
170
|
+
|
171
|
+
unless valid_token?(request.headers["X-Perfm-Token"])
|
172
|
+
head :unauthorized
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def valid_token?(token)
|
177
|
+
return false if token.blank? || Perfm.configuration.admin_token.blank?
|
178
|
+
|
179
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
180
|
+
token,
|
181
|
+
Perfm.configuration.admin_token
|
182
|
+
)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
#### Usage
|
188
|
+
|
189
|
+
```bash
|
190
|
+
curl -X POST https://your-app.com/perfm/admin/heap_dumps -H "X-Perfm-Token: your-secure-token"
|
191
|
+
```
|
192
|
+
|
193
|
+
The generated heap dump will be stored via ActiveStorage and the response includes the blob ID and filename for later reference.
|
194
|
+
|
195
|
+
#### Configuration
|
196
|
+
|
197
|
+
Configure the admin token in your Perfm initializer:
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
# config/initializers/perfm.rb
|
201
|
+
Perfm.configure do |config|
|
202
|
+
config.admin_token = ENV["PERFM_ADMIN_TOKEN"]
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
The generated heap dumps can be downloaded and analyzed using [heapy](https://github.com/zombocom/heapy)
|
207
|
+
|
208
|
+
We're planning to add a heap analyzer within perfm itself to make the process seamless.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
require 'bundler/gem_tasks'
|
9
|
+
|
10
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
11
|
+
load 'rails/tasks/engine.rake'
|
12
|
+
load 'rails/tasks/statistics.rake'
|
13
|
+
|
14
|
+
require 'bundler/gem_tasks'
|
15
|
+
|
16
|
+
task default: :test
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Perfm
|
2
|
+
class GvlMetric < ApplicationRecord
|
3
|
+
self.table_name = "perfm_gvl_metrics"
|
4
|
+
|
5
|
+
scope :within_time_range, ->(start_time, end_time) {
|
6
|
+
where(created_at: start_time..end_time)
|
7
|
+
}
|
8
|
+
|
9
|
+
def action_path
|
10
|
+
return "rack middleware" if controller.blank?
|
11
|
+
"#{controller}##{action}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
require 'rails/generators/active_record'
|
4
|
+
|
5
|
+
module Perfm
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
include ActiveRecord::Generators::Migration
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def create_migration_file
|
11
|
+
migration_template(
|
12
|
+
'create_perfm_gvl_metrics.rb.erb',
|
13
|
+
File.join(db_migrate_path, "create_perfm_gvl_metrics.rb")
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def migration_version
|
20
|
+
"[#{ActiveRecord::VERSION::STRING.to_f}]"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreatePerfmGvlMetrics < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :perfm_gvl_metrics do |t|
|
4
|
+
t.float :gc_ms
|
5
|
+
t.float :run_ms
|
6
|
+
t.float :idle_ms
|
7
|
+
t.float :stall_ms
|
8
|
+
t.float :io_percent
|
9
|
+
t.string :method
|
10
|
+
t.string :controller
|
11
|
+
t.string :action
|
12
|
+
t.integer :puma_max_threads
|
13
|
+
|
14
|
+
t.index [:controller, :action]
|
15
|
+
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class DropPerfmGvlMetrics < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def up
|
3
|
+
drop_table :perfm_gvl_metrics
|
4
|
+
end
|
5
|
+
|
6
|
+
def down
|
7
|
+
create_table :perfm_gvl_metrics do |t|
|
8
|
+
t.float :gc_ms
|
9
|
+
t.float :run_ms
|
10
|
+
t.float :idle_ms
|
11
|
+
t.float :stall_ms
|
12
|
+
t.float :io_percent
|
13
|
+
t.string :method
|
14
|
+
t.string :controller
|
15
|
+
t.string :action
|
16
|
+
|
17
|
+
t.index [:controller, :action]
|
18
|
+
|
19
|
+
t.timestamps
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
require 'rails/generators/active_record'
|
4
|
+
|
5
|
+
module Perfm
|
6
|
+
class UninstallGenerator < Rails::Generators::Base
|
7
|
+
include ActiveRecord::Generators::Migration
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def create_migration_file
|
11
|
+
migration_template(
|
12
|
+
'drop_perfm_gvl_metrics.rb.erb',
|
13
|
+
File.join(db_migrate_path, "drop_perfm_gvl_metrics.rb")
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def migration_version
|
20
|
+
"[#{ActiveRecord::VERSION::STRING.to_f}]"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/perfm/agent.rb
ADDED
data/lib/perfm/client.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Perfm
|
5
|
+
class Client
|
6
|
+
HTTPS = "https".freeze
|
7
|
+
DEFAULT_TIMEOUT = 10
|
8
|
+
|
9
|
+
def initialize(config)
|
10
|
+
@api_url = config.api_url
|
11
|
+
@api_key = config.api_key
|
12
|
+
@timeout = DEFAULT_TIMEOUT
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@connections = []
|
15
|
+
@headers = {
|
16
|
+
"Content-Type" => "application/json",
|
17
|
+
"Authorization" => "Bearer #{@api_key}",
|
18
|
+
"X-Perfm-Version" => Perfm::VERSION,
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def post(path, data)
|
23
|
+
uri = URI(@api_url + path)
|
24
|
+
request = Net::HTTP::Post.new(uri.path, @headers)
|
25
|
+
request.body = data.to_json
|
26
|
+
transmit(request)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def transmit(request)
|
32
|
+
http = take_connection
|
33
|
+
response = http.request(request)
|
34
|
+
handle_response(response)
|
35
|
+
rescue => e
|
36
|
+
puts "HTTP Error: #{e.message}"
|
37
|
+
ensure
|
38
|
+
release_connection(http) if http
|
39
|
+
end
|
40
|
+
|
41
|
+
def take_connection
|
42
|
+
@mutex.synchronize do
|
43
|
+
if conn = @connections.pop
|
44
|
+
conn.start unless conn.started?
|
45
|
+
conn
|
46
|
+
else
|
47
|
+
create_connection
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def release_connection(conn)
|
53
|
+
@mutex.synchronize { @connections << conn }
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_connection
|
57
|
+
uri = URI(@api_url)
|
58
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
59
|
+
http.use_ssl = uri.scheme == HTTPS
|
60
|
+
http.open_timeout = @timeout
|
61
|
+
http.read_timeout = @timeout
|
62
|
+
http
|
63
|
+
end
|
64
|
+
|
65
|
+
def handle_response(response)
|
66
|
+
case response
|
67
|
+
when Net::HTTPSuccess
|
68
|
+
true
|
69
|
+
when Net::HTTPUnauthorized
|
70
|
+
puts "Invalid API key"
|
71
|
+
false
|
72
|
+
else
|
73
|
+
puts "Unexpected response: #{response.code}"
|
74
|
+
false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Perfm
|
2
|
+
class Configuration < Anyway::Config
|
3
|
+
config_name :perfm
|
4
|
+
|
5
|
+
attr_config(
|
6
|
+
enabled: true,
|
7
|
+
monitor_sidekiq: false,
|
8
|
+
monitor_gvl: false,
|
9
|
+
monitor_sidekiq_queues: false,
|
10
|
+
storage: :api,
|
11
|
+
api_url: nil,
|
12
|
+
api_key: nil,
|
13
|
+
)
|
14
|
+
|
15
|
+
def monitor_sidekiq?
|
16
|
+
enabled? && monitor_sidekiq
|
17
|
+
end
|
18
|
+
|
19
|
+
def monitor_gvl?
|
20
|
+
enabled? && monitor_gvl
|
21
|
+
end
|
22
|
+
|
23
|
+
def enabled?
|
24
|
+
enabled
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/perfm/engine.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
module Perfm
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace Perfm
|
4
|
+
|
5
|
+
initializer "perfm.gvl_instrumentation" do |app|
|
6
|
+
if Perfm.configuration.monitor_gvl?
|
7
|
+
app.config.middleware.insert(0, Perfm::Middleware::GvlInstrumentation)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Perfm
|
2
|
+
module Errors
|
3
|
+
class LatencyExceededError < StandardError
|
4
|
+
attr_reader :queue, :latency, :expected_latency
|
5
|
+
|
6
|
+
def initialize(queue:, latency:, expected_latency:)
|
7
|
+
@queue = queue
|
8
|
+
@latency = latency
|
9
|
+
@expected_latency = expected_latency
|
10
|
+
|
11
|
+
message = "Queue latency exceeded SLA: #{latency.round(2)}s " \
|
12
|
+
"(limit: #{expected_latency}s) for queue #{queue}"
|
13
|
+
super(message)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module Perfm
|
2
|
+
class GvlMetricsAnalyzer
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
def initialize(start_time:, end_time:, puma_max_threads: nil)
|
6
|
+
@start_time = start_time
|
7
|
+
@end_time = end_time
|
8
|
+
@puma_max_threads = puma_max_threads
|
9
|
+
end
|
10
|
+
|
11
|
+
def analyze
|
12
|
+
return empty_results if metrics.empty?
|
13
|
+
|
14
|
+
{
|
15
|
+
summary: calculate_summary(metrics),
|
16
|
+
percentiles: calculate_percentiles(metrics),
|
17
|
+
action_breakdowns: calculate_action_breakdowns(metrics)
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def metrics
|
24
|
+
@_metrics ||= begin
|
25
|
+
base_scope = GvlMetric.within_time_range(@start_time, @end_time)
|
26
|
+
return base_scope unless @puma_max_threads
|
27
|
+
|
28
|
+
base_scope.where(puma_max_threads: @puma_max_threads)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def empty_results
|
34
|
+
{
|
35
|
+
total_io_percentage: 0.0,
|
36
|
+
total_stall_percentage: 0.0,
|
37
|
+
average_response_time_ms: 0.0,
|
38
|
+
average_stall_ms: 0.0,
|
39
|
+
average_gc_ms: 0.0,
|
40
|
+
request_count: 0,
|
41
|
+
time_range: {
|
42
|
+
start_time: @start_time,
|
43
|
+
end_time: @end_time,
|
44
|
+
duration_seconds: (@end_time - @start_time).to_i
|
45
|
+
}
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def calculate_io_percentage(run_ms, idle_ms)
|
50
|
+
total_time = run_ms + idle_ms
|
51
|
+
return 0.0 if total_time == 0
|
52
|
+
((idle_ms / total_time) * 100.0).round(2)
|
53
|
+
end
|
54
|
+
|
55
|
+
def calculate_avg_response_time(run_ms, idle_ms, stall_ms, count)
|
56
|
+
return 0.0 if count == 0
|
57
|
+
((stall_ms + run_ms + idle_ms) / count).round(2)
|
58
|
+
end
|
59
|
+
|
60
|
+
def calculate_summary(metrics)
|
61
|
+
total_run_ms = metrics.sum(:run_ms)
|
62
|
+
total_idle_ms = metrics.sum(:idle_ms)
|
63
|
+
total_stall_ms = metrics.sum(:stall_ms)
|
64
|
+
total_gc_ms = metrics.sum(:gc_ms)
|
65
|
+
count = metrics.count
|
66
|
+
|
67
|
+
{
|
68
|
+
total_io_percentage: calculate_io_percentage(total_run_ms, total_idle_ms),
|
69
|
+
average_response_time_ms: calculate_avg_response_time(total_run_ms, total_idle_ms, total_stall_ms, count),
|
70
|
+
average_stall_ms: (total_stall_ms / count).round(2),
|
71
|
+
average_gc_ms: (total_gc_ms / count).round(2),
|
72
|
+
request_count: count,
|
73
|
+
time_range: {
|
74
|
+
start_time: @start_time,
|
75
|
+
end_time: @end_time,
|
76
|
+
duration_seconds: (@end_time - @start_time).to_i
|
77
|
+
}
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def calculate_percentiles(metrics)
|
82
|
+
total_count = metrics.size
|
83
|
+
|
84
|
+
sorted_metrics = if metrics.is_a?(ActiveRecord::Relation)
|
85
|
+
metrics.order(Arel.sql("run_ms + idle_ms + stall_ms")).to_a
|
86
|
+
else
|
87
|
+
metrics.sort_by { |m| m.run_ms + m.idle_ms + m.stall_ms }
|
88
|
+
end
|
89
|
+
|
90
|
+
p10 = (total_count * 0.1).floor
|
91
|
+
p50 = (total_count * 0.5).floor
|
92
|
+
p60 = (total_count * 0.6).floor
|
93
|
+
p90 = (total_count * 0.9).floor
|
94
|
+
p99 = (total_count * 0.99).floor
|
95
|
+
p999 = (total_count * 0.999).floor
|
96
|
+
|
97
|
+
percentile_ranges = {
|
98
|
+
"p0-10": 0...p10,
|
99
|
+
"p50-60": p50...p60,
|
100
|
+
"p90-99": p90...p99,
|
101
|
+
"p99-99.9": p99...p999,
|
102
|
+
"p99.9-100": p999...total_count
|
103
|
+
}
|
104
|
+
|
105
|
+
result = {
|
106
|
+
overall: "#{total_count} requests"
|
107
|
+
}
|
108
|
+
|
109
|
+
result.merge!(
|
110
|
+
percentile_ranges.transform_values do |range|
|
111
|
+
range_metrics = sorted_metrics[range]
|
112
|
+
calculate_group_stats_in_memory(range_metrics || [])
|
113
|
+
end
|
114
|
+
)
|
115
|
+
|
116
|
+
result
|
117
|
+
end
|
118
|
+
|
119
|
+
def calculate_action_breakdowns(metrics)
|
120
|
+
metrics_by_action = metrics.group_by do |metric|
|
121
|
+
[metric.controller, metric.action]
|
122
|
+
end
|
123
|
+
|
124
|
+
metrics_by_action.transform_keys do |(controller, action)|
|
125
|
+
"#{controller}##{action}"
|
126
|
+
end.transform_values do |action_metrics|
|
127
|
+
calculate_percentiles(action_metrics)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def calculate_group_stats_in_memory(metrics)
|
132
|
+
return empty_group_stats if metrics.empty?
|
133
|
+
|
134
|
+
avg_run_ms = (metrics.sum(&:run_ms) / metrics.size).round(1)
|
135
|
+
avg_idle_ms = (metrics.sum(&:idle_ms) / metrics.size).round(1)
|
136
|
+
avg_stall_ms = (metrics.sum(&:stall_ms) / metrics.size).round(1)
|
137
|
+
avg_gc_ms = (metrics.sum(&:gc_ms) / metrics.size).round(1)
|
138
|
+
total_ms = (avg_run_ms + avg_idle_ms + avg_stall_ms).round(1)
|
139
|
+
|
140
|
+
io_percentage = if (avg_run_ms + avg_idle_ms) > 0
|
141
|
+
((avg_idle_ms / (avg_run_ms + avg_idle_ms)) * 100).round(1)
|
142
|
+
else
|
143
|
+
0.0
|
144
|
+
end
|
145
|
+
|
146
|
+
{
|
147
|
+
cpu: avg_run_ms,
|
148
|
+
io: avg_idle_ms,
|
149
|
+
stall: avg_stall_ms,
|
150
|
+
gc: avg_gc_ms,
|
151
|
+
total: total_ms,
|
152
|
+
"io%": "#{io_percentage}%",
|
153
|
+
count: metrics.size
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
def empty_group_stats
|
158
|
+
{
|
159
|
+
cpu: 0.0,
|
160
|
+
io: 0.0,
|
161
|
+
stall: 0.0,
|
162
|
+
gc: 0.0,
|
163
|
+
total: 0.0,
|
164
|
+
"io%": "0.0%",
|
165
|
+
count: 0
|
166
|
+
}
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "rbtrace"
|
2
|
+
|
3
|
+
module Perfm
|
4
|
+
class HeapDumper
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def generate
|
9
|
+
new.generate
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate
|
14
|
+
worker_pid = find_worker_pid
|
15
|
+
generate_dump(worker_pid)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def find_worker_pid
|
21
|
+
pid = PidStore.instance.get_first_worker_pid
|
22
|
+
return pid if pid
|
23
|
+
|
24
|
+
raise Error, "No Puma worker processes available"
|
25
|
+
end
|
26
|
+
|
27
|
+
def generate_dump(worker_pid)
|
28
|
+
filename = "heap_dump_pid_#{worker_pid}_time_#{Time.current.to_i}.json"
|
29
|
+
temp_path = Rails.root.join("tmp", filename)
|
30
|
+
FileUtils.mkdir_p(File.dirname(temp_path))
|
31
|
+
|
32
|
+
generate_heap_dump(worker_pid, temp_path)
|
33
|
+
store_dump(temp_path, filename)
|
34
|
+
ensure
|
35
|
+
FileUtils.rm_f(temp_path) if defined?(temp_path) && temp_path
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate_heap_dump(worker_pid, output_path)
|
39
|
+
cmd = build_rbtrace_command(worker_pid, output_path)
|
40
|
+
execute_rbtrace(cmd)
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_rbtrace_command(worker_pid, output_path)
|
44
|
+
<<~COMMAND
|
45
|
+
rbtrace -p #{worker_pid} -e '
|
46
|
+
Thread.new {
|
47
|
+
require "objspace"
|
48
|
+
ObjectSpace.trace_object_allocations_start
|
49
|
+
GC.start
|
50
|
+
File.open("#{output_path}", "w") { |f|
|
51
|
+
ObjectSpace.dump_all(output: f)
|
52
|
+
}
|
53
|
+
}.join
|
54
|
+
'
|
55
|
+
COMMAND
|
56
|
+
end
|
57
|
+
|
58
|
+
def execute_rbtrace(cmd)
|
59
|
+
output = `#{cmd} 2>&1`
|
60
|
+
return if $?.success?
|
61
|
+
|
62
|
+
raise Error, "rbtrace failed: #{output}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def store_dump(temp_path, filename)
|
66
|
+
File.open(temp_path) do |file|
|
67
|
+
blob = ActiveStorage::Blob.create_and_upload!(
|
68
|
+
io: file,
|
69
|
+
filename: filename,
|
70
|
+
content_type: "application/json",
|
71
|
+
identify: false
|
72
|
+
)
|
73
|
+
|
74
|
+
puts "Heap dump stored with key: #{blob.key}"
|
75
|
+
|
76
|
+
blob
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Perfm
|
2
|
+
module Metrics
|
3
|
+
class Sidekiq
|
4
|
+
class << self
|
5
|
+
def setup
|
6
|
+
start_monitoring
|
7
|
+
end
|
8
|
+
|
9
|
+
def start_monitoring
|
10
|
+
Thread.new do
|
11
|
+
while true
|
12
|
+
collect_metrics
|
13
|
+
sleep 5
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def collect_metrics
|
19
|
+
return unless defined?(::Sidekiq)
|
20
|
+
|
21
|
+
::Sidekiq::Queue.all.each do |queue|
|
22
|
+
record_queue_metrics(queue)
|
23
|
+
|
24
|
+
if monitor_sidekiq_queues? && valid_queue_name?(queue.name)
|
25
|
+
check_latency_threshold(queue)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def record_queue_metrics(queue)
|
31
|
+
# TODO: Replace this with sending metrics to NewRelic as perfm-ingester work is not complete
|
32
|
+
Perfm.agent.push_metrics({
|
33
|
+
type: "sidekiq_queue",
|
34
|
+
queue: queue.name,
|
35
|
+
latency: queue.latency,
|
36
|
+
size: queue.size,
|
37
|
+
})
|
38
|
+
end
|
39
|
+
|
40
|
+
def check_latency_threshold(queue)
|
41
|
+
return unless expected_latency = QueueLatency.parse_latency(queue.name)
|
42
|
+
|
43
|
+
if queue.latency > expected_latency
|
44
|
+
handle_exceeded_latency(queue, expected_latency)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def handle_exceeded_latency(queue, expected_latency)
|
49
|
+
Thread.new do
|
50
|
+
# TODO: Prevent flooding the error monitoring tool if there are a lot of latency exceeded errors
|
51
|
+
raise Errors::LatencyExceededError.new(
|
52
|
+
queue: queue.name,
|
53
|
+
latency: queue.latency,
|
54
|
+
expected_latency: expected_latency
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def monitor_sidekiq_queues?
|
60
|
+
@monitor_sidekiq_queues ||= Perfm.configuration.monitor_sidekiq_queues?
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid_queue_name?(queue_name)
|
64
|
+
QueueLatency.valid_queue_name?(queue_name)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "gvl_timing"
|
4
|
+
|
5
|
+
module Perfm
|
6
|
+
module Middleware
|
7
|
+
class GvlInstrumentation
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
@puma_max_threads = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
response = nil
|
15
|
+
before_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
16
|
+
before_gc_time = GC.total_time
|
17
|
+
|
18
|
+
timer = GVLTiming.measure do
|
19
|
+
response = @app.call(env)
|
20
|
+
end
|
21
|
+
|
22
|
+
total_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - before_time
|
23
|
+
gc_time = GC.total_time - before_gc_time
|
24
|
+
|
25
|
+
begin
|
26
|
+
@puma_max_threads ||= get_puma_max_threads if defined?(::Puma)
|
27
|
+
|
28
|
+
data = {
|
29
|
+
gc_ms: (gc_time / 1_000_000.0).round(2),
|
30
|
+
run_ms: (timer.cpu_duration * 1000.0).round(2),
|
31
|
+
idle_ms: (timer.idle_duration * 1000.0).round(2),
|
32
|
+
stall_ms: (timer.stalled_duration * 1000.0).round(2),
|
33
|
+
io_percent: (timer.idle_duration / total_time * 100.0).round(1),
|
34
|
+
method: env["REQUEST_METHOD"],
|
35
|
+
controller: nil,
|
36
|
+
action: nil,
|
37
|
+
puma_max_threads: @puma_max_threads
|
38
|
+
}
|
39
|
+
|
40
|
+
if (controller = env["action_controller.instance"])
|
41
|
+
data[:controller] = controller.controller_path
|
42
|
+
data[:action] = controller.action_name
|
43
|
+
end
|
44
|
+
|
45
|
+
Perfm.agent.push_metrics(data)
|
46
|
+
rescue => e
|
47
|
+
puts "GVL metrics collection failed: #{e.message}"
|
48
|
+
end
|
49
|
+
|
50
|
+
response
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def get_puma_max_threads
|
56
|
+
JSON.parse(Puma.stats)["max_threads"]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "singleton"
|
2
|
+
|
3
|
+
# TODO: Or read from Puma pidfile?
|
4
|
+
# TODO: Handle single mode
|
5
|
+
|
6
|
+
module Perfm
|
7
|
+
class PidStore
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@worker_pids = Concurrent::Array.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_worker_pid(pid)
|
15
|
+
@worker_pids << pid
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_first_worker_pid
|
19
|
+
@worker_pids.sample
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear
|
23
|
+
@worker_pids.clear
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/perfm/queue.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Perfm
|
2
|
+
class Queue
|
3
|
+
FLUSH_INTERVAL = 60
|
4
|
+
FLUSH_THRESHOLD = 100
|
5
|
+
|
6
|
+
def initialize(storage)
|
7
|
+
@metrics = []
|
8
|
+
@storage = storage
|
9
|
+
@mutex = Mutex.new
|
10
|
+
Kernel.at_exit { flush }
|
11
|
+
start_thread
|
12
|
+
end
|
13
|
+
|
14
|
+
def push_metrics(data)
|
15
|
+
mutex.synchronize do
|
16
|
+
@metrics.push(data)
|
17
|
+
wakeup_thread if @metrics.size >= FLUSH_THRESHOLD || !thread.alive?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def collect_pending_metrics
|
22
|
+
result = nil
|
23
|
+
mutex.synchronize do
|
24
|
+
if @metrics.size > 0
|
25
|
+
result = @metrics
|
26
|
+
@metrics = []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
def flush
|
33
|
+
if data = collect_pending_metrics
|
34
|
+
@storage.store(data)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def flush_indefinitely
|
39
|
+
while true
|
40
|
+
sleep(FLUSH_INTERVAL) and flush
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :mutex, :thread
|
47
|
+
|
48
|
+
def start_thread
|
49
|
+
@thread = Thread.new { flush_indefinitely }
|
50
|
+
end
|
51
|
+
|
52
|
+
def wakeup_thread
|
53
|
+
(thread && thread.alive?) ? thread.wakeup : start_thread
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Perfm
|
2
|
+
module QueueLatency
|
3
|
+
QUEUE_PATTERN = /\Awithin_(\d+)_(second|seconds|minute|minutes|hour|hours)\z/i
|
4
|
+
|
5
|
+
def self.parse_latency(queue_name)
|
6
|
+
return unless valid_queue_name?(queue_name)
|
7
|
+
|
8
|
+
match_data = queue_name.match(QUEUE_PATTERN)
|
9
|
+
value = match_data[1].to_i
|
10
|
+
unit = match_data[2].downcase.sub(/s\z/, '')
|
11
|
+
|
12
|
+
case unit
|
13
|
+
when 'second' then value
|
14
|
+
when 'minute' then value * 60
|
15
|
+
when 'hour' then value * 3600
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.valid_queue_name?(queue_name)
|
20
|
+
queue_name&.match?(QUEUE_PATTERN)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/perfm.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require "rails/engine"
|
2
|
+
require "active_support/all"
|
3
|
+
require "anyway_config"
|
4
|
+
|
5
|
+
require "perfm/version"
|
6
|
+
require "perfm/engine"
|
7
|
+
|
8
|
+
module Perfm
|
9
|
+
autoload :Configuration, "perfm/configuration"
|
10
|
+
autoload :Client, "perfm/client"
|
11
|
+
autoload :Queue, "perfm/queue"
|
12
|
+
autoload :Agent, "perfm/agent"
|
13
|
+
autoload :GvlMetricsAnalyzer, "perfm/gvl_metrics_analyzer"
|
14
|
+
|
15
|
+
module Storage
|
16
|
+
autoload :Base, "perfm/storage/base"
|
17
|
+
autoload :Api, "perfm/storage/api"
|
18
|
+
autoload :Local, "perfm/storage/local"
|
19
|
+
end
|
20
|
+
|
21
|
+
module Middleware
|
22
|
+
autoload :GvlInstrumentation, "perfm/middleware/gvl_instrumentation"
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
attr_writer :configuration
|
27
|
+
|
28
|
+
def configuration
|
29
|
+
@configuration ||= Configuration.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def configure
|
33
|
+
yield(configuration)
|
34
|
+
end
|
35
|
+
|
36
|
+
def agent
|
37
|
+
@agent
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup!
|
41
|
+
return unless configuration.enabled?
|
42
|
+
|
43
|
+
setup_sidekiq if configuration.monitor_sidekiq?
|
44
|
+
|
45
|
+
storage = if configuration.storage == :local
|
46
|
+
Storage::Local.new
|
47
|
+
else
|
48
|
+
Storage::Api.new(Client.new(configuration))
|
49
|
+
end
|
50
|
+
|
51
|
+
@agent = Agent.new(configuration, storage)
|
52
|
+
end
|
53
|
+
|
54
|
+
def generate_heap_dump
|
55
|
+
HeapDumper.generate
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def setup_sidekiq
|
61
|
+
return unless defined?(::Sidekiq)
|
62
|
+
Metrics::Sidekiq.setup
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
require "perfm/engine" if defined?(Rails)
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: perfm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vishnu M
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-05-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: anyway_config
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '3'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '1.3'
|
44
|
+
- - "<"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: sidekiq
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '6.0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '6.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rbtrace
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0.5'
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0.5'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: gvl_timing
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rake
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '13.0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '13.0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rubocop
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.21'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '1.21'
|
117
|
+
description: Monitor Rails application performance metrics
|
118
|
+
email:
|
119
|
+
- vishnu.m@bigbinary.com
|
120
|
+
executables: []
|
121
|
+
extensions: []
|
122
|
+
extra_rdoc_files: []
|
123
|
+
files:
|
124
|
+
- LICENSE
|
125
|
+
- README.md
|
126
|
+
- Rakefile
|
127
|
+
- app/models/perfm/application_record.rb
|
128
|
+
- app/models/perfm/gvl_metric.rb
|
129
|
+
- lib/generators/perfm/install/install_generator.rb
|
130
|
+
- lib/generators/perfm/install/templates/create_perfm_gvl_metrics.rb.erb
|
131
|
+
- lib/generators/perfm/uninstall/templates/drop_perfm_gvl_metrics.rb.erb
|
132
|
+
- lib/generators/perfm/uninstall/uninstall_generator.rb
|
133
|
+
- lib/perfm.rb
|
134
|
+
- lib/perfm/agent.rb
|
135
|
+
- lib/perfm/client.rb
|
136
|
+
- lib/perfm/configuration.rb
|
137
|
+
- lib/perfm/engine.rb
|
138
|
+
- lib/perfm/errors/latency_exceeded_error.rb
|
139
|
+
- lib/perfm/gvl_metrics_analyzer.rb
|
140
|
+
- lib/perfm/heap_dumper.rb
|
141
|
+
- lib/perfm/metrics/sidekiq.rb
|
142
|
+
- lib/perfm/middleware/gvl_instrumentation.rb
|
143
|
+
- lib/perfm/pid_store.rb
|
144
|
+
- lib/perfm/queue.rb
|
145
|
+
- lib/perfm/queue_latency.rb
|
146
|
+
- lib/perfm/storage/api.rb
|
147
|
+
- lib/perfm/storage/base.rb
|
148
|
+
- lib/perfm/storage/local.rb
|
149
|
+
- lib/perfm/version.rb
|
150
|
+
homepage: https://github.com/vishnu-m/perfm
|
151
|
+
licenses:
|
152
|
+
- MIT
|
153
|
+
metadata:
|
154
|
+
homepage_uri: https://github.com/vishnu-m/perfm
|
155
|
+
source_code_uri: https://github.com/vishnu-m/perfm
|
156
|
+
changelog_uri: https://github.com/vishnu-m/perfm/blob/master/CHANGELOG.md
|
157
|
+
post_install_message:
|
158
|
+
rdoc_options: []
|
159
|
+
require_paths:
|
160
|
+
- lib
|
161
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: 3.2.0
|
166
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
167
|
+
requirements:
|
168
|
+
- - ">="
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '0'
|
171
|
+
requirements: []
|
172
|
+
rubygems_version: 3.4.1
|
173
|
+
signing_key:
|
174
|
+
specification_version: 4
|
175
|
+
summary: Everything Rails performance monitoring
|
176
|
+
test_files: []
|