rack-mini-profiler 0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack-mini-profiler might be problematic. Click here for more details.

@@ -0,0 +1,9 @@
1
+ tbody tr:nth-child(odd) { background-color:#eee; }
2
+ tbody tr:nth-child(even) { background-color:#fff; }
3
+ table { border: 0; border-spacing:0;}
4
+ tr {border: 0;}
5
+ .date {font-size: 11px; color: #666;}
6
+ td {padding: 8px;}
7
+ .time {text-align:center;}
8
+ thead tr {background-color: #bbb; color: #444; font-size: 12px;}
9
+ thead tr th { padding: 5px 15px;}
@@ -0,0 +1,37 @@
1
+ var MiniProfiler = MiniProfiler || {};
2
+ MiniProfiler.list = {
3
+ init:
4
+ function (options) {
5
+ var opt = options || {};
6
+
7
+ var updateGrid = function (id) {
8
+ jQueryMP.ajax({
9
+ url: options.path + 'results-list',
10
+ data: { "last-id": id },
11
+ dataType: 'json',
12
+ type: 'GET',
13
+ success: function (data) {
14
+ jQueryMP('table tbody').append(jQueryMP("#rowTemplate").tmpl(data));
15
+ var oldId = id;
16
+ var oldData = data;
17
+ setTimeout(function () {
18
+ var newId = oldId;
19
+ if (oldData.length > 0) {
20
+ newId = oldData[oldData.length - 1].Id;
21
+ }
22
+ updateGrid(newId);
23
+ }, 4000);
24
+ }
25
+ });
26
+ }
27
+
28
+ MiniProfiler.path = options.path;
29
+ jQueryMP.get(options.path + 'list.tmpl?v=' + options.version, function (data) {
30
+ if (data) {
31
+ jQueryMP('body').append(data);
32
+ jQueryMP('body').append(jQueryMP('#tableTemplate').tmpl());
33
+ updateGrid();
34
+ }
35
+ });
36
+ }
37
+ };
@@ -0,0 +1,34 @@
1
+ <script id="tableTemplate" type="text/x-jquery-tmpl">
2
+ <table>
3
+ <thead>
4
+ <tr>
5
+ <th>Name</th>
6
+ <th>Started</th>
7
+ <th>Sql Duration</th>
8
+ <th>Total Duration</th>
9
+ <th>Request Start</th>
10
+ <th>Response Start</th>
11
+ <th>Dom Complete</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+
16
+ </tbody>
17
+ </table>
18
+ </script>
19
+ <script id="rowTemplate" type="text/x-jquery-tmpl">
20
+ <tr>
21
+ <td>
22
+ <a href="${MiniProfiler.path}results?id=${Id}">${Name}</a></td>
23
+ <td class="date">${MiniProfiler.renderDate(Started)}</td>
24
+ <td class="time">${DurationMillisecondsInSql}</td>
25
+ <td class="time">${DurationMilliseconds}</td>
26
+ {{if ClientTimings}}
27
+ <td class="time">${MiniProfiler.getClientTimingByName(ClientTimings,"Request").Start}</td>
28
+ <td class="time">${MiniProfiler.getClientTimingByName(ClientTimings,"Response").Start}</td>
29
+ <td class="time">${MiniProfiler.getClientTimingByName(ClientTimings,"Dom Complete").Start}</td>
30
+ {{else}}
31
+ <td colspan="3"></td>
32
+ {{/if}}
33
+ </tr>
34
+ </script>
@@ -0,0 +1,62 @@
1
+ <script type="text/javascript">
2
+ (function(){{
3
+ var init = function() {{
4
+ var load = function(s,f){{
5
+ var sc = document.createElement('script');
6
+ sc.async = 'async';
7
+ sc.type = 'text/javascript';
8
+ sc.src = s;
9
+ var l = false;
10
+ sc.onload = sc.onreadystatechange = function(_, abort) {{
11
+ if (!l && (!sc.readyState || /loaded|complete/.test(sc.readyState))) {{
12
+ if (!abort){{l=true; f();}}
13
+ }}
14
+ }};
15
+
16
+ document.getElementsByTagName('head')[0].appendChild(sc);
17
+ }};
18
+
19
+ var initMp = function(){{
20
+ load('{path}includes.js?v={version}',function(){{
21
+ MiniProfiler.init({{
22
+ ids: {ids},
23
+ path: '{path}',
24
+ version: '{version}',
25
+ renderPosition: '{position}',
26
+ showTrivial: {showTrivial},
27
+ showChildrenTime: {showChildren},
28
+ maxTracesToShow: {maxTracesToShow},
29
+ showControls: {showControls},
30
+ currentId: '{currentId}',
31
+ authorized: {authorized}
32
+ }});
33
+ }});
34
+ }};
35
+ if ({useExistingjQuery}) {{
36
+ jQueryMP = jQuery;
37
+ initMp();
38
+ }} else {{
39
+ load('{path}jquery.1.7.1.js?v={version}', initMp);
40
+ }}
41
+
42
+ }};
43
+
44
+ var w = 0;
45
+ var f = false;
46
+ var deferInit = function(){{
47
+ if (f) return;
48
+ if (window.performance && window.performance.timing && window.performance.timing.loadEventEnd == 0 && w < 10000){{
49
+ setTimeout(deferInit, 100);
50
+ w += 100;
51
+ }} else {{
52
+ f = true;
53
+ init();
54
+ }}
55
+ }};
56
+ if (document.addEventListener) {{
57
+ document.addEventListener('DOMContentLoaded',deferInit);
58
+ }}
59
+ var o = window.onload;
60
+ window.onload = function(){{if(o)o; deferInit()}};
61
+ }})();
62
+ </script>
@@ -0,0 +1,11 @@
1
+ <html>
2
+ <head>
3
+ <title>{name} ({duration} ms) - Profiling Results</title>
4
+ <script type='text/javascript' src='{path}jquery.1.7.1.js?v={version}'></script>
5
+ <script type='text/javascript'> var profiler = {json}; </script>
6
+ {includes}
7
+ </head>
8
+ <body>
9
+ <div class='profiler-result-full'></div>
10
+ </body>
11
+ </html>
@@ -0,0 +1,45 @@
1
+ module Rack
2
+ class MiniProfiler
3
+
4
+ # This class acts as a proxy to the Body so that we can
5
+ # safely append to the end without knowing about the internals
6
+ # of the body class.
7
+ class BodyAddProxy
8
+ def initialize(body, additional_text)
9
+ @body = body
10
+ @additional_text = additional_text
11
+ end
12
+
13
+ def respond_to?(*args)
14
+ super or @body.respond_to?(*args)
15
+ end
16
+
17
+ def method_missing(*args, &block)
18
+ @body.__send__(*args, &block)
19
+ end
20
+
21
+ # In the case of to_str we don't want to use method_missing as it might avoid
22
+ # a call to each (such as in Rack::Test)
23
+ def to_str
24
+ result = ""
25
+ each {|token| result << token}
26
+ result
27
+ end
28
+
29
+ def each(&block)
30
+
31
+ # In ruby 1.9 we don't support String#each
32
+ if @body.is_a?(String)
33
+ yield @body
34
+ else
35
+ @body.each(&block)
36
+ end
37
+
38
+ yield @additional_text
39
+ self
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ require 'mini_profiler/timer_struct'
2
+
3
+ module Rack
4
+ class MiniProfiler
5
+
6
+ # This class holds the client timings
7
+ class ClientTimerStruct < TimerStruct
8
+
9
+ def initialize(env={})
10
+ super
11
+ end
12
+
13
+ def init_from_form_data(env, page_struct)
14
+ timings = []
15
+ clientTimes, clientPerf, baseTime = nil
16
+ form = env['rack.request.form_hash']
17
+
18
+ clientPerf = form['clientPerformance'] if form
19
+ clientTimes = clientPerf['timing'] if clientPerf
20
+
21
+ baseTime = clientTimes['navigationStart'].to_i if clientTimes
22
+ return unless clientTimes && baseTime
23
+
24
+ clientTimes.keys.find_all{|k| k =~ /Start$/ }.each do |k|
25
+ start = clientTimes[k].to_i - baseTime
26
+ finish = clientTimes[k.sub(/Start$/, "End")].to_i - baseTime
27
+ duration = 0
28
+ duration = finish - start if finish > start
29
+ name = k.sub(/Start$/, "").split(/(?=[A-Z])/).map{|s| s.capitalize}.join(' ')
30
+ timings.push({"Name" => name, "Start" => start, "Duration" => duration}) if start >= 0
31
+ end
32
+
33
+ clientTimes.keys.find_all{|k| !(k =~ /(End|Start)$/)}.each do |k|
34
+ timings.push("Name" => k, "Start" => clientTimes[k].to_i - baseTime, "Duration" => -1)
35
+ end
36
+
37
+ self['RedirectCount'] = env['rack.request.form_hash']['clientPerformance']['navigation']['redirectCount']
38
+ self['Timings'] = timings
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,49 @@
1
+ require 'mini_profiler/timer_struct'
2
+
3
+ module Rack
4
+ class MiniProfiler
5
+
6
+ # PageTimerStruct
7
+ # Root: RequestTimer
8
+ # :has_many RequestTimer children
9
+ # :has_many SqlTimer children
10
+ class PageTimerStruct < TimerStruct
11
+ def initialize(env)
12
+ super("Id" => MiniProfiler.generate_id,
13
+ "Name" => env['PATH_INFO'],
14
+ "Started" => (Time.now.to_f * 1000).to_i,
15
+ "MachineName" => env['SERVER_NAME'],
16
+ "Level" => 0,
17
+ "User" => "unknown user",
18
+ "HasUserViewed" => false,
19
+ "ClientTimings" => ClientTimerStruct.new,
20
+ "DurationMilliseconds" => 0,
21
+ "HasTrivialTimings" => true,
22
+ "HasAllTrivialTimigs" => false,
23
+ "TrivialDurationThresholdMilliseconds" => 2,
24
+ "Head" => nil,
25
+ "DurationMillisecondsInSql" => 0,
26
+ "HasSqlTimings" => true,
27
+ "HasDuplicateSqlTimings" => false,
28
+ "ExecutedReaders" => 0,
29
+ "ExecutedScalars" => 0,
30
+ "ExecutedNonQueries" => 0)
31
+ name = "#{env['REQUEST_METHOD']} http://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
32
+ self['Root'] = RequestTimerStruct.createRoot(name, self)
33
+ end
34
+
35
+ def duration_ms
36
+ @attributes['Root']['DurationMilliseconds']
37
+ end
38
+
39
+ def to_json(*a)
40
+ attribs = @attributes.merge(
41
+ "Started" => '/Date(%d)/' % @attributes['Started'],
42
+ "DurationMilliseconds" => @attributes['Root']['DurationMilliseconds']
43
+ )
44
+ ::JSON.generate(attribs, a[0])
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,309 @@
1
+ require 'json'
2
+ require 'timeout'
3
+ require 'thread'
4
+
5
+ require 'mini_profiler/page_timer_struct'
6
+ require 'mini_profiler/sql_timer_struct'
7
+ require 'mini_profiler/client_timer_struct'
8
+ require 'mini_profiler/request_timer_struct'
9
+ require 'mini_profiler/body_add_proxy'
10
+ require 'mini_profiler/storage/abstract_store'
11
+ require 'mini_profiler/storage/memory_store'
12
+ require 'mini_profiler/storage/redis_store'
13
+ require 'mini_profiler/storage/file_store'
14
+
15
+ module Rack
16
+
17
+ class MiniProfiler
18
+
19
+ VERSION = 'rZlycOOTnzxZvxTmFuOEV0dSmu4P5m5bLrCtwJHVXPA='.freeze
20
+ @@instance = nil
21
+
22
+ def self.instance
23
+ @@instance
24
+ end
25
+
26
+ def self.generate_id
27
+ rand(36**20).to_s(36)
28
+ end
29
+
30
+ # Defaults for MiniProfiler's configuration
31
+ def self.configuration_defaults
32
+ {
33
+ :auto_inject => true, # automatically inject on every html page
34
+ :base_url_path => "/mini-profiler-resources/",
35
+ :authorize_cb => lambda {|env| true}, # callback returns true if this request is authorized to profile
36
+ :position => 'left', # Where it is displayed
37
+ :backtrace_remove => nil,
38
+ :backtrace_filter => nil,
39
+ :skip_schema_queries => true,
40
+ :storage => MiniProfiler::MemoryStore,
41
+ :user_provider => Proc.new{|env| "TODO" }
42
+ }
43
+ end
44
+
45
+ def self.reset_configuration
46
+ @configuration = configuration_defaults
47
+ end
48
+
49
+ # So we can change the configuration if we want
50
+ def self.configuration
51
+ @configuration ||= configuration_defaults.dup
52
+ end
53
+
54
+ def self.share_template
55
+ return @share_template unless @share_template.nil?
56
+ @share_template = ::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__)))
57
+ end
58
+
59
+ #
60
+ # options:
61
+ # :auto_inject - should script be automatically injected on every html page (not xhr)
62
+ def initialize(app, opts={})
63
+ @@instance = self
64
+ MiniProfiler.configuration.merge!(opts)
65
+ @options = MiniProfiler.configuration
66
+ @app = app
67
+ @options[:base_url_path] << "/" unless @options[:base_url_path].end_with? "/"
68
+ unless @options[:storage_instance]
69
+ @storage = @options[:storage_instance] = @options[:storage].new(@options[:storage_options])
70
+ end
71
+ end
72
+
73
+ def user(env)
74
+ options[:user_provider].call(env)
75
+ end
76
+
77
+ def serve_results(env)
78
+ request = Rack::Request.new(env)
79
+ page_struct = @storage.load(request['id'])
80
+ unless page_struct
81
+ @storage.set_viewed(user(env), request['Id'])
82
+ return [404, {}, ["No such result #{request['id']}"]]
83
+ end
84
+ unless page_struct['HasUserViewed']
85
+ page_struct['ClientTimings'].init_from_form_data(env, page_struct)
86
+ page_struct['HasUserViewed'] = true
87
+ @storage.save(page_struct)
88
+ @storage.set_viewed(user(env), page_struct['Id'])
89
+ end
90
+
91
+ result_json = page_struct.to_json
92
+ # If we're an XMLHttpRequest, serve up the contents as JSON
93
+ if request.xhr?
94
+ [200, { 'Content-Type' => 'application/json'}, [result_json]]
95
+ else
96
+
97
+ # Otherwise give the HTML back
98
+ html = MiniProfiler.share_template.dup
99
+ html.gsub!(/\{path\}/, @options[:base_url_path])
100
+ html.gsub!(/\{version\}/, MiniProfiler::VERSION)
101
+ html.gsub!(/\{json\}/, result_json)
102
+ html.gsub!(/\{includes\}/, get_profile_script(env))
103
+ html.gsub!(/\{name\}/, page_struct['Name'])
104
+ html.gsub!(/\{duration\}/, page_struct.duration_ms.round(1).to_s)
105
+
106
+ [200, {'Content-Type' => 'text/html'}, [html]]
107
+ end
108
+
109
+ end
110
+
111
+ def serve_html(env)
112
+ file_name = env['PATH_INFO'][(@options[:base_url_path].length)..1000]
113
+ return serve_results(env) if file_name.eql?('results')
114
+ full_path = ::File.expand_path("../html/#{file_name}", ::File.dirname(__FILE__))
115
+ return [404, {}, ["Not found"]] unless ::File.exists? full_path
116
+ f = Rack::File.new nil
117
+ f.path = full_path
118
+ f.cache_control = "max-age:86400"
119
+ f.serving env
120
+ end
121
+
122
+ def self.current
123
+ Thread.current['profiler.mini.private']
124
+ end
125
+
126
+ def self.current=(c)
127
+ # we use TLS cause we need access to this from sql blocks and code blocks that have no access to env
128
+ Thread.current['profiler.mini.private'] = c
129
+ end
130
+
131
+ def current
132
+ MiniProfiler.current
133
+ end
134
+
135
+ def current=(c)
136
+ MiniProfiler.current=c
137
+ end
138
+
139
+ def options
140
+ @options
141
+ end
142
+
143
+ def self.create_current(env={}, options={})
144
+ # profiling the request
145
+ self.current = {}
146
+ self.current['inject_js'] = options[:auto_inject] && (!env['HTTP_X_REQUESTED_WITH'].eql? 'XMLHttpRequest')
147
+ self.current['page_struct'] = PageTimerStruct.new(env)
148
+ self.current['current_timer'] = current['page_struct']['Root']
149
+ end
150
+
151
+ def call(env)
152
+ status = headers = body = nil
153
+
154
+ # only profile if authorized
155
+ return @app.call(env) unless @options[:authorize_cb].call(env)
156
+
157
+ # handle all /mini-profiler requests here
158
+ return serve_html(env) if env['PATH_INFO'].start_with? @options[:base_url_path]
159
+
160
+ MiniProfiler.create_current(env, @options)
161
+ if env["QUERY_STRING"] =~ /pp=skip-backtrace/
162
+ current['skip-backtrace'] = true
163
+ end
164
+
165
+ start = Time.now
166
+
167
+ done_sampling = false
168
+ quit_sampler = false
169
+ backtraces = nil
170
+ if env["QUERY_STRING"] =~ /pp=sample/
171
+ backtraces = []
172
+ t = Thread.current
173
+ Thread.new {
174
+ i = 10000 # for sanity never grab more than 10k samples
175
+ unless done_sampling || i < 0
176
+ i -= 1
177
+ backtraces << t.backtrace
178
+ sleep 0.001
179
+ end
180
+ quit_sampler = true
181
+ }
182
+ end
183
+
184
+ status, headers, body = nil
185
+ begin
186
+ status,headers, body = @app.call(env)
187
+ ensure
188
+ if backtraces
189
+ done_sampling = true
190
+ sleep 0.001 until quit_sampler
191
+ end
192
+ end
193
+
194
+ page_struct = current['page_struct']
195
+ page_struct['Root'].record_time((Time.now - start) * 1000)
196
+
197
+ # inject headers, script
198
+ if status == 200
199
+ @storage.save(page_struct)
200
+ @storage.set_unviewed(user(env), page_struct['Id'])
201
+
202
+ # inject header
203
+ if headers.is_a? Hash
204
+ headers['X-MiniProfiler-Ids'] = ids_json(env)
205
+ end
206
+
207
+ # inject script
208
+ if current['inject_js'] \
209
+ && headers.has_key?('Content-Type') \
210
+ && !headers['Content-Type'].match(/text\/html/).nil? then
211
+ body = MiniProfiler::BodyAddProxy.new(body, self.get_profile_script(env))
212
+ end
213
+ end
214
+
215
+ # mini profiler is meddling with stuff, we can not cache cause we will get incorrect data
216
+ # Rack::ETag has already inserted some nonesense in the chain
217
+ headers.delete('ETag')
218
+ headers.delete('Date')
219
+ headers['Cache-Control'] = 'must-revalidate, private, max-age=0'
220
+ [status, headers, body]
221
+ ensure
222
+ # Make sure this always happens
223
+ current = nil
224
+ end
225
+
226
+ def ids_json(env)
227
+ ids = [current['page_struct']["Id"]] + (@storage.get_unviewed_ids(user(env)) || [])
228
+ ::JSON.generate(ids.uniq)
229
+ end
230
+
231
+ # get_profile_script returns script to be injected inside current html page
232
+ # By default, profile_script is appended to the end of all html requests automatically.
233
+ # Calling get_profile_script cancels automatic append for the current page
234
+ # Use it when:
235
+ # * you have disabled auto append behaviour throught :auto_inject => false flag
236
+ # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
237
+ def get_profile_script(env)
238
+ ids = ids_json(env)
239
+ path = @options[:base_url_path]
240
+ version = MiniProfiler::VERSION
241
+ position = @options[:position]
242
+ showTrivial = false
243
+ showChildren = false
244
+ maxTracesToShow = 10
245
+ showControls = false
246
+ currentId = current['page_struct']["Id"]
247
+ authorized = true
248
+ useExistingjQuery = false
249
+ # TODO : cache this snippet
250
+ script = IO.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
251
+ # replace the variables
252
+ [:ids, :path, :version, :position, :showTrivial, :showChildren, :maxTracesToShow, :showControls, :currentId, :authorized, :useExistingjQuery].each do |v|
253
+ regex = Regexp.new("\\{#{v.to_s}\\}")
254
+ script.gsub!(regex, eval(v.to_s).to_s)
255
+ end
256
+ # replace the '{{' and '}}''
257
+ script.gsub!(/\{\{/, '{').gsub!(/\}\}/, '}')
258
+ current['inject_js'] = false
259
+ script
260
+ end
261
+
262
+ # cancels automatic injection of profile script for the current page
263
+ def cancel_auto_inject(env)
264
+ current['inject_js'] = false
265
+ end
266
+
267
+ # perform a profiling step on given block
268
+ def self.step(name)
269
+ if current
270
+ old_timer = current['current_timer']
271
+ new_step = RequestTimerStruct.new(name, current['page_struct'])
272
+ current['current_timer'] = new_step
273
+ new_step['Name'] = name
274
+ start = Time.now
275
+ result = yield if block_given?
276
+ new_step.record_time((Time.now - start)*1000)
277
+ old_timer.add_child(new_step)
278
+ current['current_timer'] = old_timer
279
+ result
280
+ else
281
+ yield if block_given?
282
+ end
283
+ end
284
+
285
+ def self.profile_method(klass, method, &blk)
286
+ default_name = klass.to_s + " " + method.to_s
287
+ with_profiling = (method.to_s + "_with_mini_profiler").intern
288
+ without_profiling = (method.to_s + "_without_mini_profiler").intern
289
+
290
+ klass.send :alias_method, without_profiling, method
291
+ klass.send :define_method, with_profiling do |*args, &orig|
292
+ name = default_name
293
+ name = blk.bind(self).call(*args) if blk
294
+ ::Rack::MiniProfiler.step name do
295
+ self.send without_profiling, *args, &orig
296
+ end
297
+ end
298
+ klass.send :alias_method, method, with_profiling
299
+ end
300
+
301
+ def record_sql(query, elapsed_ms)
302
+ c = current
303
+ c['current_timer'].add_sql(query, elapsed_ms, c['page_struct'], c['skip-backtrace']) if (c && c['current_timer'])
304
+ end
305
+
306
+ end
307
+
308
+ end
309
+