rack-mini-profiler 3.1.1 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+ module Rack
3
+ class MiniProfiler
4
+ module Actions
5
+ def serve_snapshot(env)
6
+ MiniProfiler.authorize_request
7
+ status = 200
8
+ headers = { 'Content-Type' => 'text/html' }
9
+ qp = Rack::Utils.parse_nested_query(env['QUERY_STRING'])
10
+ if group_name = qp["group_name"]
11
+ list = @storage.snapshots_group(group_name)
12
+ list.each do |snapshot|
13
+ snapshot[:url] = url_for_snapshot(snapshot[:id], group_name)
14
+ end
15
+ data = {
16
+ group_name: group_name,
17
+ list: list
18
+ }
19
+ else
20
+ list = @storage.snapshots_overview
21
+ list.each do |group|
22
+ group[:url] = url_for_snapshots_group(group[:name])
23
+ end
24
+ data = {
25
+ page: "overview",
26
+ list: list
27
+ }
28
+ end
29
+ data_html = <<~HTML
30
+ <div style="display: none;" id="snapshots-data">
31
+ #{data.to_json}
32
+ </div>
33
+ HTML
34
+ response = Rack::Response.new([], status, headers)
35
+
36
+ response.write <<~HTML
37
+ <!DOCTYPE html>
38
+ <html>
39
+ <head>
40
+ <title>Rack::MiniProfiler Snapshots</title>
41
+ </head>
42
+ <body class="mp-snapshots">
43
+ HTML
44
+ response.write(data_html)
45
+ script = self.get_profile_script(env)
46
+ response.write(script)
47
+ response.write <<~HTML
48
+ </body>
49
+ </html>
50
+ HTML
51
+ response.finish
52
+ end
53
+
54
+ def serve_file(env, file_name:)
55
+ resources_env = env.dup
56
+ resources_env['PATH_INFO'] = file_name
57
+
58
+ rack_file = Rack::File.new(resources_root, 'Cache-Control' => "max-age=#{cache_control_value}")
59
+ rack_file.call(resources_env)
60
+ end
61
+
62
+ def serve_results(env)
63
+ request = Rack::Request.new(env)
64
+ id = request.params['id']
65
+ group_name = request.params['group']
66
+ is_snapshot = group_name && group_name.size > 0
67
+ if is_snapshot
68
+ page_struct = @storage.load_snapshot(id, group_name)
69
+ else
70
+ page_struct = @storage.load(id)
71
+ end
72
+ if !page_struct && is_snapshot
73
+ id = ERB::Util.html_escape(id)
74
+ return [404, {}, ["Snapshot with id '#{id}' not found"]]
75
+ elsif !page_struct
76
+ @storage.set_viewed(user(env), id)
77
+ id = ERB::Util.html_escape(id)
78
+ user_info = ERB::Util.html_escape(user(env))
79
+ return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
80
+ end
81
+ if !page_struct[:has_user_viewed] && !is_snapshot
82
+ page_struct[:client_timings] = TimerStruct::Client.init_from_form_data(env, page_struct)
83
+ page_struct[:has_user_viewed] = true
84
+ @storage.save(page_struct)
85
+ @storage.set_viewed(user(env), id)
86
+ end
87
+
88
+ # If we're an XMLHttpRequest, serve up the contents as JSON
89
+ if request.xhr?
90
+ result_json = page_struct.to_json
91
+ [200, { 'Content-Type' => 'application/json' }, [result_json]]
92
+ else
93
+ # Otherwise give the HTML back
94
+ html = generate_html(page_struct, env)
95
+ [200, { 'Content-Type' => 'text/html' }, [html]]
96
+ end
97
+ end
98
+
99
+ def serve_flamegraph(env)
100
+ request = Rack::Request.new(env)
101
+ id = request.params['id']
102
+ page_struct = @storage.load(id)
103
+
104
+ if !page_struct
105
+ id = ERB::Util.html_escape(id)
106
+ user_info = ERB::Util.html_escape(user(env))
107
+ return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
108
+ end
109
+
110
+ if !page_struct[:flamegraph]
111
+ return [404, {}, ["No flamegraph available for #{ERB::Util.html_escape(id)}"]]
112
+ end
113
+
114
+ self.flamegraph(page_struct[:flamegraph], page_struct[:request_path], env)
115
+ end
116
+
117
+ def serve_profile_gc(env, client_settings)
118
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
119
+
120
+ client_settings.handle_cookie(Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env))
121
+ end
122
+
123
+ def serve_profile_memory(env, client_settings)
124
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
125
+
126
+ unless defined?(MemoryProfiler) && MemoryProfiler.respond_to?(:report)
127
+ message = "Please install the memory_profiler gem and require it: add gem 'memory_profiler' to your Gemfile"
128
+ status, headers, body = @app.call(env)
129
+ body.close if body.respond_to? :close
130
+
131
+ return client_settings.handle_cookie(
132
+ text_result(message, status: 500, headers: headers)
133
+ )
134
+ end
135
+
136
+ query_params = Rack::Utils.parse_nested_query(env['QUERY_STRING'])
137
+ options = {
138
+ ignore_files: query_params['memory_profiler_ignore_files'],
139
+ allow_files: query_params['memory_profiler_allow_files'],
140
+ }
141
+ options[:top] = Integer(query_params['memory_profiler_top']) if query_params.key?('memory_profiler_top')
142
+ result = StringIO.new
143
+ report = MemoryProfiler.report(options) do
144
+ _, _, body = @app.call(env)
145
+ body.close if body.respond_to? :close
146
+ end
147
+ report.pretty_print(result)
148
+ client_settings.handle_cookie(text_result(result.string))
149
+ end
150
+ end
151
+ end
152
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Rack
3
3
  class MiniProfiler
4
- ASSET_VERSION = '90a68676a0c0d704b4438ca3f27d46c4'
4
+ ASSET_VERSION = 'c336b1303a50b21f5222bc23262ca7bf'
5
5
  end
6
6
  end
@@ -30,6 +30,7 @@ module Rack
30
30
  @backtrace_threshold_ms = 0
31
31
  @flamegraph_sample_rate = 0.5
32
32
  @flamegraph_mode = :wall
33
+ @flamegraph_ignore_gc = false
33
34
  @storage_failure = Proc.new do |exception|
34
35
  if @logger
35
36
  @logger.warn("MiniProfiler storage failure: #{exception.message}")
@@ -76,7 +77,7 @@ module Rack
76
77
  :storage_options, :user_provider, :enable_advanced_debugging_tools,
77
78
  :skip_sql_param_names, :suppress_encoding, :max_sql_param_length,
78
79
  :content_security_policy_nonce, :enable_hotwire_turbo_drive_support,
79
- :flamegraph_mode, :profile_parameter
80
+ :flamegraph_mode, :flamegraph_ignore_gc, :profile_parameter
80
81
 
81
82
  # ui accessors
82
83
  attr_accessor :collapse_results, :max_traces_to_show, :position,
@@ -17,6 +17,10 @@ module Rack
17
17
  )
18
18
  end
19
19
 
20
+ def report_reader_duration(elapsed_ms, row_count = nil, class_name = nil)
21
+ current&.current_timer&.report_reader_duration(elapsed_ms, row_count, class_name)
22
+ end
23
+
20
24
  def start_step(name)
21
25
  return unless current
22
26
  parent_timer = current.current_timer
@@ -18,6 +18,15 @@ module Rack
18
18
  @expires_in_seconds = args[:expires_in] || EXPIRES_IN_SECONDS
19
19
  end
20
20
 
21
+ def alive?
22
+ begin
23
+ @client.alive!
24
+ true
25
+ rescue Dalli::RingError
26
+ false
27
+ end
28
+ end
29
+
21
30
  def save(page_struct)
22
31
  @client.set("#{@prefix}#{page_struct[:id]}", Marshal::dump(page_struct), @expires_in_seconds)
23
32
  end
@@ -125,6 +125,12 @@ module Rack
125
125
  end
126
126
  end
127
127
 
128
+ # please call SqlTiming#report_reader_duration instead
129
+ def report_reader_duration(elapsed_ms, row_count = nil, class_name = nil)
130
+ last_time = self[:sql_timings]&.last
131
+ last_time&.report_reader_duration(elapsed_ms, row_count, class_name)
132
+ end
133
+
128
134
  def add_custom(type, elapsed_ms, page)
129
135
  TimerStruct::Custom.new(type, elapsed_ms, page, self).tap do |timer|
130
136
  timer[:parent_timing_id] = self[:id]
@@ -51,12 +51,14 @@ module Rack
51
51
  )
52
52
  end
53
53
 
54
- def report_reader_duration(elapsed_ms)
54
+ def report_reader_duration(elapsed_ms, row_count = nil, class_name = nil)
55
55
  return if @reported
56
56
  @reported = true
57
57
  self[:duration_milliseconds] += elapsed_ms
58
58
  @parent[:sql_timings_duration_milliseconds] += elapsed_ms
59
59
  @page[:duration_milliseconds_in_sql] += elapsed_ms
60
+ self[:row_count] = self[:row_count].to_i + row_count if row_count
61
+ self[:class_name] = class_name if class_name
60
62
  end
61
63
 
62
64
  def trim_binds(binds)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Rack
4
4
  class MiniProfiler
5
- VERSION = '3.1.1'
5
+ VERSION = '3.3.0'
6
6
  SOURCE_CODE_URI = 'https://github.com/MiniProfiler/rack-mini-profiler'
7
7
  end
8
8
  end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+ module Rack
3
+ class MiniProfiler
4
+ module Views
5
+ def resources_root
6
+ @resources_root ||= ::File.expand_path("../../html", __FILE__)
7
+ end
8
+
9
+ def share_template
10
+ @share_template ||= ERB.new(::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__))))
11
+ end
12
+
13
+ def generate_html(page_struct, env, result_json = page_struct.to_json)
14
+ # double-assigning to suppress "assigned but unused variable" warnings
15
+ path = path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
16
+ version = version = MiniProfiler::ASSET_VERSION
17
+ json = json = result_json
18
+ includes = includes = get_profile_script(env)
19
+ name = name = page_struct[:name]
20
+ duration = duration = page_struct.duration_ms.round(1).to_s
21
+
22
+ share_template.result(binding)
23
+ end
24
+
25
+ # get_profile_script returns script to be injected inside current html page
26
+ # By default, profile_script is appended to the end of all html requests automatically.
27
+ # Calling get_profile_script cancels automatic append for the current page
28
+ # Use it when:
29
+ # * you have disabled auto append behaviour throught :auto_inject => false flag
30
+ # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
31
+ def get_profile_script(env)
32
+ path = public_base_path(env)
33
+ version = MiniProfiler::ASSET_VERSION
34
+ if @config.assets_url
35
+ url = @config.assets_url.call('rack-mini-profiler.js', version, env)
36
+ css_url = @config.assets_url.call('rack-mini-profiler.css', version, env)
37
+ end
38
+
39
+ url = "#{path}includes.js?v=#{version}" if !url
40
+ css_url = "#{path}includes.css?v=#{version}" if !css_url
41
+
42
+ content_security_policy_nonce = @config.content_security_policy_nonce ||
43
+ env["action_dispatch.content_security_policy_nonce"] ||
44
+ env["secure_headers_content_security_policy_nonce"]
45
+
46
+ settings = {
47
+ path: path,
48
+ url: url,
49
+ cssUrl: css_url,
50
+ version: version,
51
+ verticalPosition: @config.vertical_position,
52
+ horizontalPosition: @config.horizontal_position,
53
+ showTrivial: @config.show_trivial,
54
+ showChildren: @config.show_children,
55
+ maxTracesToShow: @config.max_traces_to_show,
56
+ showControls: @config.show_controls,
57
+ showTotalSqlCount: @config.show_total_sql_count,
58
+ authorized: true,
59
+ toggleShortcut: @config.toggle_shortcut,
60
+ startHidden: @config.start_hidden,
61
+ collapseResults: @config.collapse_results,
62
+ htmlContainer: @config.html_container,
63
+ hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(','),
64
+ cspNonce: content_security_policy_nonce,
65
+ hotwireTurboDriveSupport: @config.enable_hotwire_turbo_drive_support,
66
+ }
67
+
68
+ if current && current.page_struct
69
+ settings[:ids] = ids_comma_separated(env)
70
+ settings[:currentId] = current.page_struct[:id]
71
+ else
72
+ settings[:ids] = []
73
+ settings[:currentId] = ""
74
+ end
75
+
76
+ # TODO : cache this snippet
77
+ script = ::File.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
78
+ # replace the variables
79
+ settings.each do |k, v|
80
+ regex = Regexp.new("\\{#{k.to_s}\\}")
81
+ script.gsub!(regex, v.to_s)
82
+ end
83
+
84
+ current.inject_js = false if current
85
+ script
86
+ end
87
+
88
+ BLANK_PAGE = <<~HTML
89
+ <!DOCTYPE html>
90
+ <html>
91
+ <head>
92
+ <title>Rack::MiniProfiler Requests</title>
93
+ </head>
94
+ <body>
95
+ </body>
96
+ </html>
97
+ HTML
98
+ def blank_page_html
99
+ BLANK_PAGE
100
+ end
101
+
102
+ def make_link(postfix, env)
103
+ link = env["PATH_INFO"] + "?" + env["QUERY_STRING"].sub("#{@config.profile_parameter}=help", "#{@config.profile_parameter}=#{postfix}")
104
+ "#{@config.profile_parameter}=<a href='#{ERB::Util.html_escape(link)}'>#{postfix}</a>"
105
+ end
106
+
107
+ def flamegraph(graph, path, env)
108
+ headers = { 'Content-Type' => 'text/html' }
109
+ iframe_src = "#{public_base_path(env)}speedscope/index.html"
110
+ html = <<~HTML
111
+ <!DOCTYPE html>
112
+ <html>
113
+ <head>
114
+ <title>Rack::MiniProfiler Flamegraph</title>
115
+ <style>
116
+ body { margin: 0; height: 100vh; }
117
+ #speedscope-iframe { width: 100%; height: 100%; border: none; }
118
+ </style>
119
+ </head>
120
+ <body>
121
+ <script type="text/javascript">
122
+ var graph = #{JSON.generate(graph)};
123
+ var json = JSON.stringify(graph);
124
+ var blob = new Blob([json], { type: 'text/plain' });
125
+ var objUrl = encodeURIComponent(URL.createObjectURL(blob));
126
+ var iframe = document.createElement('IFRAME');
127
+ iframe.setAttribute('id', 'speedscope-iframe');
128
+ document.body.appendChild(iframe);
129
+ var iframeUrl = '#{iframe_src}#profileURL=' + objUrl + '&title=' + 'Flamegraph for #{CGI.escape(path)}';
130
+ iframe.setAttribute('src', iframeUrl);
131
+ </script>
132
+ </body>
133
+ </html>
134
+ HTML
135
+ [200, headers, [html]]
136
+ end
137
+
138
+ def help(client_settings, env)
139
+ headers = { 'Content-Type' => 'text/html' }
140
+ html = <<~HTML
141
+ <!DOCTYPE html>
142
+ <html>
143
+ <head>
144
+ <title>Rack::MiniProfiler Help</title>
145
+ </head>
146
+ <body>
147
+ <pre style='line-height: 30px; font-size: 16px'>
148
+ This is the help menu of the <a href='#{Rack::MiniProfiler::SOURCE_CODE_URI}'>rack-mini-profiler</a> gem, append the following to your query string for more options:
149
+
150
+ #{make_link "help", env} : display this screen
151
+ #{make_link "env", env} : display the rack environment
152
+ #{make_link "skip", env} : skip mini profiler for this request
153
+ #{make_link "no-backtrace", env} #{"(*) " if client_settings.backtrace_none?}: don't collect stack traces from all the SQL executed (sticky, use #{@config.profile_parameter}=normal-backtrace to enable)
154
+ #{make_link "normal-backtrace", env} #{"(*) " if client_settings.backtrace_default?}: collect stack traces from all the SQL executed and filter normally
155
+ #{make_link "full-backtrace", env} #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use #{@config.profile_parameter}=normal-backtrace to disable)
156
+ #{make_link "disable", env} : disable profiling for this session
157
+ #{make_link "enable", env} : enable profiling for this session (if previously disabled)
158
+ #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request
159
+ #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
160
+ #{make_link "flamegraph", env} : a graph representing sampled activity (requires the stackprof gem).
161
+ #{make_link "async-flamegraph", env} : store flamegraph data for this page and all its AJAX requests. Flamegraph links will be available in the mini-profiler UI (requires the stackprof gem).
162
+ #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
163
+ #{make_link "flamegraph&flamegraph_mode=cpu", env}: creates a flamegraph with the specified mode (one of cpu, wall, object, or custom). Overrides value set in config
164
+ #{make_link "flamegraph&flamegraph_ignore_gc=true", env}: ignore garbage collection frames in flamegraphs. Overrides value set in config
165
+ #{make_link "flamegraph_embed", env} : a graph representing sampled activity (requires the stackprof gem), embedded resources for use on an intranet.
166
+ #{make_link "trace-exceptions", env} : will return all the spots where your application raises exceptions
167
+ #{make_link "analyze-memory", env} : will perform basic memory analysis of heap
168
+
169
+ All features can also be accessed by adding the X-Rack-Mini-Profiler header to the request, with any of the values above (e.g. 'X-Rack-Mini-Profiler: flamegraph')
170
+ </pre>
171
+ </body>
172
+ </html>
173
+ HTML
174
+
175
+ [200, headers, [html]]
176
+ end
177
+
178
+ def url_for_snapshots_group(group_name)
179
+ qs = Rack::Utils.build_query({ group_name: group_name })
180
+ "/#{@config.base_url_path.gsub('/', '')}/snapshots?#{qs}"
181
+ end
182
+
183
+ def url_for_snapshot(id, group_name)
184
+ qs = Rack::Utils.build_query({ id: id, group: group_name })
185
+ "/#{@config.base_url_path.gsub('/', '')}/results?#{qs}"
186
+ end
187
+
188
+ def public_base_path(env)
189
+ "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
190
+ end
191
+ end
192
+ end
193
+ end