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 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.