app_profiler 0.1.2 → 0.1.4

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