perfm 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e1cff83c55f6245ed510d9f41fe5c169a662173e243ba0e8fb58c10d3274933
4
- data.tar.gz: 3084b6db3933db8c23045b49374ac1474ad070b586a716452c095075be135a71
3
+ metadata.gz: c533c4d2398428da1e5d70056f68282566ffb4a6521737b8fa402a133ede71dd
4
+ data.tar.gz: 50f92a5880b9bd8773ea4f8692f0e40eb531606b1e5b9e38b5cf77d4c3fc2453
5
5
  SHA512:
6
- metadata.gz: a511d3fbefdb4554f5f87fa27f09e3f1c64d590dcac763e9cc0ca8b9e53c867ccf80337ff465edd0bcd2b6a63f88b27277d26121cc0dd87abec6411bab1e7e3e
7
- data.tar.gz: cb3838701d25771a72c52866b6aaa7bddf215452f1980495c36854655da73ec254088a2e6d38d660e9b59242f5b6cd8f7db3001e74d7d89a2cf68eb76cee50f2
6
+ metadata.gz: 70e239a276d658595cae7ad6f2dd366d1d25faaff2f42e6404688fe860b92eafb9a2c94df2ea74b5424a44f592f52422261de81bc26ab1512553f37a2705b5df
7
+ data.tar.gz: 2ccdb7cda8634d8d381d36c3ccf798020fa6837c1aaa9e63db749ab334674f28892f85fcf4308fe87aaf9b7446923104f1c54e01d11516f358f91410057cac86
data/README.md CHANGED
@@ -1,16 +1,17 @@
1
- Perfm
2
- ==============
1
+ # Perfm
2
+
3
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
4
 
5
- Requirements
6
- -----------------
5
+ ## Requirements
6
+
7
7
  - Ruby: MRI 3.2+
8
8
 
9
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
10
 
11
- Installation
12
- -----------------
11
+ ## Installation
12
+
13
13
  Add perfm to your Gemfile.
14
+
14
15
  ```ruby
15
16
  gem 'perfm'
16
17
  ```
@@ -23,8 +24,8 @@ bin/rails generate perfm:install
23
24
 
24
25
  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
 
26
- Configuration
27
- -----------------
27
+ ## Configuration
28
+
28
29
  Configure Perfm in an initializer:
29
30
 
30
31
  ```ruby
@@ -83,7 +84,6 @@ The following features are currently in beta and may have limited functionality
83
84
 
84
85
  ### Perfm queue latency monitor
85
86
 
86
-
87
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
88
 
89
89
  ruby
@@ -101,9 +101,31 @@ When enabled, Perfm will monitor your Sidekiq queues and raise a `Perfm::Errors:
101
101
 
102
102
  Perfm expects queues that need latency monitoring to follow this naming pattern:
103
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)
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
+ ### Sidekiq GVL Instrumentation
109
+
110
+ To enable GVL instrumentation for Sidekiq, first run the generator to add migrations for the required table to store the metrics.
111
+
112
+ ```bash
113
+ bin/rails generate perfm:sidekiq_gvl_metrics
114
+ ```
115
+
116
+ Then enable the `monitor_sidekiq_gvl` configuration in your initializer.
117
+
118
+ ```ruby
119
+ Perfm.configure do |config|
120
+ config.monitor_sidekiq_gvl = true
121
+ end
122
+ ```
123
+
124
+ When enabled, Perfm will collect GVL metrics at a job level, similar to how it collects metrics for HTTP requests. This can be used to analyze GVL metrics specifically for Sidekiq queues to understand their I/O characteristics.
125
+
126
+ ```ruby
127
+ Perfm::SidekiqGvlMetric.calculate_queue_io_percentage("within_5_seconds")
128
+ ```
107
129
 
108
130
  ### Heap analyzer
109
131
 
@@ -145,6 +167,7 @@ end
145
167
  #### Controller to generate heap dumps
146
168
 
147
169
  As we need to invoke rbtrace from the same process, we'll use a controller itself to invoke the `HeapDumper`.
170
+
148
171
  ```ruby
149
172
  class Perfm::Admin::HeapDumpsController < ActionController::Base
150
173
  skip_forgery_protection
@@ -0,0 +1,22 @@
1
+ module Perfm
2
+ class SidekiqGvlMetric < ApplicationRecord
3
+ self.table_name = "perfm_sidekiq_gvl_metrics"
4
+
5
+ scope :within_time_range, ->(start_time, end_time) {
6
+ where(created_at: start_time..end_time)
7
+ }
8
+
9
+ def self.calculate_queue_io_percentage(queue_name, start_time: nil, end_time: nil)
10
+ scope = where(queue: queue_name)
11
+ scope = scope.within_time_range(start_time, end_time) if start_time && end_time
12
+
13
+ total_run_ms = scope.sum(:run_ms)
14
+ total_idle_ms = scope.sum(:idle_ms)
15
+
16
+ total_time = total_run_ms + total_idle_ms
17
+ return 0.0 if total_time == 0
18
+
19
+ ((total_idle_ms.to_f / total_time) * 100.0).round(2)
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 SidekiqGvlMetricsGenerator < 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_sidekiq_gvl_metrics.rb.erb',
13
+ File.join(db_migrate_path, "create_perfm_sidekiq_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,17 @@
1
+ class CreatePerfmSidekiqGvlMetrics < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :perfm_sidekiq_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 :job_class
10
+ t.string :queue
11
+
12
+ t.index [:job_class, :queue]
13
+
14
+ t.timestamps
15
+ end
16
+ end
17
+ end
data/lib/perfm/agent.rb CHANGED
@@ -1,14 +1,19 @@
1
1
  module Perfm
2
2
  class Agent
3
- attr_reader :config, :queue
3
+ attr_reader :config, :queue, :sidekiq_queue
4
4
 
5
5
  def initialize(config, storage)
6
6
  @config = config
7
7
  @queue = Queue.new(storage)
8
+ @sidekiq_queue = Queue.new(storage)
8
9
  end
9
10
 
10
11
  def push_metrics(data)
11
12
  queue.push_metrics(data)
12
13
  end
14
+
15
+ def push_sidekiq_metrics(data)
16
+ sidekiq_queue.push_metrics(data)
17
+ end
13
18
  end
14
19
  end
@@ -6,6 +6,7 @@ module Perfm
6
6
  enabled: true,
7
7
  monitor_sidekiq: false,
8
8
  monitor_gvl: false,
9
+ monitor_sidekiq_gvl: false,
9
10
  monitor_sidekiq_queues: false,
10
11
  storage: :api,
11
12
  api_url: nil,
@@ -20,6 +21,10 @@ module Perfm
20
21
  enabled? && monitor_gvl
21
22
  end
22
23
 
24
+ def monitor_sidekiq_gvl?
25
+ enabled? && monitor_sidekiq_gvl
26
+ end
27
+
23
28
  def enabled?
24
29
  enabled
25
30
  end
data/lib/perfm/engine.rb CHANGED
@@ -6,6 +6,14 @@ module Perfm
6
6
  if Perfm.configuration.monitor_gvl?
7
7
  app.config.middleware.insert(0, Perfm::Middleware::GvlInstrumentation)
8
8
  end
9
+
10
+ if Perfm.configuration.monitor_sidekiq_gvl? && defined?(Sidekiq)
11
+ Sidekiq.configure_server do |config|
12
+ config.server_middleware do |chain|
13
+ chain.add Perfm::Middleware::SidekiqGvlInstrumentation
14
+ end
15
+ end
16
+ end
9
17
  end
10
18
  end
11
19
  end
@@ -0,0 +1,35 @@
1
+ require "gvl_timing"
2
+
3
+ module Perfm
4
+ module Middleware
5
+ class SidekiqGvlInstrumentation
6
+ def call(job_instance, job_payload, queue)
7
+ before_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
8
+ before_gc_time = GC.total_time
9
+
10
+ timer = GVLTiming.measure do
11
+ yield
12
+ end
13
+
14
+ total_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - before_time
15
+ gc_time = GC.total_time - before_gc_time
16
+
17
+ begin
18
+ data = {
19
+ gc_ms: (gc_time / 1_000_000.0).round(2),
20
+ run_ms: (timer.cpu_duration * 1000.0).round(2),
21
+ idle_ms: (timer.idle_duration * 1000.0).round(2),
22
+ stall_ms: (timer.stalled_duration * 1000.0).round(2),
23
+ io_percent: (timer.idle_duration / total_time * 100.0).round(1),
24
+ job_class: job_payload["class"],
25
+ queue: queue
26
+ }
27
+
28
+ Perfm.agent.push_sidekiq_metrics(data)
29
+ rescue => e
30
+ puts "Sidekiq GVL metrics collection failed: #{e.message}"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,127 @@
1
+ require "json"
2
+
3
+ module Perfm
4
+ class SidekiqGvlMetricsAnalyzer
5
+ attr_reader :start_time, :end_time, :query
6
+
7
+ def initialize(start_time: nil, end_time: nil, job_class: nil, queue: nil)
8
+ @start_time = start_time
9
+ @end_time = end_time
10
+ @query = SidekiqGvlMetric.where(true)
11
+ @query = @query.where(job_class: job_class) if job_class
12
+ @query = @query.where(queue: queue) if queue
13
+ @query = @query.where(created_at: start_time..end_time) if start_time && end_time
14
+ end
15
+
16
+ def analyze
17
+ return {} if query.count.zero?
18
+
19
+ result = {}
20
+ result[:total_io_percentage] = total_io_percentage
21
+ result[:total_io_and_stall_percentage] = total_io_and_stall_percentage
22
+ result[:average_processing_time_ms] = average_processing_time_ms
23
+ result[:average_stall_ms] = average_stall_ms
24
+ result[:job_count] = job_count
25
+ result[:time_range] = time_range_info
26
+ result[:by_job_class] = metrics_by_job_class
27
+ result[:by_queue] = metrics_by_queue
28
+ result
29
+ end
30
+
31
+ private
32
+
33
+ def average_io_ms
34
+ query.average(:idle_ms).to_f
35
+ end
36
+
37
+ def average_stall_ms
38
+ query.average(:stall_ms).to_f
39
+ end
40
+
41
+ def average_run_ms
42
+ query.average(:run_ms).to_f
43
+ end
44
+
45
+ def average_processing_time_ms
46
+ average_run_ms + average_io_ms + average_stall_ms
47
+ end
48
+
49
+ def job_count
50
+ query.count
51
+ end
52
+
53
+ def total_io_percentage
54
+ (average_io_ms / average_processing_time_ms * 100).round(1)
55
+ end
56
+
57
+ def total_io_and_stall_percentage
58
+ ((average_io_ms + average_stall_ms) / average_processing_time_ms * 100).round(1)
59
+ end
60
+
61
+ def time_range_info
62
+ range_start = start_time || query.minimum(:created_at)
63
+ range_end = end_time || query.maximum(:created_at)
64
+ duration = range_end - range_start
65
+
66
+ {
67
+ start_time: range_start.iso8601,
68
+ end_time: range_end.iso8601,
69
+ duration_seconds: duration.to_i
70
+ }
71
+ end
72
+
73
+ def metrics_by_job_class
74
+ results = {}
75
+
76
+ job_classes = query.distinct.pluck(:job_class)
77
+ job_classes.each do |job_class|
78
+ job_query = query.where(job_class: job_class)
79
+
80
+ job_avg_processing_ms = job_query.average(:run_ms).to_f +
81
+ job_query.average(:idle_ms).to_f +
82
+ job_query.average(:stall_ms).to_f
83
+
84
+ job_avg_io_ms = job_query.average(:idle_ms).to_f
85
+ job_avg_stall_ms = job_query.average(:stall_ms).to_f
86
+
87
+ results[job_class] = {
88
+ count: job_query.count,
89
+ average_processing_time_ms: job_avg_processing_ms.round(1),
90
+ average_io_ms: job_avg_io_ms.round(1),
91
+ average_stall_ms: job_avg_stall_ms.round(1),
92
+ io_percentage: (job_avg_io_ms / job_avg_processing_ms * 100).round(1),
93
+ io_and_stall_percentage: ((job_avg_io_ms + job_avg_stall_ms) / job_avg_processing_ms * 100).round(1)
94
+ }
95
+ end
96
+
97
+ results
98
+ end
99
+
100
+ def metrics_by_queue
101
+ results = {}
102
+
103
+ queues = query.distinct.pluck(:queue)
104
+ queues.each do |queue|
105
+ queue_query = query.where(queue: queue)
106
+
107
+ queue_avg_processing_ms = queue_query.average(:run_ms).to_f +
108
+ queue_query.average(:idle_ms).to_f +
109
+ queue_query.average(:stall_ms).to_f
110
+
111
+ queue_avg_io_ms = queue_query.average(:idle_ms).to_f
112
+ queue_avg_stall_ms = queue_query.average(:stall_ms).to_f
113
+
114
+ results[queue] = {
115
+ count: queue_query.count,
116
+ average_processing_time_ms: queue_avg_processing_ms.round(1),
117
+ average_io_ms: queue_avg_io_ms.round(1),
118
+ average_stall_ms: queue_avg_stall_ms.round(1),
119
+ io_percentage: (queue_avg_io_ms / queue_avg_processing_ms * 100).round(1),
120
+ io_and_stall_percentage: ((queue_avg_io_ms + queue_avg_stall_ms) / queue_avg_processing_ms * 100).round(1)
121
+ }
122
+ end
123
+
124
+ results
125
+ end
126
+ end
127
+ end
@@ -4,7 +4,11 @@ module Perfm
4
4
  def store(metrics)
5
5
  return if metrics.empty?
6
6
 
7
- Perfm::GvlMetric.insert_all(metrics)
7
+ if metrics.first.key?(:job_class)
8
+ SidekiqGvlMetric.insert_all(metrics)
9
+ else
10
+ Perfm::GvlMetric.insert_all(metrics)
11
+ end
8
12
  end
9
13
  end
10
14
  end
data/lib/perfm/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Perfm
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/perfm.rb CHANGED
@@ -11,6 +11,7 @@ module Perfm
11
11
  autoload :Queue, "perfm/queue"
12
12
  autoload :Agent, "perfm/agent"
13
13
  autoload :GvlMetricsAnalyzer, "perfm/gvl_metrics_analyzer"
14
+ autoload :SidekiqGvlMetricsAnalyzer, "perfm/sidekiq_gvl_metrics_analyzer"
14
15
 
15
16
  module Storage
16
17
  autoload :Base, "perfm/storage/base"
@@ -20,6 +21,7 @@ module Perfm
20
21
 
21
22
  module Middleware
22
23
  autoload :GvlInstrumentation, "perfm/middleware/gvl_instrumentation"
24
+ autoload :SidekiqGvlInstrumentation, "perfm/middleware/sidekiq_gvl_instrumentation"
23
25
  end
24
26
 
25
27
  class << self
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perfm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vishnu M
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-05 00:00:00.000000000 Z
11
+ date: 2025-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -114,7 +114,7 @@ dependencies:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
116
  version: '1.21'
117
- description: Monitor Rails application performance metrics
117
+ description: Perfm aims to be a performance monitoring tool for Ruby on Rails applications.
118
118
  email:
119
119
  - vishnu.m@bigbinary.com
120
120
  executables: []
@@ -126,8 +126,11 @@ files:
126
126
  - Rakefile
127
127
  - app/models/perfm/application_record.rb
128
128
  - app/models/perfm/gvl_metric.rb
129
+ - app/models/perfm/sidekiq_gvl_metric.rb
129
130
  - lib/generators/perfm/install/install_generator.rb
130
131
  - lib/generators/perfm/install/templates/create_perfm_gvl_metrics.rb.erb
132
+ - lib/generators/perfm/sidekiq_gvl_metrics/sidekiq_gvl_metrics_generator.rb
133
+ - lib/generators/perfm/sidekiq_gvl_metrics/templates/create_perfm_sidekiq_gvl_metrics.rb.erb
131
134
  - lib/generators/perfm/uninstall/templates/drop_perfm_gvl_metrics.rb.erb
132
135
  - lib/generators/perfm/uninstall/uninstall_generator.rb
133
136
  - lib/perfm.rb
@@ -140,20 +143,22 @@ files:
140
143
  - lib/perfm/heap_dumper.rb
141
144
  - lib/perfm/metrics/sidekiq.rb
142
145
  - lib/perfm/middleware/gvl_instrumentation.rb
146
+ - lib/perfm/middleware/sidekiq_gvl_instrumentation.rb
143
147
  - lib/perfm/pid_store.rb
144
148
  - lib/perfm/queue.rb
145
149
  - lib/perfm/queue_latency.rb
150
+ - lib/perfm/sidekiq_gvl_metrics_analyzer.rb
146
151
  - lib/perfm/storage/api.rb
147
152
  - lib/perfm/storage/base.rb
148
153
  - lib/perfm/storage/local.rb
149
154
  - lib/perfm/version.rb
150
- homepage: https://github.com/vishnu-m/perfm
155
+ homepage: https://github.com/bigbinary/perfm
151
156
  licenses:
152
157
  - MIT
153
158
  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
159
+ homepage_uri: https://github.com/bigbinary/perfm
160
+ source_code_uri: https://github.com/bigbinary/perfm
161
+ changelog_uri: https://github.com/bigbinary/perfm/blob/master/CHANGELOG.md
157
162
  post_install_message:
158
163
  rdoc_options: []
159
164
  require_paths:
@@ -169,8 +174,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
174
  - !ruby/object:Gem::Version
170
175
  version: '0'
171
176
  requirements: []
172
- rubygems_version: 3.4.1
177
+ rubygems_version: 3.4.19
173
178
  signing_key:
174
179
  specification_version: 4
175
- summary: Everything Rails performance monitoring
180
+ summary: Perfm aims to be a performance monitoring tool for Ruby on Rails applications.
181
+ Currently, it has support for GVL instrumentation and provides analytics to help
182
+ optimize Puma thread concurrency settings based on the collected GVL data.
176
183
  test_files: []