rails_mini_profiler 0.1.0

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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +220 -0
  3. data/Rakefile +18 -0
  4. data/app/assets/config/rails_mini_profiler_manifest.js +1 -0
  5. data/app/assets/images/rails_mini_profiler/bookmark.svg +10 -0
  6. data/app/assets/images/rails_mini_profiler/chart.svg +12 -0
  7. data/app/assets/images/rails_mini_profiler/delete.svg +9 -0
  8. data/app/assets/images/rails_mini_profiler/graph.svg +11 -0
  9. data/app/assets/images/rails_mini_profiler/logo.svg +18 -0
  10. data/app/assets/images/rails_mini_profiler/logo_variant.svg +32 -0
  11. data/app/assets/images/rails_mini_profiler/search.svg +10 -0
  12. data/app/assets/images/rails_mini_profiler/setting.svg +10 -0
  13. data/app/assets/images/rails_mini_profiler/show.svg +11 -0
  14. data/app/assets/javascripts/rails_mini_profiler.js +90 -0
  15. data/app/assets/stylesheets/rails_mini_profiler/application.css +164 -0
  16. data/app/assets/stylesheets/rails_mini_profiler/flamegraph.css +14 -0
  17. data/app/assets/stylesheets/rails_mini_profiler/flashes.css +17 -0
  18. data/app/assets/stylesheets/rails_mini_profiler/navbar.css +50 -0
  19. data/app/assets/stylesheets/rails_mini_profiler/profiled_requests.css +180 -0
  20. data/app/assets/stylesheets/rails_mini_profiler/traces.css +87 -0
  21. data/app/controllers/rails_mini_profiler/application_controller.rb +28 -0
  22. data/app/controllers/rails_mini_profiler/flamegraphs_controller.rb +23 -0
  23. data/app/controllers/rails_mini_profiler/profiled_requests_controller.rb +55 -0
  24. data/app/helpers/rails_mini_profiler/application_helper.rb +12 -0
  25. data/app/helpers/rails_mini_profiler/profiled_requests_helper.rb +16 -0
  26. data/app/models/rails_mini_profiler/application_record.rb +17 -0
  27. data/app/models/rails_mini_profiler/controller_trace.rb +33 -0
  28. data/app/models/rails_mini_profiler/flamegraph.rb +33 -0
  29. data/app/models/rails_mini_profiler/instantiation_trace.rb +33 -0
  30. data/app/models/rails_mini_profiler/profiled_request.rb +59 -0
  31. data/app/models/rails_mini_profiler/render_partial_trace.rb +33 -0
  32. data/app/models/rails_mini_profiler/render_template_trace.rb +33 -0
  33. data/app/models/rails_mini_profiler/rmp_trace.rb +31 -0
  34. data/app/models/rails_mini_profiler/sequel_trace.rb +33 -0
  35. data/app/models/rails_mini_profiler/trace.rb +42 -0
  36. data/app/presenters/rails_mini_profiler/base_presenter.rb +25 -0
  37. data/app/presenters/rails_mini_profiler/controller_trace_presenter.rb +18 -0
  38. data/app/presenters/rails_mini_profiler/instantiation_trace_presenter.rb +14 -0
  39. data/app/presenters/rails_mini_profiler/profiled_request_presenter.rb +45 -0
  40. data/app/presenters/rails_mini_profiler/render_partial_trace_presenter.rb +11 -0
  41. data/app/presenters/rails_mini_profiler/render_template_trace_presenter.rb +15 -0
  42. data/app/presenters/rails_mini_profiler/rmp_trace_presenter.rb +9 -0
  43. data/app/presenters/rails_mini_profiler/sequel_trace_presenter.rb +69 -0
  44. data/app/presenters/rails_mini_profiler/trace_presenter.rb +61 -0
  45. data/app/views/layouts/rails_mini_profiler/application.html.erb +26 -0
  46. data/app/views/layouts/rails_mini_profiler/flamegraph.html.erb +18 -0
  47. data/app/views/rails_mini_profiler/badge.html.erb +37 -0
  48. data/app/views/rails_mini_profiler/flamegraphs/show.html.erb +13 -0
  49. data/app/views/rails_mini_profiler/profiled_requests/index.html.erb +59 -0
  50. data/app/views/rails_mini_profiler/profiled_requests/shared/_trace.html.erb +40 -0
  51. data/app/views/rails_mini_profiler/profiled_requests/show.html.erb +40 -0
  52. data/app/views/rails_mini_profiler/shared/_flashes.html.erb +8 -0
  53. data/app/views/rails_mini_profiler/shared/_navbar.html.erb +15 -0
  54. data/config/routes.rb +11 -0
  55. data/db/migrate/20210621185018_create_rmp.rb +44 -0
  56. data/lib/generators/rails_mini_profiler/USAGE +2 -0
  57. data/lib/generators/rails_mini_profiler/install_generator.rb +16 -0
  58. data/lib/generators/rails_mini_profiler/templates/rails_mini_profiler.rb.erb +13 -0
  59. data/lib/rails_mini_profiler.rb +55 -0
  60. data/lib/rails_mini_profiler/badge.rb +62 -0
  61. data/lib/rails_mini_profiler/configuration.rb +41 -0
  62. data/lib/rails_mini_profiler/engine.rb +23 -0
  63. data/lib/rails_mini_profiler/errors.rb +8 -0
  64. data/lib/rails_mini_profiler/flamegraph_guard.rb +47 -0
  65. data/lib/rails_mini_profiler/guard.rb +46 -0
  66. data/lib/rails_mini_profiler/logger.rb +20 -0
  67. data/lib/rails_mini_profiler/middleware.rb +74 -0
  68. data/lib/rails_mini_profiler/models/base_model.rb +18 -0
  69. data/lib/rails_mini_profiler/models/trace.rb +9 -0
  70. data/lib/rails_mini_profiler/redirect.rb +25 -0
  71. data/lib/rails_mini_profiler/request_context.rb +62 -0
  72. data/lib/rails_mini_profiler/request_wrapper.rb +33 -0
  73. data/lib/rails_mini_profiler/response_wrapper.rb +32 -0
  74. data/lib/rails_mini_profiler/storage.rb +29 -0
  75. data/lib/rails_mini_profiler/tracers.rb +85 -0
  76. data/lib/rails_mini_profiler/user.rb +40 -0
  77. data/lib/rails_mini_profiler/version.rb +5 -0
  78. data/lib/tasks/rails_mini_profiler_tasks.rake +8 -0
  79. metadata +151 -0
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMiniProfiler
4
+ class Guard
5
+ def initialize(request_context, configuration: RailsMiniProfiler.configuration)
6
+ @request_context = request_context
7
+ @request = request_context.request
8
+ @configuration = configuration
9
+ end
10
+
11
+ def profile?
12
+ return false unless enabled?
13
+
14
+ return false if ignored_path?
15
+
16
+ true
17
+ end
18
+
19
+ def store?
20
+ return false unless @request_context.user
21
+
22
+ true
23
+ end
24
+
25
+ private
26
+
27
+ def ignored_path?
28
+ return true if /#{Engine.routes.find_script_name({})}/.match?(@request.path)
29
+
30
+ return true if /assets/.match?(@request.path)
31
+
32
+ ignored_paths = @configuration.skip_paths
33
+
34
+ return true if Regexp.union(ignored_paths).match?(@request.path)
35
+
36
+ false
37
+ end
38
+
39
+ def enabled?
40
+ enabled = @configuration.enabled
41
+ return enabled unless enabled.respond_to?(:call)
42
+
43
+ enabled.call(@request.env)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMiniProfiler
4
+ module Logger
5
+ def self.new(logger)
6
+ logger = logger.dup
7
+
8
+ logger.formatter = logger.formatter ? logger.formatter.dup : ActiveSupport::Logger::SimpleFormatter.new
9
+
10
+ logger.formatter.extend Formatter
11
+ logger.extend(self)
12
+ end
13
+
14
+ module Formatter
15
+ def call(severity, timestamp, progname, msg)
16
+ super(severity, timestamp, progname, "[RailsMiniProfiler] #{msg}")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMiniProfiler
4
+ class Middleware
5
+ def initialize(app)
6
+ @app = app
7
+ @config = RailsMiniProfiler.configuration
8
+ Tracers.setup! { |trace| track_trace(trace) }
9
+ end
10
+
11
+ def call(env)
12
+ request = RequestWrapper.new(env)
13
+ request_context = RequestContext.new(request)
14
+ return @app.call(env) unless Guard.new(request_context).profile?
15
+
16
+ request_context.profiled_request = ProfiledRequest.new
17
+ result = with_tracing(request_context) { profile(request_context) }
18
+ return result unless request_context.authorized?
19
+
20
+ request_context.response = ResponseWrapper.new(*result)
21
+ complete!(request_context)
22
+ request_context.saved? ? render_response(request_context) : result
23
+ end
24
+
25
+ def traces
26
+ Thread.current[:rails_mini_profiler_traces]
27
+ end
28
+
29
+ def traces=(traces)
30
+ Thread.current[:rails_mini_profiler_traces] = traces
31
+ end
32
+
33
+ def track_trace(event)
34
+ return if traces.nil?
35
+
36
+ trace = Tracers.build_trace(event)
37
+ traces.append(trace)
38
+ end
39
+
40
+ private
41
+
42
+ def complete!(request_context)
43
+ request_context.complete_profiling!
44
+ request_context.save_results!
45
+ true
46
+ rescue ActiveRecord::ActiveRecordError => e
47
+ RailsMiniProfiler.logger.error("Could not save profile: #{e}")
48
+ false
49
+ end
50
+
51
+ def render_response(request_context)
52
+ redirect = Redirect.new(request_context).render
53
+ return redirect if redirect
54
+
55
+ modified_response = Badge.new(request_context).render
56
+ [modified_response.status, modified_response.headers, modified_response.response]
57
+ end
58
+
59
+ def profile(request_context)
60
+ ActiveSupport::Notifications.instrument('rails_mini_profiler.total_time') do
61
+ request = request_context.request
62
+ FlamegraphGuard.new(request_context).record { @app.call(request.env) }
63
+ end
64
+ end
65
+
66
+ def with_tracing(request_context)
67
+ self.traces = []
68
+ result = yield
69
+ request_context.traces = traces
70
+ self.traces = nil
71
+ result
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMiniProfiler
4
+ module Models
5
+ class BaseModel
6
+ include ActiveModel::Model
7
+
8
+ def initialize(*_args, **attributes)
9
+ super(attributes)
10
+ end
11
+
12
+ def to_h
13
+ instance_variables
14
+ .each_with_object({}) { |var, hash| hash[var.to_s.delete('@')] = instance_variable_get(var) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMiniProfiler
4
+ module Models
5
+ class Trace < BaseModel
6
+ attr_accessor :id, :name, :start, :finish, :duration, :payload, :backtrace, :allocations, :created_at, :updated_at
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMiniProfiler
4
+ class Redirect
5
+ include Engine.routes.url_helpers
6
+
7
+ def initialize(request_context)
8
+ @request = request_context.request
9
+ @profiled_request = request_context.profiled_request
10
+ end
11
+
12
+ def render
13
+ params = CGI.parse(@request.query_string).transform_values(&:first).with_indifferent_access
14
+ return redirect_to(flamegraph_path(@profiled_request.id)) if params[:rmp_flamegraph]
15
+
16
+ false
17
+ end
18
+
19
+ private
20
+
21
+ def redirect_to(location)
22
+ [302, { 'Location' => location, 'Content-Type' => 'text/html' }, ['Moved Temporarily']]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMiniProfiler
4
+ class RequestContext
5
+ attr_reader :request
6
+
7
+ attr_accessor :response, :profiled_request, :traces, :flamegraph
8
+
9
+ def initialize(request)
10
+ @request = request
11
+ @env = request.env
12
+ @saved = false
13
+ @complete = false
14
+ end
15
+
16
+ def user_id
17
+ @user_id ||= User.current_user
18
+ end
19
+
20
+ def authorized?
21
+ @authorized ||= User.get(@env).present?
22
+ end
23
+
24
+ def complete_profiling!
25
+ profiled_request.user_id = user_id
26
+ profiled_request.request = @request
27
+ profiled_request.response = @response
28
+ total_time = traces.find { |trace| trace.name == 'rails_mini_profiler.total_time' }
29
+ profiled_request.total_time = total_time
30
+ @complete = true
31
+ end
32
+
33
+ def save_results!
34
+ ActiveRecord::Base.transaction do
35
+ profiled_request.flamegraph = Flamegraph.new(data: flamegraph) if flamegraph.present?
36
+ profiled_request.save
37
+ insert_traces unless traces.empty?
38
+ end
39
+ @saved = true
40
+ end
41
+
42
+ def complete?
43
+ @complete
44
+ end
45
+
46
+ def saved?
47
+ @saved
48
+ end
49
+
50
+ private
51
+
52
+ def insert_traces
53
+ return if traces.empty?
54
+
55
+ timestamp = Time.zone.now
56
+ inserts = traces.map do |trace|
57
+ { rmp_profiled_request_id: profiled_request.id, **trace.to_h, created_at: timestamp, updated_at: timestamp }
58
+ end
59
+ RailsMiniProfiler::Trace.insert_all(inserts)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMiniProfiler
4
+ class RequestWrapper
5
+ attr_reader :body,
6
+ :method,
7
+ :path,
8
+ :query_string,
9
+ :env
10
+
11
+ def initialize(env = {})
12
+ @env = env
13
+ @method = @env['REQUEST_METHOD'] || 'GET'
14
+ @query_string = @env['QUERY_STRING'] || ''
15
+ @path = @env['PATH_INFO'] || '/'
16
+ @body = read_body
17
+ end
18
+
19
+ def headers
20
+ @env.select { |k, _v| k.start_with? 'HTTP_' } || []
21
+ end
22
+
23
+ private
24
+
25
+ def read_body
26
+ return '' unless @env['rack.input']
27
+
28
+ body = @env['rack.input'].read
29
+ @env['rack.input'].rewind
30
+ body
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMiniProfiler
4
+ class ResponseWrapper
5
+ attr_reader :response, :rack_response
6
+
7
+ delegate :status, :headers, to: :rack_response
8
+
9
+ def initialize(status, headers, response)
10
+ @rack_response = Rack::Response.new(response, status, headers)
11
+ @response = response
12
+ end
13
+
14
+ def body
15
+ return '' unless json? || xml?
16
+
17
+ response&.body || ''
18
+ end
19
+
20
+ def media_type
21
+ @media_type ||= @rack_response.media_type
22
+ end
23
+
24
+ def json?
25
+ media_type =~ %r{application/json}
26
+ end
27
+
28
+ def xml?
29
+ media_type =~ %r{application/xml}
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMiniProfiler
4
+ class Storage
5
+ class << self
6
+ def configuration
7
+ @configuration ||= new
8
+ end
9
+
10
+ def configure
11
+ yield(configuration)
12
+ end
13
+ end
14
+
15
+ attr_accessor :database, :profiled_requests_table, :traces_table, :flamegraphs_table
16
+
17
+ def initialize(**kwargs)
18
+ defaults!
19
+ kwargs.each { |key, value| instance_variable_set("@#{key}", value) }
20
+ end
21
+
22
+ def defaults!
23
+ @database = nil
24
+ @profiled_requests_table = 'rmp_profiled_requests'
25
+ @flamegraphs_table = 'rmp_flamegraphs'
26
+ @traces_table = 'rmp_traces'
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMiniProfiler
4
+ class Tracers
5
+ DEFAULT_SUBSCRIPTIONS = %w[
6
+ sql.active_record
7
+ instantiation.active_record
8
+ render_template.action_view
9
+ render_partial.action_view
10
+ process_action.action_controller
11
+ rails_mini_profiler.total_time
12
+ ].freeze
13
+
14
+ class << self
15
+ def setup!(&callback)
16
+ DEFAULT_SUBSCRIPTIONS.each do |event|
17
+ subscribe(event, &callback)
18
+ end
19
+ end
20
+
21
+ def build_trace(event)
22
+ start = (event.time.to_f * 100_000).to_i
23
+ finish = (event.end.to_f * 100_000).to_i
24
+ Models::Trace.new(
25
+ name: event.name,
26
+ start: start,
27
+ finish: finish,
28
+ duration: finish - start,
29
+ allocations: event.allocations,
30
+ backtrace: Rails.backtrace_cleaner.clean(caller),
31
+ payload: format_payload(event)
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ def subscribe(*subscriptions, &callback)
38
+ subscriptions.each do |subscription|
39
+ ActiveSupport::Notifications.subscribe(subscription) do |event|
40
+ callback.call(event)
41
+ end
42
+ end
43
+ end
44
+
45
+ def format_payload(event)
46
+ case event.name
47
+ when 'sql.active_record'
48
+ transform_sql_event(event)
49
+ when 'render_template.action_view', 'render_partial.action_view'
50
+ event.payload.slice(:identifier, :count)
51
+ when 'process_action.action_controller'
52
+ transform_controller_event(event)
53
+ else
54
+ event.payload
55
+ end
56
+ end
57
+
58
+ def transform_sql_event(event)
59
+ payload = event.payload.slice(:name, :sql, :binds, :type_casted_binds)
60
+ typecasted_binds = payload[:type_casted_binds]
61
+ # Sometimes, typecasted binds are a proc. Not sure why. In those instances, we extract the typecasted
62
+ # values from the proc by executing call.
63
+ typecasted_binds = typecasted_binds.respond_to?(:call) ? typecasted_binds.call : typecasted_binds
64
+ payload[:binds] = transform_binds(payload[:binds], typecasted_binds)
65
+ payload.delete(:type_casted_binds)
66
+ payload.reject { |_k, v| v.blank? }
67
+ end
68
+
69
+ def transform_binds(binds, type_casted_binds)
70
+ binds.each_with_object([]).with_index do |(binding, object), i|
71
+ name = binding.name
72
+ value = type_casted_binds[i]
73
+ object << { name: name, value: value }
74
+ end
75
+ end
76
+
77
+ def transform_controller_event(event)
78
+ payload = event.payload
79
+ .slice(:view_runtime, :db_runtime)
80
+ .transform_values { |value| value&.round(2) }
81
+ payload.reject { |_k, v| v.blank? }
82
+ end
83
+ end
84
+ end
85
+ end