app_profiler 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ class Middleware
5
+ class ViewAction < BaseAction
6
+ class << self
7
+ def call(profile, _params = {})
8
+ profile.view
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppProfiler
4
+ module Viewer
5
+ class BaseViewer
6
+ def self.view(_profile)
7
+ raise NotImplementedError
8
+ end
9
+ end
10
+ end
11
+ 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: []