rack-mini-profiler 3.1.0 → 3.3.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.
data/lib/html/vendor.js CHANGED
@@ -11,7 +11,7 @@ var out=' <div class="profiler-result"> <div class="profiler-button ';if(it.has_
11
11
  }
12
12
  MiniProfiler.templates["linksTemplate"] = function anonymous(it
13
13
  ) {
14
- var out=' <a href="'+( MiniProfiler.shareUrl(it.page.id) )+'" class="profiler-share-profiler-results" target="_blank">share</a> <a href="'+( MiniProfiler.moreUrl(it.timing.name) )+'" class="profiler-more-actions">more</a> ';if(it.page.has_flamegraph){out+=' <a href="'+( MiniProfiler.flamegraphUrl(it.page.id) )+'" class="profiler-show-flamegraph" target="_blank">flamegraph</a> ';}out+=' ';if(it.custom_link){out+=' <a href="'+( it.custom_link )+'" class="profiler-custom-link" target="_blank">'+( it.custom_link_name )+'</a> ';}out+=' ';if(it.page.has_trivial_timings){out+=' <a class="profiler-toggle-trivial" data-show-on-load="'+( it.page.has_all_trivial_timings )+'" title="toggles any rows with &lt; '+( it.page.trivial_duration_threshold_milliseconds )+' ms"> show trivial </a> ';}return out;
14
+ var out=' <a href="'+( MiniProfiler.shareUrl(it.page.id) )+'" class="profiler-share-profiler-results" target="_blank">share</a> <a href="'+( MiniProfiler.moreUrl(it.page.name) )+'" class="profiler-more-actions">more</a> ';if(it.page.has_flamegraph){out+=' <a href="'+( MiniProfiler.flamegraphUrl(it.page.id) )+'" class="profiler-show-flamegraph" target="_blank">flamegraph</a> ';}out+=' ';if(it.custom_link){out+=' <a href="'+( it.custom_link )+'" class="profiler-custom-link" target="_blank">'+( it.custom_link_name )+'</a> ';}out+=' ';if(it.page.has_trivial_timings){out+=' <a class="profiler-toggle-trivial" data-show-on-load="'+( it.page.has_all_trivial_timings )+'" title="toggles any rows with &lt; '+( it.page.trivial_duration_threshold_milliseconds )+' ms"> show trivial </a> ';}return out;
15
15
  }
16
16
  MiniProfiler.templates["timingTemplate"] = function anonymous(it
17
17
  ) {
@@ -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 = '35a79b300ab5afa978cb59af0b05e059'
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
@@ -10,11 +10,11 @@ module Rack
10
10
  class CacheCleanupThread < Thread
11
11
 
12
12
  def initialize(interval, cycle, store)
13
- super
14
13
  @store = store
15
14
  @interval = interval
16
15
  @cycle = cycle
17
16
  @cycle_count = 1
17
+ super
18
18
  end
19
19
 
20
20
  def should_cleanup?
@@ -78,7 +78,7 @@ module Rack
78
78
  cleanup_cycle = args.fetch(:cleanup_cycle) { CLEANUP_CYCLE }
79
79
  t = CacheCleanupThread.new(cleanup_interval, cleanup_cycle, self) do
80
80
  until Thread.current[:should_exit] do
81
- t.sleepy_run
81
+ Thread.current.sleepy_run
82
82
  end
83
83
  end
84
84
  at_exit { t[:should_exit] = true }
@@ -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.0'
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