rack-mini-profiler 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.

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
+