app_profiler 0.2.5 → 0.2.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/app_profiler/{profile.rb → base_profile.rb} +10 -12
- data/lib/app_profiler/exec.rb +37 -0
- data/lib/app_profiler/middleware.rb +0 -1
- data/lib/app_profiler/railtie.rb +10 -4
- data/lib/app_profiler/sampler/config.rb +2 -1
- data/lib/app_profiler/sampler.rb +1 -0
- data/lib/app_profiler/{profile/stackprof.rb → stackprof_profile.rb} +2 -2
- data/lib/app_profiler/storage/google_cloud_storage.rb +5 -9
- data/lib/app_profiler/{profile/vernier.rb → vernier_profile.rb} +2 -2
- data/lib/app_profiler/version.rb +1 -1
- data/lib/app_profiler/viewer/base_middleware.rb +141 -0
- data/lib/app_profiler/viewer/base_viewer.rb +3 -3
- data/lib/app_profiler/viewer/firefox_remote_viewer/middleware.rb +66 -0
- data/lib/app_profiler/viewer/firefox_remote_viewer.rb +33 -0
- data/lib/app_profiler/viewer/firefox_viewer.rb +79 -0
- data/lib/app_profiler/viewer/speedscope_remote_viewer/middleware.rb +11 -20
- data/lib/app_profiler/viewer/speedscope_remote_viewer.rb +12 -4
- data/lib/app_profiler/viewer/speedscope_viewer.rb +0 -6
- data/lib/app_profiler/yarn/command.rb +18 -24
- data/lib/app_profiler/yarn/with_firefox_profiler.rb +81 -0
- data/lib/app_profiler.rb +73 -41
- metadata +12 -11
- data/lib/app_profiler/viewer/speedscope_remote_viewer/base_middleware.rb +0 -142
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1d088d71f0d625a8627a17aa418840597b1d26e01bc05a93848440ce710f461a
|
4
|
+
data.tar.gz: 5f65cc77daaeaacbeed54cc913c08e89fddbc30e64458e1538c2cce2f5e9b381
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
data/lib/app_profiler/railtie.rb
CHANGED
@@ -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
|
-
|
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
|
53
|
-
app.middleware.insert_before(0,
|
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
|
-
|
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
|
data/lib/app_profiler/sampler.rb
CHANGED
@@ -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.
|
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
|
-
|
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
|
70
|
-
@process_queue_thread
|
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 = ".
|
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
|
-
|
20
|
+
AppProfiler.vernier_viewer.view(self, **params)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
data/lib/app_profiler/version.rb
CHANGED
@@ -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
|
@@ -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
|
-
|
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 "
|
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
|
11
|
-
|
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")
|
@@ -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
|
-
|
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
|
-
|
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
|
41
|
-
|
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
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
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
|
-
|
73
|
-
|
74
|
-
self.backend = 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
|
-
|
91
|
+
self.backend = original_backend if backend
|
84
92
|
end
|
85
93
|
|
86
|
-
def start(*args)
|
87
|
-
|
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
|
-
@
|
105
|
+
@profiler&.running?
|
97
106
|
end
|
98
107
|
|
99
108
|
def profiler
|
100
|
-
|
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 ==
|
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
|
-
|
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
|
-
|
214
|
-
@
|
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.
|
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:
|
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.
|
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
|