app_profiler 0.1.3 → 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: 69d1e8d957672c105e954b64a547955e4e1c499d3d70b529e1de1b56b2fb7d51
4
- data.tar.gz: d7f022da1ebf4ead06ebadae4ded97260276afb12cb885866d76f53e520171c8
3
+ metadata.gz: eaffd76839111b3280bce8241889441b85b4346121e1478cc60839f791916445
4
+ data.tar.gz: f9aab66b3b52953756610b9acb8a314178ce535075d8702d4ced0624e4880409
5
5
  SHA512:
6
- metadata.gz: 831da45ae0c02c629fc0e1788a6b1a220872b4c449291d72df9adbfdbfada6bf0ab815fe41e5034ad5376176b6235cfcbb83d00e4688a1ba4150faedb5ed5820
7
- data.tar.gz: 0177d93977123800440539da265fac443023d8f654f74d64ccd7c4f9a74ed2750c83c2472a7e8285b065a79589fb6024280b4767ee6df148722d34339396cb72
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|
@@ -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
@@ -261,7 +271,15 @@ module AppProfiler
261
271
 
262
272
  @listen_thread = Thread.new do
263
273
  loop do
264
- 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|
265
283
  request = session.gets
266
284
 
267
285
  if request.nil?
@@ -315,9 +333,12 @@ module AppProfiler
315
333
 
316
334
  private_constant :ProfileApplication, :ProfileServer
317
335
 
336
+ @pid = Process.pid
337
+ @profile_server = nil
338
+
318
339
  class << self
319
340
  def reset
320
- profile_servers.clear
341
+ self.profile_server = nil
321
342
 
322
343
  DEFAULTS.each do |config, value|
323
344
  class_variable_set(:"@@#{config}", value) # rubocop:disable Style/ClassVars
@@ -327,7 +348,7 @@ module AppProfiler
327
348
  def start(logger = Logger.new(IO::NULL))
328
349
  return if profile_server
329
350
 
330
- profile_servers[Process.pid] = ProfileServer.new(AppProfiler::Server.transport, logger)
351
+ self.profile_server = ProfileServer.new(AppProfiler::Server.transport, logger)
331
352
  profile_server.serve
332
353
  profile_server
333
354
  end
@@ -341,20 +362,22 @@ module AppProfiler
341
362
  def stop
342
363
  return unless profile_server
343
364
 
344
- profile_server.stop
345
- profile_servers.delete(Process.pid)
365
+ server = profile_server
366
+ server.stop
367
+ self.profile_server = nil
368
+ server
346
369
  end
347
370
 
348
371
  private
349
372
 
350
373
  def profile_server
351
- profile_servers[Process.pid]
374
+ @profile_server = nil if @pid != Process.pid
375
+ @profile_server
352
376
  end
353
377
 
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 ||= {}
378
+ def profile_server=(server)
379
+ @pid = Process.pid
380
+ @profile_server = server
358
381
  end
359
382
  end
360
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.3"
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.3
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
@@ -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.