app_profiler 0.0.6 → 0.0.9

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: e470d5d476934d052b8f5229cea27500fd089d46228458bee9caccd6147d4fdc
4
- data.tar.gz: a7087d13e92ccb0f709bd566c171395bcbc7a3a4d3cdd840cdaf54b712f371f2
3
+ metadata.gz: 160040c64c01368d5a6832e6fccc20ed14eecdd7ff0a4a9a18e4f7aeee62c83b
4
+ data.tar.gz: 76107cd56b460c68e33ef32677a3d822e83319b3cb9084e39e30841ee8bb8db7
5
5
  SHA512:
6
- metadata.gz: e87134e41f02914bce8da256ab75e11eb160dee8dfe05121aff17c9f537c71af68c832efa750ab4713b3d5365f51fa08938e678f434e04fc9de4b6b830d870ce
7
- data.tar.gz: fa2d66d349c5328ad24a6fac732ab04dd67fe77184ac32f680d55f06b204deaf0d08412d42abf87a627e25250bb9a442d6474c8a197e696494f8a6cc5d99e221
6
+ metadata.gz: 2c926b73ab9abe0773f5f7d2415672a89f444c2c3448387f57e07fe69b359c6a70175beaaba91171bda2f24702dae7073b76f094ef9d5de8fcc7ca360c3cc0a4
7
+ data.tar.gz: eb3af95b1164676ec400acd49732532e74c91565cfa70db31db7fbbcb00b188d6e68c5e35ae02ef526bf55694438e48a1fe97ece448a7de649cd0198871fd9de
@@ -42,7 +42,7 @@ module AppProfiler
42
42
  end
43
43
 
44
44
  def profile_data_url(upload)
45
- upload.url
45
+ upload.url.to_s
46
46
  end
47
47
 
48
48
  def profile_header
@@ -4,6 +4,7 @@ module AppProfiler
4
4
  class Profile
5
5
  INTERNAL_METADATA_KEYS = %i(id context)
6
6
  private_constant :INTERNAL_METADATA_KEYS
7
+ class UnsafeFilename < StandardError; end
7
8
 
8
9
  delegate :[], to: :@data
9
10
  attr_reader :id, :context
@@ -70,6 +71,8 @@ module AppProfiler
70
71
  Socket.gethostname,
71
72
  ].compact.join("-") << ".json"
72
73
 
74
+ raise UnsafeFilename if /[^0-9A-Za-z.\-\_]/.match(filename)
75
+
73
76
  AppProfiler.profile_root.join(filename)
74
77
  end
75
78
  end
@@ -24,21 +24,6 @@ module AppProfiler
24
24
  stop if started
25
25
  end
26
26
 
27
- def results
28
- stackprof_profile = stackprof_results
29
-
30
- return unless stackprof_profile
31
-
32
- Profile.from_stackprof(stackprof_profile)
33
- rescue => error
34
- AppProfiler.logger.info(
35
- "[Profiler] failed to obtain the profile error_class=#{error.class} error_message=#{error.message}"
36
- )
37
- nil
38
- end
39
-
40
- private
41
-
42
27
  def start(params = {})
43
28
  # Do not start the profiler if StackProf was started somewhere else.
44
29
  return false if running?
@@ -59,6 +44,21 @@ module AppProfiler
59
44
  StackProf.stop
60
45
  end
61
46
 
47
+ def results
48
+ stackprof_profile = stackprof_results
49
+
50
+ return unless stackprof_profile
51
+
52
+ Profile.from_stackprof(stackprof_profile)
53
+ rescue => error
54
+ AppProfiler.logger.info(
55
+ "[Profiler] failed to obtain the profile error_class=#{error.class} error_message=#{error.message}"
56
+ )
57
+ nil
58
+ end
59
+
60
+ private
61
+
62
62
  def stackprof_results
63
63
  StackProf.results
64
64
  end
@@ -30,6 +30,9 @@ module AppProfiler
30
30
 
31
31
  initializer "app_profiler.add_middleware" do |app|
32
32
  unless AppProfiler.middleware.disabled
33
+ if AppProfiler.viewer == Viewer::SpeedscopeRemoteViewer
34
+ app.middleware.insert_before(0, Viewer::SpeedscopeRemoteViewer::Middleware)
35
+ end
33
36
  app.middleware.insert_before(0, AppProfiler.middleware)
34
37
  end
35
38
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppProfiler
4
- VERSION = "0.0.6"
4
+ VERSION = "0.0.9"
5
5
  end
@@ -3,7 +3,13 @@
3
3
  module AppProfiler
4
4
  module Viewer
5
5
  class BaseViewer
6
- def self.view(_profile)
6
+ class << self
7
+ def view(profile)
8
+ new(profile).view
9
+ end
10
+ end
11
+
12
+ def view(_profile)
7
13
  raise NotImplementedError
8
14
  end
9
15
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails-html-sanitizer"
4
+
5
+ module AppProfiler
6
+ module Viewer
7
+ class SpeedscopeRemoteViewer < BaseViewer
8
+ class BaseMiddleware
9
+ class Sanitizer < Rails::Html::SafeListSanitizer
10
+ self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
11
+ sup dfn cite big small address hr br div span
12
+ h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
13
+ acronym a img blockquote del ins script))
14
+ end
15
+
16
+ private_constant(:Sanitizer)
17
+
18
+ def self.id(file)
19
+ file.basename.to_s.delete_suffix(".json")
20
+ end
21
+
22
+ def initialize(app)
23
+ @app = app
24
+ end
25
+
26
+ def call(env)
27
+ request = Rack::Request.new(env)
28
+
29
+ return index(env) if request.path_info =~ %r(\A/app_profiler/?\z)
30
+ return viewer(env, Regexp.last_match(1)) if request.path_info =~ %r(\A/app_profiler/viewer/(.*)\z)
31
+ return show(env, Regexp.last_match(1)) if request.path_info =~ %r(\A/app_profiler/(.*)\z)
32
+
33
+ @app.call(env)
34
+ end
35
+
36
+ protected
37
+
38
+ def id(file)
39
+ self.class.id(file)
40
+ end
41
+
42
+ def profile_files
43
+ AppProfiler.profile_root.glob("**/*.json")
44
+ end
45
+
46
+ def render(html)
47
+ [
48
+ 200,
49
+ { "Content-Type" => "text/html" },
50
+ [
51
+ +<<~HTML,
52
+ <!doctype html>
53
+ <html>
54
+ <head>
55
+ <title>App Profiler</title>
56
+ </head>
57
+ <body>
58
+ #{sanitizer.sanitize(html)}
59
+ </body>
60
+ </html>
61
+ HTML
62
+ ],
63
+ ]
64
+ end
65
+
66
+ def sanitizer
67
+ @sanitizer ||= Sanitizer.new
68
+ end
69
+
70
+ def viewer(_env, path)
71
+ raise NotImplementedError
72
+ end
73
+
74
+ def index(_env)
75
+ render(
76
+ String.new.tap do |content|
77
+ content << "<h1>Profiles</h1>"
78
+ profile_files.each do |file|
79
+ content << <<~HTML
80
+ <p>
81
+ <a href="/app_profiler/#{id(file)}">
82
+ #{id(file)}
83
+ </a>
84
+ </p>
85
+ HTML
86
+ end
87
+ end
88
+ )
89
+ end
90
+
91
+ def show(env, id)
92
+ raise NotImplementedError
93
+ end
94
+ end
95
+
96
+ private_constant(:BaseMiddleware)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "app_profiler/yarn/command"
4
+ require "app_profiler/yarn/with_speedscope"
5
+
6
+ module AppProfiler
7
+ module Viewer
8
+ class SpeedscopeRemoteViewer < BaseViewer
9
+ class Middleware < BaseMiddleware
10
+ include Yarn::WithSpeedscope
11
+
12
+ def initialize(app)
13
+ super
14
+ @speedscope = Rack::File.new(
15
+ File.join(AppProfiler.root, "node_modules/speedscope/dist/release")
16
+ )
17
+ end
18
+
19
+ protected
20
+
21
+ attr_reader(:speedscope)
22
+
23
+ def viewer(env, path)
24
+ setup_yarn unless yarn_setup
25
+ env[Rack::PATH_INFO] = path.delete_prefix("/app_profiler")
26
+
27
+ speedscope.call(env)
28
+ end
29
+
30
+ def show(_env, name)
31
+ profile = profile_files.find do |file|
32
+ id(file) == name
33
+ end || raise(ArgumentError)
34
+
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
+ )
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "app_profiler/viewer/speedscope_remote_viewer/base_middleware"
4
+ require "app_profiler/viewer/speedscope_remote_viewer/middleware"
5
+
6
+ module AppProfiler
7
+ module Viewer
8
+ class SpeedscopeRemoteViewer < BaseViewer
9
+ class << self
10
+ def view(profile)
11
+ new(profile).view
12
+ end
13
+ end
14
+
15
+ def initialize(profile)
16
+ super()
17
+ @profile = profile
18
+ end
19
+
20
+ def view
21
+ id = Middleware.id(@profile.file)
22
+ AppProfiler.logger.info("[Profiler] Profile available at /app_profiler/#{id}\n")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "app_profiler/yarn/command"
4
+ require "app_profiler/yarn/with_speedscope"
5
+
3
6
  module AppProfiler
4
7
  module Viewer
5
8
  class SpeedscopeViewer < BaseViewer
6
- mattr_accessor :yarn_setup, default: false
7
-
8
- class YarnError < StandardError; end
9
+ include Yarn::WithSpeedscope
9
10
 
10
11
  class << self
11
12
  def view(profile)
@@ -14,52 +15,12 @@ module AppProfiler
14
15
  end
15
16
 
16
17
  def initialize(profile)
18
+ super()
17
19
  @profile = profile
18
20
  end
19
21
 
20
22
  def view
21
- yarn("run speedscope \"#{@profile.file}\"")
22
- end
23
-
24
- private
25
-
26
- def yarn(command)
27
- setup_yarn unless yarn_setup
28
- exec("yarn #{command}") do
29
- raise YarnError, "Failed to run #{command}."
30
- end
31
- end
32
-
33
- def setup_yarn
34
- ensure_yarn_installed
35
- yarn("init --yes") unless package_json_exists?
36
- # We currently only support this gem in the root Gemfile.
37
- # See https://github.com/Shopify/app_profiler/issues/15
38
- # for more information
39
- yarn("add --dev --ignore-workspace-root-check speedscope")
40
- end
41
-
42
- def ensure_yarn_installed
43
- exec("which yarn > /dev/null") do
44
- raise(
45
- YarnError,
46
- <<~MSG.squish
47
- `yarn` command not found.
48
- Please install `yarn` or make it available in PATH.
49
- MSG
50
- )
51
- end
52
- self.yarn_setup = true
53
- end
54
-
55
- def package_json_exists?
56
- AppProfiler.root.join("package.json").exist?
57
- end
58
-
59
- def exec(command)
60
- system(command).tap do |return_code|
61
- yield unless return_code
62
- end
23
+ yarn("run", "speedscope", @profile.file.to_s)
63
24
  end
64
25
  end
65
26
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+ module AppProfiler
3
+ module Yarn
4
+ module Command
5
+ class YarnError < StandardError; end
6
+
7
+ VALID_COMMANDS = [
8
+ ["which", "yarn"],
9
+ ["yarn", "init", "--yes"],
10
+ ["yarn", "add", "speedscope", "--dev", "--ignore-workspace-root-check"],
11
+ ["yarn", "run", "speedscope", /.*\.json/],
12
+ ]
13
+
14
+ private_constant(:VALID_COMMANDS)
15
+ mattr_accessor(:yarn_setup, default: false)
16
+
17
+ def yarn(command, *options)
18
+ setup_yarn unless yarn_setup
19
+
20
+ exec("yarn", command, *options) do
21
+ raise YarnError, "Failed to run #{command}."
22
+ end
23
+ end
24
+
25
+ def setup_yarn
26
+ ensure_yarn_installed
27
+
28
+ yarn("init", "--yes") unless package_json_exists?
29
+ end
30
+
31
+ private
32
+
33
+ def ensure_command_valid(command)
34
+ unless valid_command?(command)
35
+ raise YarnError, "Illegal command: #{command.join(' ')}."
36
+ end
37
+ end
38
+
39
+ def valid_command?(command)
40
+ VALID_COMMANDS.any? do |valid_command|
41
+ valid_command.zip(command).all? do |valid_part, part|
42
+ part.match?(valid_part)
43
+ end
44
+ end
45
+ end
46
+
47
+ def ensure_yarn_installed
48
+ exec("which", "yarn", silent: true) do
49
+ raise(
50
+ YarnError,
51
+ <<~MSG.squish
52
+ `yarn` command not found.
53
+ Please install `yarn` or make it available in PATH.
54
+ MSG
55
+ )
56
+ end
57
+ self.yarn_setup = true
58
+ end
59
+
60
+ def package_json_exists?
61
+ AppProfiler.root.join("package.json").exist?
62
+ end
63
+
64
+ def exec(*command, silent: false)
65
+ ensure_command_valid(command)
66
+
67
+ if silent
68
+ system(*command, out: File::NULL).tap { |return_code| yield unless return_code }
69
+ else
70
+ system(*command).tap { |return_code| yield unless return_code }
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ module AppProfiler
3
+ module Yarn
4
+ module WithSpeedscope
5
+ include Command
6
+
7
+ def setup_yarn
8
+ super
9
+ # We currently only support this gem in the root Gemfile.
10
+ # See https://github.com/Shopify/app_profiler/issues/15
11
+ # for more information
12
+ yarn("add", "speedscope", "--dev", "--ignore-workspace-root-check") unless speedscope_added?
13
+ end
14
+
15
+ private
16
+
17
+ def speedscope_added?
18
+ AppProfiler.root.join("node_modules/speedscope").exist?
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/app_profiler.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support"
3
+ require "active_support/core_ext/class"
4
+ require "active_support/core_ext/module"
5
+ require "logger"
4
6
  require "app_profiler/version"
5
7
  require "app_profiler/railtie" if defined?(Rails::Railtie)
6
8
 
@@ -17,14 +19,15 @@ module AppProfiler
17
19
  module Viewer
18
20
  autoload :BaseViewer, "app_profiler/viewer/base_viewer"
19
21
  autoload :SpeedscopeViewer, "app_profiler/viewer/speedscope_viewer"
22
+ autoload :SpeedscopeRemoteViewer, "app_profiler/viewer/speedscope_remote_viewer"
20
23
  end
21
24
 
22
- autoload :Middleware, "app_profiler/middleware"
23
- autoload :RequestParameters, "app_profiler/request_parameters"
24
- autoload :Profiler, "app_profiler/profiler"
25
- autoload :Profile, "app_profiler/profile"
25
+ require "app_profiler/middleware"
26
+ require "app_profiler/request_parameters"
27
+ require "app_profiler/profiler"
28
+ require "app_profiler/profile"
26
29
 
27
- mattr_accessor :logger
30
+ mattr_accessor :logger, default: Logger.new($stdout)
28
31
  mattr_accessor :root
29
32
  mattr_accessor :profile_root
30
33
 
@@ -43,6 +46,15 @@ module AppProfiler
43
46
  Profiler.run(*args, &block)
44
47
  end
45
48
 
49
+ def start(*args)
50
+ Profiler.start(*args)
51
+ end
52
+
53
+ def stop
54
+ Profiler.stop
55
+ Profiler.results
56
+ end
57
+
46
58
  def profile_header=(profile_header)
47
59
  @@profile_header = profile_header # rubocop:disable Style/ClassVars
48
60
  @@request_profile_header = nil # rubocop:disable Style/ClassVars
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.0.6
4
+ version: 0.0.9
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: 2020-07-08 00:00:00.000000000 Z
16
+ date: 2022-03-02 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: activesupport
@@ -162,7 +162,12 @@ files:
162
162
  - lib/app_profiler/storage/google_cloud_storage.rb
163
163
  - lib/app_profiler/version.rb
164
164
  - lib/app_profiler/viewer/base_viewer.rb
165
+ - lib/app_profiler/viewer/speedscope_remote_viewer.rb
166
+ - lib/app_profiler/viewer/speedscope_remote_viewer/base_middleware.rb
167
+ - lib/app_profiler/viewer/speedscope_remote_viewer/middleware.rb
165
168
  - lib/app_profiler/viewer/speedscope_viewer.rb
169
+ - lib/app_profiler/yarn/command.rb
170
+ - lib/app_profiler/yarn/with_speedscope.rb
166
171
  homepage: https://github.com/Shopify/app_profiler
167
172
  licenses: []
168
173
  metadata:
@@ -182,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
182
187
  - !ruby/object:Gem::Version
183
188
  version: '0'
184
189
  requirements: []
185
- rubygems_version: 3.0.3
190
+ rubygems_version: 3.2.20
186
191
  signing_key:
187
192
  specification_version: 4
188
193
  summary: Collect performance profiles for your Rails application.