app_profiler 0.1.10 → 0.2.1

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: 70f971ebae2492140483b90ada2f0d1daf33874803dcc4ea5404877093651974
4
- data.tar.gz: df8395bf47f6e378f9322386a17c1934e9b38ac9269ea7ded80a721de7a5c4e9
3
+ metadata.gz: daf174f3e936f7c9e466d16f75cc866f779944ae781e39501dae6d51cd74da42
4
+ data.tar.gz: 42871b642bf002450af142941793c41a1bfd76d6b6c6932b45e6cca9c287cb58
5
5
  SHA512:
6
- metadata.gz: 2a10f138198666ddc840658e9599c05c40d4d2a14442b0cfa52df8776e00601fda95aaea871138656cdf2a05ae697966e30fbc62c3a1e5351b04987639a63b82
7
- data.tar.gz: 2c6af888489015d1bcd827704a7710512455bd2c05d4e3aee596c32063d3b134049d592e5c0012d947e7f1f4731e8a55752303064f866cb535d8fb9ae06b91b9
6
+ metadata.gz: cf696efe9cfe2582aafd201e30fe6f82aae17f1b137e93a7d2e2b2c34256f4ace88de0d771fc398844c1010999b5c9424f1b2ab0026395ee88b2ad440a88d54a
7
+ data.tar.gz: eb47ff8a3c0131065400a702d447fb8c170c9d2769860921be61d082ad6affe1ca714bbd211fd2cfe2aec78a89a57bbd32fc76d950d5fa1531feb5b43e29dac9
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ module Backend
5
+ class BaseBackend
6
+ def run(params = {}, &block)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def start(params = {})
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def stop
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def results
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def running?
23
+ raise NotImplementedError
24
+ end
25
+
26
+ class << self
27
+ def run_lock
28
+ @run_lock ||= Mutex.new
29
+ end
30
+
31
+ def name
32
+ raise NotImplementedError
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ def acquire_run_lock
39
+ self.class.run_lock.try_lock
40
+ end
41
+
42
+ def release_run_lock
43
+ self.class.run_lock.unlock
44
+ rescue ThreadError
45
+ AppProfiler.logger.warn("[AppProfiler] run lock not released as it was never acquired")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -3,13 +3,25 @@
3
3
  require "stackprof"
4
4
 
5
5
  module AppProfiler
6
- module Profiler
7
- DEFAULTS = {
8
- mode: :cpu,
9
- raw: true,
10
- }.freeze
6
+ module Backend
7
+ class StackprofBackend < BaseBackend
8
+ DEFAULTS = {
9
+ mode: :cpu,
10
+ raw: true,
11
+ }.freeze
12
+
13
+ AVAILABLE_MODES = [
14
+ :wall,
15
+ :cpu,
16
+ :object,
17
+ ].freeze
18
+
19
+ class << self
20
+ def name
21
+ :stackprof
22
+ end
23
+ end
11
24
 
12
- class << self
13
25
  def run(params = {})
14
26
  started = start(params)
15
27
 
@@ -27,14 +39,16 @@ module AppProfiler
27
39
  def start(params = {})
28
40
  # Do not start the profiler if StackProf was started somewhere else.
29
41
  return false if running?
42
+ return false unless acquire_run_lock
30
43
 
31
44
  clear
32
45
 
33
46
  StackProf.start(**DEFAULTS, **params)
34
47
  rescue => error
35
48
  AppProfiler.logger.info(
36
- "[Profiler] failed to start the profiler error_class=#{error.class} error_message=#{error.message}"
49
+ "[Profiler] failed to start the profiler error_class=#{error.class} error_message=#{error.message}",
37
50
  )
51
+ release_run_lock
38
52
  # This is a boolean instead of nil because StackProf#start returns a
39
53
  # boolean as well.
40
54
  false
@@ -42,24 +56,30 @@ module AppProfiler
42
56
 
43
57
  def stop
44
58
  StackProf.stop
59
+ ensure
60
+ release_run_lock
45
61
  end
46
62
 
47
63
  def results
48
- stackprof_profile = stackprof_results
64
+ stackprof_profile = backend_results
49
65
 
50
66
  return unless stackprof_profile
51
67
 
52
- Profile.from_stackprof(stackprof_profile)
68
+ BaseProfile.from_stackprof(stackprof_profile)
53
69
  rescue => error
54
70
  AppProfiler.logger.info(
55
- "[Profiler] failed to obtain the profile error_class=#{error.class} error_message=#{error.message}"
71
+ "[Profiler] failed to obtain the profile error_class=#{error.class} error_message=#{error.message}",
56
72
  )
57
73
  nil
58
74
  end
59
75
 
76
+ def running?
77
+ StackProf.running?
78
+ end
79
+
60
80
  private
61
81
 
62
- def stackprof_results
82
+ def backend_results
63
83
  StackProf.results
64
84
  end
65
85
 
@@ -73,14 +93,8 @@ module AppProfiler
73
93
  # Ref: https://github.com/tmm1/stackprof/blob/0ded6c/ext/stackprof/stackprof.c#L118-L123
74
94
  #
75
95
  def clear
76
- stackprof_results
77
- end
78
-
79
- def running?
80
- StackProf.running?
96
+ backend_results
81
97
  end
82
98
  end
83
99
  end
84
-
85
- private_constant :Profiler
86
100
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem("vernier", ">= 0.7.0")
4
+ require "vernier"
5
+
6
+ module AppProfiler
7
+ module Backend
8
+ class VernierBackend < BaseBackend
9
+ DEFAULTS = {
10
+ mode: :wall,
11
+ }.freeze
12
+
13
+ AVAILABLE_MODES = [
14
+ :wall,
15
+ :retained,
16
+ ].freeze
17
+
18
+ class << self
19
+ def name
20
+ :vernier
21
+ end
22
+ end
23
+
24
+ def run(params = {})
25
+ started = start(params)
26
+
27
+ yield
28
+
29
+ return unless started
30
+
31
+ stop
32
+ results
33
+ ensure
34
+ # Only stop the profiler if profiling was started in this context.
35
+ stop if started
36
+ end
37
+
38
+ def start(params = {})
39
+ # Do not start the profiler if we already have a collector started somewhere else.
40
+ return false if running?
41
+ return false unless acquire_run_lock
42
+
43
+ @mode = params.delete(:mode) || DEFAULTS[:mode]
44
+ raise ArgumentError unless AVAILABLE_MODES.include?(@mode)
45
+
46
+ @metadata = params.delete(:metadata)
47
+ clear
48
+
49
+ @collector ||= ::Vernier::Collector.new(@mode, **params)
50
+ @collector.start
51
+ rescue => error
52
+ AppProfiler.logger.info(
53
+ "[Profiler] failed to start the profiler error_class=#{error.class} error_message=#{error.message}",
54
+ )
55
+ release_run_lock
56
+ # This is a boolean instead of nil to be consistent with the stackprof backend behaviour
57
+ # boolean as well.
58
+ false
59
+ end
60
+
61
+ def stop
62
+ return false unless running?
63
+
64
+ @results = @collector&.stop
65
+ @collector = nil
66
+ !@results.nil?
67
+ ensure
68
+ release_run_lock
69
+ end
70
+
71
+ def results
72
+ vernier_profile = backend_results
73
+ clear
74
+
75
+ return unless vernier_profile
76
+
77
+ # HACK: - "data" is private, but we want to avoid serializing to JSON then
78
+ # parsing back from JSON by just directly getting the hash
79
+ data = ::Vernier::Output::Firefox.new(vernier_profile).send(:data)
80
+ data[:meta][:mode] = @mode # TODO: https://github.com/jhawthorn/vernier/issues/30
81
+ data[:meta].merge!(@metadata) if @metadata
82
+ @mode = nil
83
+ @metadata = nil
84
+
85
+ BaseProfile.from_vernier(data)
86
+ rescue => error
87
+ AppProfiler.logger.info(
88
+ "[Profiler] failed to obtain the profile error_class=#{error.class} error_message=#{error.message}",
89
+ )
90
+ nil
91
+ end
92
+
93
+ def running?
94
+ @collector != nil
95
+ end
96
+
97
+ private
98
+
99
+ def backend_results
100
+ @results
101
+ end
102
+
103
+ def clear
104
+ @results = nil
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ module Backend
5
+ autoload :BaseBackend, "app_profiler/backend/base_backend"
6
+ autoload :StackprofBackend, "app_profiler/backend/stackprof_backend"
7
+ autoload :VernierBackend, "app_profiler/backend/vernier_backend"
8
+ end
9
+ end
@@ -9,7 +9,7 @@ module AppProfiler
9
9
  end
10
10
 
11
11
  def cleanup
12
- profile = Profiler.results
12
+ profile = AppProfiler.profiler.results
13
13
  call(profile) if profile
14
14
  end
15
15
  end
@@ -14,7 +14,7 @@ module AppProfiler
14
14
  append_headers(
15
15
  response,
16
16
  upload: profile_upload,
17
- autoredirect: autoredirect.nil? ? AppProfiler.autoredirect : autoredirect
17
+ autoredirect: autoredirect.nil? ? AppProfiler.autoredirect : autoredirect,
18
18
  ) if response
19
19
  end
20
20
  end
@@ -31,7 +31,7 @@ module AppProfiler
31
31
 
32
32
  return yield unless before_profile(env, params_hash)
33
33
 
34
- profile = AppProfiler.run(params_hash) do
34
+ profile = AppProfiler.run(**params_hash) do
35
35
  response = yield
36
36
  end
37
37
 
@@ -41,7 +41,7 @@ module AppProfiler
41
41
  profile,
42
42
  response: response,
43
43
  autoredirect: params.autoredirect,
44
- async: params.async
44
+ async: params.async,
45
45
  )
46
46
 
47
47
  response
@@ -4,17 +4,18 @@ require "rack"
4
4
 
5
5
  module AppProfiler
6
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
7
+ DEFAULT_INTERVALS = { "cpu" => 1000, "wall" => 1000, "object" => 2000, "retained" => 0 }.freeze
8
+ MIN_INTERVALS = { "cpu" => 200, "wall" => 200, "object" => 400, "retained" => 0 }.freeze
10
9
 
11
- attr_reader :autoredirect, :async
10
+ attr_reader :autoredirect, :async, :backend
12
11
 
13
- def initialize(mode: :wall, interval: nil, ignore_gc: false, autoredirect: false, async: false, metadata: {})
12
+ def initialize(mode: :wall, interval: nil, ignore_gc: false, autoredirect: false,
13
+ async: false, backend: nil, metadata: {})
14
14
  @mode = mode.to_sym
15
15
  @interval = [interval&.to_i || DEFAULT_INTERVALS.fetch(@mode.to_s), MIN_INTERVALS.fetch(@mode.to_s)].max
16
16
  @ignore_gc = !!ignore_gc
17
17
  @autoredirect = autoredirect
18
+ @backend = backend || AppProfiler::Backend::StackprofBackend.name
18
19
  @metadata = { context: AppProfiler.context }.merge(metadata)
19
20
  @async = async
20
21
  end
@@ -29,6 +30,7 @@ module AppProfiler
29
30
  interval: @interval,
30
31
  ignore_gc: @ignore_gc,
31
32
  metadata: @metadata,
33
+ backend: @backend,
32
34
  }
33
35
  end
34
36
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ class StackprofProfile < BaseProfile
5
+ FILE_EXTENSION = ".json"
6
+
7
+ def mode
8
+ @data[:mode]
9
+ end
10
+
11
+ def format
12
+ FILE_EXTENSION
13
+ end
14
+
15
+ def view(params = {})
16
+ AppProfiler.viewer.view(self, **params)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ class VernierProfile < BaseProfile
5
+ FILE_EXTENSION = ".gecko.json"
6
+
7
+ def mode
8
+ @data[:meta][:mode]
9
+ end
10
+
11
+ def format
12
+ FILE_EXTENSION
13
+ end
14
+
15
+ def view(params = {})
16
+ raise NotImplementedError
17
+ end
18
+ end
19
+ end
@@ -1,47 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/deprecation/constant_accessor"
4
+
3
5
  module AppProfiler
4
- class Profile
6
+ autoload :StackprofProfile, "app_profiler/profile/stackprof"
7
+ autoload :VernierProfile, "app_profiler/profile/vernier"
8
+
9
+ class BaseProfile
5
10
  INTERNAL_METADATA_KEYS = [:id, :context]
6
11
  private_constant :INTERNAL_METADATA_KEYS
7
12
  class UnsafeFilename < StandardError; end
8
13
 
9
- delegate :[], to: :@data
10
14
  attr_reader :id, :context
11
15
 
12
- # This function should not be called if `StackProf.results` returns nil.
13
- def self.from_stackprof(data)
14
- options = INTERNAL_METADATA_KEYS.map { |key| [key, data[:metadata]&.delete(key)] }.to_h
16
+ delegate :[], to: :@data
17
+
18
+ class << self
19
+ # This function should not be called if `StackProf.results` returns nil.
20
+ def from_stackprof(data)
21
+ options = INTERNAL_METADATA_KEYS.map { |key| [key, data[:metadata]&.delete(key)] }.to_h
15
22
 
16
- new(data, **options).tap do |profile|
17
- raise ArgumentError, "invalid profile data" unless profile.valid?
23
+ StackprofProfile.new(data, **options).tap do |profile|
24
+ raise ArgumentError, "invalid profile data" unless profile.valid?
25
+ end
26
+ end
27
+
28
+ def from_vernier(data)
29
+ options = INTERNAL_METADATA_KEYS.map { |key| [key, data[:meta]&.delete(key)] }.to_h
30
+
31
+ VernierProfile.new(data, **options).tap do |profile|
32
+ raise ArgumentError, "invalid profile data" unless profile.valid?
33
+ end
18
34
  end
19
35
  end
20
36
 
21
- # `data` is assumed to be a Hash.
37
+ # `data` is assumed to be a Hash for Stackprof,
38
+ # a vernier "result" object for vernier
22
39
  def initialize(data, id: nil, context: nil)
23
40
  @id = id.presence || SecureRandom.hex
24
41
  @context = context
25
42
  @data = data
26
43
  end
27
44
 
28
- def valid?
29
- mode.present?
30
- end
31
-
32
- def mode
33
- @data[:mode]
34
- end
35
-
36
- def view(params = {})
37
- AppProfiler.viewer.view(self, **params)
38
- end
39
-
40
45
  def upload
41
46
  AppProfiler.storage.upload(self).tap do |upload|
42
47
  if upload && defined?(upload.url)
43
48
  AppProfiler.logger.info(
44
- <<~INFO.squish
49
+ <<~INFO.squish,
45
50
  [Profiler] data uploaded:
46
51
  profile_url=#{upload.url}
47
52
  profile_viewer_url=#{AppProfiler.profile_url(upload)}
@@ -51,7 +56,7 @@ module AppProfiler
51
56
  end
52
57
  rescue => error
53
58
  AppProfiler.logger.info(
54
- "[Profiler] failed to upload profile error_class=#{error.class} error_message=#{error.message}"
59
+ "[Profiler] failed to upload profile error_class=#{error.class} error_message=#{error.message}",
55
60
  )
56
61
  nil
57
62
  end
@@ -60,6 +65,10 @@ module AppProfiler
60
65
  AppProfiler.storage.enqueue_upload(self)
61
66
  end
62
67
 
68
+ def valid?
69
+ mode.present?
70
+ end
71
+
63
72
  def file
64
73
  @file ||= path.tap do |p|
65
74
  p.dirname.mkpath
@@ -71,6 +80,22 @@ module AppProfiler
71
80
  @data
72
81
  end
73
82
 
83
+ def metadata
84
+ @data[:metadata]
85
+ end
86
+
87
+ def mode
88
+ raise NotImplementedError
89
+ end
90
+
91
+ def format
92
+ raise NotImplementedError
93
+ end
94
+
95
+ def view(params = {})
96
+ raise NotImplementedError
97
+ end
98
+
74
99
  private
75
100
 
76
101
  def path
@@ -79,11 +104,14 @@ module AppProfiler
79
104
  mode,
80
105
  id,
81
106
  Socket.gethostname,
82
- ].compact.join("-") << ".json"
107
+ ].compact.join("-") << format
83
108
 
84
109
  raise UnsafeFilename if /[^0-9A-Za-z.\-\_]/.match?(filename)
85
110
 
86
111
  AppProfiler.profile_root.join(filename)
87
112
  end
88
113
  end
114
+
115
+ include ActiveSupport::Deprecation::DeprecatedConstantAccessor
116
+ deprecate_constant "Profile", "AppProfiler::BaseProfile", deprecator: ActiveSupport::Deprecation.new
89
117
  end
@@ -40,6 +40,8 @@ module AppProfiler
40
40
  AppProfiler.profile_enqueue_success = app.config.app_profiler.profile_enqueue_success
41
41
  AppProfiler.profile_enqueue_failure = app.config.app_profiler.profile_enqueue_failure
42
42
  AppProfiler.after_process_queue = app.config.app_profiler.after_process_queue
43
+ AppProfiler.backend = app.config.app_profiler.profiler_backend || :stackprof
44
+ AppProfiler.forward_metadata_on_upload = app.config.app_profiler.forward_metadata_on_upload || false
43
45
  end
44
46
 
45
47
  initializer "app_profiler.add_middleware" do |app|
@@ -16,17 +16,30 @@ module AppProfiler
16
16
  query_param("async")
17
17
  end
18
18
 
19
+ def backend
20
+ backend = query_param("backend") || profile_header_param("backend") ||
21
+ AppProfiler.backend
22
+ backend.to_sym
23
+ end
24
+
19
25
  def valid?
20
26
  if mode.blank?
21
27
  return false
22
28
  end
23
29
 
24
- unless Parameters::MODES.include?(mode)
25
- AppProfiler.logger.info("[Profiler] unsupported profiling mode=#{mode}")
30
+ return false if backend != AppProfiler::Backend::StackprofBackend.name && !AppProfiler.vernier_supported?
31
+
32
+ if AppProfiler.vernier_supported? && backend == AppProfiler::Backend::VernierBackend.name &&
33
+ !AppProfiler::Backend::VernierBackend::AVAILABLE_MODES.include?(mode.to_sym)
34
+ AppProfiler.logger.info("[AppProfiler] unsupported profiling mode=#{mode} for backend #{backend}")
35
+ return false
36
+ elsif backend == AppProfiler::Backend::StackprofBackend.name &&
37
+ !AppProfiler::Backend::StackprofBackend::AVAILABLE_MODES.include?(mode.to_sym)
38
+ AppProfiler.logger.info("[AppProfiler] unsupported profiling mode=#{mode} for backend #{backend}")
26
39
  return false
27
40
  end
28
41
 
29
- if interval.to_i < Parameters::MIN_INTERVALS[mode]
42
+ if interval.to_i < Parameters::MIN_INTERVALS[mode.to_s]
30
43
  return false
31
44
  end
32
45
 
@@ -38,6 +51,7 @@ module AppProfiler
38
51
  mode: mode.to_sym,
39
52
  interval: interval.to_i,
40
53
  ignore_gc: !!ignore_gc,
54
+ backend: backend,
41
55
  metadata: {
42
56
  id: request_id,
43
57
  context: context,
@@ -56,7 +70,7 @@ module AppProfiler
56
70
  end
57
71
 
58
72
  def interval
59
- query_param("interval") || profile_header_param("interval") || Parameters::DEFAULT_INTERVALS[mode]
73
+ query_param("interval") || profile_header_param("interval") || Parameters::DEFAULT_INTERVALS[mode.to_s]
60
74
  end
61
75
 
62
76
  def request_id
@@ -280,7 +280,7 @@ module AppProfiler
280
280
  @listen_thread = nil
281
281
 
282
282
  @logger.info(
283
- "[AppProfiler::Server] listening on addr=#{@transport.socket.addr}"
283
+ "[AppProfiler::Server] listening on addr=#{@transport.socket.addr}",
284
284
  )
285
285
  @pid = Process.pid
286
286
  end
@@ -342,7 +342,7 @@ module AppProfiler
342
342
  end
343
343
  rescue => e
344
344
  @logger.error(
345
- "[AppProfiler::Server] exception #{e} responding to request #{request}: #{e.message}"
345
+ "[AppProfiler::Server] exception #{e} responding to request #{request}: #{e.message}",
346
346
  )
347
347
  ensure
348
348
  session.close
@@ -6,12 +6,14 @@ module AppProfiler
6
6
  class_attribute :bucket_name, default: "profiles"
7
7
  class_attribute :credentials, default: {}
8
8
 
9
- def self.upload(_profile)
10
- raise NotImplementedError
11
- end
9
+ class << self
10
+ def upload(_profile)
11
+ raise NotImplementedError
12
+ end
12
13
 
13
- def self.enqueue_upload(_profile)
14
- raise NotImplementedError
14
+ def enqueue_upload(_profile)
15
+ raise NotImplementedError
16
+ end
15
17
  end
16
18
  end
17
19
  end
@@ -15,6 +15,10 @@ module AppProfiler
15
15
  def upload(profile, _params = {})
16
16
  file = profile.file.open
17
17
 
18
+ metadata = if AppProfiler.forward_metadata_on_upload && profile.metadata.present?
19
+ profile.metadata
20
+ end
21
+
18
22
  ActiveSupport::Notifications.instrument(
19
23
  "gcs_upload.app_profiler",
20
24
  file_size: file.size,
@@ -24,6 +28,7 @@ module AppProfiler
24
28
  gcs_filename(profile),
25
29
  content_type: "application/json",
26
30
  content_encoding: "gzip",
31
+ metadata: metadata,
27
32
  )
28
33
  ensure
29
34
  profile.file.unlink
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppProfiler
4
- VERSION = "0.1.10"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -9,17 +9,57 @@ module AppProfiler
9
9
  class BaseMiddleware
10
10
  class Sanitizer < Rails::HTML::Sanitizer.best_supported_vendor.safe_list_sanitizer
11
11
  self.allowed_tags = Set.new([
12
- "strong", "em", "b", "i", "p", "code", "pre", "tt", "samp", "kbd", "var", "sub",
13
- "sup", "dfn", "cite", "big", "small", "address", "hr", "br", "div", "span", "h1",
14
- "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "dl", "dt", "dd", "abbr", "acronym",
15
- "a", "img", "blockquote", "del", "ins", "script",
12
+ "strong",
13
+ "em",
14
+ "b",
15
+ "i",
16
+ "p",
17
+ "code",
18
+ "pre",
19
+ "tt",
20
+ "samp",
21
+ "kbd",
22
+ "var",
23
+ "sub",
24
+ "sup",
25
+ "dfn",
26
+ "cite",
27
+ "big",
28
+ "small",
29
+ "address",
30
+ "hr",
31
+ "br",
32
+ "div",
33
+ "span",
34
+ "h1",
35
+ "h2",
36
+ "h3",
37
+ "h4",
38
+ "h5",
39
+ "h6",
40
+ "ul",
41
+ "ol",
42
+ "li",
43
+ "dl",
44
+ "dt",
45
+ "dd",
46
+ "abbr",
47
+ "acronym",
48
+ "a",
49
+ "img",
50
+ "blockquote",
51
+ "del",
52
+ "ins",
53
+ "script",
16
54
  ])
17
55
  end
18
56
 
19
57
  private_constant(:Sanitizer)
20
58
 
21
- def self.id(file)
22
- file.basename.to_s.delete_suffix(".json")
59
+ class << self
60
+ def id(file)
61
+ file.basename.to_s.delete_suffix(".json")
62
+ end
23
63
  end
24
64
 
25
65
  def initialize(app)
@@ -87,7 +127,7 @@ module AppProfiler
87
127
  </p>
88
128
  HTML
89
129
  end
90
- end
130
+ end,
91
131
  )
92
132
  end
93
133
 
@@ -12,7 +12,7 @@ module AppProfiler
12
12
  def initialize(app)
13
13
  super
14
14
  @speedscope = Rack::File.new(
15
- File.join(AppProfiler.root, "node_modules/speedscope/dist/release")
15
+ File.join(AppProfiler.root, "node_modules/speedscope/dist/release"),
16
16
  )
17
17
  end
18
18
 
@@ -33,7 +33,7 @@ module AppProfiler
33
33
  end || raise(ArgumentError)
34
34
 
35
35
  render(
36
- <<~HTML
36
+ <<~HTML,
37
37
  <script type="text/javascript">
38
38
  var graph = #{profile.read};
39
39
  var json = JSON.stringify(graph);
@@ -49,7 +49,7 @@ module AppProfiler
49
49
  exec("which", "yarn", silent: true) do
50
50
  raise(
51
51
  YarnError,
52
- <<~MSG.squish
52
+ <<~MSG.squish,
53
53
  `yarn` command not found.
54
54
  Please install `yarn` or make it available in PATH.
55
55
  MSG
data/lib/app_profiler.rb CHANGED
@@ -9,6 +9,9 @@ module AppProfiler
9
9
  class ConfigurationError < StandardError
10
10
  end
11
11
 
12
+ class BackendError < StandardError
13
+ end
14
+
12
15
  DefaultProfileFormatter = proc do |upload|
13
16
  "#{AppProfiler.speedscope_host}#profileURL=#{upload.url}"
14
17
  end
@@ -32,8 +35,8 @@ module AppProfiler
32
35
  require "app_profiler/middleware"
33
36
  require "app_profiler/parameters"
34
37
  require "app_profiler/request_parameters"
35
- require "app_profiler/profiler"
36
38
  require "app_profiler/profile"
39
+ require "app_profiler/backend"
37
40
  require "app_profiler/server"
38
41
 
39
42
  mattr_accessor :logger, default: Logger.new($stdout)
@@ -58,19 +61,76 @@ module AppProfiler
58
61
  mattr_reader :profile_enqueue_success, default: nil
59
62
  mattr_reader :profile_enqueue_failure, default: nil
60
63
  mattr_reader :after_process_queue, default: nil
64
+ mattr_accessor :forward_metadata_on_upload, default: false
61
65
 
62
66
  class << self
63
- def run(*args, &block)
64
- Profiler.run(*args, &block)
67
+ def run(*args, backend: nil, **kwargs, &block)
68
+ orig_backend = self.backend
69
+ begin
70
+ self.backend = backend if backend
71
+ profiler.run(*args, **kwargs, &block)
72
+ rescue BackendError => e
73
+ logger.error(
74
+ "[AppProfiler.run] exception #{e} configuring backend #{backend}: #{e.message}",
75
+ )
76
+ yield
77
+ end
78
+ ensure
79
+ AppProfiler.backend = orig_backend
65
80
  end
66
81
 
67
82
  def start(*args)
68
- Profiler.start(*args)
83
+ profiler.start(*args)
69
84
  end
70
85
 
71
86
  def stop
72
- Profiler.stop
73
- Profiler.results
87
+ profiler.stop
88
+ profiler.results
89
+ end
90
+
91
+ def running?
92
+ @backend&.running?
93
+ end
94
+
95
+ def profiler
96
+ backend
97
+ @backend ||= @profiler_backend.new
98
+ end
99
+
100
+ def backend=(new_backend)
101
+ return if new_backend == backend
102
+
103
+ new_profiler_backend = backend_for(new_backend)
104
+
105
+ if running?
106
+ raise BackendError,
107
+ "cannot change backend to #{new_backend} while #{backend} backend is running"
108
+ end
109
+
110
+ return if @profiler_backend == new_profiler_backend
111
+
112
+ clear
113
+ @profiler_backend = new_profiler_backend
114
+ end
115
+
116
+ def backend_for(backend_name)
117
+ if vernier_supported? &&
118
+ backend_name == AppProfiler::Backend::VernierBackend.name
119
+ AppProfiler::Backend::VernierBackend
120
+ elsif backend_name == AppProfiler::Backend::StackprofBackend.name
121
+ AppProfiler::Backend::StackprofBackend
122
+ else
123
+ raise BackendError, "unknown backend #{backend_name}"
124
+ end
125
+ end
126
+
127
+ def backend
128
+ @profiler_backend ||= Backend::StackprofBackend
129
+ @profiler_backend.name
130
+ end
131
+
132
+ def vernier_supported?
133
+ defined?(AppProfiler::Backend::VernierBackend.name)
74
134
  end
75
135
 
76
136
  def profile_header=(profile_header)
@@ -120,6 +180,13 @@ module AppProfiler
120
180
 
121
181
  AppProfiler.profile_url_formatter.call(upload)
122
182
  end
183
+
184
+ private
185
+
186
+ def clear
187
+ @backend.stop if @backend&.running?
188
+ @backend = nil
189
+ end
123
190
  end
124
191
 
125
192
  require "app_profiler/railtie" if defined?(Rails::Railtie)
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.10
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gannon McGibbon
@@ -10,10 +10,10 @@ authors:
10
10
  - Jon Simpson
11
11
  - Kevin Jalbert
12
12
  - Scott Francis
13
- autorequire:
13
+ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2023-12-13 00:00:00.000000000 Z
16
+ date: 2024-08-01 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: activesupport
@@ -127,7 +127,7 @@ dependencies:
127
127
  - - ">="
128
128
  - !ruby/object:Gem::Version
129
129
  version: '0'
130
- description:
130
+ description:
131
131
  email:
132
132
  - gems@shopify.com
133
133
  executables: []
@@ -135,13 +135,18 @@ extensions: []
135
135
  extra_rdoc_files: []
136
136
  files:
137
137
  - lib/app_profiler.rb
138
+ - lib/app_profiler/backend.rb
139
+ - lib/app_profiler/backend/base_backend.rb
140
+ - lib/app_profiler/backend/stackprof_backend.rb
141
+ - lib/app_profiler/backend/vernier_backend.rb
138
142
  - lib/app_profiler/middleware.rb
139
143
  - lib/app_profiler/middleware/base_action.rb
140
144
  - lib/app_profiler/middleware/upload_action.rb
141
145
  - lib/app_profiler/middleware/view_action.rb
142
146
  - lib/app_profiler/parameters.rb
143
147
  - lib/app_profiler/profile.rb
144
- - lib/app_profiler/profiler.rb
148
+ - lib/app_profiler/profile/stackprof.rb
149
+ - lib/app_profiler/profile/vernier.rb
145
150
  - lib/app_profiler/railtie.rb
146
151
  - lib/app_profiler/request_parameters.rb
147
152
  - lib/app_profiler/server.rb
@@ -160,7 +165,7 @@ homepage: https://github.com/Shopify/app_profiler
160
165
  licenses: []
161
166
  metadata:
162
167
  allowed_push_host: https://rubygems.org
163
- post_install_message:
168
+ post_install_message:
164
169
  rdoc_options: []
165
170
  require_paths:
166
171
  - lib
@@ -175,8 +180,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
180
  - !ruby/object:Gem::Version
176
181
  version: '0'
177
182
  requirements: []
178
- rubygems_version: 3.4.22
179
- signing_key:
183
+ rubygems_version: 3.5.16
184
+ signing_key:
180
185
  specification_version: 4
181
186
  summary: Collect performance profiles for your Rails application.
182
187
  test_files: []