app_profiler 0.1.3 → 0.1.5
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/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.
|