app_profiler 0.0.1
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 +7 -0
- data/lib/app_profiler.rb +61 -0
- data/lib/app_profiler/middleware.rb +58 -0
- data/lib/app_profiler/middleware/base_action.rb +18 -0
- data/lib/app_profiler/middleware/upload_action.rb +54 -0
- data/lib/app_profiler/middleware/view_action.rb +13 -0
- data/lib/app_profiler/profile.rb +76 -0
- data/lib/app_profiler/profiler.rb +86 -0
- data/lib/app_profiler/railtie.rb +46 -0
- data/lib/app_profiler/request_parameters.rb +90 -0
- data/lib/app_profiler/storage/base_storage.rb +14 -0
- data/lib/app_profiler/storage/file_storage.rb +23 -0
- data/lib/app_profiler/storage/google_cloud_storage.rb +65 -0
- data/lib/app_profiler/version.rb +5 -0
- data/lib/app_profiler/viewer/base_viewer.rb +11 -0
- data/lib/app_profiler/viewer/speedscope_viewer.rb +63 -0
- metadata +188 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e14252a4cfb042174369c9a1db2b36944b69e9ea3a1c5deecda4eb6875b88c66
|
4
|
+
data.tar.gz: 10c8227518e0cf32755479fc231df4d676c08bdd0052c347372fde5d39a81f73
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ad09ac71560705b5ccb548f9fab69c9c7ffa2d1183621ab7eb8faa51af73020b7efa3a9323db65b9a092f98e2e649d31641a92f5ac3d5433d3f8f2f298274b73
|
7
|
+
data.tar.gz: 95ddee7961e0a52829ea9bc31819c13b8bec3531f00a202cbd03532b11266f52c8c47c5171fed54c7bf55bbe19be4fb2664c08681d38f0229587166d2fa1d1ff
|
data/lib/app_profiler.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "app_profiler/version"
|
5
|
+
require "app_profiler/railtie" if defined?(Rails::Railtie)
|
6
|
+
|
7
|
+
module AppProfiler
|
8
|
+
class ConfigurationError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
module Storage
|
12
|
+
autoload :BaseStorage, "app_profiler/storage/base_storage"
|
13
|
+
autoload :FileStorage, "app_profiler/storage/file_storage"
|
14
|
+
autoload :GoogleCloudStorage, "app_profiler/storage/google_cloud_storage"
|
15
|
+
end
|
16
|
+
|
17
|
+
module Viewer
|
18
|
+
autoload :BaseViewer, "app_profiler/viewer/base_viewer"
|
19
|
+
autoload :SpeedscopeViewer, "app_profiler/viewer/speedscope_viewer"
|
20
|
+
end
|
21
|
+
|
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"
|
26
|
+
|
27
|
+
mattr_accessor :logger
|
28
|
+
mattr_accessor :root
|
29
|
+
mattr_accessor :profile_root
|
30
|
+
|
31
|
+
mattr_accessor :speedscope_host, default: "https://speedscope.app"
|
32
|
+
mattr_accessor :autoredirect, default: false
|
33
|
+
mattr_reader :profile_header, default: "X-Profile"
|
34
|
+
mattr_accessor :context, default: nil
|
35
|
+
|
36
|
+
mattr_accessor :storage, default: Storage::FileStorage
|
37
|
+
mattr_accessor :viewer, default: Viewer::SpeedscopeViewer
|
38
|
+
mattr_accessor :middleware, default: Middleware
|
39
|
+
|
40
|
+
class << self
|
41
|
+
def run(*args, &block)
|
42
|
+
Profiler.run(*args, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def profile_header=(profile_header)
|
46
|
+
@@profile_header = profile_header # rubocop:disable Style/ClassVars
|
47
|
+
@@request_profile_header = nil # rubocop:disable Style/ClassVars
|
48
|
+
@@profile_data_header = nil # rubocop:disable Style/ClassVars
|
49
|
+
end
|
50
|
+
|
51
|
+
def request_profile_header
|
52
|
+
@@request_profile_header ||= begin # rubocop:disable Style/ClassVars
|
53
|
+
profile_header.upcase.gsub("-", "_").prepend("HTTP_")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def profile_data_header
|
58
|
+
@@profile_data_header ||= profile_header.dup << "-Data" # rubocop:disable Style/ClassVars
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
require "app_profiler/middleware/base_action"
|
5
|
+
require "app_profiler/middleware/upload_action"
|
6
|
+
require "app_profiler/middleware/view_action"
|
7
|
+
|
8
|
+
module AppProfiler
|
9
|
+
class Middleware
|
10
|
+
class_attribute :action, default: UploadAction
|
11
|
+
class_attribute :disabled, default: false
|
12
|
+
|
13
|
+
def initialize(app)
|
14
|
+
@app = app
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
profile(env) do
|
19
|
+
@app.call(env)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def profile(env)
|
26
|
+
params = AppProfiler::RequestParameters.new(Rack::Request.new(env))
|
27
|
+
response = nil
|
28
|
+
|
29
|
+
return yield unless params.valid?
|
30
|
+
|
31
|
+
params_hash = params.to_h
|
32
|
+
|
33
|
+
return yield unless before_profile(env, params_hash)
|
34
|
+
|
35
|
+
profile = AppProfiler.run(params_hash) do
|
36
|
+
response = yield
|
37
|
+
end
|
38
|
+
|
39
|
+
return response unless profile && after_profile(env, profile)
|
40
|
+
|
41
|
+
action.call(
|
42
|
+
profile,
|
43
|
+
response: response,
|
44
|
+
autoredirect: params.autoredirect,
|
45
|
+
)
|
46
|
+
|
47
|
+
response
|
48
|
+
end
|
49
|
+
|
50
|
+
def before_profile(_env, _params)
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def after_profile(_env, _profile)
|
55
|
+
true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppProfiler
|
4
|
+
class Middleware
|
5
|
+
class BaseAction
|
6
|
+
class << self
|
7
|
+
def call(_profile, _params = {})
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
def cleanup
|
12
|
+
profile = Profiler.results
|
13
|
+
call(profile) if profile
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppProfiler
|
4
|
+
class Middleware
|
5
|
+
class UploadAction < BaseAction
|
6
|
+
class << self
|
7
|
+
def call(profile, response: nil, autoredirect: nil)
|
8
|
+
profile_upload = profile.upload
|
9
|
+
|
10
|
+
return unless response
|
11
|
+
|
12
|
+
append_headers(
|
13
|
+
response,
|
14
|
+
upload: profile_upload,
|
15
|
+
autoredirect: autoredirect.nil? ? AppProfiler.autoredirect : autoredirect
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def append_headers(response, upload:, autoredirect:)
|
22
|
+
return unless upload
|
23
|
+
|
24
|
+
response[1][profile_header] = profile_url(upload)
|
25
|
+
response[1][profile_data_header] = profile_data_url(upload)
|
26
|
+
|
27
|
+
return unless autoredirect
|
28
|
+
|
29
|
+
# Automatically redirect to profile if autoredirect is true.
|
30
|
+
if response[0].to_i < 500
|
31
|
+
response[1]["Location"] = profile_url(upload)
|
32
|
+
response[0] = 303
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def profile_url(upload)
|
37
|
+
"#{AppProfiler.speedscope_host}#profileURL=#{upload.url}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def profile_data_url(upload)
|
41
|
+
upload.url
|
42
|
+
end
|
43
|
+
|
44
|
+
def profile_header
|
45
|
+
AppProfiler.profile_header
|
46
|
+
end
|
47
|
+
|
48
|
+
def profile_data_header
|
49
|
+
AppProfiler.profile_data_header
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppProfiler
|
4
|
+
class Profile
|
5
|
+
INTERNAL_METADATA_KEYS = %i(id context)
|
6
|
+
private_constant :INTERNAL_METADATA_KEYS
|
7
|
+
|
8
|
+
delegate :[], to: :@data
|
9
|
+
attr_reader :id, :context
|
10
|
+
|
11
|
+
# This function should not be called if `StackProf.results` returns nil.
|
12
|
+
def self.from_stackprof(data)
|
13
|
+
options = INTERNAL_METADATA_KEYS.map { |key| [key, data[:metadata]&.delete(key)] }.to_h
|
14
|
+
|
15
|
+
new(data, **options).tap do |profile|
|
16
|
+
raise ArgumentError, "invalid profile data" unless profile.valid?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# `data` is assumed to be a Hash.
|
21
|
+
def initialize(data, id: nil, context: nil)
|
22
|
+
@id = id.presence || SecureRandom.hex
|
23
|
+
@context = context
|
24
|
+
@data = data
|
25
|
+
end
|
26
|
+
|
27
|
+
def valid?
|
28
|
+
mode.present?
|
29
|
+
end
|
30
|
+
|
31
|
+
def mode
|
32
|
+
@data[:mode]
|
33
|
+
end
|
34
|
+
|
35
|
+
def view
|
36
|
+
AppProfiler.viewer.view(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
def upload
|
40
|
+
AppProfiler.storage.upload(self).tap do |upload|
|
41
|
+
if upload && defined?(upload.url)
|
42
|
+
AppProfiler.logger.info("[Profiler] uploaded profile profile_url=#{upload.url}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
rescue => error
|
46
|
+
AppProfiler.logger.info(
|
47
|
+
"[Profiler] failed to upload profile error_class=#{error.class} error_message=#{error.message}"
|
48
|
+
)
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def file
|
53
|
+
@file ||= path.tap do |p|
|
54
|
+
p.dirname.mkpath
|
55
|
+
p.write(JSON.dump(@data))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_h
|
60
|
+
@data
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def path
|
66
|
+
filename = [
|
67
|
+
Time.zone.now.strftime("%Y%m%d-%H%M%S"),
|
68
|
+
mode,
|
69
|
+
id,
|
70
|
+
Socket.gethostname,
|
71
|
+
].compact.join("-") << ".json"
|
72
|
+
|
73
|
+
AppProfiler.profile_root.join(filename)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stackprof"
|
4
|
+
|
5
|
+
module AppProfiler
|
6
|
+
module Profiler
|
7
|
+
DEFAULTS = {
|
8
|
+
mode: :cpu,
|
9
|
+
raw: true,
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def run(params = {})
|
14
|
+
started = start(params)
|
15
|
+
|
16
|
+
yield
|
17
|
+
|
18
|
+
return unless started
|
19
|
+
|
20
|
+
stop
|
21
|
+
results
|
22
|
+
ensure
|
23
|
+
# Only stop the profiler if profiling was started in this context.
|
24
|
+
stop if started
|
25
|
+
end
|
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
|
+
def start(params = {})
|
43
|
+
# Do not start the profiler if StackProf was started somewhere else.
|
44
|
+
return false if running?
|
45
|
+
|
46
|
+
clear
|
47
|
+
|
48
|
+
StackProf.start(DEFAULTS.merge(params))
|
49
|
+
rescue => error
|
50
|
+
AppProfiler.logger.info(
|
51
|
+
"[Profiler] failed to start the profiler error_class=#{error.class} error_message=#{error.message}"
|
52
|
+
)
|
53
|
+
# This is a boolean instead of nil because StackProf#start returns a
|
54
|
+
# boolean as well.
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def stop
|
59
|
+
StackProf.stop
|
60
|
+
end
|
61
|
+
|
62
|
+
def stackprof_results
|
63
|
+
StackProf.results
|
64
|
+
end
|
65
|
+
|
66
|
+
# Clears the previous profiling session.
|
67
|
+
#
|
68
|
+
# StackProf will attempt to reuse frames from the previous profiling
|
69
|
+
# session if the results are not collected. This is usually called before
|
70
|
+
# StackProf#start is invoked to ensure that new profiling sessions do
|
71
|
+
# not reuse previous frames if they exist.
|
72
|
+
#
|
73
|
+
# Ref: https://github.com/tmm1/stackprof/blob/0ded6c/ext/stackprof/stackprof.c#L118-L123
|
74
|
+
#
|
75
|
+
def clear
|
76
|
+
stackprof_results
|
77
|
+
end
|
78
|
+
|
79
|
+
def running?
|
80
|
+
StackProf.running?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private_constant :Profiler
|
86
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
|
5
|
+
module AppProfiler
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
config.app_profiler = ActiveSupport::OrderedOptions.new
|
8
|
+
|
9
|
+
initializer "app_profiler.configs" do |app|
|
10
|
+
AppProfiler.logger = app.config.app_profiler.logger || Rails.logger
|
11
|
+
AppProfiler.root = app.config.app_profiler.root || Rails.root
|
12
|
+
AppProfiler.storage = app.config.app_profiler.storage || Storage::FileStorage
|
13
|
+
AppProfiler.viewer = app.config.app_profiler.viewer || Viewer::SpeedscopeViewer
|
14
|
+
AppProfiler.storage.bucket_name = app.config.app_profiler.storage_bucket_name || "profiles"
|
15
|
+
AppProfiler.storage.credentials = app.config.app_profiler.storage_credentials || {}
|
16
|
+
AppProfiler.middleware = app.config.app_profiler.middleware || Middleware
|
17
|
+
AppProfiler.middleware.action = app.config.app_profiler.middleware_action || default_middleware_action
|
18
|
+
AppProfiler.middleware.disabled = app.config.app_profiler.middleware_disabled || false
|
19
|
+
AppProfiler.autoredirect = app.config.app_profiler.autoredirect || false
|
20
|
+
AppProfiler.speedscope_host = app.config.app_profiler.speedscope_host || ENV.fetch(
|
21
|
+
"APP_PROFILER_SPEEDSCOPE_URL", "https://speedscope.app"
|
22
|
+
)
|
23
|
+
AppProfiler.profile_header = app.config.app_profiler.profile_header || "X-Profile"
|
24
|
+
AppProfiler.profile_root = app.config.app_profiler.profile_root || Rails.root.join(
|
25
|
+
"tmp", "app_profiler"
|
26
|
+
)
|
27
|
+
AppProfiler.context = app.config.app_profiler.context || Rails.env
|
28
|
+
end
|
29
|
+
|
30
|
+
initializer "app_profiler.add_middleware" do |app|
|
31
|
+
unless AppProfiler.middleware.disabled
|
32
|
+
app.middleware.insert_before(0, AppProfiler.middleware)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def default_middleware_action
|
39
|
+
if Rails.env.development?
|
40
|
+
Middleware::ViewAction
|
41
|
+
else
|
42
|
+
Middleware::UploadAction
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
|
5
|
+
module AppProfiler
|
6
|
+
class RequestParameters
|
7
|
+
DEFAULT_INTERVALS = { "cpu" => 1000, "wall" => 1000, "object" => 10000 }.freeze
|
8
|
+
MIN_INTERVALS = { "cpu" => 200, "wall" => 200, "object" => 10000 }.freeze
|
9
|
+
MODES = DEFAULT_INTERVALS.keys.freeze
|
10
|
+
|
11
|
+
def initialize(request)
|
12
|
+
@request = request
|
13
|
+
end
|
14
|
+
|
15
|
+
def autoredirect
|
16
|
+
query_param("autoredirect") || profile_header_param("autoredirect")
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid?
|
20
|
+
if mode.blank?
|
21
|
+
return false
|
22
|
+
end
|
23
|
+
|
24
|
+
unless MODES.include?(mode)
|
25
|
+
AppProfiler.logger.info("[Profiler] unsupported profiling mode=#{mode}")
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
|
29
|
+
if interval.to_i < MIN_INTERVALS[mode]
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_h
|
37
|
+
{
|
38
|
+
mode: mode.to_sym,
|
39
|
+
interval: interval.to_i,
|
40
|
+
metadata: {
|
41
|
+
id: request_id,
|
42
|
+
context: context,
|
43
|
+
},
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def mode
|
50
|
+
query_param("profile") || profile_header_param("mode")
|
51
|
+
end
|
52
|
+
|
53
|
+
def interval
|
54
|
+
query_param("interval") || profile_header_param("interval") || DEFAULT_INTERVALS[mode]
|
55
|
+
end
|
56
|
+
|
57
|
+
def request_id
|
58
|
+
header("HTTP_X_REQUEST_ID")
|
59
|
+
end
|
60
|
+
|
61
|
+
def context
|
62
|
+
profile_header_param("context").presence || AppProfiler.context
|
63
|
+
end
|
64
|
+
|
65
|
+
def profile_header_param(name)
|
66
|
+
query_parser.parse_nested_query(header(profile_header), ";")[name]
|
67
|
+
rescue Rack::QueryParser::ParameterTypeError, RangeError
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def query_param(name)
|
72
|
+
@request.GET[name]
|
73
|
+
rescue Rack::QueryParser::ParameterTypeError, RangeError
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def header(name)
|
78
|
+
return unless @request.has_header?(name)
|
79
|
+
@request.get_header(name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def query_parser
|
83
|
+
Rack::Utils.default_query_parser
|
84
|
+
end
|
85
|
+
|
86
|
+
def profile_header
|
87
|
+
AppProfiler.request_profile_header
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppProfiler
|
4
|
+
module Storage
|
5
|
+
class BaseStorage
|
6
|
+
class_attribute :bucket_name, default: "profiles"
|
7
|
+
class_attribute :credentials, default: {}
|
8
|
+
|
9
|
+
def self.upload(_profile)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppProfiler
|
4
|
+
module Storage
|
5
|
+
class FileStorage < BaseStorage
|
6
|
+
class Location
|
7
|
+
def initialize(file)
|
8
|
+
@file = file
|
9
|
+
end
|
10
|
+
|
11
|
+
def url
|
12
|
+
@file
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def upload(profile)
|
18
|
+
Location.new(profile.file)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem("google-cloud-storage", "~> 1.21")
|
4
|
+
|
5
|
+
require "google/cloud/storage"
|
6
|
+
require "active_support/notifications"
|
7
|
+
require "zlib"
|
8
|
+
|
9
|
+
module AppProfiler
|
10
|
+
module Storage
|
11
|
+
class GoogleCloudStorage < BaseStorage
|
12
|
+
GOOGLE_SCOPE = "https://www.googleapis.com/auth/devstorage.read_write"
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def upload(profile, _params = {})
|
16
|
+
file = profile.file.open
|
17
|
+
|
18
|
+
ActiveSupport::Notifications.instrument(
|
19
|
+
"gcs_upload.app_profiler",
|
20
|
+
file_size: file.size,
|
21
|
+
) do
|
22
|
+
bucket.create_file(
|
23
|
+
StringIO.new(gzipped_reader(file).read),
|
24
|
+
gcs_filename(profile),
|
25
|
+
content_type: "application/json",
|
26
|
+
content_encoding: "gzip",
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def gcs_filename(profile)
|
34
|
+
File.join(profile.context.to_s, profile.file.basename)
|
35
|
+
end
|
36
|
+
|
37
|
+
def bucket
|
38
|
+
# The GCS gem requires the `storage.buckets.get`
|
39
|
+
# permission to retrieve bucket details. We will set `skip_lookup` to
|
40
|
+
# be true to skip this process.
|
41
|
+
@bucket ||= Google::Cloud::Storage.new(
|
42
|
+
credentials: credentials.presence,
|
43
|
+
scope: GOOGLE_SCOPE,
|
44
|
+
retries: 3,
|
45
|
+
).bucket(bucket_name, skip_lookup: true)
|
46
|
+
end
|
47
|
+
|
48
|
+
# We could compress everything at once using `ActiveSupport::Gzip.compress`.
|
49
|
+
# Since we expect large files for profiles generated by stackprof,
|
50
|
+
# compressing in chunks avoids keeping all of them in memory.
|
51
|
+
def gzipped_reader(file)
|
52
|
+
reader, writer = IO.pipe(binmode: true)
|
53
|
+
Thread.new do
|
54
|
+
writer.set_encoding("binary")
|
55
|
+
gz = Zlib::GzipWriter.new(writer)
|
56
|
+
# Default chunk size for gzip is 16384.
|
57
|
+
gz.write(file.read(16384)) until file.eof?
|
58
|
+
gz.close
|
59
|
+
end
|
60
|
+
reader
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppProfiler
|
4
|
+
module Viewer
|
5
|
+
class SpeedscopeViewer < BaseViewer
|
6
|
+
mattr_accessor :yarn_setup, default: false
|
7
|
+
|
8
|
+
class YarnError < StandardError; end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def view(profile)
|
12
|
+
new(profile).view
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(profile)
|
17
|
+
@profile = profile
|
18
|
+
end
|
19
|
+
|
20
|
+
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
|
+
yarn("add --dev speedscope")
|
37
|
+
end
|
38
|
+
|
39
|
+
def ensure_yarn_installed
|
40
|
+
exec("which yarn > /dev/null") do
|
41
|
+
raise(
|
42
|
+
YarnError,
|
43
|
+
<<~MSG.squish
|
44
|
+
`yarn` command not found.
|
45
|
+
Please install `yarn` or make it available in PATH.
|
46
|
+
MSG
|
47
|
+
)
|
48
|
+
end
|
49
|
+
self.yarn_setup = true
|
50
|
+
end
|
51
|
+
|
52
|
+
def package_json_exists?
|
53
|
+
AppProfiler.root.join("package.json").exist?
|
54
|
+
end
|
55
|
+
|
56
|
+
def exec(command)
|
57
|
+
system(command).tap do |return_code|
|
58
|
+
yield unless return_code
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
metadata
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: app_profiler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gannon McGibbon
|
8
|
+
- Jay Ching Lim
|
9
|
+
- João Júnior
|
10
|
+
- Jon Simpson
|
11
|
+
- Kevin Jalbert
|
12
|
+
- Scott Francis
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
date: 2020-02-19 00:00:00.000000000 Z
|
17
|
+
dependencies:
|
18
|
+
- !ruby/object:Gem::Dependency
|
19
|
+
name: activesupport
|
20
|
+
requirement: !ruby/object:Gem::Requirement
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: '5.2'
|
25
|
+
type: :runtime
|
26
|
+
prerelease: false
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: '5.2'
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: railties
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '5.2'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '5.2'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rack
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
type: :runtime
|
54
|
+
prerelease: false
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: stackprof
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0.2'
|
67
|
+
type: :runtime
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0.2'
|
74
|
+
- !ruby/object:Gem::Dependency
|
75
|
+
name: bundler
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
type: :development
|
82
|
+
prerelease: false
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
- !ruby/object:Gem::Dependency
|
89
|
+
name: rake
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
type: :development
|
96
|
+
prerelease: false
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
- !ruby/object:Gem::Dependency
|
103
|
+
name: minitest
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
type: :development
|
110
|
+
prerelease: false
|
111
|
+
version_requirements: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
- !ruby/object:Gem::Dependency
|
117
|
+
name: mocha
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
type: :development
|
124
|
+
prerelease: false
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
- !ruby/object:Gem::Dependency
|
131
|
+
name: minitest-stub-const
|
132
|
+
requirement: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - '='
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0.6'
|
137
|
+
type: :development
|
138
|
+
prerelease: false
|
139
|
+
version_requirements: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - '='
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0.6'
|
144
|
+
description:
|
145
|
+
email:
|
146
|
+
- gems@shopify.com
|
147
|
+
executables: []
|
148
|
+
extensions: []
|
149
|
+
extra_rdoc_files: []
|
150
|
+
files:
|
151
|
+
- lib/app_profiler.rb
|
152
|
+
- lib/app_profiler/middleware.rb
|
153
|
+
- lib/app_profiler/middleware/base_action.rb
|
154
|
+
- lib/app_profiler/middleware/upload_action.rb
|
155
|
+
- lib/app_profiler/middleware/view_action.rb
|
156
|
+
- lib/app_profiler/profile.rb
|
157
|
+
- lib/app_profiler/profiler.rb
|
158
|
+
- lib/app_profiler/railtie.rb
|
159
|
+
- lib/app_profiler/request_parameters.rb
|
160
|
+
- lib/app_profiler/storage/base_storage.rb
|
161
|
+
- lib/app_profiler/storage/file_storage.rb
|
162
|
+
- lib/app_profiler/storage/google_cloud_storage.rb
|
163
|
+
- lib/app_profiler/version.rb
|
164
|
+
- lib/app_profiler/viewer/base_viewer.rb
|
165
|
+
- lib/app_profiler/viewer/speedscope_viewer.rb
|
166
|
+
homepage: https://github.com/Shopify/app_profiler
|
167
|
+
licenses: []
|
168
|
+
metadata: {}
|
169
|
+
post_install_message:
|
170
|
+
rdoc_options: []
|
171
|
+
require_paths:
|
172
|
+
- lib
|
173
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
174
|
+
requirements:
|
175
|
+
- - ">="
|
176
|
+
- !ruby/object:Gem::Version
|
177
|
+
version: '0'
|
178
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
|
+
requirements:
|
180
|
+
- - ">="
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: '0'
|
183
|
+
requirements: []
|
184
|
+
rubygems_version: 3.0.3
|
185
|
+
signing_key:
|
186
|
+
specification_version: 4
|
187
|
+
summary: Collect performance profiles for your Rails application.
|
188
|
+
test_files: []
|