app_profiler 0.2.5 → 0.2.7

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: 36a0afed63f70a16ab782039e981583c16e80f846eb6caf6cefce219d7934d0d
4
- data.tar.gz: eda36bd2a579b25757db0e872c3f2aa1cbdda60ddf486b4bd3ad90f34f74829c
3
+ metadata.gz: 1d088d71f0d625a8627a17aa418840597b1d26e01bc05a93848440ce710f461a
4
+ data.tar.gz: 5f65cc77daaeaacbeed54cc913c08e89fddbc30e64458e1538c2cce2f5e9b381
5
5
  SHA512:
6
- metadata.gz: 8ef999eb872722bca7b910b88df8a091c756b4d9f0e95da4ac770aacfb0b5454a160ca37b8c13852e25e5fd8aecf698f6bc06f6669104cbbb02c5af033b66a80
7
- data.tar.gz: 2896ca3138e8b631445b628bcd6206e77bcd94fbef57c00560b0a63f8677b3103555eef97aabbb936722406d5667ec6c8885c9224fe85cd82d2619f45fdb9051
6
+ metadata.gz: a99c98c4bcf876d9c7147bf552b47ae5b8052f2e3c314a2279c8e34a2f37a9781f47c9f7ee99260281490682d1ea776f00be946494dd8fcd0bb83cbd5db07537
7
+ data.tar.gz: 4fafc43c3a589636928566a4bdf96fde0abf91eebde85e267852c281f7b5439ce76048c1cbca1cfe18119b99057bc7355fe41c2e48537c6b6a62f7a41b0ffa4b
@@ -3,9 +3,6 @@
3
3
  require "active_support/deprecation/constant_accessor"
4
4
 
5
5
  module AppProfiler
6
- autoload :StackprofProfile, "app_profiler/profile/stackprof"
7
- autoload :VernierProfile, "app_profiler/profile/vernier"
8
-
9
6
  class BaseProfile
10
7
  INTERNAL_METADATA_KEYS = [:id, :context]
11
8
  private_constant :INTERNAL_METADATA_KEYS
@@ -99,19 +96,20 @@ module AppProfiler
99
96
  private
100
97
 
101
98
  def path
102
- filename = [
103
- AppProfiler.profile_file_prefix.call,
104
- mode,
105
- id,
106
- Socket.gethostname,
107
- ].compact.join("-") << format
99
+ filename = if AppProfiler.profile_file_name.present?
100
+ AppProfiler.profile_file_name.call(metadata) + format
101
+ else
102
+ [
103
+ AppProfiler.profile_file_prefix.call,
104
+ mode,
105
+ id,
106
+ Socket.gethostname,
107
+ ].compact.join("-") << format
108
+ end
108
109
 
109
110
  raise UnsafeFilename if /[^0-9A-Za-z.\-\_]/.match?(filename)
110
111
 
111
112
  AppProfiler.profile_root.join(filename)
112
113
  end
113
114
  end
114
-
115
- include ActiveSupport::Deprecation::DeprecatedConstantAccessor
116
- deprecate_constant "Profile", "AppProfiler::BaseProfile", deprecator: ActiveSupport::Deprecation.new
117
115
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ module Exec # :nodoc:
5
+ protected
6
+
7
+ def valid_commands
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def ensure_command_valid(command)
12
+ unless valid_command?(command)
13
+ raise ArgumentError, "Illegal command: #{command.join(" ")}."
14
+ end
15
+ end
16
+
17
+ def valid_command?(command)
18
+ valid_commands.any? do |valid_command|
19
+ next unless valid_command.size == command.size
20
+
21
+ valid_command.zip(command).all? do |valid_part, part|
22
+ part.match?(valid_part)
23
+ end
24
+ end
25
+ end
26
+
27
+ def exec(*command, silent: false, environment: {})
28
+ ensure_command_valid(command)
29
+
30
+ if silent
31
+ system(environment, *command, out: File::NULL).tap { |return_code| yield unless return_code }
32
+ else
33
+ system(environment, *command).tap { |return_code| yield unless return_code }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -4,7 +4,6 @@ require "rack"
4
4
  require "app_profiler/middleware/base_action"
5
5
  require "app_profiler/middleware/upload_action"
6
6
  require "app_profiler/middleware/view_action"
7
- require "app_profiler/sampler/config"
8
7
 
9
8
  module AppProfiler
10
9
  class Middleware
@@ -11,7 +11,12 @@ module AppProfiler
11
11
  AppProfiler.logger = app.config.app_profiler.logger || Rails.logger
12
12
  AppProfiler.root = app.config.app_profiler.root || Rails.root
13
13
  AppProfiler.storage = app.config.app_profiler.storage || Storage::FileStorage
14
- AppProfiler.viewer = app.config.app_profiler.viewer || Viewer::SpeedscopeViewer
14
+ if app.config.app_profiler.stackprof_viewer
15
+ AppProfiler.stackprof_viewer = app.config.app_profiler.stackprof_viewer
16
+ end
17
+ if app.config.app_profiler.vernier_viewer
18
+ AppProfiler.vernier_viewer = app.config.app_profiler.vernier_viewer
19
+ end
15
20
  AppProfiler.storage.bucket_name = app.config.app_profiler.storage_bucket_name || "profiles"
16
21
  AppProfiler.storage.credentials = app.config.app_profiler.storage_credentials || {}
17
22
  AppProfiler.middleware = app.config.app_profiler.middleware || Middleware
@@ -37,10 +42,11 @@ module AppProfiler
37
42
  AppProfiler.upload_queue_max_length = app.config.app_profiler.upload_queue_max_length || 10
38
43
  AppProfiler.upload_queue_interval_secs = app.config.app_profiler.upload_queue_interval_secs || 5
39
44
  AppProfiler.profile_file_prefix = app.config.app_profiler.profile_file_prefix || DefaultProfilePrefix
45
+ AppProfiler.profile_file_name = app.config.app_profiler.profile_file_name
40
46
  AppProfiler.profile_enqueue_success = app.config.app_profiler.profile_enqueue_success
41
47
  AppProfiler.profile_enqueue_failure = app.config.app_profiler.profile_enqueue_failure
42
48
  AppProfiler.after_process_queue = app.config.app_profiler.after_process_queue
43
- AppProfiler.backend = app.config.app_profiler.profiler_backend || :stackprof
49
+ AppProfiler.backend = app.config.app_profiler.profiler_backend || :stackprof unless AppProfiler.running?
44
50
  AppProfiler.forward_metadata_on_upload = app.config.app_profiler.forward_metadata_on_upload || false
45
51
  AppProfiler.profile_sampler_enabled = app.config.app_profiler.profile_sampler_enabled || false
46
52
  AppProfiler.profile_sampler_config = app.config.app_profiler.profile_sampler_config ||
@@ -49,8 +55,8 @@ module AppProfiler
49
55
 
50
56
  initializer "app_profiler.add_middleware" do |app|
51
57
  unless AppProfiler.middleware.disabled
52
- if AppProfiler.viewer == Viewer::SpeedscopeRemoteViewer
53
- app.middleware.insert_before(0, Viewer::SpeedscopeRemoteViewer::Middleware)
58
+ if (Rails.env.development? || Rails.env.test?) && AppProfiler.stackprof_viewer.remote?
59
+ app.middleware.insert_before(0, AppProfiler.viewer::Middleware)
54
60
  end
55
61
  app.middleware.insert_before(0, AppProfiler.middleware)
56
62
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "app_profiler/sampler/stackprof_config"
4
4
  require "app_profiler/sampler/vernier_config"
5
+
5
6
  module AppProfiler
6
7
  module Sampler
7
8
  class Config
@@ -27,7 +28,7 @@ module AppProfiler
27
28
 
28
29
  raise ArgumentError, "mode probabilities must sum to 1" unless backends_probability.values.sum == 1.0
29
30
 
30
- ActiveSupport::Deprecation.new.warn("passing paths is deprecated, use targets instead") if paths
31
+ AppProfiler.deprecator.warn("passing paths is deprecated, use targets instead") if paths
31
32
 
32
33
  @sample_rate = sample_rate
33
34
  @targets = paths || targets
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "app_profiler/sampler/config"
4
+
4
5
  module AppProfiler
5
6
  module Sampler
6
7
  @excluded_cache = {}
@@ -2,7 +2,7 @@
2
2
 
3
3
  module AppProfiler
4
4
  class StackprofProfile < BaseProfile
5
- FILE_EXTENSION = ".json"
5
+ FILE_EXTENSION = ".stackprof.json"
6
6
 
7
7
  def mode
8
8
  @data[:mode]
@@ -17,7 +17,7 @@ module AppProfiler
17
17
  end
18
18
 
19
19
  def view(params = {})
20
- AppProfiler.viewer.view(self, **params)
20
+ AppProfiler.stackprof_viewer.view(self, **params)
21
21
  end
22
22
  end
23
23
  end
@@ -37,7 +37,7 @@ module AppProfiler
37
37
 
38
38
  def enqueue_upload(profile)
39
39
  mutex.synchronize do
40
- process_queue_thread unless @process_queue_thread&.alive?
40
+ start_process_queue_thread
41
41
 
42
42
  @queue ||= init_queue
43
43
  begin
@@ -50,12 +50,6 @@ module AppProfiler
50
50
  end
51
51
  end
52
52
 
53
- def reset_queue # for testing
54
- init_queue
55
- @process_queue_thread&.kill
56
- @process_queue_thread = nil
57
- end
58
-
59
53
  private
60
54
 
61
55
  def mutex
@@ -66,8 +60,10 @@ module AppProfiler
66
60
  @queue = SizedQueue.new(AppProfiler.upload_queue_max_length)
67
61
  end
68
62
 
69
- def process_queue_thread
70
- @process_queue_thread ||= Thread.new do
63
+ def start_process_queue_thread
64
+ return if @process_queue_thread&.alive?
65
+
66
+ @process_queue_thread = Thread.new do
71
67
  loop do
72
68
  process_queue
73
69
  sleep(AppProfiler.upload_queue_interval_secs)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module AppProfiler
4
4
  class VernierProfile < BaseProfile
5
- FILE_EXTENSION = ".gecko.json"
5
+ FILE_EXTENSION = ".vernier.json"
6
6
 
7
7
  def mode
8
8
  @data[:meta][:mode]
@@ -17,7 +17,7 @@ module AppProfiler
17
17
  end
18
18
 
19
19
  def view(params = {})
20
- raise NotImplementedError
20
+ AppProfiler.vernier_viewer.view(self, **params)
21
21
  end
22
22
  end
23
23
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppProfiler
4
- VERSION = "0.2.5"
4
+ VERSION = "0.2.7"
5
5
  end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem "rails-html-sanitizer", ">= 1.6.0"
4
+ require "rails-html-sanitizer"
5
+
6
+ module AppProfiler
7
+ module Viewer
8
+ class BaseMiddleware
9
+ class Sanitizer < Rails::HTML::Sanitizer.best_supported_vendor.safe_list_sanitizer
10
+ self.allowed_tags = Set.new([
11
+ "strong",
12
+ "em",
13
+ "b",
14
+ "i",
15
+ "p",
16
+ "code",
17
+ "pre",
18
+ "tt",
19
+ "samp",
20
+ "kbd",
21
+ "var",
22
+ "sub",
23
+ "sup",
24
+ "dfn",
25
+ "cite",
26
+ "big",
27
+ "small",
28
+ "address",
29
+ "hr",
30
+ "br",
31
+ "div",
32
+ "span",
33
+ "h1",
34
+ "h2",
35
+ "h3",
36
+ "h4",
37
+ "h5",
38
+ "h6",
39
+ "ul",
40
+ "ol",
41
+ "li",
42
+ "dl",
43
+ "dt",
44
+ "dd",
45
+ "abbr",
46
+ "acronym",
47
+ "a",
48
+ "img",
49
+ "blockquote",
50
+ "del",
51
+ "ins",
52
+ "script",
53
+ ])
54
+ end
55
+
56
+ private_constant(:Sanitizer)
57
+
58
+ class << self
59
+ def id(file)
60
+ file.basename.to_s
61
+ end
62
+ end
63
+
64
+ def initialize(app)
65
+ @app = app
66
+ end
67
+
68
+ def call(env)
69
+ request = Rack::Request.new(env)
70
+
71
+ return index(env) if %r(\A/app_profiler/?\z).match?(request.path_info)
72
+
73
+ @app.call(env)
74
+ end
75
+
76
+ protected
77
+
78
+ def id(file)
79
+ self.class.id(file)
80
+ end
81
+
82
+ def profile_files
83
+ AppProfiler.profile_root.glob("**/*.json")
84
+ end
85
+
86
+ def render(html)
87
+ [
88
+ 200,
89
+ { "Content-Type" => "text/html" },
90
+ [
91
+ +<<~HTML,
92
+ <!doctype html>
93
+ <html>
94
+ <head>
95
+ <title>App Profiler</title>
96
+ </head>
97
+ <body>
98
+ #{sanitizer.sanitize(html)}
99
+ </body>
100
+ </html>
101
+ HTML
102
+ ],
103
+ ]
104
+ end
105
+
106
+ def sanitizer
107
+ @sanitizer ||= Sanitizer.new
108
+ end
109
+
110
+ def viewer(_env, path)
111
+ raise NotImplementedError
112
+ end
113
+
114
+ def show(env, id)
115
+ raise NotImplementedError
116
+ end
117
+
118
+ def index(_env)
119
+ render(
120
+ (+"").tap do |content|
121
+ content << "<h1>Profiles</h1>"
122
+ profile_files.each do |file|
123
+ viewer = if file.to_s.end_with?(AppProfiler::VernierProfile::FILE_EXTENSION)
124
+ AppProfiler::Viewer::FirefoxRemoteViewer::NAME
125
+ else
126
+ AppProfiler::Viewer::SpeedscopeRemoteViewer::NAME
127
+ end
128
+ content << <<~HTML
129
+ <p>
130
+ <a href="/app_profiler/#{viewer}/viewer/#{id(file)}">
131
+ #{id(file)}
132
+ </a>
133
+ </p>
134
+ HTML
135
+ end
136
+ end,
137
+ )
138
+ end
139
+ end
140
+ end
141
+ end
@@ -7,10 +7,10 @@ module AppProfiler
7
7
  def view(profile, params = {})
8
8
  new(profile).view(**params)
9
9
  end
10
- end
11
10
 
12
- def view(_params = {})
13
- raise NotImplementedError
11
+ def remote?
12
+ false
13
+ end
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "app_profiler/yarn/command"
4
+ require "app_profiler/yarn/with_firefox_profiler"
5
+
6
+ module AppProfiler
7
+ module Viewer
8
+ class FirefoxRemoteViewer < BaseViewer
9
+ class Middleware < AppProfiler::Viewer::BaseMiddleware
10
+ include Yarn::WithFirefoxProfiler
11
+
12
+ def initialize(app)
13
+ super
14
+ @firefox_profiler = Rack::File.new(
15
+ File.join(AppProfiler.root, "node_modules/firefox-profiler/dist"),
16
+ )
17
+ end
18
+
19
+ def call(env)
20
+ request = Rack::Request.new(env)
21
+ # Firefox profiler *really* doesn't like for /from-url/ to be at any other mount point
22
+ # so with this enabled, we take over both /app_profiler and /from-url in the app in development.
23
+ return from(env, Regexp.last_match(1)) if request.path_info =~ %r(\A/from-url/(.*)\z)
24
+ return viewer(env, Regexp.last_match(1)) if request.path_info =~ %r(\A/app_profiler/firefox/viewer/(.*)\z)
25
+ return show(env, Regexp.last_match(1)) if request.path_info =~ %r(\A/app_profiler/firefox/(.*)\z)
26
+
27
+ super
28
+ end
29
+
30
+ protected
31
+
32
+ attr_reader(:firefox_profiler)
33
+
34
+ def viewer(env, path)
35
+ setup_yarn unless yarn_setup
36
+
37
+ if path.end_with?(AppProfiler::VernierProfile::FILE_EXTENSION)
38
+ proto = env["rack.url_scheme"]
39
+ host = env["HTTP_HOST"]
40
+ source = "#{proto}://#{host}/app_profiler/firefox/#{path}"
41
+ target = "/from-url/#{CGI.escape(source)}"
42
+
43
+ [302, { "Location" => target }, [""]]
44
+ else
45
+ env[Rack::PATH_INFO] = path.delete_prefix("/app_profiler")
46
+ firefox_profiler.call(env)
47
+ end
48
+ end
49
+
50
+ def from(env, path)
51
+ setup_yarn unless yarn_setup
52
+ index = File.read(File.join(AppProfiler.root, "node_modules/firefox-profiler/dist/index.html"))
53
+ [200, { "Content-Type" => "text/html" }, [index]]
54
+ end
55
+
56
+ def show(_env, name)
57
+ profile = profile_files.find do |file|
58
+ id(file) == name
59
+ end || raise(ArgumentError)
60
+
61
+ [200, { "Content-Type" => "application/json" }, [profile.read]]
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "app_profiler/viewer/firefox_remote_viewer/middleware"
4
+
5
+ module AppProfiler
6
+ module Viewer
7
+ class FirefoxRemoteViewer < BaseViewer
8
+ NAME = "firefox"
9
+
10
+ class << self
11
+ def remote?
12
+ true
13
+ end
14
+ end
15
+
16
+ def initialize(profile)
17
+ super()
18
+ @profile = profile
19
+ end
20
+
21
+ def view(response: nil, autoredirect: nil, async: false)
22
+ id = Middleware.id(@profile.file)
23
+
24
+ if response && response[0].to_i < 500
25
+ response[1]["Location"] = "/app_profiler/#{NAME}/viewer/#{id}"
26
+ response[0] = 303
27
+ else
28
+ AppProfiler.logger.info("[Profiler] Profile available at /app_profiler/#{id}\n")
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "app_profiler/exec"
4
+
5
+ module AppProfiler
6
+ module Viewer
7
+ class FirefoxViewer < BaseViewer
8
+ include Exec
9
+
10
+ CHILD_PIDS = []
11
+
12
+ at_exit { Process.wait if CHILD_PIDS.any? }
13
+
14
+ trap("INT") do
15
+ CHILD_PIDS.each { |pid| Process.kill("INT", pid) }
16
+ sleep(0.5)
17
+ end
18
+
19
+ class ProfileViewerError < StandardError; end
20
+
21
+ VALID_COMMANDS = [
22
+ ["which", "profile-viewer"],
23
+ ["gem", "install", "profile-viewer"],
24
+ ["profile-viewer", /.*\.json/],
25
+ ]
26
+ private_constant(:VALID_COMMANDS)
27
+
28
+ class << self
29
+ def view(profile, params = {})
30
+ new(profile).view(**params)
31
+ end
32
+ end
33
+
34
+ def valid_commands
35
+ VALID_COMMANDS
36
+ end
37
+
38
+ def initialize(profile)
39
+ super()
40
+ @profile = profile
41
+ end
42
+
43
+ def view(_params = {})
44
+ profile_viewer(@profile.file.to_s)
45
+ end
46
+
47
+ private
48
+
49
+ def setup_profile_viewer
50
+ exec("which", "profile-viewer", silent: true) do
51
+ gem_install("profile-viewer")
52
+ end
53
+ @profile_viewer_initialized = true
54
+ end
55
+
56
+ def profile_viewer_setup
57
+ @profile_viewer_initialized || false
58
+ end
59
+
60
+ def gem_install(gem)
61
+ exec("gem", "install", gem) do
62
+ raise ProfileViewerError, "Failed to run gem install #{gem}."
63
+ end
64
+ end
65
+
66
+ def profile_viewer(path)
67
+ setup_profile_viewer unless profile_viewer_setup
68
+
69
+ CHILD_PIDS << fork do
70
+ Bundler.with_unbundled_env do
71
+ exec("profile-viewer", path) do
72
+ raise ProfileViewerError, "Failed to run profile-viewer."
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -16,14 +16,23 @@ module AppProfiler
16
16
  )
17
17
  end
18
18
 
19
+ def call(env)
20
+ request = Rack::Request.new(env)
21
+
22
+ return viewer(env, Regexp.last_match(1)) if request.path_info =~ %r(\A/app_profiler/speedscope/viewer/(.*)\z)
23
+ return show(env, Regexp.last_match(1)) if request.path_info =~ %r(\A/app_profiler/speedscope/(.*)\z)
24
+
25
+ super
26
+ end
27
+
19
28
  protected
20
29
 
21
30
  attr_reader(:speedscope)
22
31
 
23
32
  def viewer(env, path)
24
33
  setup_yarn unless yarn_setup
25
- env[Rack::PATH_INFO] = path.delete_prefix("/app_profiler")
26
34
 
35
+ env[Rack::PATH_INFO] = path.delete_prefix("/app_profiler/speedscope")
27
36
  speedscope.call(env)
28
37
  end
29
38
 
@@ -32,25 +41,7 @@ module AppProfiler
32
41
  id(file) == name
33
42
  end || raise(ArgumentError)
34
43
 
35
- render(
36
- <<~HTML,
37
- <script type="text/javascript">
38
- var graph = #{profile.read};
39
- var json = JSON.stringify(graph);
40
- var blob = new Blob([json], { type: 'text/plain' });
41
- var objUrl = encodeURIComponent(URL.createObjectURL(blob));
42
- var iframe = document.createElement('iframe');
43
-
44
- document.body.style.margin = '0px';
45
- document.body.appendChild(iframe);
46
-
47
- iframe.style.width = '100vw';
48
- iframe.style.height = '100vh';
49
- iframe.style.border = 'none';
50
- iframe.setAttribute('src', '/app_profiler/viewer/index.html#profileURL=' + objUrl + '&title=' + 'Flamegraph for #{name}');
51
- </script>
52
- HTML
53
- )
44
+ [200, { "Content-Type" => "application/json" }, [profile.read]]
54
45
  end
55
46
  end
56
47
  end
@@ -1,14 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "app_profiler/viewer/speedscope_remote_viewer/base_middleware"
3
+ require "active_support/deprecation/constant_accessor"
4
4
  require "app_profiler/viewer/speedscope_remote_viewer/middleware"
5
5
 
6
6
  module AppProfiler
7
7
  module Viewer
8
8
  class SpeedscopeRemoteViewer < BaseViewer
9
+ include ActiveSupport::Deprecation::DeprecatedConstantAccessor
10
+ deprecate_constant(
11
+ "BaseMiddleware",
12
+ "AppProfiler::Viewer::BaseMiddleware",
13
+ deprecator: AppProfiler.deprecator,
14
+ )
15
+ NAME = "speedscope"
16
+
9
17
  class << self
10
- def view(profile, params = {})
11
- new(profile).view(**params)
18
+ def remote?
19
+ true
12
20
  end
13
21
  end
14
22
 
@@ -21,7 +29,7 @@ module AppProfiler
21
29
  id = Middleware.id(@profile.file)
22
30
 
23
31
  if response && response[0].to_i < 500
24
- response[1]["Location"] = "/app_profiler/#{id}"
32
+ response[1]["Location"] = "/app_profiler/#{NAME}/viewer/#{id}"
25
33
  response[0] = 303
26
34
  else
27
35
  AppProfiler.logger.info("[Profiler] Profile available at /app_profiler/#{id}\n")
@@ -8,12 +8,6 @@ module AppProfiler
8
8
  class SpeedscopeViewer < BaseViewer
9
9
  include Yarn::WithSpeedscope
10
10
 
11
- class << self
12
- def view(profile, params = {})
13
- new(profile).view(**params)
14
- end
15
- end
16
-
17
11
  def initialize(profile)
18
12
  super()
19
13
  @profile = profile
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "app_profiler/exec"
4
+
3
5
  module AppProfiler
4
6
  module Yarn
5
7
  module Command
8
+ include Exec
9
+
6
10
  class YarnError < StandardError; end
7
11
 
8
12
  VALID_COMMANDS = [
@@ -10,10 +14,16 @@ module AppProfiler
10
14
  ["yarn", "init", "--yes"],
11
15
  ["yarn", "add", "speedscope", "--dev", "--ignore-workspace-root-check"],
12
16
  ["yarn", "run", "speedscope", /.*\.json/],
17
+ ["yarn", "add", "--dev", %r{.*/firefox-profiler}],
18
+ ["yarn", "--cwd", %r{.*/firefox-profiler}],
19
+ ["yarn", "--cwd", %r{.*/firefox-profiler}, "build-prod"],
13
20
  ]
14
21
 
15
22
  private_constant(:VALID_COMMANDS)
16
- mattr_accessor(:yarn_setup, default: false)
23
+
24
+ def valid_commands
25
+ VALID_COMMANDS
26
+ end
17
27
 
18
28
  def yarn(command, *options)
19
29
  setup_yarn unless yarn_setup
@@ -29,22 +39,16 @@ module AppProfiler
29
39
  yarn("init", "--yes") unless package_json_exists?
30
40
  end
31
41
 
32
- private
33
-
34
- def ensure_command_valid(command)
35
- unless valid_command?(command)
36
- raise YarnError, "Illegal command: #{command.join(" ")}."
37
- end
42
+ def yarn_setup
43
+ @yarn_initialized || false
38
44
  end
39
45
 
40
- def valid_command?(command)
41
- VALID_COMMANDS.any? do |valid_command|
42
- valid_command.zip(command).all? do |valid_part, part|
43
- part.match?(valid_part)
44
- end
45
- end
46
+ def yarn_setup=(state)
47
+ @yarn_initialized = state
46
48
  end
47
49
 
50
+ private
51
+
48
52
  def ensure_yarn_installed
49
53
  exec("which", "yarn", silent: true) do
50
54
  raise(
@@ -55,22 +59,12 @@ module AppProfiler
55
59
  MSG
56
60
  )
57
61
  end
58
- self.yarn_setup = true
62
+ @yarn_initialized = true
59
63
  end
60
64
 
61
65
  def package_json_exists?
62
66
  AppProfiler.root.join("package.json").exist?
63
67
  end
64
-
65
- def exec(*command, silent: false)
66
- ensure_command_valid(command)
67
-
68
- if silent
69
- system(*command, out: File::NULL).tap { |return_code| yield unless return_code }
70
- else
71
- system(*command).tap { |return_code| yield unless return_code }
72
- end
73
- end
74
68
  end
75
69
  end
76
70
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ module Yarn
5
+ module WithFirefoxProfiler
6
+ include Command
7
+
8
+ PACKAGE = "https://github.com/tenderlove/profiler#v0.0.2"
9
+ VALID_COMMANDS = [
10
+ *VALID_COMMANDS,
11
+ ["git", "clone", "https://github.com/tenderlove/profiler", "firefox-profiler", "--branch=v0.0.2"],
12
+ ]
13
+ private_constant(:PACKAGE, :VALID_COMMANDS)
14
+
15
+ def setup_yarn
16
+ super
17
+ return if firefox_profiler_added?
18
+
19
+ fetch_firefox_profiler
20
+ end
21
+
22
+ def valid_commands
23
+ VALID_COMMANDS
24
+ end
25
+
26
+ private
27
+
28
+ def firefox_profiler_added?
29
+ AppProfiler.root.join("node_modules/firefox-profiler/dist").exist?
30
+ end
31
+
32
+ def fetch_firefox_profiler
33
+ repo, branch = PACKAGE.to_s.split("#")
34
+
35
+ dir = "./tmp"
36
+ FileUtils.mkdir_p(dir)
37
+ Dir.chdir(dir) do
38
+ clone_args = ["git", "clone", repo, "firefox-profiler"]
39
+ clone_args.push("--branch=#{branch}") unless branch.nil? || branch&.empty?
40
+ exec(*clone_args)
41
+ package_contents = File.read("firefox-profiler/package.json")
42
+ package_json = JSON.parse(package_contents)
43
+ package_json["name"] ||= "firefox-profiler"
44
+ package_json["version"] ||= "0.0.1"
45
+ File.write("firefox-profiler/package.json", package_json.to_json)
46
+ end
47
+ yarn("--cwd", "#{dir}/firefox-profiler")
48
+
49
+ patch_firefox_profiler(dir)
50
+ yarn("--cwd", "#{dir}/firefox-profiler", "build-prod")
51
+ patch_file(
52
+ "#{dir}/firefox-profiler/dist/index.html",
53
+ 'href="locales/en-US/app.ftl"',
54
+ 'href="/app_profiler/firefox/viewer/locales/en-US/app.ftl"',
55
+ )
56
+
57
+ yarn("add", "--dev", "#{dir}/firefox-profiler")
58
+ end
59
+
60
+ def patch_firefox_profiler(dir)
61
+ # Patch the publicPath so that the app can be "mounted" at the right location
62
+ patch_file(
63
+ "#{dir}/firefox-profiler/webpack.config.js",
64
+ "publicPath: '/'",
65
+ "publicPath: '/app_profiler/firefox/viewer/'",
66
+ )
67
+ patch_file(
68
+ "#{dir}/firefox-profiler/src/app-logic/l10n.js",
69
+ "fetch(`/locales/",
70
+ "fetch(`/app_profiler/firefox/viewer/locales/",
71
+ )
72
+ end
73
+
74
+ def patch_file(file, find, replace)
75
+ contents = File.read(file)
76
+ new_contents = contents.gsub(find, replace)
77
+ File.write(file, new_contents)
78
+ end
79
+ end
80
+ end
81
+ end
data/lib/app_profiler.rb CHANGED
@@ -30,16 +30,21 @@ module AppProfiler
30
30
  module Viewer
31
31
  autoload :BaseViewer, "app_profiler/viewer/base_viewer"
32
32
  autoload :SpeedscopeViewer, "app_profiler/viewer/speedscope_viewer"
33
+ autoload :FirefoxViewer, "app_profiler/viewer/firefox_viewer"
34
+ autoload :BaseMiddleware, "app_profiler/viewer/base_middleware"
33
35
  autoload :SpeedscopeRemoteViewer, "app_profiler/viewer/speedscope_remote_viewer"
36
+ autoload :FirefoxRemoteViewer, "app_profiler/viewer/firefox_remote_viewer"
34
37
  end
35
38
 
36
- require "app_profiler/middleware"
37
- require "app_profiler/parameters"
38
- require "app_profiler/request_parameters"
39
- require "app_profiler/profile"
40
- require "app_profiler/backend"
41
- require "app_profiler/server"
42
- require "app_profiler/sampler"
39
+ autoload(:Middleware, "app_profiler/middleware")
40
+ autoload(:Parameters, "app_profiler/parameters")
41
+ autoload(:RequestParameters, "app_profiler/request_parameters")
42
+ autoload(:BaseProfile, "app_profiler/base_profile")
43
+ autoload :StackprofProfile, "app_profiler/stackprof_profile"
44
+ autoload :VernierProfile, "app_profiler/vernier_profile"
45
+ autoload(:Backend, "app_profiler/backend")
46
+ autoload(:Server, "app_profiler/server")
47
+ autoload(:Sampler, "app_profiler/sampler")
43
48
 
44
49
  mattr_accessor :logger, default: Logger.new($stdout)
45
50
  mattr_accessor :root
@@ -50,11 +55,10 @@ module AppProfiler
50
55
  mattr_reader :profile_header, default: "X-Profile"
51
56
  mattr_accessor :profile_async_header, default: "X-Profile-Async"
52
57
  mattr_accessor :context, default: nil
53
- mattr_reader :profile_url_formatter,
54
- default: DefaultProfileFormatter
55
-
58
+ mattr_reader :profile_url_formatter, default: DefaultProfileFormatter
56
59
  mattr_accessor :storage, default: Storage::FileStorage
57
- mattr_accessor :viewer, default: Viewer::SpeedscopeViewer
60
+ mattr_writer :stackprof_viewer, default: nil
61
+ mattr_writer :vernier_viewer, default: nil
58
62
  mattr_accessor :middleware, default: Middleware
59
63
  mattr_accessor :server, default: Server
60
64
  mattr_accessor :upload_queue_max_length, default: 10
@@ -64,59 +68,73 @@ module AppProfiler
64
68
  mattr_reader :profile_enqueue_failure, default: nil
65
69
  mattr_reader :after_process_queue, default: nil
66
70
  mattr_accessor :forward_metadata_on_upload, default: false
67
-
68
71
  mattr_accessor :profile_sampler_config
72
+ mattr_reader :profile_file_name
69
73
 
70
74
  class << self
75
+ def deprecator # :nodoc:
76
+ @deprecator ||= ActiveSupport::Deprecation.new("in future releases", "app_profiler")
77
+ end
78
+
71
79
  def run(*args, backend: nil, **kwargs, &block)
72
- orig_backend = self.backend
73
- begin
74
- self.backend = backend if backend
75
- profiler.run(*args, **kwargs, &block)
76
- rescue BackendError => e
77
- logger.error(
78
- "[AppProfiler.run] exception #{e} configuring backend #{backend}: #{e.message}",
79
- )
80
- yield
80
+ if backend
81
+ original_backend = self.backend
82
+ self.backend = backend
81
83
  end
84
+ profiler.run(*args, **kwargs, &block)
85
+ rescue BackendError => e
86
+ logger.error(
87
+ "[AppProfiler.run] exception #{e} configuring backend #{backend}: #{e.message}",
88
+ )
89
+ yield
82
90
  ensure
83
- AppProfiler.backend = orig_backend
91
+ self.backend = original_backend if backend
84
92
  end
85
93
 
86
- def start(*args)
87
- profiler.start(*args)
94
+ def start(*args, backend: nil, **kwargs)
95
+ self.backend = backend if backend
96
+ profiler.start(*args, **kwargs)
88
97
  end
89
98
 
90
99
  def stop
91
100
  profiler.stop
92
- profiler.results
101
+ profiler.results.tap { clear }
93
102
  end
94
103
 
95
104
  def running?
96
- @backend&.running?
105
+ @profiler&.running?
97
106
  end
98
107
 
99
108
  def profiler
100
- backend
101
- @backend ||= @profiler_backend.new
109
+ @profiler ||= profiler_backend.new
102
110
  end
103
111
 
104
112
  def backend=(new_backend)
105
- return if new_backend == backend
106
-
107
- new_profiler_backend = backend_for(new_backend)
113
+ return if (new_profiler_backend = backend_for(new_backend)) == profiler_backend
108
114
 
109
115
  if running?
110
116
  raise BackendError,
111
117
  "cannot change backend to #{new_backend} while #{backend} backend is running"
112
118
  end
113
119
 
114
- return if @profiler_backend == new_profiler_backend
115
-
116
120
  clear
117
121
  @profiler_backend = new_profiler_backend
118
122
  end
119
123
 
124
+ def stackprof_viewer
125
+ @@stackprof_viewer ||= Viewer::SpeedscopeViewer # rubocop:disable Style/ClassVars
126
+ end
127
+
128
+ def vernier_viewer
129
+ @@vernier_viewer ||= Viewer::FirefoxViewer # rubocop:disable Style/ClassVars
130
+ end
131
+
132
+ def profile_file_name=(value)
133
+ raise ArgumentError, "profile_file_name must be a proc" if value && !value.is_a?(Proc)
134
+
135
+ @@profile_file_name = value # rubocop:disable Style/ClassVars
136
+ end
137
+
120
138
  def profile_sampler_enabled=(value)
121
139
  if value.is_a?(Proc)
122
140
  raise ArgumentError,
@@ -141,22 +159,21 @@ module AppProfiler
141
159
 
142
160
  def backend_for(backend_name)
143
161
  if vernier_supported? &&
144
- backend_name == AppProfiler::Backend::VernierBackend.name
162
+ backend_name&.to_sym == AppProfiler::Backend::VernierBackend.name
145
163
  AppProfiler::Backend::VernierBackend
146
- elsif backend_name == AppProfiler::Backend::StackprofBackend.name
164
+ elsif backend_name&.to_sym == AppProfiler::Backend::StackprofBackend.name
147
165
  AppProfiler::Backend::StackprofBackend
148
166
  else
149
- raise BackendError, "unknown backend #{backend_name}"
167
+ raise BackendError, "unknown backend #{backend_name.inspect}"
150
168
  end
151
169
  end
152
170
 
153
171
  def backend
154
- @profiler_backend ||= Backend::StackprofBackend
155
- @profiler_backend.name
172
+ profiler_backend.name
156
173
  end
157
174
 
158
175
  def vernier_supported?
159
- defined?(AppProfiler::Backend::VernierBackend.name)
176
+ RUBY_VERSION >= "3.2.1" && defined?(AppProfiler::Backend::VernierBackend.name)
160
177
  end
161
178
 
162
179
  def profile_header=(profile_header)
@@ -207,11 +224,26 @@ module AppProfiler
207
224
  AppProfiler.profile_url_formatter.call(upload)
208
225
  end
209
226
 
227
+ def viewer
228
+ deprecator.warn("AppProfiler.viewer is deprecated, please use stackprof_viewer instead.")
229
+ stackprof_viewer
230
+ end
231
+
232
+ def viewer=(viewer)
233
+ deprecator.warn("AppProfiler.viewer= is deprecated, please use stackprof_viewer= instead.")
234
+ self.stackprof_viewer = viewer
235
+ end
236
+
210
237
  private
211
238
 
239
+ def profiler_backend
240
+ @profiler_backend ||= Backend::StackprofBackend
241
+ end
242
+
212
243
  def clear
213
- @backend.stop if @backend&.running?
214
- @backend = nil
244
+ profiler.stop if running?
245
+ @profiler = nil
246
+ @profiler_backend = nil
215
247
  end
216
248
  end
217
249
 
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.2.5
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gannon McGibbon
@@ -10,10 +10,9 @@ authors:
10
10
  - Jon Simpson
11
11
  - Kevin Jalbert
12
12
  - Scott Francis
13
- autorequire:
14
13
  bindir: bin
15
14
  cert_chain: []
16
- date: 2024-10-15 00:00:00.000000000 Z
15
+ date: 2025-01-30 00:00:00.000000000 Z
17
16
  dependencies:
18
17
  - !ruby/object:Gem::Dependency
19
18
  name: activesupport
@@ -127,7 +126,6 @@ dependencies:
127
126
  - - ">="
128
127
  - !ruby/object:Gem::Version
129
128
  version: '0'
130
- description:
131
129
  email:
132
130
  - gems@shopify.com
133
131
  executables: []
@@ -139,14 +137,13 @@ files:
139
137
  - lib/app_profiler/backend/base_backend.rb
140
138
  - lib/app_profiler/backend/stackprof_backend.rb
141
139
  - lib/app_profiler/backend/vernier_backend.rb
140
+ - lib/app_profiler/base_profile.rb
141
+ - lib/app_profiler/exec.rb
142
142
  - lib/app_profiler/middleware.rb
143
143
  - lib/app_profiler/middleware/base_action.rb
144
144
  - lib/app_profiler/middleware/upload_action.rb
145
145
  - lib/app_profiler/middleware/view_action.rb
146
146
  - lib/app_profiler/parameters.rb
147
- - lib/app_profiler/profile.rb
148
- - lib/app_profiler/profile/stackprof.rb
149
- - lib/app_profiler/profile/vernier.rb
150
147
  - lib/app_profiler/railtie.rb
151
148
  - lib/app_profiler/request_parameters.rb
152
149
  - lib/app_profiler/sampler.rb
@@ -154,22 +151,27 @@ files:
154
151
  - lib/app_profiler/sampler/stackprof_config.rb
155
152
  - lib/app_profiler/sampler/vernier_config.rb
156
153
  - lib/app_profiler/server.rb
154
+ - lib/app_profiler/stackprof_profile.rb
157
155
  - lib/app_profiler/storage/base_storage.rb
158
156
  - lib/app_profiler/storage/file_storage.rb
159
157
  - lib/app_profiler/storage/google_cloud_storage.rb
158
+ - lib/app_profiler/vernier_profile.rb
160
159
  - lib/app_profiler/version.rb
160
+ - lib/app_profiler/viewer/base_middleware.rb
161
161
  - lib/app_profiler/viewer/base_viewer.rb
162
+ - lib/app_profiler/viewer/firefox_remote_viewer.rb
163
+ - lib/app_profiler/viewer/firefox_remote_viewer/middleware.rb
164
+ - lib/app_profiler/viewer/firefox_viewer.rb
162
165
  - lib/app_profiler/viewer/speedscope_remote_viewer.rb
163
- - lib/app_profiler/viewer/speedscope_remote_viewer/base_middleware.rb
164
166
  - lib/app_profiler/viewer/speedscope_remote_viewer/middleware.rb
165
167
  - lib/app_profiler/viewer/speedscope_viewer.rb
166
168
  - lib/app_profiler/yarn/command.rb
169
+ - lib/app_profiler/yarn/with_firefox_profiler.rb
167
170
  - lib/app_profiler/yarn/with_speedscope.rb
168
171
  homepage: https://github.com/Shopify/app_profiler
169
172
  licenses: []
170
173
  metadata:
171
174
  allowed_push_host: https://rubygems.org
172
- post_install_message:
173
175
  rdoc_options: []
174
176
  require_paths:
175
177
  - lib
@@ -184,8 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
184
186
  - !ruby/object:Gem::Version
185
187
  version: '0'
186
188
  requirements: []
187
- rubygems_version: 3.5.21
188
- signing_key:
189
+ rubygems_version: 3.6.3
189
190
  specification_version: 4
190
191
  summary: Collect performance profiles for your Rails application.
191
192
  test_files: []
@@ -1,142 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- gem "rails-html-sanitizer", ">= 1.6.0"
4
- require "rails-html-sanitizer"
5
-
6
- module AppProfiler
7
- module Viewer
8
- class SpeedscopeRemoteViewer < BaseViewer
9
- class BaseMiddleware
10
- class Sanitizer < Rails::HTML::Sanitizer.best_supported_vendor.safe_list_sanitizer
11
- self.allowed_tags = Set.new([
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",
54
- ])
55
- end
56
-
57
- private_constant(:Sanitizer)
58
-
59
- class << self
60
- def id(file)
61
- file.basename.to_s.delete_suffix(".json")
62
- end
63
- end
64
-
65
- def initialize(app)
66
- @app = app
67
- end
68
-
69
- def call(env)
70
- request = Rack::Request.new(env)
71
-
72
- return index(env) if request.path_info =~ %r(\A/app_profiler/?\z)
73
- return viewer(env, Regexp.last_match(1)) if request.path_info =~ %r(\A/app_profiler/viewer/(.*)\z)
74
- return show(env, Regexp.last_match(1)) if request.path_info =~ %r(\A/app_profiler/(.*)\z)
75
-
76
- @app.call(env)
77
- end
78
-
79
- protected
80
-
81
- def id(file)
82
- self.class.id(file)
83
- end
84
-
85
- def profile_files
86
- AppProfiler.profile_root.glob("**/*.json")
87
- end
88
-
89
- def render(html)
90
- [
91
- 200,
92
- { "Content-Type" => "text/html" },
93
- [
94
- +<<~HTML,
95
- <!doctype html>
96
- <html>
97
- <head>
98
- <title>App Profiler</title>
99
- </head>
100
- <body>
101
- #{sanitizer.sanitize(html)}
102
- </body>
103
- </html>
104
- HTML
105
- ],
106
- ]
107
- end
108
-
109
- def sanitizer
110
- @sanitizer ||= Sanitizer.new
111
- end
112
-
113
- def viewer(_env, path)
114
- raise NotImplementedError
115
- end
116
-
117
- def index(_env)
118
- render(
119
- (+"").tap do |content|
120
- content << "<h1>Profiles</h1>"
121
- profile_files.each do |file|
122
- content << <<~HTML
123
- <p>
124
- <a href="/app_profiler/#{id(file)}">
125
- #{id(file)}
126
- </a>
127
- </p>
128
- HTML
129
- end
130
- end,
131
- )
132
- end
133
-
134
- def show(env, id)
135
- raise NotImplementedError
136
- end
137
- end
138
-
139
- private_constant(:BaseMiddleware)
140
- end
141
- end
142
- end