app_profiler 0.1.10 → 0.2.1

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: 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: []