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.
@@ -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: []