app_profiler 0.1.10 → 0.2.0

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: 70f971ebae2492140483b90ada2f0d1daf33874803dcc4ea5404877093651974
4
- data.tar.gz: df8395bf47f6e378f9322386a17c1934e9b38ac9269ea7ded80a721de7a5c4e9
3
+ metadata.gz: 1c36371d12c24dfed23e6e492b5c3912e01480bfd7a5973bf1f311a86adc86d3
4
+ data.tar.gz: 1b78da495c55cee24ab6b0d6f8d8194ca6cfb27030de7a4ba90db37fa60e3988
5
5
  SHA512:
6
- metadata.gz: 2a10f138198666ddc840658e9599c05c40d4d2a14442b0cfa52df8776e00601fda95aaea871138656cdf2a05ae697966e30fbc62c3a1e5351b04987639a63b82
7
- data.tar.gz: 2c6af888489015d1bcd827704a7710512455bd2c05d4e3aee596c32063d3b134049d592e5c0012d947e7f1f4731e8a55752303064f866cb535d8fb9ae06b91b9
6
+ metadata.gz: bd4e66293f491f25caba2a352b509012344bdc6e04c880cb4c3d60d00f3dc38cab50b9c59cf0cbe14c36adc2591610bd004addc28521cf077b7d3b7c91585947
7
+ data.tar.gz: 036c9de32a86b5e99e1d244fe1200aa3e0dabdf6d610d91c34aa6621bf8a194b9a769a8a9bae254432d2e2bfae67b137a1209c4214bb2c2ce774aa9db9bea907
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ module Backend
5
+ class BaseBackend
6
+ def self.name
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def run(params = {}, &block)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def start(params = {})
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def stop
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def results
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def running?
27
+ raise NotImplementedError
28
+ end
29
+
30
+ class << self
31
+ def run_lock
32
+ @run_lock ||= Mutex.new
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ def acquire_run_lock
39
+ self.class.run_lock.try_lock
40
+ end
41
+
42
+ def release_run_lock
43
+ self.class.run_lock.unlock
44
+ rescue ThreadError
45
+ AppProfiler.logger.warn("[AppProfiler] run lock not released as it was never acquired")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -3,13 +3,23 @@
3
3
  require "stackprof"
4
4
 
5
5
  module AppProfiler
6
- module Profiler
7
- DEFAULTS = {
8
- mode: :cpu,
9
- raw: true,
10
- }.freeze
6
+ module Backend
7
+ class StackprofBackend < BaseBackend
8
+ DEFAULTS = {
9
+ mode: :cpu,
10
+ raw: true,
11
+ }.freeze
12
+
13
+ AVAILABLE_MODES = [
14
+ :wall,
15
+ :cpu,
16
+ :object,
17
+ ].freeze
18
+
19
+ def self.name
20
+ :stackprof
21
+ end
11
22
 
12
- class << self
13
23
  def run(params = {})
14
24
  started = start(params)
15
25
 
@@ -27,6 +37,7 @@ module AppProfiler
27
37
  def start(params = {})
28
38
  # Do not start the profiler if StackProf was started somewhere else.
29
39
  return false if running?
40
+ return false unless acquire_run_lock
30
41
 
31
42
  clear
32
43
 
@@ -35,6 +46,7 @@ module AppProfiler
35
46
  AppProfiler.logger.info(
36
47
  "[Profiler] failed to start the profiler error_class=#{error.class} error_message=#{error.message}"
37
48
  )
49
+ release_run_lock
38
50
  # This is a boolean instead of nil because StackProf#start returns a
39
51
  # boolean as well.
40
52
  false
@@ -42,14 +54,16 @@ module AppProfiler
42
54
 
43
55
  def stop
44
56
  StackProf.stop
57
+ ensure
58
+ release_run_lock
45
59
  end
46
60
 
47
61
  def results
48
- stackprof_profile = stackprof_results
62
+ stackprof_profile = backend_results
49
63
 
50
64
  return unless stackprof_profile
51
65
 
52
- Profile.from_stackprof(stackprof_profile)
66
+ BaseProfile.from_stackprof(stackprof_profile)
53
67
  rescue => error
54
68
  AppProfiler.logger.info(
55
69
  "[Profiler] failed to obtain the profile error_class=#{error.class} error_message=#{error.message}"
@@ -57,9 +71,13 @@ module AppProfiler
57
71
  nil
58
72
  end
59
73
 
74
+ def running?
75
+ StackProf.running?
76
+ end
77
+
60
78
  private
61
79
 
62
- def stackprof_results
80
+ def backend_results
63
81
  StackProf.results
64
82
  end
65
83
 
@@ -73,14 +91,8 @@ module AppProfiler
73
91
  # Ref: https://github.com/tmm1/stackprof/blob/0ded6c/ext/stackprof/stackprof.c#L118-L123
74
92
  #
75
93
  def clear
76
- stackprof_results
77
- end
78
-
79
- def running?
80
- StackProf.running?
94
+ backend_results
81
95
  end
82
96
  end
83
97
  end
84
-
85
- private_constant :Profiler
86
98
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem("vernier", ">= 0.7.0")
4
+ require "vernier"
5
+
6
+ module AppProfiler
7
+ module Backend
8
+ class VernierBackend < BaseBackend
9
+ DEFAULTS = {
10
+ mode: :wall,
11
+ }.freeze
12
+
13
+ AVAILABLE_MODES = [
14
+ :wall,
15
+ :retained,
16
+ ].freeze
17
+
18
+ def self.name
19
+ :vernier
20
+ end
21
+
22
+ def run(params = {})
23
+ started = start(params)
24
+
25
+ yield
26
+
27
+ return unless started
28
+
29
+ stop
30
+ results
31
+ ensure
32
+ # Only stop the profiler if profiling was started in this context.
33
+ stop if started
34
+ end
35
+
36
+ def start(params = {})
37
+ # Do not start the profiler if we already have a collector started somewhere else.
38
+ return false if running?
39
+ return false unless acquire_run_lock
40
+
41
+ @mode = params.delete(:mode) || DEFAULTS[:mode]
42
+ raise ArgumentError unless AVAILABLE_MODES.include?(@mode)
43
+
44
+ @metadata = params.delete(:metadata)
45
+ clear
46
+
47
+ @collector ||= ::Vernier::Collector.new(@mode, **params)
48
+ @collector.start
49
+ rescue => error
50
+ AppProfiler.logger.info(
51
+ "[Profiler] failed to start the profiler error_class=#{error.class} error_message=#{error.message}"
52
+ )
53
+ release_run_lock
54
+ # This is a boolean instead of nil to be consistent with the stackprof backend behaviour
55
+ # boolean as well.
56
+ false
57
+ end
58
+
59
+ def stop
60
+ return false unless running?
61
+
62
+ @results = @collector&.stop
63
+ @collector = nil
64
+ !@results.nil?
65
+ ensure
66
+ release_run_lock
67
+ end
68
+
69
+ def results
70
+ vernier_profile = backend_results
71
+ clear
72
+
73
+ return unless vernier_profile
74
+
75
+ # HACK: - "data" is private, but we want to avoid serializing to JSON then
76
+ # parsing back from JSON by just directly getting the hash
77
+ data = ::Vernier::Output::Firefox.new(vernier_profile).send(:data)
78
+ data[:meta][:mode] = @mode # TODO: https://github.com/jhawthorn/vernier/issues/30
79
+ data[:meta].merge!(@metadata) if @metadata
80
+ @mode = nil
81
+ @metadata = nil
82
+
83
+ BaseProfile.from_vernier(data)
84
+ rescue => error
85
+ AppProfiler.logger.info(
86
+ "[Profiler] failed to obtain the profile error_class=#{error.class} error_message=#{error.message}"
87
+ )
88
+ nil
89
+ end
90
+
91
+ def running?
92
+ @collector != nil
93
+ end
94
+
95
+ private
96
+
97
+ def backend_results
98
+ @results
99
+ end
100
+
101
+ def clear
102
+ @results = nil
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ module Backend
5
+ autoload :BaseBackend, "app_profiler/backend/base_backend"
6
+ autoload :StackprofBackend, "app_profiler/backend/stackprof_backend"
7
+ autoload :VernierBackend, "app_profiler/backend/vernier_backend"
8
+ end
9
+ end
@@ -9,7 +9,7 @@ module AppProfiler
9
9
  end
10
10
 
11
11
  def cleanup
12
- profile = Profiler.results
12
+ profile = AppProfiler.profiler.results
13
13
  call(profile) if profile
14
14
  end
15
15
  end
@@ -31,7 +31,7 @@ module AppProfiler
31
31
 
32
32
  return yield unless before_profile(env, params_hash)
33
33
 
34
- profile = AppProfiler.run(params_hash) do
34
+ profile = AppProfiler.run(**params_hash) do
35
35
  response = yield
36
36
  end
37
37
 
@@ -4,17 +4,18 @@ require "rack"
4
4
 
5
5
  module AppProfiler
6
6
  class Parameters
7
- DEFAULT_INTERVALS = { "cpu" => 1000, "wall" => 1000, "object" => 2000 }.freeze
8
- MIN_INTERVALS = { "cpu" => 200, "wall" => 200, "object" => 400 }.freeze
9
- MODES = DEFAULT_INTERVALS.keys.freeze
7
+ DEFAULT_INTERVALS = { "cpu" => 1000, "wall" => 1000, "object" => 2000, "retained" => 0 }.freeze
8
+ MIN_INTERVALS = { "cpu" => 200, "wall" => 200, "object" => 400, "retained" => 0 }.freeze
10
9
 
11
- attr_reader :autoredirect, :async
10
+ attr_reader :autoredirect, :async, :backend
12
11
 
13
- def initialize(mode: :wall, interval: nil, ignore_gc: false, autoredirect: false, async: false, metadata: {})
12
+ def initialize(mode: :wall, interval: nil, ignore_gc: false, autoredirect: false,
13
+ async: false, backend: nil, metadata: {})
14
14
  @mode = mode.to_sym
15
15
  @interval = [interval&.to_i || DEFAULT_INTERVALS.fetch(@mode.to_s), MIN_INTERVALS.fetch(@mode.to_s)].max
16
16
  @ignore_gc = !!ignore_gc
17
17
  @autoredirect = autoredirect
18
+ @backend = backend || AppProfiler::Backend::StackprofBackend.name
18
19
  @metadata = { context: AppProfiler.context }.merge(metadata)
19
20
  @async = async
20
21
  end
@@ -29,6 +30,7 @@ module AppProfiler
29
30
  interval: @interval,
30
31
  ignore_gc: @ignore_gc,
31
32
  metadata: @metadata,
33
+ backend: @backend,
32
34
  }
33
35
  end
34
36
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ class StackprofProfile < BaseProfile
5
+ FILE_EXTENSION = ".json"
6
+
7
+ def mode
8
+ @data[:mode]
9
+ end
10
+
11
+ def format
12
+ FILE_EXTENSION
13
+ end
14
+
15
+ def view(params = {})
16
+ AppProfiler.viewer.view(self, **params)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ class VernierProfile < BaseProfile
5
+ FILE_EXTENSION = ".gecko.json"
6
+
7
+ def mode
8
+ @data[:meta][:mode]
9
+ end
10
+
11
+ def format
12
+ FILE_EXTENSION
13
+ end
14
+
15
+ def view(params = {})
16
+ raise NotImplementedError
17
+ end
18
+ end
19
+ end
@@ -1,42 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/deprecation/constant_accessor"
4
+
3
5
  module AppProfiler
4
- class Profile
6
+ autoload :StackprofProfile, "app_profiler/profile/stackprof"
7
+ autoload :VernierProfile, "app_profiler/profile/vernier"
8
+
9
+ class BaseProfile
5
10
  INTERNAL_METADATA_KEYS = [:id, :context]
6
11
  private_constant :INTERNAL_METADATA_KEYS
7
12
  class UnsafeFilename < StandardError; end
8
13
 
9
- delegate :[], to: :@data
10
14
  attr_reader :id, :context
11
15
 
16
+ delegate :[], to: :@data
17
+
12
18
  # This function should not be called if `StackProf.results` returns nil.
13
19
  def self.from_stackprof(data)
14
20
  options = INTERNAL_METADATA_KEYS.map { |key| [key, data[:metadata]&.delete(key)] }.to_h
15
21
 
16
- new(data, **options).tap do |profile|
22
+ StackprofProfile.new(data, **options).tap do |profile|
23
+ raise ArgumentError, "invalid profile data" unless profile.valid?
24
+ end
25
+ end
26
+
27
+ def self.from_vernier(data)
28
+ options = INTERNAL_METADATA_KEYS.map { |key| [key, data[:meta]&.delete(key)] }.to_h
29
+
30
+ VernierProfile.new(data, **options).tap do |profile|
17
31
  raise ArgumentError, "invalid profile data" unless profile.valid?
18
32
  end
19
33
  end
20
34
 
21
- # `data` is assumed to be a Hash.
35
+ # `data` is assumed to be a Hash for Stackprof,
36
+ # a vernier "result" object for vernier
22
37
  def initialize(data, id: nil, context: nil)
23
38
  @id = id.presence || SecureRandom.hex
24
39
  @context = context
25
40
  @data = data
26
41
  end
27
42
 
28
- def valid?
29
- mode.present?
30
- end
31
-
32
- def mode
33
- @data[:mode]
34
- end
35
-
36
- def view(params = {})
37
- AppProfiler.viewer.view(self, **params)
38
- end
39
-
40
43
  def upload
41
44
  AppProfiler.storage.upload(self).tap do |upload|
42
45
  if upload && defined?(upload.url)
@@ -60,6 +63,10 @@ module AppProfiler
60
63
  AppProfiler.storage.enqueue_upload(self)
61
64
  end
62
65
 
66
+ def valid?
67
+ mode.present?
68
+ end
69
+
63
70
  def file
64
71
  @file ||= path.tap do |p|
65
72
  p.dirname.mkpath
@@ -71,6 +78,18 @@ module AppProfiler
71
78
  @data
72
79
  end
73
80
 
81
+ def mode
82
+ raise NotImplementedError
83
+ end
84
+
85
+ def format
86
+ raise NotImplementedError
87
+ end
88
+
89
+ def view(params = {})
90
+ raise NotImplementedError
91
+ end
92
+
74
93
  private
75
94
 
76
95
  def path
@@ -79,11 +98,14 @@ module AppProfiler
79
98
  mode,
80
99
  id,
81
100
  Socket.gethostname,
82
- ].compact.join("-") << ".json"
101
+ ].compact.join("-") << format
83
102
 
84
103
  raise UnsafeFilename if /[^0-9A-Za-z.\-\_]/.match?(filename)
85
104
 
86
105
  AppProfiler.profile_root.join(filename)
87
106
  end
88
107
  end
108
+
109
+ include ActiveSupport::Deprecation::DeprecatedConstantAccessor
110
+ deprecate_constant "Profile", "AppProfiler::BaseProfile", deprecator: ActiveSupport::Deprecation.new
89
111
  end
@@ -40,6 +40,7 @@ module AppProfiler
40
40
  AppProfiler.profile_enqueue_success = app.config.app_profiler.profile_enqueue_success
41
41
  AppProfiler.profile_enqueue_failure = app.config.app_profiler.profile_enqueue_failure
42
42
  AppProfiler.after_process_queue = app.config.app_profiler.after_process_queue
43
+ AppProfiler.backend = app.config.app_profiler.profiler_backend || :stackprof
43
44
  end
44
45
 
45
46
  initializer "app_profiler.add_middleware" do |app|
@@ -16,17 +16,30 @@ module AppProfiler
16
16
  query_param("async")
17
17
  end
18
18
 
19
+ def backend
20
+ backend = query_param("backend") || profile_header_param("backend") ||
21
+ AppProfiler.backend
22
+ backend.to_sym
23
+ end
24
+
19
25
  def valid?
20
26
  if mode.blank?
21
27
  return false
22
28
  end
23
29
 
24
- unless Parameters::MODES.include?(mode)
25
- AppProfiler.logger.info("[Profiler] unsupported profiling mode=#{mode}")
30
+ return false if backend != AppProfiler::Backend::StackprofBackend.name && !AppProfiler.vernier_supported?
31
+
32
+ if AppProfiler.vernier_supported? && backend == AppProfiler::Backend::VernierBackend.name &&
33
+ !AppProfiler::Backend::VernierBackend::AVAILABLE_MODES.include?(mode.to_sym)
34
+ AppProfiler.logger.info("[AppProfiler] unsupported profiling mode=#{mode} for backend #{backend}")
35
+ return false
36
+ elsif backend == AppProfiler::Backend::StackprofBackend.name &&
37
+ !AppProfiler::Backend::StackprofBackend::AVAILABLE_MODES.include?(mode.to_sym)
38
+ AppProfiler.logger.info("[AppProfiler] unsupported profiling mode=#{mode} for backend #{backend}")
26
39
  return false
27
40
  end
28
41
 
29
- if interval.to_i < Parameters::MIN_INTERVALS[mode]
42
+ if interval.to_i < Parameters::MIN_INTERVALS[mode.to_s]
30
43
  return false
31
44
  end
32
45
 
@@ -38,6 +51,7 @@ module AppProfiler
38
51
  mode: mode.to_sym,
39
52
  interval: interval.to_i,
40
53
  ignore_gc: !!ignore_gc,
54
+ backend: backend,
41
55
  metadata: {
42
56
  id: request_id,
43
57
  context: context,
@@ -56,7 +70,7 @@ module AppProfiler
56
70
  end
57
71
 
58
72
  def interval
59
- query_param("interval") || profile_header_param("interval") || Parameters::DEFAULT_INTERVALS[mode]
73
+ query_param("interval") || profile_header_param("interval") || Parameters::DEFAULT_INTERVALS[mode.to_s]
60
74
  end
61
75
 
62
76
  def request_id
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppProfiler
4
- VERSION = "0.1.10"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/app_profiler.rb CHANGED
@@ -9,6 +9,9 @@ module AppProfiler
9
9
  class ConfigurationError < StandardError
10
10
  end
11
11
 
12
+ class BackendError < StandardError
13
+ end
14
+
12
15
  DefaultProfileFormatter = proc do |upload|
13
16
  "#{AppProfiler.speedscope_host}#profileURL=#{upload.url}"
14
17
  end
@@ -32,8 +35,8 @@ module AppProfiler
32
35
  require "app_profiler/middleware"
33
36
  require "app_profiler/parameters"
34
37
  require "app_profiler/request_parameters"
35
- require "app_profiler/profiler"
36
38
  require "app_profiler/profile"
39
+ require "app_profiler/backend"
37
40
  require "app_profiler/server"
38
41
 
39
42
  mattr_accessor :logger, default: Logger.new($stdout)
@@ -60,17 +63,73 @@ module AppProfiler
60
63
  mattr_reader :after_process_queue, default: nil
61
64
 
62
65
  class << self
63
- def run(*args, &block)
64
- Profiler.run(*args, &block)
66
+ def run(*args, backend: nil, **kwargs, &block)
67
+ orig_backend = self.backend
68
+ begin
69
+ self.backend = backend if backend
70
+ profiler.run(*args, **kwargs, &block)
71
+ rescue BackendError => e
72
+ logger.error(
73
+ "[AppProfiler.run] exception #{e} configuring backend #{backend}: #{e.message}"
74
+ )
75
+ yield
76
+ end
77
+ ensure
78
+ AppProfiler.backend = orig_backend
65
79
  end
66
80
 
67
81
  def start(*args)
68
- Profiler.start(*args)
82
+ profiler.start(*args)
69
83
  end
70
84
 
71
85
  def stop
72
- Profiler.stop
73
- Profiler.results
86
+ profiler.stop
87
+ profiler.results
88
+ end
89
+
90
+ def running?
91
+ @backend&.running?
92
+ end
93
+
94
+ def profiler
95
+ backend
96
+ @backend ||= @profiler_backend.new
97
+ end
98
+
99
+ def backend=(new_backend)
100
+ return if new_backend == backend
101
+
102
+ new_profiler_backend = backend_for(new_backend)
103
+
104
+ if running?
105
+ raise BackendError,
106
+ "cannot change backend to #{new_backend} while #{backend} backend is running"
107
+ end
108
+
109
+ return if @profiler_backend == new_profiler_backend
110
+
111
+ clear
112
+ @profiler_backend = new_profiler_backend
113
+ end
114
+
115
+ def backend_for(backend_name)
116
+ if vernier_supported? &&
117
+ backend_name == AppProfiler::Backend::VernierBackend.name
118
+ AppProfiler::Backend::VernierBackend
119
+ elsif backend_name == AppProfiler::Backend::StackprofBackend.name
120
+ AppProfiler::Backend::StackprofBackend
121
+ else
122
+ raise BackendError, "unknown backend #{backend_name}"
123
+ end
124
+ end
125
+
126
+ def backend
127
+ @profiler_backend ||= Backend::StackprofBackend
128
+ @profiler_backend.name
129
+ end
130
+
131
+ def vernier_supported?
132
+ defined?(AppProfiler::Backend::VernierBackend.name)
74
133
  end
75
134
 
76
135
  def profile_header=(profile_header)
@@ -120,6 +179,13 @@ module AppProfiler
120
179
 
121
180
  AppProfiler.profile_url_formatter.call(upload)
122
181
  end
182
+
183
+ private
184
+
185
+ def clear
186
+ @backend.stop if @backend&.running?
187
+ @backend = nil
188
+ end
123
189
  end
124
190
 
125
191
  require "app_profiler/railtie" if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: app_profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gannon McGibbon
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2023-12-13 00:00:00.000000000 Z
16
+ date: 2024-06-18 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: activesupport
@@ -135,13 +135,18 @@ extensions: []
135
135
  extra_rdoc_files: []
136
136
  files:
137
137
  - lib/app_profiler.rb
138
+ - lib/app_profiler/backend.rb
139
+ - lib/app_profiler/backend/base_backend.rb
140
+ - lib/app_profiler/backend/stackprof_backend.rb
141
+ - lib/app_profiler/backend/vernier_backend.rb
138
142
  - lib/app_profiler/middleware.rb
139
143
  - lib/app_profiler/middleware/base_action.rb
140
144
  - lib/app_profiler/middleware/upload_action.rb
141
145
  - lib/app_profiler/middleware/view_action.rb
142
146
  - lib/app_profiler/parameters.rb
143
147
  - lib/app_profiler/profile.rb
144
- - lib/app_profiler/profiler.rb
148
+ - lib/app_profiler/profile/stackprof.rb
149
+ - lib/app_profiler/profile/vernier.rb
145
150
  - lib/app_profiler/railtie.rb
146
151
  - lib/app_profiler/request_parameters.rb
147
152
  - lib/app_profiler/server.rb
@@ -175,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
180
  - !ruby/object:Gem::Version
176
181
  version: '0'
177
182
  requirements: []
178
- rubygems_version: 3.4.22
183
+ rubygems_version: 3.5.11
179
184
  signing_key:
180
185
  specification_version: 4
181
186
  summary: Collect performance profiles for your Rails application.