app_profiler 0.5.0 → 0.7.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 +4 -4
- data/lib/app_profiler/backend/base_backend.rb +4 -0
- data/lib/app_profiler/backend/vernier_backend.rb +17 -3
- data/lib/app_profiler/base_profile.rb +1 -1
- data/lib/app_profiler/middleware/upload_action.rb +3 -3
- data/lib/app_profiler/railtie.rb +2 -1
- data/lib/app_profiler/request_parameters.rb +1 -3
- data/lib/app_profiler/vernier_profile.rb +3 -2
- data/lib/app_profiler/version.rb +1 -1
- data/lib/app_profiler/viewer/base_middleware.rb +1 -1
- data/lib/app_profiler/viewer/firefox_remote_viewer/middleware.rb +5 -5
- data/lib/app_profiler/viewer/firefox_remote_viewer.rb +1 -1
- data/lib/app_profiler/viewer/speedscope_remote_viewer/middleware.rb +3 -3
- data/lib/app_profiler/viewer/speedscope_remote_viewer.rb +1 -1
- data/lib/app_profiler/yarn/with_firefox_profiler.rb +1 -1
- data/lib/app_profiler.rb +14 -26
- metadata +51 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 414e73739d0778f252bdb142852ff9a7428a3b76388a5b46108546b91d6a682f
|
|
4
|
+
data.tar.gz: 1d87ba7dbcd9de74c55bbab01f6fcfa9c2882ca887c9384e06d9c8f54237b58b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ccd99ffeb214e7a854bde04fca23191d5cd46ab2b370df07e75e5866a6f0bdbb4af4125cad65774bed7984d228ab4ea90387466075aead7da484dc117b9bb70d
|
|
7
|
+
data.tar.gz: 2629a0e89e29791313b40f25b397cea321f1e27a58d5e42e2c2f18d31577842f94c127d1b2a2d5fa3fc8945539e0f81aa882ac792efb2f7ec8799069f5816cce
|
|
@@ -15,6 +15,10 @@ module AppProfiler
|
|
|
15
15
|
:retained,
|
|
16
16
|
].freeze
|
|
17
17
|
|
|
18
|
+
PRIVATE_METADATA = [
|
|
19
|
+
:started_at, # started_at uses a monotonic source, not realtime
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
18
22
|
class << self
|
|
19
23
|
def name
|
|
20
24
|
:vernier
|
|
@@ -29,7 +33,9 @@ module AppProfiler
|
|
|
29
33
|
@mode = params.delete(:mode) || DEFAULTS[:mode]
|
|
30
34
|
raise ArgumentError unless AVAILABLE_MODES.include?(@mode)
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
if Gem.loaded_specs["vernier"].version < Gem::Version.new("1.7.0")
|
|
37
|
+
@metadata = params.delete(:metadata)
|
|
38
|
+
end
|
|
33
39
|
clear
|
|
34
40
|
|
|
35
41
|
@collector ||= ::Vernier::Collector.new(@mode, **params)
|
|
@@ -60,11 +66,19 @@ module AppProfiler
|
|
|
60
66
|
|
|
61
67
|
return unless vernier_profile
|
|
62
68
|
|
|
69
|
+
# Store all vernier metadata
|
|
70
|
+
meta = vernier_profile.meta.reject { |k, v| k == :user_metadata || v.nil? || PRIVATE_METADATA.include?(k) }
|
|
71
|
+
meta.merge!(@metadata) if @metadata
|
|
72
|
+
|
|
73
|
+
# Internal metadata takes precedence over user metadata, but store
|
|
74
|
+
# everything in user metadata
|
|
75
|
+
vernier_profile.meta[:user_metadata]&.merge!(meta)
|
|
76
|
+
|
|
63
77
|
# HACK: - "data" is private, but we want to avoid serializing to JSON then
|
|
64
78
|
# parsing back from JSON by just directly getting the hash
|
|
65
79
|
data = ::Vernier::Output::Firefox.new(vernier_profile).send(:data)
|
|
66
|
-
data[:meta][:mode] = @mode
|
|
67
|
-
data[:meta]
|
|
80
|
+
data[:meta][:mode] = @mode
|
|
81
|
+
data[:meta][:vernierUserMetadata] ||= meta # for compatibility with < 1.7.0
|
|
68
82
|
@mode = nil
|
|
69
83
|
@metadata = nil
|
|
70
84
|
|
|
@@ -23,7 +23,7 @@ module AppProfiler
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def from_vernier(data)
|
|
26
|
-
options = INTERNAL_METADATA_KEYS.map { |key| [key, data[:meta]&.delete(key)] }.to_h
|
|
26
|
+
options = INTERNAL_METADATA_KEYS.map { |key| [key, data[:meta]&.[](:vernierUserMetadata)&.delete(key)] }.to_h
|
|
27
27
|
|
|
28
28
|
VernierProfile.new(data, **options).tap do |profile|
|
|
29
29
|
raise ArgumentError, "invalid profile data" unless profile.valid?
|
|
@@ -24,15 +24,15 @@ module AppProfiler
|
|
|
24
24
|
def append_headers(response, upload:, autoredirect:)
|
|
25
25
|
return unless upload
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
location = AppProfiler.profile_url(upload)
|
|
28
|
+
response[1][profile_header] = location if location
|
|
28
29
|
response[1][profile_data_header] = profile_data_url(upload)
|
|
29
30
|
|
|
30
31
|
return unless autoredirect
|
|
31
32
|
|
|
32
33
|
# Automatically redirect to profile if autoredirect is true.
|
|
33
|
-
location = AppProfiler.profile_url(upload)
|
|
34
34
|
if response[0].to_i < 500 && location
|
|
35
|
-
response[1]["
|
|
35
|
+
response[1]["location"] = location
|
|
36
36
|
response[0] = 303
|
|
37
37
|
end
|
|
38
38
|
end
|
data/lib/app_profiler/railtie.rb
CHANGED
|
@@ -26,7 +26,8 @@ module AppProfiler
|
|
|
26
26
|
AppProfiler.server.transport = app.config.app_profiler.server_transport || default_appprofiler_transport
|
|
27
27
|
AppProfiler.server.port = app.config.app_profiler.server_port || 0
|
|
28
28
|
AppProfiler.server.duration = app.config.app_profiler.server_duration || 30
|
|
29
|
-
|
|
29
|
+
server_cors = app.config.app_profiler.server_cors
|
|
30
|
+
AppProfiler.server.cors = server_cors.nil? ? true : server_cors
|
|
30
31
|
AppProfiler.server.cors_host = app.config.app_profiler.server_cors_host || "*"
|
|
31
32
|
AppProfiler.autoredirect = app.config.app_profiler.autoredirect || false
|
|
32
33
|
AppProfiler.speedscope_host = app.config.app_profiler.speedscope_host || ENV.fetch(
|
|
@@ -31,9 +31,7 @@ module AppProfiler
|
|
|
31
31
|
return false
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if AppProfiler.vernier_supported? && backend == AppProfiler::VernierProfile::BACKEND_NAME &&
|
|
34
|
+
if backend == AppProfiler::VernierProfile::BACKEND_NAME &&
|
|
37
35
|
!AppProfiler::Backend::VernierBackend::AVAILABLE_MODES.include?(mode.to_sym)
|
|
38
36
|
AppProfiler.logger.info("[AppProfiler] unsupported profiling mode=#{mode} for backend #{backend}")
|
|
39
37
|
return false
|
|
@@ -7,13 +7,14 @@ module AppProfiler
|
|
|
7
7
|
|
|
8
8
|
class << self
|
|
9
9
|
def backend_name
|
|
10
|
-
#
|
|
10
|
+
# Keep metadata access independent of the optional Vernier gem.
|
|
11
11
|
BACKEND_NAME.to_s
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def initialize(data, id: nil, context: nil)
|
|
16
16
|
data[:meta] ||= {}
|
|
17
|
+
data[:meta][:vernierUserMetadata] ||= {}
|
|
17
18
|
super(data, id: id, context: context)
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -22,7 +23,7 @@ module AppProfiler
|
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def metadata
|
|
25
|
-
@data[:meta]
|
|
26
|
+
@data[:meta][:vernierUserMetadata]
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def format
|
data/lib/app_profiler/version.rb
CHANGED
|
@@ -11,7 +11,7 @@ module AppProfiler
|
|
|
11
11
|
|
|
12
12
|
def initialize(app)
|
|
13
13
|
super
|
|
14
|
-
@firefox_profiler = Rack::
|
|
14
|
+
@firefox_profiler = Rack::Files.new(
|
|
15
15
|
File.join(AppProfiler.root, "node_modules/firefox-profiler/dist"),
|
|
16
16
|
)
|
|
17
17
|
end
|
|
@@ -40,9 +40,9 @@ module AppProfiler
|
|
|
40
40
|
source = "#{proto}://#{host}/app_profiler/firefox/#{path}"
|
|
41
41
|
target = "/from-url/#{CGI.escape(source)}"
|
|
42
42
|
|
|
43
|
-
[302, { "
|
|
43
|
+
[302, { "location" => target }, [""]]
|
|
44
44
|
else
|
|
45
|
-
env[Rack::PATH_INFO] = path.delete_prefix("/
|
|
45
|
+
env[Rack::PATH_INFO] = "/#{path.delete_prefix("/")}"
|
|
46
46
|
firefox_profiler.call(env)
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -50,7 +50,7 @@ module AppProfiler
|
|
|
50
50
|
def from(env, path)
|
|
51
51
|
setup_yarn unless yarn_setup
|
|
52
52
|
index = File.read(File.join(AppProfiler.root, "node_modules/firefox-profiler/dist/index.html"))
|
|
53
|
-
[200, { "
|
|
53
|
+
[200, { "content-type" => "text/html" }, [index]]
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def show(_env, name)
|
|
@@ -58,7 +58,7 @@ module AppProfiler
|
|
|
58
58
|
id(file) == name
|
|
59
59
|
end || raise(ArgumentError)
|
|
60
60
|
|
|
61
|
-
[200, { "
|
|
61
|
+
[200, { "content-type" => "application/json" }, [profile.read]]
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
end
|
|
@@ -22,7 +22,7 @@ module AppProfiler
|
|
|
22
22
|
id = Middleware.id(@profile.file)
|
|
23
23
|
|
|
24
24
|
if response && response[0].to_i < 500
|
|
25
|
-
response[1]["
|
|
25
|
+
response[1]["location"] = "/app_profiler/#{NAME}/viewer/#{id}"
|
|
26
26
|
response[0] = 303
|
|
27
27
|
else
|
|
28
28
|
AppProfiler.logger.info("[Profiler] Profile available at /app_profiler/#{id}\n")
|
|
@@ -11,7 +11,7 @@ module AppProfiler
|
|
|
11
11
|
|
|
12
12
|
def initialize(app)
|
|
13
13
|
super
|
|
14
|
-
@speedscope = Rack::
|
|
14
|
+
@speedscope = Rack::Files.new(
|
|
15
15
|
File.join(AppProfiler.root, "node_modules/speedscope/dist/release"),
|
|
16
16
|
)
|
|
17
17
|
end
|
|
@@ -32,7 +32,7 @@ module AppProfiler
|
|
|
32
32
|
def viewer(env, path)
|
|
33
33
|
setup_yarn unless yarn_setup
|
|
34
34
|
|
|
35
|
-
env[Rack::PATH_INFO] = path.delete_prefix("/
|
|
35
|
+
env[Rack::PATH_INFO] = "/#{path.delete_prefix("/")}"
|
|
36
36
|
speedscope.call(env)
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -41,7 +41,7 @@ module AppProfiler
|
|
|
41
41
|
id(file) == name
|
|
42
42
|
end || raise(ArgumentError)
|
|
43
43
|
|
|
44
|
-
[200, { "
|
|
44
|
+
[200, { "content-type" => "application/json" }, [profile.read]]
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
end
|
|
@@ -29,7 +29,7 @@ module AppProfiler
|
|
|
29
29
|
id = Middleware.id(@profile.file)
|
|
30
30
|
|
|
31
31
|
if response && response[0].to_i < 500
|
|
32
|
-
response[1]["
|
|
32
|
+
response[1]["location"] = "/app_profiler/#{NAME}/viewer/#{id}"
|
|
33
33
|
response[0] = 303
|
|
34
34
|
else
|
|
35
35
|
AppProfiler.logger.info("[Profiler] Profile available at /app_profiler/#{id}\n")
|
|
@@ -42,7 +42,7 @@ module AppProfiler
|
|
|
42
42
|
package_json = JSON.parse(package_contents)
|
|
43
43
|
package_json["name"] ||= "firefox-profiler"
|
|
44
44
|
package_json["version"] ||= "0.0.1"
|
|
45
|
-
File.write("firefox-profiler/package.json", package_json
|
|
45
|
+
File.write("firefox-profiler/package.json", JSON.generate(package_json))
|
|
46
46
|
end
|
|
47
47
|
yarn("--cwd", "#{dir}/firefox-profiler")
|
|
48
48
|
|
data/lib/app_profiler.rb
CHANGED
|
@@ -58,8 +58,8 @@ module AppProfiler
|
|
|
58
58
|
|
|
59
59
|
mattr_accessor :speedscope_host, default: "https://speedscope.app"
|
|
60
60
|
mattr_accessor :autoredirect, default: false
|
|
61
|
-
mattr_reader :profile_header, default: "
|
|
62
|
-
|
|
61
|
+
mattr_reader :profile_header, default: "x-profile"
|
|
62
|
+
mattr_reader :profile_async_header, default: "x-profile-async"
|
|
63
63
|
mattr_accessor :profile_param, default: "profile"
|
|
64
64
|
mattr_accessor :context, default: nil
|
|
65
65
|
mattr_reader :profile_url_formatter, default: DefaultProfileFormatter
|
|
@@ -92,14 +92,9 @@ module AppProfiler
|
|
|
92
92
|
end
|
|
93
93
|
profiler.run(*args, **kwargs, &block)
|
|
94
94
|
rescue BackendError => e
|
|
95
|
-
|
|
96
|
-
ActiveSupport.error_reporter.report(e, context: { app_profiler: { backend: backend } })
|
|
97
|
-
else
|
|
98
|
-
logger.error(
|
|
99
|
-
"[AppProfiler.run] exception #{e} configuring backend #{backend}: #{e.message}",
|
|
100
|
-
)
|
|
101
|
-
end
|
|
95
|
+
ActiveSupport.error_reporter.report(e, context: { app_profiler: { backend: backend } })
|
|
102
96
|
yield
|
|
97
|
+
nil # no profile result
|
|
103
98
|
ensure
|
|
104
99
|
self.backend = original_backend if backend
|
|
105
100
|
ProfileId::Current.reset
|
|
@@ -126,7 +121,7 @@ module AppProfiler
|
|
|
126
121
|
def backend=(new_backend)
|
|
127
122
|
return if (new_profiler_backend = backend_for(new_backend)) == profiler_backend
|
|
128
123
|
|
|
129
|
-
if
|
|
124
|
+
if profiler_backend.locked?
|
|
130
125
|
raise BackendError,
|
|
131
126
|
"cannot change backend to #{new_backend} while #{backend} backend is running"
|
|
132
127
|
end
|
|
@@ -165,13 +160,7 @@ module AppProfiler
|
|
|
165
160
|
|
|
166
161
|
@profile_sampler_enabled.is_a?(Proc) ? @profile_sampler_enabled.call : @profile_sampler_enabled
|
|
167
162
|
rescue => e
|
|
168
|
-
|
|
169
|
-
ActiveSupport.error_reporter.report(e)
|
|
170
|
-
else
|
|
171
|
-
logger.error(
|
|
172
|
-
"[AppProfiler.profile_sampler_enabled] exception: #{e}, message: #{e.message}",
|
|
173
|
-
)
|
|
174
|
-
end
|
|
163
|
+
ActiveSupport.error_reporter.report(e)
|
|
175
164
|
false
|
|
176
165
|
end
|
|
177
166
|
|
|
@@ -185,8 +174,7 @@ module AppProfiler
|
|
|
185
174
|
end
|
|
186
175
|
|
|
187
176
|
def backend_for(backend_name)
|
|
188
|
-
if
|
|
189
|
-
backend_name&.to_sym == AppProfiler::VernierProfile::BACKEND_NAME
|
|
177
|
+
if backend_name&.to_sym == AppProfiler::VernierProfile::BACKEND_NAME
|
|
190
178
|
AppProfiler::Backend::VernierBackend
|
|
191
179
|
elsif backend_name&.to_sym == AppProfiler::Backend::StackprofBackend.name
|
|
192
180
|
AppProfiler::Backend::StackprofBackend
|
|
@@ -199,22 +187,22 @@ module AppProfiler
|
|
|
199
187
|
profiler_backend.name
|
|
200
188
|
end
|
|
201
189
|
|
|
202
|
-
def vernier_supported?
|
|
203
|
-
RUBY_VERSION >= "3.2.1" && defined?(AppProfiler::VernierProfile::BACKEND_NAME)
|
|
204
|
-
end
|
|
205
|
-
|
|
206
190
|
def profile_header=(profile_header)
|
|
207
|
-
@@profile_header = profile_header # rubocop:disable Style/ClassVars
|
|
191
|
+
@@profile_header = profile_header.downcase # rubocop:disable Style/ClassVars
|
|
208
192
|
@@request_profile_header = nil # rubocop:disable Style/ClassVars
|
|
209
193
|
@@profile_data_header = nil # rubocop:disable Style/ClassVars
|
|
210
194
|
end
|
|
211
195
|
|
|
196
|
+
def profile_async_header=(profile_async_header)
|
|
197
|
+
@@profile_async_header = profile_async_header.downcase # rubocop:disable Style/ClassVars
|
|
198
|
+
end
|
|
199
|
+
|
|
212
200
|
def request_profile_header
|
|
213
201
|
@@request_profile_header ||= profile_header.upcase.tr("-", "_").prepend("HTTP_") # rubocop:disable Style/ClassVars
|
|
214
202
|
end
|
|
215
203
|
|
|
216
204
|
def profile_data_header
|
|
217
|
-
@@profile_data_header ||= profile_header.dup << "-
|
|
205
|
+
@@profile_data_header ||= profile_header.dup << "-data" # rubocop:disable Style/ClassVars
|
|
218
206
|
end
|
|
219
207
|
|
|
220
208
|
def profile_url_formatter=(block)
|
|
@@ -268,7 +256,7 @@ module AppProfiler
|
|
|
268
256
|
end
|
|
269
257
|
|
|
270
258
|
def clear
|
|
271
|
-
profiler.stop if
|
|
259
|
+
profiler.stop if profiler_backend.locked?
|
|
272
260
|
@profiler = nil
|
|
273
261
|
@profiler_backend = nil
|
|
274
262
|
end
|
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.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gannon McGibbon
|
|
@@ -20,14 +20,56 @@ dependencies:
|
|
|
20
20
|
requirements:
|
|
21
21
|
- - ">="
|
|
22
22
|
- !ruby/object:Gem::Version
|
|
23
|
-
version: '
|
|
23
|
+
version: '7.2'
|
|
24
24
|
type: :runtime
|
|
25
25
|
prerelease: false
|
|
26
26
|
version_requirements: !ruby/object:Gem::Requirement
|
|
27
27
|
requirements:
|
|
28
28
|
- - ">="
|
|
29
29
|
- !ruby/object:Gem::Version
|
|
30
|
-
version: '
|
|
30
|
+
version: '7.2'
|
|
31
|
+
- !ruby/object:Gem::Dependency
|
|
32
|
+
name: base64
|
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
|
34
|
+
requirements:
|
|
35
|
+
- - ">="
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '0'
|
|
38
|
+
type: :runtime
|
|
39
|
+
prerelease: false
|
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '0'
|
|
45
|
+
- !ruby/object:Gem::Dependency
|
|
46
|
+
name: bigdecimal
|
|
47
|
+
requirement: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
type: :runtime
|
|
53
|
+
prerelease: false
|
|
54
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
- !ruby/object:Gem::Dependency
|
|
60
|
+
name: mutex_m
|
|
61
|
+
requirement: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0'
|
|
66
|
+
type: :runtime
|
|
67
|
+
prerelease: false
|
|
68
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
31
73
|
- !ruby/object:Gem::Dependency
|
|
32
74
|
name: rack
|
|
33
75
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -74,16 +116,16 @@ dependencies:
|
|
|
74
116
|
name: minitest
|
|
75
117
|
requirement: !ruby/object:Gem::Requirement
|
|
76
118
|
requirements:
|
|
77
|
-
- - "
|
|
119
|
+
- - "<"
|
|
78
120
|
- !ruby/object:Gem::Version
|
|
79
|
-
version: '
|
|
121
|
+
version: '6'
|
|
80
122
|
type: :development
|
|
81
123
|
prerelease: false
|
|
82
124
|
version_requirements: !ruby/object:Gem::Requirement
|
|
83
125
|
requirements:
|
|
84
|
-
- - "
|
|
126
|
+
- - "<"
|
|
85
127
|
- !ruby/object:Gem::Version
|
|
86
|
-
version: '
|
|
128
|
+
version: '6'
|
|
87
129
|
- !ruby/object:Gem::Dependency
|
|
88
130
|
name: minitest-stub-const
|
|
89
131
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -194,14 +236,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
194
236
|
requirements:
|
|
195
237
|
- - ">="
|
|
196
238
|
- !ruby/object:Gem::Version
|
|
197
|
-
version: '
|
|
239
|
+
version: '3.3'
|
|
198
240
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
199
241
|
requirements:
|
|
200
242
|
- - ">="
|
|
201
243
|
- !ruby/object:Gem::Version
|
|
202
244
|
version: '0'
|
|
203
245
|
requirements: []
|
|
204
|
-
rubygems_version: 4.0.
|
|
246
|
+
rubygems_version: 4.0.21
|
|
205
247
|
specification_version: 4
|
|
206
248
|
summary: Collect performance profiles for your Rails application.
|
|
207
249
|
test_files: []
|