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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69d1e8d957672c105e954b64a547955e4e1c499d3d70b529e1de1b56b2fb7d51
4
- data.tar.gz: d7f022da1ebf4ead06ebadae4ded97260276afb12cb885866d76f53e520171c8
3
+ metadata.gz: 11304574ae151e5775c7cbac93ace38b9784c25bec088b39b4288eae719a0d1e
4
+ data.tar.gz: d0a698c95c617c4a337c9e31953b374f5f3a9d6126df942674928491c81e97c5
5
5
  SHA512:
6
- metadata.gz: 831da45ae0c02c629fc0e1788a6b1a220872b4c449291d72df9adbfdbfada6bf0ab815fe41e5034ad5376176b6235cfcbb83d00e4688a1ba4150faedb5ed5820
7
- data.tar.gz: 0177d93977123800440539da265fac443023d8f654f74d64ccd7c4f9a74ed2750c83c2472a7e8285b065a79589fb6024280b4767ee6df148722d34339396cb72
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
- profile_upload = profile.upload
9
-
10
- return unless response
11
-
12
- append_headers(
13
- response,
14
- upload: profile_upload,
15
- autoredirect: autoredirect.nil? ? AppProfiler.autoredirect : autoredirect
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
@@ -56,6 +56,10 @@ module AppProfiler
56
56
  nil
57
57
  end
58
58
 
59
+ def enqueue_upload
60
+ AppProfiler.storage.enqueue_upload(self)
61
+ end
62
+
59
63
  def file
60
64
  @file ||= path.tap do |p|
61
65
  p.dirname.mkpath
@@ -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
@@ -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
- def initialize
194
- super
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, port)
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
- Thread.new(@transport.socket.accept) do |session|
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
- @transport.stop
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
- profile_servers.clear
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
- profile_servers[Process.pid] = ProfileServer.new(AppProfiler::Server.transport, logger)
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.stop
345
- profile_servers.delete(Process.pid)
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
- profile_servers[Process.pid]
403
+ if @pid != Process.pid
404
+ @profile_server&.stop
405
+ @profile_server = nil
406
+ end
407
+ @profile_server
352
408
  end
353
409
 
354
- # It is possible there will be a server pre-fork, this is to distinguish
355
- # the child server from its parent via PID
356
- def profile_servers
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
@@ -9,6 +9,10 @@ module AppProfiler
9
9
  def self.upload(_profile)
10
10
  raise NotImplementedError
11
11
  end
12
+
13
+ def self.enqueue_upload(_profile)
14
+ raise NotImplementedError
15
+ end
12
16
  end
13
17
  end
14
18
  end
@@ -21,6 +21,10 @@ module AppProfiler
21
21
  def upload(profile)
22
22
  Location.new(profile.file)
23
23
  end
24
+
25
+ def enqueue_upload(profile)
26
+ upload(profile)
27
+ end
24
28
  end
25
29
  end
26
30
  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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppProfiler
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.5"
5
5
  end
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.3
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: 2022-09-23 00:00:00.000000000 Z
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.3.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.