app_profiler 0.2.4 → 0.2.6
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 +4 -4
- data/lib/app_profiler/backend/base_backend.rb +1 -1
- data/lib/app_profiler/{profile.rb → base_profile.rb} +1 -7
- data/lib/app_profiler/exec.rb +37 -0
- data/lib/app_profiler/middleware.rb +0 -1
- data/lib/app_profiler/railtie.rb +9 -4
- data/lib/app_profiler/sampler/config.rb +7 -4
- data/lib/app_profiler/sampler.rb +8 -0
- data/lib/app_profiler/{profile/stackprof.rb → stackprof_profile.rb} +6 -2
- data/lib/app_profiler/storage/google_cloud_storage.rb +5 -9
- data/lib/app_profiler/{profile/vernier.rb → vernier_profile.rb} +6 -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 +66 -40
- 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: b5be705606d138c4e042f4d87820e97413be62f3eeeb92cbb34d5e02aac600ea
|
4
|
+
data.tar.gz: d15e16f16c53f939cb874ab841b60528f4b46b5359e755ba5ce6acfe01265b3e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ccbe4b8fb2b5a4a1882fb7e33e03f35b7f91afa2a8eeb836e705bae104131e05b1d48d6c7d118cd1c69665e9736c2d9d621a1b05626ba112d63299615de609b1
|
7
|
+
data.tar.gz: ff0cb2c2cced5f0edd575d9b6ddb2d6cf83021225d528c4f7e194ed527360bab09a511864e6e5ed047e2e84ddbab737a7060c39b9931ac8fbc976a4489aa128e
|
@@ -40,7 +40,7 @@ module AppProfiler
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def release_run_lock
|
43
|
-
self.class.run_lock.unlock
|
43
|
+
self.class.run_lock.unlock if self.class.run_lock.locked?
|
44
44
|
rescue ThreadError
|
45
45
|
AppProfiler.logger.warn("[AppProfiler] run lock not released as it was never acquired")
|
46
46
|
end
|
@@ -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
|
@@ -81,7 +78,7 @@ module AppProfiler
|
|
81
78
|
end
|
82
79
|
|
83
80
|
def metadata
|
84
|
-
|
81
|
+
raise NotImplementedError
|
85
82
|
end
|
86
83
|
|
87
84
|
def mode
|
@@ -111,7 +108,4 @@ module AppProfiler
|
|
111
108
|
AppProfiler.profile_root.join(filename)
|
112
109
|
end
|
113
110
|
end
|
114
|
-
|
115
|
-
include ActiveSupport::Deprecation::DeprecatedConstantAccessor
|
116
|
-
deprecate_constant "Profile", "AppProfiler::BaseProfile", deprecator: ActiveSupport::Deprecation.new
|
117
111
|
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
|
@@ -40,7 +45,7 @@ module AppProfiler
|
|
40
45
|
AppProfiler.profile_enqueue_success = app.config.app_profiler.profile_enqueue_success
|
41
46
|
AppProfiler.profile_enqueue_failure = app.config.app_profiler.profile_enqueue_failure
|
42
47
|
AppProfiler.after_process_queue = app.config.app_profiler.after_process_queue
|
43
|
-
AppProfiler.backend = app.config.app_profiler.profiler_backend || :stackprof
|
48
|
+
AppProfiler.backend = app.config.app_profiler.profiler_backend || :stackprof unless AppProfiler.running?
|
44
49
|
AppProfiler.forward_metadata_on_upload = app.config.app_profiler.forward_metadata_on_upload || false
|
45
50
|
AppProfiler.profile_sampler_enabled = app.config.app_profiler.profile_sampler_enabled || false
|
46
51
|
AppProfiler.profile_sampler_config = app.config.app_profiler.profile_sampler_config ||
|
@@ -49,8 +54,8 @@ module AppProfiler
|
|
49
54
|
|
50
55
|
initializer "app_profiler.add_middleware" do |app|
|
51
56
|
unless AppProfiler.middleware.disabled
|
52
|
-
if
|
53
|
-
app.middleware.insert_before(0,
|
57
|
+
if (Rails.env.development? || Rails.env.test?) && AppProfiler.stackprof_viewer.remote?
|
58
|
+
app.middleware.insert_before(0, AppProfiler.viewer::Middleware)
|
54
59
|
end
|
55
60
|
app.middleware.insert_before(0, AppProfiler.middleware)
|
56
61
|
end
|
@@ -2,15 +2,16 @@
|
|
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
|
8
|
-
attr_reader :sample_rate, :targets, :cpu_interval, :backends_probability
|
9
|
+
attr_reader :sample_rate, :targets, :exclude_targets, :cpu_interval, :backends_probability
|
9
10
|
|
10
11
|
SAMPLE_RATE = 0.001 # 0.1%
|
11
12
|
TARGETS = ["/"]
|
12
13
|
BACKEND_PROBABILITES = { stackprof: 1.0, vernier: 0.0 }
|
13
|
-
|
14
|
+
EMPTY_ARRAY = []
|
14
15
|
|
15
16
|
def initialize(sample_rate: SAMPLE_RATE,
|
16
17
|
targets: TARGETS,
|
@@ -18,7 +19,8 @@ module AppProfiler
|
|
18
19
|
backends_config: {
|
19
20
|
stackprof: StackprofConfig.new,
|
20
21
|
},
|
21
|
-
paths: nil
|
22
|
+
paths: nil,
|
23
|
+
exclude_targets: EMPTY_ARRAY)
|
22
24
|
|
23
25
|
if sample_rate < 0.0 || sample_rate > 1.0
|
24
26
|
raise ArgumentError, "sample_rate must be between 0 and 1"
|
@@ -26,12 +28,13 @@ module AppProfiler
|
|
26
28
|
|
27
29
|
raise ArgumentError, "mode probabilities must sum to 1" unless backends_probability.values.sum == 1.0
|
28
30
|
|
29
|
-
|
31
|
+
AppProfiler.deprecator.warn("passing paths is deprecated, use targets instead") if paths
|
30
32
|
|
31
33
|
@sample_rate = sample_rate
|
32
34
|
@targets = paths || targets
|
33
35
|
@backends_config = backends_config
|
34
36
|
@backends_probability = backends_probability
|
37
|
+
@exclude_targets = exclude_targets
|
35
38
|
end
|
36
39
|
|
37
40
|
def get_backend_config(backend_name)
|
data/lib/app_profiler/sampler.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "app_profiler/sampler/config"
|
4
|
+
|
4
5
|
module AppProfiler
|
5
6
|
module Sampler
|
7
|
+
@excluded_cache = {}
|
6
8
|
class << self
|
7
9
|
def profile_params(request, config)
|
8
10
|
profile_params_for(request.path, config)
|
@@ -18,6 +20,12 @@ module AppProfiler
|
|
18
20
|
|
19
21
|
def sample?(config, target)
|
20
22
|
return false if Kernel.rand > config.sample_rate
|
23
|
+
return false if @excluded_cache[target]
|
24
|
+
|
25
|
+
if config.exclude_targets.any? { |t| target.match?(t) }
|
26
|
+
@excluded_cache[target] = true
|
27
|
+
return false
|
28
|
+
end
|
21
29
|
|
22
30
|
return false unless config.targets.any? { |t| target.match?(t) }
|
23
31
|
|
@@ -2,18 +2,22 @@
|
|
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]
|
9
9
|
end
|
10
10
|
|
11
|
+
def metadata
|
12
|
+
@data[:metadata]
|
13
|
+
end
|
14
|
+
|
11
15
|
def format
|
12
16
|
FILE_EXTENSION
|
13
17
|
end
|
14
18
|
|
15
19
|
def view(params = {})
|
16
|
-
AppProfiler.
|
20
|
+
AppProfiler.stackprof_viewer.view(self, **params)
|
17
21
|
end
|
18
22
|
end
|
19
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,18 +2,22 @@
|
|
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]
|
9
9
|
end
|
10
10
|
|
11
|
+
def metadata
|
12
|
+
@data[:meta]
|
13
|
+
end
|
14
|
+
|
11
15
|
def format
|
12
16
|
FILE_EXTENSION
|
13
17
|
end
|
14
18
|
|
15
19
|
def view(params = {})
|
16
|
-
|
20
|
+
AppProfiler.vernier_viewer.view(self, **params)
|
17
21
|
end
|
18
22
|
end
|
19
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
|
@@ -68,55 +72,63 @@ module AppProfiler
|
|
68
72
|
mattr_accessor :profile_sampler_config
|
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
|
+
|
120
132
|
def profile_sampler_enabled=(value)
|
121
133
|
if value.is_a?(Proc)
|
122
134
|
raise ArgumentError,
|
@@ -141,22 +153,21 @@ module AppProfiler
|
|
141
153
|
|
142
154
|
def backend_for(backend_name)
|
143
155
|
if vernier_supported? &&
|
144
|
-
backend_name == AppProfiler::Backend::VernierBackend.name
|
156
|
+
backend_name&.to_sym == AppProfiler::Backend::VernierBackend.name
|
145
157
|
AppProfiler::Backend::VernierBackend
|
146
|
-
elsif backend_name == AppProfiler::Backend::StackprofBackend.name
|
158
|
+
elsif backend_name&.to_sym == AppProfiler::Backend::StackprofBackend.name
|
147
159
|
AppProfiler::Backend::StackprofBackend
|
148
160
|
else
|
149
|
-
raise BackendError, "unknown backend #{backend_name}"
|
161
|
+
raise BackendError, "unknown backend #{backend_name.inspect}"
|
150
162
|
end
|
151
163
|
end
|
152
164
|
|
153
165
|
def backend
|
154
|
-
|
155
|
-
@profiler_backend.name
|
166
|
+
profiler_backend.name
|
156
167
|
end
|
157
168
|
|
158
169
|
def vernier_supported?
|
159
|
-
|
170
|
+
RUBY_VERSION >= "3.2.1"
|
160
171
|
end
|
161
172
|
|
162
173
|
def profile_header=(profile_header)
|
@@ -207,11 +218,26 @@ module AppProfiler
|
|
207
218
|
AppProfiler.profile_url_formatter.call(upload)
|
208
219
|
end
|
209
220
|
|
221
|
+
def viewer
|
222
|
+
deprecator.warn("AppProfiler.viewer is deprecated, please use stackprof_viewer instead.")
|
223
|
+
stackprof_viewer
|
224
|
+
end
|
225
|
+
|
226
|
+
def viewer=(viewer)
|
227
|
+
deprecator.warn("AppProfiler.viewer= is deprecated, please use stackprof_viewer= instead.")
|
228
|
+
self.stackprof_viewer = viewer
|
229
|
+
end
|
230
|
+
|
210
231
|
private
|
211
232
|
|
233
|
+
def profiler_backend
|
234
|
+
@profiler_backend ||= Backend::StackprofBackend
|
235
|
+
end
|
236
|
+
|
212
237
|
def clear
|
213
|
-
|
214
|
-
@
|
238
|
+
profiler.stop if running?
|
239
|
+
@profiler = nil
|
240
|
+
@profiler_backend = nil
|
215
241
|
end
|
216
242
|
end
|
217
243
|
|
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.6
|
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-22 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.2
|
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
|