app_profiler 0.1.3 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/app_profiler/middleware/upload_action.rb +13 -10
- data/lib/app_profiler/middleware.rb +4 -4
- data/lib/app_profiler/parameters.rb +35 -0
- data/lib/app_profiler/profile.rb +4 -0
- data/lib/app_profiler/railtie.rb +3 -0
- data/lib/app_profiler/request_parameters.rb +7 -7
- data/lib/app_profiler/server.rb +72 -17
- data/lib/app_profiler/storage/base_storage.rb +4 -0
- data/lib/app_profiler/storage/file_storage.rb +4 -0
- data/lib/app_profiler/storage/google_cloud_storage.rb +51 -0
- data/lib/app_profiler/version.rb +1 -1
- data/lib/app_profiler.rb +4 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 11304574ae151e5775c7cbac93ace38b9784c25bec088b39b4288eae719a0d1e
|
4
|
+
data.tar.gz: d0a698c95c617c4a337c9e31953b374f5f3a9d6126df942674928491c81e97c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b511cd5273c64b1f58597e6a12e668b78e2d5037e63626e4cb34927b3814fb0fb7962fee01973a6be71d928f04596a71525fac3c62bbaadabea6b49121dbae5c
|
7
|
+
data.tar.gz: 507918c0fcb2d4e2321b5401bdd243a3a74fdc256576bdd4be0eddf7441a73bbed4738a9a880d6e507918db0d2a6d9c05bc10adb67a3b6741b7e22fdf16e67e2
|
@@ -4,16 +4,19 @@ module AppProfiler
|
|
4
4
|
class Middleware
|
5
5
|
class UploadAction < BaseAction
|
6
6
|
class << self
|
7
|
-
def call(profile, response: nil, autoredirect: nil)
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
7
|
+
def call(profile, response: nil, autoredirect: nil, async: false)
|
8
|
+
if async
|
9
|
+
profile.enqueue_upload
|
10
|
+
response[1][AppProfiler.profile_async_header] = true
|
11
|
+
else
|
12
|
+
profile_upload = profile.upload
|
13
|
+
|
14
|
+
append_headers(
|
15
|
+
response,
|
16
|
+
upload: profile_upload,
|
17
|
+
autoredirect: autoredirect.nil? ? AppProfiler.autoredirect : autoredirect
|
18
|
+
) if response
|
19
|
+
end
|
17
20
|
end
|
18
21
|
|
19
22
|
private
|
@@ -14,16 +14,15 @@ module AppProfiler
|
|
14
14
|
@app = app
|
15
15
|
end
|
16
16
|
|
17
|
-
def call(env)
|
18
|
-
profile(env) do
|
17
|
+
def call(env, params = AppProfiler::RequestParameters.new(Rack::Request.new(env)))
|
18
|
+
profile(env, params) do
|
19
19
|
@app.call(env)
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
23
|
private
|
24
24
|
|
25
|
-
def profile(env)
|
26
|
-
params = AppProfiler::RequestParameters.new(Rack::Request.new(env))
|
25
|
+
def profile(env, params)
|
27
26
|
response = nil
|
28
27
|
|
29
28
|
return yield unless params.valid?
|
@@ -42,6 +41,7 @@ module AppProfiler
|
|
42
41
|
profile,
|
43
42
|
response: response,
|
44
43
|
autoredirect: params.autoredirect,
|
44
|
+
async: params.async
|
45
45
|
)
|
46
46
|
|
47
47
|
response
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
|
5
|
+
module AppProfiler
|
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
|
10
|
+
|
11
|
+
attr_reader :autoredirect, :async
|
12
|
+
|
13
|
+
def initialize(mode: :wall, interval: nil, ignore_gc: false, autoredirect: false, async: false, metadata: {})
|
14
|
+
@mode = mode.to_sym
|
15
|
+
@interval = [interval&.to_i || DEFAULT_INTERVALS.fetch(@mode.to_s), MIN_INTERVALS.fetch(@mode.to_s)].max
|
16
|
+
@ignore_gc = !!ignore_gc
|
17
|
+
@autoredirect = autoredirect
|
18
|
+
@metadata = { context: AppProfiler.context }.merge(metadata)
|
19
|
+
@async = async
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_h
|
27
|
+
{
|
28
|
+
mode: @mode,
|
29
|
+
interval: @interval,
|
30
|
+
ignore_gc: @ignore_gc,
|
31
|
+
metadata: @metadata,
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/app_profiler/profile.rb
CHANGED
data/lib/app_profiler/railtie.rb
CHANGED
@@ -28,11 +28,14 @@ module AppProfiler
|
|
28
28
|
"APP_PROFILER_SPEEDSCOPE_URL", "https://speedscope.app"
|
29
29
|
)
|
30
30
|
AppProfiler.profile_header = app.config.app_profiler.profile_header || "X-Profile"
|
31
|
+
AppProfiler.profile_async_header = app.config.app_profiler.profile_async_header || "X-Profile-Async"
|
31
32
|
AppProfiler.profile_root = app.config.app_profiler.profile_root || Rails.root.join(
|
32
33
|
"tmp", "app_profiler"
|
33
34
|
)
|
34
35
|
AppProfiler.context = app.config.app_profiler.context || Rails.env
|
35
36
|
AppProfiler.profile_url_formatter = app.config.app_profiler.profile_url_formatter
|
37
|
+
AppProfiler.upload_queue_max_length = app.config.app_profiler.upload_queue_max_length || 10
|
38
|
+
AppProfiler.upload_queue_interval_secs = app.config.app_profiler.upload_queue_interval_secs || 5
|
36
39
|
end
|
37
40
|
|
38
41
|
initializer "app_profiler.add_middleware" do |app|
|
@@ -4,10 +4,6 @@ require "rack"
|
|
4
4
|
|
5
5
|
module AppProfiler
|
6
6
|
class RequestParameters
|
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
|
10
|
-
|
11
7
|
def initialize(request)
|
12
8
|
@request = request
|
13
9
|
end
|
@@ -16,17 +12,21 @@ module AppProfiler
|
|
16
12
|
query_param("autoredirect") || profile_header_param("autoredirect")
|
17
13
|
end
|
18
14
|
|
15
|
+
def async
|
16
|
+
query_param("async")
|
17
|
+
end
|
18
|
+
|
19
19
|
def valid?
|
20
20
|
if mode.blank?
|
21
21
|
return false
|
22
22
|
end
|
23
23
|
|
24
|
-
unless MODES.include?(mode)
|
24
|
+
unless Parameters::MODES.include?(mode)
|
25
25
|
AppProfiler.logger.info("[Profiler] unsupported profiling mode=#{mode}")
|
26
26
|
return false
|
27
27
|
end
|
28
28
|
|
29
|
-
if interval.to_i < MIN_INTERVALS[mode]
|
29
|
+
if interval.to_i < Parameters::MIN_INTERVALS[mode]
|
30
30
|
return false
|
31
31
|
end
|
32
32
|
|
@@ -56,7 +56,7 @@ module AppProfiler
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def interval
|
59
|
-
query_param("interval") || profile_header_param("interval") || DEFAULT_INTERVALS[mode]
|
59
|
+
query_param("interval") || profile_header_param("interval") || Parameters::DEFAULT_INTERVALS[mode]
|
60
60
|
end
|
61
61
|
|
62
62
|
def request_id
|
data/lib/app_profiler/server.rb
CHANGED
@@ -180,6 +180,14 @@ module AppProfiler
|
|
180
180
|
class Transport
|
181
181
|
attr_reader :socket
|
182
182
|
|
183
|
+
def initialize
|
184
|
+
start
|
185
|
+
end
|
186
|
+
|
187
|
+
def start
|
188
|
+
raise(NotImplementedError)
|
189
|
+
end
|
190
|
+
|
183
191
|
def client
|
184
192
|
raise(NotImplementedError)
|
185
193
|
end
|
@@ -190,13 +198,26 @@ module AppProfiler
|
|
190
198
|
end
|
191
199
|
|
192
200
|
class UNIX < Transport
|
193
|
-
|
194
|
-
|
201
|
+
class << self
|
202
|
+
def unlink_socket(path, pid)
|
203
|
+
->(_) do
|
204
|
+
if Process.pid == pid && File.exist?(path)
|
205
|
+
begin
|
206
|
+
File.unlink(path)
|
207
|
+
rescue SystemCallError
|
208
|
+
# Let not raise in a finalizer
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
195
214
|
|
215
|
+
def start
|
196
216
|
FileUtils.mkdir_p(PROFILER_TEMPFILE_PATH)
|
197
217
|
@socket_file = File.join(PROFILER_TEMPFILE_PATH, "app-profiler-#{Process.pid}.sock")
|
198
218
|
File.unlink(@socket_file) if File.exist?(@socket_file) && File.socket?(@socket_file)
|
199
219
|
@socket = UNIXServer.new(@socket_file)
|
220
|
+
ObjectSpace.define_finalizer(self, self.class.unlink_socket(@socket_file, Process.pid))
|
200
221
|
end
|
201
222
|
|
202
223
|
def client
|
@@ -207,15 +228,23 @@ module AppProfiler
|
|
207
228
|
File.unlink(@socket_file) if File.exist?(@socket_file) && File.socket?(@socket_file)
|
208
229
|
@socket.close
|
209
230
|
end
|
231
|
+
|
232
|
+
def abandon
|
233
|
+
@socket.close
|
234
|
+
end
|
210
235
|
end
|
211
236
|
|
212
237
|
class TCP < Transport
|
213
238
|
SERVER_ADDRESS = "127.0.0.1" # it is ONLY safe to run this bound to localhost
|
214
239
|
|
215
240
|
def initialize(port = 0)
|
241
|
+
@port_argument = port
|
216
242
|
super()
|
243
|
+
end
|
244
|
+
|
245
|
+
def start
|
217
246
|
FileUtils.mkdir_p(PROFILER_TEMPFILE_PATH)
|
218
|
-
@socket = TCPServer.new(SERVER_ADDRESS,
|
247
|
+
@socket = TCPServer.new(SERVER_ADDRESS, @port_argument)
|
219
248
|
@port = @socket.addr[1]
|
220
249
|
@port_file = Tempfile.new("profileserver-#{Process.pid}-port-#{@port}-", PROFILER_TEMPFILE_PATH)
|
221
250
|
end
|
@@ -228,6 +257,11 @@ module AppProfiler
|
|
228
257
|
@port_file.unlink
|
229
258
|
@socket.close
|
230
259
|
end
|
260
|
+
|
261
|
+
def abandon
|
262
|
+
@port_file.close # NB: Tempfile finalizer checks Process.pid to avoid unlinking inherited IOs.
|
263
|
+
@socket.close
|
264
|
+
end
|
231
265
|
end
|
232
266
|
|
233
267
|
def initialize(transport, logger)
|
@@ -247,13 +281,16 @@ module AppProfiler
|
|
247
281
|
"[AppProfiler::Server] listening on addr=#{@transport.socket.addr}"
|
248
282
|
)
|
249
283
|
@pid = Process.pid
|
250
|
-
at_exit { stop }
|
251
284
|
end
|
252
285
|
|
253
286
|
def client
|
254
287
|
@transport.client
|
255
288
|
end
|
256
289
|
|
290
|
+
def join(...)
|
291
|
+
@listen_thread.join(...)
|
292
|
+
end
|
293
|
+
|
257
294
|
def serve
|
258
295
|
return unless @listen_thread.nil?
|
259
296
|
|
@@ -261,7 +298,15 @@ module AppProfiler
|
|
261
298
|
|
262
299
|
@listen_thread = Thread.new do
|
263
300
|
loop do
|
264
|
-
|
301
|
+
session = begin
|
302
|
+
@transport.socket.accept
|
303
|
+
rescue
|
304
|
+
@transport.close
|
305
|
+
@transport.start
|
306
|
+
next
|
307
|
+
end
|
308
|
+
|
309
|
+
Thread.new(session) do |session|
|
265
310
|
request = session.gets
|
266
311
|
|
267
312
|
if request.nil?
|
@@ -306,18 +351,23 @@ module AppProfiler
|
|
306
351
|
end
|
307
352
|
|
308
353
|
def stop
|
309
|
-
return unless @pid == Process.pid
|
310
|
-
|
311
354
|
@listen_thread.kill
|
312
|
-
@
|
355
|
+
if @pid == Process.pid
|
356
|
+
@transport.stop
|
357
|
+
else
|
358
|
+
@transport.abandon
|
359
|
+
end
|
313
360
|
end
|
314
361
|
end
|
315
362
|
|
316
363
|
private_constant :ProfileApplication, :ProfileServer
|
317
364
|
|
365
|
+
@pid = Process.pid
|
366
|
+
@profile_server = nil
|
367
|
+
|
318
368
|
class << self
|
319
369
|
def reset
|
320
|
-
|
370
|
+
self.profile_server = nil
|
321
371
|
|
322
372
|
DEFAULTS.each do |config, value|
|
323
373
|
class_variable_set(:"@@#{config}", value) # rubocop:disable Style/ClassVars
|
@@ -327,7 +377,7 @@ module AppProfiler
|
|
327
377
|
def start(logger = Logger.new(IO::NULL))
|
328
378
|
return if profile_server
|
329
379
|
|
330
|
-
|
380
|
+
self.profile_server = ProfileServer.new(AppProfiler::Server.transport, logger)
|
331
381
|
profile_server.serve
|
332
382
|
profile_server
|
333
383
|
end
|
@@ -341,20 +391,25 @@ module AppProfiler
|
|
341
391
|
def stop
|
342
392
|
return unless profile_server
|
343
393
|
|
344
|
-
profile_server
|
345
|
-
|
394
|
+
server = profile_server
|
395
|
+
server.stop
|
396
|
+
self.profile_server = nil
|
397
|
+
server
|
346
398
|
end
|
347
399
|
|
348
400
|
private
|
349
401
|
|
350
402
|
def profile_server
|
351
|
-
|
403
|
+
if @pid != Process.pid
|
404
|
+
@profile_server&.stop
|
405
|
+
@profile_server = nil
|
406
|
+
end
|
407
|
+
@profile_server
|
352
408
|
end
|
353
409
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
@profile_servers ||= {}
|
410
|
+
def profile_server=(server)
|
411
|
+
@pid = Process.pid
|
412
|
+
@profile_server = server
|
358
413
|
end
|
359
414
|
end
|
360
415
|
end
|
@@ -28,8 +28,59 @@ module AppProfiler
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
+
def enqueue_upload(profile)
|
32
|
+
mutex.synchronize do
|
33
|
+
process_queue_thread unless @process_queue_thread&.alive?
|
34
|
+
|
35
|
+
@queue ||= init_queue
|
36
|
+
begin
|
37
|
+
@queue.push(profile, true) # non-blocking push, raises ThreadError if queue is full
|
38
|
+
rescue ThreadError
|
39
|
+
AppProfiler.logger.info("[AppProfiler] upload queue is full, profile discarded")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def reset_queue # for testing
|
45
|
+
init_queue
|
46
|
+
@process_queue_thread&.kill
|
47
|
+
@process_queue_thread = nil
|
48
|
+
end
|
49
|
+
|
31
50
|
private
|
32
51
|
|
52
|
+
def mutex
|
53
|
+
@mutex ||= Mutex.new
|
54
|
+
end
|
55
|
+
|
56
|
+
def init_queue
|
57
|
+
@queue = SizedQueue.new(AppProfiler.upload_queue_max_length)
|
58
|
+
end
|
59
|
+
|
60
|
+
def process_queue_thread
|
61
|
+
@process_queue_thread ||= Thread.new do
|
62
|
+
loop do
|
63
|
+
process_queue
|
64
|
+
sleep(AppProfiler.upload_queue_interval_secs)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
@process_queue_thread.priority = -1 # low priority
|
68
|
+
end
|
69
|
+
|
70
|
+
def process_queue
|
71
|
+
queue = nil
|
72
|
+
mutex.synchronize do
|
73
|
+
break if @queue.nil? || @queue.empty?
|
74
|
+
|
75
|
+
queue = @queue
|
76
|
+
init_queue
|
77
|
+
end
|
78
|
+
|
79
|
+
return unless queue
|
80
|
+
|
81
|
+
queue.size.times { queue.pop(false).upload }
|
82
|
+
end
|
83
|
+
|
33
84
|
def gcs_filename(profile)
|
34
85
|
File.join(profile.context.to_s, profile.file.basename)
|
35
86
|
end
|
data/lib/app_profiler/version.rb
CHANGED
data/lib/app_profiler.rb
CHANGED
@@ -26,6 +26,7 @@ module AppProfiler
|
|
26
26
|
end
|
27
27
|
|
28
28
|
require "app_profiler/middleware"
|
29
|
+
require "app_profiler/parameters"
|
29
30
|
require "app_profiler/request_parameters"
|
30
31
|
require "app_profiler/profiler"
|
31
32
|
require "app_profiler/profile"
|
@@ -38,6 +39,7 @@ module AppProfiler
|
|
38
39
|
mattr_accessor :speedscope_host, default: "https://speedscope.app"
|
39
40
|
mattr_accessor :autoredirect, default: false
|
40
41
|
mattr_reader :profile_header, default: "X-Profile"
|
42
|
+
mattr_accessor :profile_async_header, default: "X-Profile-Async"
|
41
43
|
mattr_accessor :context, default: nil
|
42
44
|
mattr_reader :profile_url_formatter,
|
43
45
|
default: DefaultProfileFormatter
|
@@ -46,6 +48,8 @@ module AppProfiler
|
|
46
48
|
mattr_accessor :viewer, default: Viewer::SpeedscopeViewer
|
47
49
|
mattr_accessor :middleware, default: Middleware
|
48
50
|
mattr_accessor :server, default: Server
|
51
|
+
mattr_accessor :upload_queue_max_length, default: 10
|
52
|
+
mattr_accessor :upload_queue_interval_secs, default: 5
|
49
53
|
|
50
54
|
class << self
|
51
55
|
def run(*args, &block)
|
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.
|
4
|
+
version: 0.1.5
|
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:
|
16
|
+
date: 2023-09-25 00:00:00.000000000 Z
|
17
17
|
dependencies:
|
18
18
|
- !ruby/object:Gem::Dependency
|
19
19
|
name: activesupport
|
@@ -139,6 +139,7 @@ files:
|
|
139
139
|
- lib/app_profiler/middleware/base_action.rb
|
140
140
|
- lib/app_profiler/middleware/upload_action.rb
|
141
141
|
- lib/app_profiler/middleware/view_action.rb
|
142
|
+
- lib/app_profiler/parameters.rb
|
142
143
|
- lib/app_profiler/profile.rb
|
143
144
|
- lib/app_profiler/profiler.rb
|
144
145
|
- lib/app_profiler/railtie.rb
|
@@ -174,7 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
174
175
|
- !ruby/object:Gem::Version
|
175
176
|
version: '0'
|
176
177
|
requirements: []
|
177
|
-
rubygems_version: 3.
|
178
|
+
rubygems_version: 3.4.19
|
178
179
|
signing_key:
|
179
180
|
specification_version: 4
|
180
181
|
summary: Collect performance profiles for your Rails application.
|