app_profiler 0.1.2 → 0.1.4

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: e5e9b631f4b4b68fb8a2f6f2882ee0e4259cf6151d37388b299bd55c5a0ac6c3
4
- data.tar.gz: a31ef0b50de69b81c84d5d8ec411e82db94a9aa2690fe633239e6c01f2bd2165
3
+ metadata.gz: eaffd76839111b3280bce8241889441b85b4346121e1478cc60839f791916445
4
+ data.tar.gz: f9aab66b3b52953756610b9acb8a314178ce535075d8702d4ced0624e4880409
5
5
  SHA512:
6
- metadata.gz: 4829d7292b8cc680c6936763b8fed26c5ab8f8661ada68f6c3b6a1890cee01d3ad7fb01bf377a3ce4cd3083f8b3b554d5b048c74ac594c540096724083adfb37
7
- data.tar.gz: 20798219dc410c7b2ef0096e437ba8e7d8793a3dabac2a66246a765c8db06fca195c5d05981cff4adb28c6df40456876174e5f1d2b0c292fdff4f9922a1c059a
6
+ metadata.gz: c299676be9b01c62ab73885f1e6b8c6b787721acbe94266b71fec79d91e623bd599aee7b6607eb17dda0f0187a1024c43227c9bae256b660cd8155f41a9dacf4
7
+ data.tar.gz: f1fd90215f29e46175ca3cd3f7bbaeed4448a40acf07e25aa1a1f7c59d8b10cc66854e0c6453cdc4367a469b03e16c8b930da6d53034e0e1d92e1b9dd4b5b5d4
@@ -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|
@@ -46,9 +49,9 @@ module AppProfiler
46
49
 
47
50
  initializer "app_profiler.enable_server" do
48
51
  if AppProfiler.server.enabled
49
- AppProfiler::Server.start
52
+ AppProfiler::Server.start(AppProfiler.logger)
50
53
  ActiveSupport::ForkTracker.after_fork do
51
- AppProfiler::Server.start
54
+ AppProfiler::Server.start(AppProfiler.logger)
52
55
  end
53
56
  end
54
57
  end
@@ -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,9 +198,7 @@ module AppProfiler
190
198
  end
191
199
 
192
200
  class UNIX < Transport
193
- def initialize
194
- super
195
-
201
+ def start
196
202
  FileUtils.mkdir_p(PROFILER_TEMPFILE_PATH)
197
203
  @socket_file = File.join(PROFILER_TEMPFILE_PATH, "app-profiler-#{Process.pid}.sock")
198
204
  File.unlink(@socket_file) if File.exist?(@socket_file) && File.socket?(@socket_file)
@@ -213,9 +219,13 @@ module AppProfiler
213
219
  SERVER_ADDRESS = "127.0.0.1" # it is ONLY safe to run this bound to localhost
214
220
 
215
221
  def initialize(port = 0)
222
+ @port_argument = port
216
223
  super()
224
+ end
225
+
226
+ def start
217
227
  FileUtils.mkdir_p(PROFILER_TEMPFILE_PATH)
218
- @socket = TCPServer.new(SERVER_ADDRESS, port)
228
+ @socket = TCPServer.new(SERVER_ADDRESS, @port_argument)
219
229
  @port = @socket.addr[1]
220
230
  @port_file = Tempfile.new("profileserver-#{Process.pid}-port-#{@port}-", PROFILER_TEMPFILE_PATH)
221
231
  end
@@ -230,7 +240,8 @@ module AppProfiler
230
240
  end
231
241
  end
232
242
 
233
- def initialize(transport)
243
+ def initialize(transport, logger)
244
+ @logger = logger
234
245
  case transport
235
246
  when TRANSPORT_UNIX
236
247
  @transport = ProfileServer::UNIX.new
@@ -242,7 +253,7 @@ module AppProfiler
242
253
 
243
254
  @listen_thread = nil
244
255
 
245
- AppProfiler.logger.info(
256
+ @logger.info(
246
257
  "[AppProfiler::Server] listening on addr=#{@transport.socket.addr}"
247
258
  )
248
259
  @pid = Process.pid
@@ -260,7 +271,15 @@ module AppProfiler
260
271
 
261
272
  @listen_thread = Thread.new do
262
273
  loop do
263
- Thread.new(@transport.socket.accept) do |session|
274
+ session = begin
275
+ @transport.socket.accept
276
+ rescue
277
+ @transport.close
278
+ @transport.start
279
+ next
280
+ end
281
+
282
+ Thread.new(session) do |session|
264
283
  request = session.gets
265
284
 
266
285
  if request.nil?
@@ -293,7 +312,7 @@ module AppProfiler
293
312
  session.print(part)
294
313
  end
295
314
  rescue => e
296
- AppProfiler.logger.error(
315
+ @logger.error(
297
316
  "[AppProfiler::Server] exception #{e} responding to request #{request}: #{e.message}"
298
317
  )
299
318
  ensure
@@ -314,19 +333,22 @@ module AppProfiler
314
333
 
315
334
  private_constant :ProfileApplication, :ProfileServer
316
335
 
336
+ @pid = Process.pid
337
+ @profile_server = nil
338
+
317
339
  class << self
318
340
  def reset
319
- profile_servers.clear
341
+ self.profile_server = nil
320
342
 
321
343
  DEFAULTS.each do |config, value|
322
344
  class_variable_set(:"@@#{config}", value) # rubocop:disable Style/ClassVars
323
345
  end
324
346
  end
325
347
 
326
- def start
348
+ def start(logger = Logger.new(IO::NULL))
327
349
  return if profile_server
328
350
 
329
- profile_servers[Process.pid] = ProfileServer.new(AppProfiler::Server.transport)
351
+ self.profile_server = ProfileServer.new(AppProfiler::Server.transport, logger)
330
352
  profile_server.serve
331
353
  profile_server
332
354
  end
@@ -340,20 +362,22 @@ module AppProfiler
340
362
  def stop
341
363
  return unless profile_server
342
364
 
343
- profile_server.stop
344
- profile_servers.delete(Process.pid)
365
+ server = profile_server
366
+ server.stop
367
+ self.profile_server = nil
368
+ server
345
369
  end
346
370
 
347
371
  private
348
372
 
349
373
  def profile_server
350
- profile_servers[Process.pid]
374
+ @profile_server = nil if @pid != Process.pid
375
+ @profile_server
351
376
  end
352
377
 
353
- # It is possible there will be a server pre-fork, this is to distinguish
354
- # the child server from its parent via PID
355
- def profile_servers
356
- @profile_servers ||= {}
378
+ def profile_server=(server)
379
+ @pid = Process.pid
380
+ @profile_server = server
357
381
  end
358
382
  end
359
383
  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.2"
4
+ VERSION = "0.1.4"
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.2
4
+ version: 0.1.4
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-20 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: activesupport
@@ -43,20 +43,6 @@ dependencies:
43
43
  - - ">="
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
- - !ruby/object:Gem::Dependency
47
- name: railties
48
- requirement: !ruby/object:Gem::Requirement
49
- requirements:
50
- - - ">="
51
- - !ruby/object:Gem::Version
52
- version: '5.2'
53
- type: :runtime
54
- prerelease: false
55
- version_requirements: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- version: '5.2'
60
46
  - !ruby/object:Gem::Dependency
61
47
  name: stackprof
62
48
  requirement: !ruby/object:Gem::Requirement
@@ -153,6 +139,7 @@ files:
153
139
  - lib/app_profiler/middleware/base_action.rb
154
140
  - lib/app_profiler/middleware/upload_action.rb
155
141
  - lib/app_profiler/middleware/view_action.rb
142
+ - lib/app_profiler/parameters.rb
156
143
  - lib/app_profiler/profile.rb
157
144
  - lib/app_profiler/profiler.rb
158
145
  - lib/app_profiler/railtie.rb
@@ -188,7 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
175
  - !ruby/object:Gem::Version
189
176
  version: '0'
190
177
  requirements: []
191
- rubygems_version: 3.3.3
178
+ rubygems_version: 3.4.19
192
179
  signing_key:
193
180
  specification_version: 4
194
181
  summary: Collect performance profiles for your Rails application.