rack-mini-profiler 0.1.20 → 0.1.25
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.
- checksums.yaml +7 -0
- data/Ruby/CHANGELOG +130 -0
- data/{README.md → Ruby/README.md} +40 -9
- data/Ruby/lib/html/flamegraph.html +325 -0
- data/Ruby/lib/html/includes.css +451 -0
- data/{lib → Ruby/lib}/html/includes.js +135 -24
- data/{lib → Ruby/lib}/html/includes.less +38 -35
- data/{lib → Ruby/lib}/html/includes.tmpl +40 -15
- data/{lib → Ruby/lib}/html/jquery.1.7.1.js +1 -1
- data/{lib → Ruby/lib}/html/jquery.tmpl.js +1 -1
- data/{lib → Ruby/lib}/html/list.css +0 -0
- data/{lib → Ruby/lib}/html/list.js +7 -6
- data/{lib → Ruby/lib}/html/list.tmpl +0 -0
- data/Ruby/lib/html/profile_handler.js +1 -0
- data/{lib → Ruby/lib}/html/share.html +0 -0
- data/{lib → Ruby/lib}/mini_profiler/client_settings.rb +0 -0
- data/{lib → Ruby/lib}/mini_profiler/client_timer_struct.rb +1 -1
- data/{lib → Ruby/lib}/mini_profiler/config.rb +57 -52
- data/{lib → Ruby/lib}/mini_profiler/context.rb +11 -10
- data/Ruby/lib/mini_profiler/custom_timer_struct.rb +22 -0
- data/Ruby/lib/mini_profiler/flame_graph.rb +54 -0
- data/Ruby/lib/mini_profiler/gc_profiler.rb +107 -0
- data/{lib → Ruby/lib}/mini_profiler/page_timer_struct.rb +7 -2
- data/{lib → Ruby/lib}/mini_profiler/profiler.rb +206 -196
- data/{lib → Ruby/lib}/mini_profiler/profiling_methods.rb +131 -100
- data/{lib → Ruby/lib}/mini_profiler/request_timer_struct.rb +20 -1
- data/{lib → Ruby/lib}/mini_profiler/sql_timer_struct.rb +0 -0
- data/{lib → Ruby/lib}/mini_profiler/storage/abstract_store.rb +31 -27
- data/{lib → Ruby/lib}/mini_profiler/storage/file_store.rb +111 -109
- data/Ruby/lib/mini_profiler/storage/memcache_store.rb +53 -0
- data/{lib → Ruby/lib}/mini_profiler/storage/memory_store.rb +65 -63
- data/Ruby/lib/mini_profiler/storage/redis_store.rb +54 -0
- data/{lib → Ruby/lib}/mini_profiler/timer_struct.rb +0 -0
- data/Ruby/lib/mini_profiler/version.rb +5 -0
- data/{lib → Ruby/lib}/mini_profiler_rails/railtie.rb +3 -2
- data/Ruby/lib/patches/net_patches.rb +14 -0
- data/{lib → Ruby/lib}/patches/sql_patches.rb +89 -48
- data/{lib → Ruby/lib}/rack-mini-profiler.rb +2 -1
- data/rack-mini-profiler.gemspec +8 -6
- metadata +56 -65
- data/CHANGELOG +0 -93
- data/lib/html/includes.css +0 -75
- data/lib/html/profile_handler.js +0 -62
- data/lib/mini_profiler/storage/redis_store.rb +0 -44
@@ -0,0 +1,107 @@
|
|
1
|
+
class Rack::MiniProfiler::GCProfiler
|
2
|
+
|
3
|
+
def object_space_stats
|
4
|
+
stats = {}
|
5
|
+
ids = Set.new
|
6
|
+
i=0
|
7
|
+
ObjectSpace.each_object { |o|
|
8
|
+
begin
|
9
|
+
i = stats[o.class] || 0
|
10
|
+
i += 1
|
11
|
+
stats[o.class] = i
|
12
|
+
ids << o.object_id if Integer === o.object_id
|
13
|
+
rescue NoMethodError
|
14
|
+
# Redis::Future undefines .class and .object_id super weird
|
15
|
+
end
|
16
|
+
}
|
17
|
+
{:stats => stats, :ids => ids}
|
18
|
+
end
|
19
|
+
|
20
|
+
def diff_object_stats(before,after)
|
21
|
+
diff = {}
|
22
|
+
after.each do |k,v|
|
23
|
+
diff[k] = v - (before[k] || 0)
|
24
|
+
end
|
25
|
+
before.each do |k,v|
|
26
|
+
diff[k] = 0 - v unless after[k]
|
27
|
+
end
|
28
|
+
|
29
|
+
diff
|
30
|
+
end
|
31
|
+
|
32
|
+
def analyze_strings(ids_before,ids_after)
|
33
|
+
result = {}
|
34
|
+
ids_after.each do |id|
|
35
|
+
obj = ObjectSpace._id2ref(id)
|
36
|
+
if String === obj && !ids_before.include?(obj.object_id)
|
37
|
+
result[obj] ||= 0
|
38
|
+
result[obj] += 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
def profile_gc_time(app,env)
|
45
|
+
body = []
|
46
|
+
|
47
|
+
begin
|
48
|
+
GC::Profiler.clear
|
49
|
+
GC::Profiler.enable
|
50
|
+
b = app.call(env)[2]
|
51
|
+
b.close if b.respond_to? :close
|
52
|
+
body << "GC Profiler ran during this request, if it fired you will see the cost below:\n\n"
|
53
|
+
body << GC::Profiler.result
|
54
|
+
ensure
|
55
|
+
GC.enable
|
56
|
+
GC::Profiler.disable
|
57
|
+
end
|
58
|
+
|
59
|
+
return [200, {'Content-Type' => 'text/plain'}, body]
|
60
|
+
end
|
61
|
+
|
62
|
+
def profile_gc(app,env)
|
63
|
+
|
64
|
+
body = [];
|
65
|
+
|
66
|
+
stat_before,stat_after,diff,string_analysis = nil
|
67
|
+
begin
|
68
|
+
GC.disable
|
69
|
+
stat_before = object_space_stats
|
70
|
+
b = app.call(env)[2]
|
71
|
+
b.close if b.respond_to? :close
|
72
|
+
stat_after = object_space_stats
|
73
|
+
|
74
|
+
diff = diff_object_stats(stat_before[:stats],stat_after[:stats])
|
75
|
+
string_analysis = analyze_strings(stat_before[:ids], stat_after[:ids])
|
76
|
+
ensure
|
77
|
+
GC.enable
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
body << "
|
82
|
+
ObjectSpace delta caused by request:
|
83
|
+
--------------------------------------------\n"
|
84
|
+
diff.to_a.reject{|k,v| v == 0}.sort{|x,y| y[1] <=> x[1]}.each do |k,v|
|
85
|
+
body << "#{k} : #{v}\n" if v != 0
|
86
|
+
end
|
87
|
+
|
88
|
+
body << "\n
|
89
|
+
ObjectSpace stats:
|
90
|
+
-----------------\n"
|
91
|
+
|
92
|
+
stat_after[:stats].to_a.sort{|x,y| y[1] <=> x[1]}.each do |k,v|
|
93
|
+
body << "#{k} : #{v}\n"
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
body << "\n
|
98
|
+
String stats:
|
99
|
+
------------\n"
|
100
|
+
|
101
|
+
string_analysis.to_a.sort{|x,y| y[1] <=> x[1] }.take(1000).each do |string,count|
|
102
|
+
body << "#{count} : #{string}\n"
|
103
|
+
end
|
104
|
+
|
105
|
+
return [200, {'Content-Type' => 'text/plain'}, body]
|
106
|
+
end
|
107
|
+
end
|
@@ -7,6 +7,7 @@ module Rack
|
|
7
7
|
# Root: RequestTimer
|
8
8
|
# :has_many RequestTimer children
|
9
9
|
# :has_many SqlTimer children
|
10
|
+
# :has_many CustomTimer children
|
10
11
|
class PageTimerStruct < TimerStruct
|
11
12
|
def initialize(env)
|
12
13
|
super("Id" => MiniProfiler.generate_id,
|
@@ -27,7 +28,10 @@ module Rack
|
|
27
28
|
"HasDuplicateSqlTimings" => false,
|
28
29
|
"ExecutedReaders" => 0,
|
29
30
|
"ExecutedScalars" => 0,
|
30
|
-
"ExecutedNonQueries" => 0
|
31
|
+
"ExecutedNonQueries" => 0,
|
32
|
+
"CustomTimingNames" => [],
|
33
|
+
"CustomTimingStats" => {}
|
34
|
+
)
|
31
35
|
name = "#{env['REQUEST_METHOD']} http://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
|
32
36
|
self['Root'] = RequestTimerStruct.createRoot(name, self)
|
33
37
|
end
|
@@ -43,7 +47,8 @@ module Rack
|
|
43
47
|
def to_json(*a)
|
44
48
|
attribs = @attributes.merge(
|
45
49
|
"Started" => '/Date(%d)/' % @attributes['Started'],
|
46
|
-
"DurationMilliseconds" => @attributes['Root']['DurationMilliseconds']
|
50
|
+
"DurationMilliseconds" => @attributes['Root']['DurationMilliseconds'],
|
51
|
+
"CustomTimingNames" => @attributes['CustomTimingStats'].keys.sort
|
47
52
|
)
|
48
53
|
::JSON.generate(attribs, :max_nesting => 100)
|
49
54
|
end
|
@@ -2,11 +2,14 @@ require 'json'
|
|
2
2
|
require 'timeout'
|
3
3
|
require 'thread'
|
4
4
|
|
5
|
+
require 'mini_profiler/version'
|
5
6
|
require 'mini_profiler/page_timer_struct'
|
6
7
|
require 'mini_profiler/sql_timer_struct'
|
8
|
+
require 'mini_profiler/custom_timer_struct'
|
7
9
|
require 'mini_profiler/client_timer_struct'
|
8
10
|
require 'mini_profiler/request_timer_struct'
|
9
11
|
require 'mini_profiler/storage/abstract_store'
|
12
|
+
require 'mini_profiler/storage/memcache_store'
|
10
13
|
require 'mini_profiler/storage/memory_store'
|
11
14
|
require 'mini_profiler/storage/redis_store'
|
12
15
|
require 'mini_profiler/storage/file_store'
|
@@ -14,15 +17,15 @@ require 'mini_profiler/config'
|
|
14
17
|
require 'mini_profiler/profiling_methods'
|
15
18
|
require 'mini_profiler/context'
|
16
19
|
require 'mini_profiler/client_settings'
|
20
|
+
require 'mini_profiler/gc_profiler'
|
21
|
+
require 'mini_profiler/flame_graph'
|
17
22
|
|
18
23
|
module Rack
|
19
24
|
|
20
|
-
|
25
|
+
class MiniProfiler
|
21
26
|
|
22
|
-
|
27
|
+
class << self
|
23
28
|
|
24
|
-
class << self
|
25
|
-
|
26
29
|
include Rack::MiniProfiler::ProfilingMethods
|
27
30
|
|
28
31
|
def generate_id
|
@@ -42,7 +45,7 @@ module Rack
|
|
42
45
|
return @share_template unless @share_template.nil?
|
43
46
|
@share_template = ::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__)))
|
44
47
|
end
|
45
|
-
|
48
|
+
|
46
49
|
def current
|
47
50
|
Thread.current[:mini_profiler_private]
|
48
51
|
end
|
@@ -76,82 +79,83 @@ module Rack
|
|
76
79
|
def request_authorized?
|
77
80
|
Thread.current[:mp_authorized]
|
78
81
|
end
|
82
|
+
|
79
83
|
end
|
80
84
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
+
#
|
86
|
+
# options:
|
87
|
+
# :auto_inject - should script be automatically injected on every html page (not xhr)
|
88
|
+
def initialize(app, config = nil)
|
85
89
|
MiniProfiler.config.merge!(config)
|
86
|
-
@config = MiniProfiler.config
|
87
|
-
|
88
|
-
|
90
|
+
@config = MiniProfiler.config
|
91
|
+
@app = app
|
92
|
+
@config.base_url_path << "/" unless @config.base_url_path.end_with? "/"
|
89
93
|
unless @config.storage_instance
|
90
94
|
@config.storage_instance = @config.storage.new(@config.storage_options)
|
91
95
|
end
|
92
|
-
@storage = @config.storage_instance
|
93
|
-
|
94
|
-
|
96
|
+
@storage = @config.storage_instance
|
97
|
+
end
|
98
|
+
|
95
99
|
def user(env)
|
96
100
|
@config.user_provider.call(env)
|
97
101
|
end
|
98
102
|
|
99
|
-
|
100
|
-
|
103
|
+
def serve_results(env)
|
104
|
+
request = Rack::Request.new(env)
|
101
105
|
id = request['id']
|
102
|
-
|
106
|
+
page_struct = @storage.load(id)
|
103
107
|
unless page_struct
|
104
|
-
@storage.set_viewed(user(env), id)
|
105
|
-
return [404, {}, ["Request not found: #{request['id']} - user #{user(env)}"]]
|
108
|
+
@storage.set_viewed(user(env), id)
|
109
|
+
return [404, {}, ["Request not found: #{request['id']} - user #{user(env)}"]]
|
106
110
|
end
|
107
|
-
|
111
|
+
unless page_struct['HasUserViewed']
|
108
112
|
page_struct['ClientTimings'] = ClientTimerStruct.init_from_form_data(env, page_struct)
|
109
|
-
|
110
|
-
@storage.save(page_struct)
|
111
|
-
@storage.set_viewed(user(env), id)
|
112
|
-
|
113
|
+
page_struct['HasUserViewed'] = true
|
114
|
+
@storage.save(page_struct)
|
115
|
+
@storage.set_viewed(user(env), id)
|
116
|
+
end
|
113
117
|
|
114
118
|
result_json = page_struct.to_json
|
115
119
|
# If we're an XMLHttpRequest, serve up the contents as JSON
|
116
120
|
if request.xhr?
|
117
|
-
|
121
|
+
[200, { 'Content-Type' => 'application/json'}, [result_json]]
|
118
122
|
else
|
119
123
|
|
120
124
|
# Otherwise give the HTML back
|
121
|
-
html = MiniProfiler.share_template.dup
|
122
|
-
html.gsub!(/\{path\}/, @config.base_url_path)
|
123
|
-
html.gsub!(/\{version\}/, MiniProfiler::VERSION)
|
125
|
+
html = MiniProfiler.share_template.dup
|
126
|
+
html.gsub!(/\{path\}/, @config.base_url_path)
|
127
|
+
html.gsub!(/\{version\}/, MiniProfiler::VERSION)
|
124
128
|
html.gsub!(/\{json\}/, result_json)
|
125
129
|
html.gsub!(/\{includes\}/, get_profile_script(env))
|
126
130
|
html.gsub!(/\{name\}/, page_struct['Name'])
|
127
|
-
html.gsub!(/\{duration\}/, page_struct.duration_ms
|
128
|
-
|
131
|
+
html.gsub!(/\{duration\}/, "%.1f" % page_struct.duration_ms)
|
132
|
+
|
129
133
|
[200, {'Content-Type' => 'text/html'}, [html]]
|
130
134
|
end
|
131
135
|
|
132
|
-
|
136
|
+
end
|
133
137
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
138
|
+
def serve_html(env)
|
139
|
+
file_name = env['PATH_INFO'][(@config.base_url_path.length)..1000]
|
140
|
+
return serve_results(env) if file_name.eql?('results')
|
141
|
+
full_path = ::File.expand_path("../html/#{file_name}", ::File.dirname(__FILE__))
|
142
|
+
return [404, {}, ["Not found"]] unless ::File.exists? full_path
|
143
|
+
f = Rack::File.new nil
|
144
|
+
f.path = full_path
|
141
145
|
|
142
|
-
begin
|
146
|
+
begin
|
143
147
|
f.cache_control = "max-age:86400"
|
144
148
|
f.serving env
|
145
149
|
rescue
|
146
|
-
# old versions of rack have a different api
|
150
|
+
# old versions of rack have a different api
|
147
151
|
status, headers, body = f.serving
|
148
152
|
headers.merge! 'Cache-Control' => "max-age:86400"
|
149
153
|
[status, headers, body]
|
150
154
|
end
|
151
155
|
|
152
|
-
|
156
|
+
end
|
157
|
+
|
153
158
|
|
154
|
-
|
155
159
|
def current
|
156
160
|
MiniProfiler.current
|
157
161
|
end
|
@@ -166,7 +170,7 @@ module Rack
|
|
166
170
|
end
|
167
171
|
|
168
172
|
|
169
|
-
|
173
|
+
def call(env)
|
170
174
|
|
171
175
|
client_settings = ClientSettings.new(env)
|
172
176
|
|
@@ -176,20 +180,20 @@ module Rack
|
|
176
180
|
|
177
181
|
skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env)) ||
|
178
182
|
(@config.skip_paths && @config.skip_paths.any?{ |p| path[0,p.length] == p}) ||
|
179
|
-
query_string =~ /pp=skip/
|
180
|
-
|
183
|
+
query_string =~ /pp=skip/
|
184
|
+
|
181
185
|
has_profiling_cookie = client_settings.has_cookie?
|
182
|
-
|
186
|
+
|
183
187
|
if skip_it || (@config.authorization_mode == :whitelist && !has_profiling_cookie)
|
184
188
|
status,headers,body = @app.call(env)
|
185
|
-
if !skip_it && @config.authorization_mode == :whitelist && !has_profiling_cookie && MiniProfiler.request_authorized?
|
186
|
-
client_settings.write!(headers)
|
189
|
+
if !skip_it && @config.authorization_mode == :whitelist && !has_profiling_cookie && MiniProfiler.request_authorized?
|
190
|
+
client_settings.write!(headers)
|
187
191
|
end
|
188
192
|
return [status,headers,body]
|
189
193
|
end
|
190
194
|
|
191
195
|
# handle all /mini-profiler requests here
|
192
|
-
|
196
|
+
return serve_html(env) if path.start_with? @config.base_url_path
|
193
197
|
|
194
198
|
has_disable_cookie = client_settings.disable_profiling?
|
195
199
|
# manual session disable / enable
|
@@ -206,10 +210,16 @@ module Rack
|
|
206
210
|
client_settings.disable_profiling = true
|
207
211
|
client_settings.write!(headers)
|
208
212
|
return [status,headers,body]
|
213
|
+
else
|
214
|
+
client_settings.disable_profiling = false
|
209
215
|
end
|
210
216
|
|
211
217
|
if query_string =~ /pp=profile-gc/
|
212
|
-
|
218
|
+
if query_string =~ /pp=profile-gc-time/
|
219
|
+
return Rack::MiniProfiler::GCProfiler.new.profile_gc_time(@app, env)
|
220
|
+
else
|
221
|
+
return Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env)
|
222
|
+
end
|
213
223
|
end
|
214
224
|
|
215
225
|
MiniProfiler.create_current(env, @config)
|
@@ -229,31 +239,26 @@ module Rack
|
|
229
239
|
done_sampling = false
|
230
240
|
quit_sampler = false
|
231
241
|
backtraces = nil
|
232
|
-
|
233
|
-
if query_string =~ /pp=sample/
|
242
|
+
|
243
|
+
if query_string =~ /pp=sample/ || query_string =~ /pp=flamegraph/
|
244
|
+
current.measure = false
|
234
245
|
skip_frames = 0
|
235
246
|
backtraces = []
|
236
247
|
t = Thread.current
|
237
|
-
|
238
|
-
begin
|
239
|
-
require 'stacktrace'
|
240
|
-
skip_frames = stacktrace.length
|
241
|
-
rescue LoadError
|
242
|
-
stacktrace_installed = false
|
243
|
-
end
|
244
248
|
|
245
249
|
Thread.new {
|
250
|
+
# new in Ruby 2.0
|
251
|
+
has_backtrace_locations = t.respond_to?(:backtrace_locations)
|
246
252
|
begin
|
247
|
-
i = 10000 # for sanity never grab more than 10k samples
|
253
|
+
i = 10000 # for sanity never grab more than 10k samples
|
248
254
|
while i > 0
|
249
255
|
break if done_sampling
|
250
256
|
i -= 1
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
sleep 0.001
|
257
|
+
backtraces << (has_backtrace_locations ? t.backtrace_locations : t.backtrace)
|
258
|
+
|
259
|
+
# On my machine using Ruby 2.0 this give me excellent fidelity of stack trace per 1.2ms
|
260
|
+
# with this fidelity analysis becomes very powerful
|
261
|
+
sleep 0.0005
|
257
262
|
end
|
258
263
|
ensure
|
259
264
|
quit_sampler = true
|
@@ -261,11 +266,11 @@ module Rack
|
|
261
266
|
}
|
262
267
|
end
|
263
268
|
|
264
|
-
|
265
|
-
start = Time.now
|
266
|
-
begin
|
269
|
+
status, headers, body = nil
|
270
|
+
start = Time.now
|
271
|
+
begin
|
267
272
|
|
268
|
-
# Strip all the caching headers so we don't get 304s back
|
273
|
+
# Strip all the caching headers so we don't get 304s back
|
269
274
|
# This solves a very annoying bug where rack mini profiler never shows up
|
270
275
|
env['HTTP_IF_MODIFIED_SINCE'] = nil
|
271
276
|
env['HTTP_IF_NONE_MATCH'] = nil
|
@@ -273,17 +278,18 @@ module Rack
|
|
273
278
|
status,headers,body = @app.call(env)
|
274
279
|
client_settings.write!(headers)
|
275
280
|
ensure
|
276
|
-
if backtraces
|
281
|
+
if backtraces
|
277
282
|
done_sampling = true
|
278
283
|
sleep 0.001 until quit_sampler
|
279
284
|
end
|
280
285
|
end
|
281
286
|
|
282
287
|
skip_it = current.discard
|
288
|
+
|
283
289
|
if (config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
|
284
290
|
# this is non-obvious, don't kill the profiling cookie on errors or short requests
|
285
291
|
# this ensures that stuff that never reaches the rails stack does not kill profiling
|
286
|
-
if status == 200 && ((Time.now - start) > 0.1)
|
292
|
+
if status == 200 && ((Time.now - start) > 0.1)
|
287
293
|
client_settings.discard_cookie!(headers)
|
288
294
|
end
|
289
295
|
skip_it = true
|
@@ -301,43 +307,49 @@ module Rack
|
|
301
307
|
body.close if body.respond_to? :close
|
302
308
|
return help(client_settings)
|
303
309
|
end
|
304
|
-
|
310
|
+
|
305
311
|
page_struct = current.page_struct
|
306
|
-
|
312
|
+
page_struct['User'] = user(env)
|
313
|
+
page_struct['Root'].record_time((Time.now - start) * 1000)
|
307
314
|
|
308
315
|
if backtraces
|
309
316
|
body.close if body.respond_to? :close
|
310
|
-
|
317
|
+
if query_string =~ /pp=sample/
|
318
|
+
return analyze(backtraces, page_struct)
|
319
|
+
else
|
320
|
+
return flame_graph(backtraces, page_struct)
|
321
|
+
end
|
311
322
|
end
|
312
|
-
|
323
|
+
|
313
324
|
|
314
325
|
# no matter what it is, it should be unviewed, otherwise we will miss POST
|
315
|
-
@storage.set_unviewed(
|
316
|
-
|
317
|
-
|
326
|
+
@storage.set_unviewed(page_struct['User'], page_struct['Id'])
|
327
|
+
@storage.save(page_struct)
|
328
|
+
|
318
329
|
# inject headers, script
|
319
|
-
|
330
|
+
if status == 200
|
320
331
|
|
321
332
|
client_settings.write!(headers)
|
322
|
-
|
333
|
+
|
323
334
|
# mini profiler is meddling with stuff, we can not cache cause we will get incorrect data
|
324
335
|
# Rack::ETag has already inserted some nonesense in the chain
|
325
336
|
headers.delete('ETag')
|
326
337
|
headers.delete('Date')
|
327
338
|
headers['Cache-Control'] = 'must-revalidate, private, max-age=0'
|
328
339
|
|
329
|
-
|
340
|
+
# inject header
|
330
341
|
if headers.is_a? Hash
|
331
342
|
headers['X-MiniProfiler-Ids'] = ids_json(env)
|
332
343
|
end
|
333
344
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
345
|
+
# inject script
|
346
|
+
if current.inject_js \
|
347
|
+
&& headers.has_key?('Content-Type') \
|
348
|
+
&& !headers['Content-Type'].match(/text\/html/).nil? then
|
349
|
+
|
339
350
|
response = Rack::Response.new([], status, headers)
|
340
351
|
script = self.get_profile_script(env)
|
352
|
+
|
341
353
|
if String === body
|
342
354
|
response.write inject(body,script)
|
343
355
|
else
|
@@ -345,34 +357,70 @@ module Rack
|
|
345
357
|
end
|
346
358
|
body.close if body.respond_to? :close
|
347
359
|
return response.finish
|
348
|
-
|
349
|
-
|
360
|
+
end
|
361
|
+
end
|
350
362
|
|
351
363
|
client_settings.write!(headers)
|
352
|
-
|
364
|
+
[status, headers, body]
|
353
365
|
ensure
|
354
366
|
# Make sure this always happens
|
355
367
|
current = nil
|
356
|
-
|
368
|
+
end
|
357
369
|
|
358
370
|
def inject(fragment, script)
|
359
|
-
fragment.
|
360
|
-
#
|
361
|
-
|
362
|
-
|
363
|
-
|
371
|
+
if fragment.match(/<\/body>/i)
|
372
|
+
# explicit </body>
|
373
|
+
|
374
|
+
regex = /<\/body>/i
|
375
|
+
close_tag = '</body>'
|
376
|
+
elsif fragment.match(/<\/html>/i)
|
377
|
+
# implicit </body>
|
378
|
+
|
379
|
+
regex = /<\/html>/i
|
380
|
+
close_tag = '</html>'
|
381
|
+
else
|
382
|
+
# implicit </body> and </html>. Just append the script.
|
383
|
+
|
384
|
+
return fragment + script
|
385
|
+
end
|
386
|
+
|
387
|
+
matches = fragment.scan(regex).length
|
388
|
+
index = 1
|
389
|
+
fragment.gsub(regex) do
|
390
|
+
# though malformed there is an edge case where /body exists earlier in the html, work around
|
391
|
+
if index < matches
|
392
|
+
index += 1
|
393
|
+
close_tag
|
364
394
|
else
|
365
|
-
|
395
|
+
|
396
|
+
# if for whatever crazy reason we dont get a utf string,
|
397
|
+
# just force the encoding, no utf in the mp scripts anyway
|
398
|
+
if script.respond_to?(:encoding) && script.respond_to?(:force_encoding)
|
399
|
+
(script + close_tag).force_encoding(fragment.encoding)
|
400
|
+
else
|
401
|
+
script + close_tag
|
402
|
+
end
|
366
403
|
end
|
367
404
|
end
|
368
405
|
end
|
369
406
|
|
370
407
|
def dump_env(env)
|
371
408
|
headers = {'Content-Type' => 'text/plain'}
|
372
|
-
body = ""
|
409
|
+
body = "Rack Environment\n---------------\n"
|
373
410
|
env.each do |k,v|
|
374
411
|
body << "#{k}: #{v}\n"
|
375
412
|
end
|
413
|
+
|
414
|
+
body << "\n\nEnvironment\n---------------\n"
|
415
|
+
ENV.each do |k,v|
|
416
|
+
body << "#{k}: #{v}\n"
|
417
|
+
end
|
418
|
+
|
419
|
+
body << "\n\nInternals\n---------------\n"
|
420
|
+
body << "Storage Provider #{config.storage_instance}\n"
|
421
|
+
body << "User #{user(env)}\n"
|
422
|
+
body << config.storage_instance.diagnostics(user(env)) rescue "no diagnostics implemented for storage"
|
423
|
+
|
376
424
|
[200, headers, [body]]
|
377
425
|
end
|
378
426
|
|
@@ -385,72 +433,29 @@ module Rack
|
|
385
433
|
pp=skip : skip mini profiler for this request
|
386
434
|
pp=no-backtrace #{"(*) " if client_settings.backtrace_none?}: don't collect stack traces from all the SQL executed (sticky, use pp=normal-backtrace to enable)
|
387
435
|
pp=normal-backtrace #{"(*) " if client_settings.backtrace_default?}: collect stack traces from all the SQL executed and filter normally
|
388
|
-
pp=full-backtrace #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use pp=normal-backtrace to disable)
|
389
|
-
pp=sample : sample stack traces and return a report isolating heavy usage (
|
390
|
-
pp=disable : disable profiling for this session
|
436
|
+
pp=full-backtrace #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use pp=normal-backtrace to disable)
|
437
|
+
pp=sample : sample stack traces and return a report isolating heavy usage (works best on Ruby 2.0)
|
438
|
+
pp=disable : disable profiling for this session
|
391
439
|
pp=enable : enable profiling for this session (if previously disabled)
|
392
|
-
pp=profile-gc: perform gc profiling on this request (ruby 1.9.3 only)
|
440
|
+
pp=profile-gc: perform gc profiling on this request, analyzes ObjectSpace generated by request (ruby 1.9.3 only)
|
441
|
+
pp=profile-gc-time: perform built-in gc profiling on this request (ruby 1.9.3 only)
|
442
|
+
pp=flamegraph: works best on Ruby 2.0, a graph representing sampled activity.
|
393
443
|
"
|
394
|
-
|
444
|
+
|
395
445
|
client_settings.write!(headers)
|
396
446
|
[200, headers, [body]]
|
397
447
|
end
|
398
448
|
|
449
|
+
def flame_graph(traces, page_struct)
|
450
|
+
graph = FlameGraph.new(traces)
|
451
|
+
data = graph.graph_data
|
399
452
|
|
400
|
-
|
401
|
-
stats = {}
|
402
|
-
ObjectSpace.each_object { |o|
|
403
|
-
stats[o.class] ||= 1
|
404
|
-
stats[o.class] += 1
|
405
|
-
}
|
406
|
-
stats
|
407
|
-
end
|
408
|
-
|
409
|
-
def diff_object_stats(before,after)
|
410
|
-
diff = {}
|
411
|
-
after.each do |k,v|
|
412
|
-
diff[k] = v - (before[k] || 0)
|
413
|
-
end
|
414
|
-
before.each do |k,v|
|
415
|
-
diff[k] = 0 - v unless after[k]
|
416
|
-
end
|
417
|
-
|
418
|
-
diff
|
419
|
-
end
|
420
|
-
|
421
|
-
def profile_gc(env)
|
453
|
+
headers = {'Content-Type' => 'text/html'}
|
422
454
|
|
423
|
-
body =
|
424
|
-
|
425
|
-
stat_before = object_space_stats
|
426
|
-
begin
|
427
|
-
GC::Profiler.clear
|
428
|
-
GC::Profiler.enable
|
429
|
-
@app.call(env)
|
430
|
-
body << GC::Profiler.result
|
431
|
-
ensure
|
432
|
-
GC::Profiler.disable
|
433
|
-
end
|
434
|
-
stat_after = object_space_stats
|
435
|
-
|
436
|
-
diff = diff_object_stats(stat_before,stat_after)
|
437
|
-
|
438
|
-
body << "
|
439
|
-
ObjectSpace delta caused by request:
|
440
|
-
--------------------------------------------\n"
|
441
|
-
diff.to_a.reject{|k,v| v == 0}.sort{|x,y| y[1] <=> x[1]}.each do |k,v|
|
442
|
-
body << "#{k} : #{v}\n" if v != 0
|
443
|
-
end
|
455
|
+
body = IO.read(::File.expand_path('../html/flamegraph.html', ::File.dirname(__FILE__)))
|
456
|
+
body.gsub!("/*DATA*/", ::JSON.generate(data));
|
444
457
|
|
445
|
-
|
446
|
-
ObjectSpace stats:
|
447
|
-
-----------------\n"
|
448
|
-
|
449
|
-
stat_after.to_a.sort{|x,y| y[1] <=> x[1]}.each do |k,v|
|
450
|
-
body << "#{k} : #{v}\n"
|
451
|
-
end
|
452
|
-
|
453
|
-
return [200, {'Content-Type' => 'text/plain'}, body]
|
458
|
+
[200, headers, [body]]
|
454
459
|
end
|
455
460
|
|
456
461
|
def analyze(traces, page_struct)
|
@@ -459,11 +464,11 @@ ObjectSpace stats:
|
|
459
464
|
|
460
465
|
seen = {}
|
461
466
|
fulldump = ""
|
462
|
-
traces.each do |trace|
|
467
|
+
traces.each do |trace|
|
463
468
|
fulldump << "\n\n"
|
464
469
|
distinct = {}
|
465
470
|
trace.each do |frame|
|
466
|
-
frame =
|
471
|
+
frame = frame.to_s unless String === frame
|
467
472
|
unless distinct[frame]
|
468
473
|
distinct[frame] = true
|
469
474
|
seen[frame] ||= 0
|
@@ -479,7 +484,7 @@ ObjectSpace stats:
|
|
479
484
|
body << "#{name} x #{count}\n"
|
480
485
|
end
|
481
486
|
end
|
482
|
-
|
487
|
+
|
483
488
|
body << "\n\n\nRaw traces \n"
|
484
489
|
body << fulldump
|
485
490
|
|
@@ -492,43 +497,48 @@ ObjectSpace stats:
|
|
492
497
|
::JSON.generate(ids.uniq)
|
493
498
|
end
|
494
499
|
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
500
|
+
def ids_comma_separated(env)
|
501
|
+
# cap at 10 ids, otherwise there is a chance you can blow the header
|
502
|
+
ids = [current.page_struct["Id"]] + (@storage.get_unviewed_ids(user(env)) || [])[0..8]
|
503
|
+
ids.join(",")
|
504
|
+
end
|
505
|
+
|
506
|
+
# get_profile_script returns script to be injected inside current html page
|
507
|
+
# By default, profile_script is appended to the end of all html requests automatically.
|
508
|
+
# Calling get_profile_script cancels automatic append for the current page
|
509
|
+
# Use it when:
|
510
|
+
# * you have disabled auto append behaviour throught :auto_inject => false flag
|
511
|
+
# * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
|
512
|
+
def get_profile_script(env)
|
513
|
+
ids = ids_comma_separated(env)
|
514
|
+
path = @config.base_url_path
|
515
|
+
version = MiniProfiler::VERSION
|
516
|
+
position = @config.position
|
517
|
+
showTrivial = false
|
518
|
+
showChildren = false
|
519
|
+
maxTracesToShow = 10
|
520
|
+
showControls = false
|
521
|
+
currentId = current.page_struct["Id"]
|
522
|
+
authorized = true
|
523
|
+
toggleShortcut = @config.toggle_shortcut
|
524
|
+
startHidden = @config.start_hidden
|
525
|
+
# TODO : cache this snippet
|
526
|
+
script = IO.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
|
527
|
+
# replace the variables
|
528
|
+
[:ids, :path, :version, :position, :showTrivial, :showChildren, :maxTracesToShow, :showControls, :currentId, :authorized, :toggleShortcut, :startHidden].each do |v|
|
529
|
+
regex = Regexp.new("\\{#{v.to_s}\\}")
|
530
|
+
script.gsub!(regex, eval(v.to_s).to_s)
|
531
|
+
end
|
532
|
+
current.inject_js = false
|
533
|
+
script
|
534
|
+
end
|
535
|
+
|
536
|
+
# cancels automatic injection of profile script for the current page
|
537
|
+
def cancel_auto_inject(env)
|
538
|
+
current.inject_js = false
|
539
|
+
end
|
540
|
+
|
541
|
+
end
|
532
542
|
|
533
543
|
end
|
534
544
|
|