devver-rack-contrib 0.9.5 → 0.9.6
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/rack/contrib/perftools_profiler.rb +246 -201
- data/rack-contrib.gemspec +1 -1
- metadata +2 -2
@@ -1,14 +1,39 @@
|
|
1
|
+
require 'rack'
|
1
2
|
require 'perftools'
|
2
3
|
require 'rbconfig'
|
3
4
|
require 'pstore'
|
4
5
|
|
5
|
-
#
|
6
|
+
# REQUIREMENTS
|
7
|
+
|
8
|
+
# You'll need graphviz to generate call graphs using dot (for the GIF printer):
|
6
9
|
#
|
7
10
|
# sudo port install graphviz # osx
|
8
11
|
# sudo apt-get install graphviz # debian/ubuntu
|
9
12
|
|
10
|
-
# You'll need ps2pdf to generate PDFs
|
11
|
-
#
|
13
|
+
# You'll need ps2pdf to generate PDFs
|
14
|
+
# On OS X, ps2pdf comes is installed as part of Ghostscript
|
15
|
+
#
|
16
|
+
# sudo port install ghostscript # osx
|
17
|
+
# brew install ghostscript # homebrew
|
18
|
+
# sudo apt-get install ps2pdf # debian/ubuntu
|
19
|
+
|
20
|
+
# USAGE
|
21
|
+
|
22
|
+
# To configure your Rack app to use PerftoolsProfiler, call the 'use' method
|
23
|
+
#
|
24
|
+
# use Rack::PerftoolsProfiler, options
|
25
|
+
#
|
26
|
+
# For example:
|
27
|
+
# use Rack::PerftoolsProfiler, :default_printer => 'gif'
|
28
|
+
|
29
|
+
# OPTIONS
|
30
|
+
|
31
|
+
# :default_printer - can be set to 'text', 'gif', or 'pdf'. Default is :text
|
32
|
+
# :mode - can be set to 'cputime' or 'walltime'. Default is :cputime
|
33
|
+
# :frequency - in :cputime mode, the number of times per second the app will be sampled.
|
34
|
+
# Default is 100 (times/sec)
|
35
|
+
|
36
|
+
# MODES
|
12
37
|
|
13
38
|
# There are two modes for the profiler
|
14
39
|
#
|
@@ -34,208 +59,165 @@ require 'pstore'
|
|
34
59
|
# In this mode, all responses are normal. You must visit __stop__ to complete profiling and
|
35
60
|
# then you can view the profiling data by visiting __data__
|
36
61
|
|
62
|
+
# PROFILING DATA OPTIONS
|
63
|
+
#
|
64
|
+
# In both simple and start/stop modes, you can add additional params to change how the data
|
65
|
+
# is displayed. In simple mode, these params are just added to the URL being profiled. In
|
66
|
+
# start/stop mode, they are added to the __data__ URL
|
37
67
|
|
38
|
-
|
68
|
+
# printer - overrides the default_printer option (see above)
|
69
|
+
# ignore - a regular expression of the area of code to ignore
|
70
|
+
# focus - a regular expression of the area of code to solely focus on.
|
39
71
|
|
40
|
-
|
72
|
+
# (for ignore and focus, please see http://google-perftools.googlecode.com/svn/trunk/doc/cpuprofile.html#pprof
|
73
|
+
# for more details)
|
41
74
|
|
42
|
-
|
75
|
+
module Rack
|
76
|
+
|
77
|
+
class PerftoolsProfiler
|
78
|
+
include Rack::Utils
|
43
79
|
|
44
80
|
PRINTER_CONTENT_TYPE = {
|
45
81
|
:text => 'text/plain',
|
46
82
|
:gif => 'image/gif',
|
47
83
|
:pdf => 'application/pdf'
|
48
84
|
}
|
49
|
-
|
50
|
-
def
|
51
|
-
|
52
|
-
@profiler = profiler
|
85
|
+
|
86
|
+
def self.clear_data
|
87
|
+
Profiler.clear_data
|
53
88
|
end
|
54
89
|
|
55
|
-
def self.
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
StartProfiling.new(request, profiler)
|
60
|
-
when '/__stop__'
|
61
|
-
StopProfiling.new(request, profiler)
|
62
|
-
when '/__data__'
|
63
|
-
ReturnData.new(request, profiler)
|
64
|
-
else
|
65
|
-
CallAppDirectly.new(request, profiler)
|
66
|
-
end
|
90
|
+
def self.with_profiling_off(app, options = {})
|
91
|
+
instance = self.new(app, options)
|
92
|
+
instance.force_stop
|
93
|
+
instance
|
67
94
|
end
|
68
95
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
def act
|
74
|
-
@profiler.start
|
96
|
+
def initialize(app, options = {})
|
97
|
+
@app = app
|
98
|
+
@profiler = Profiler.new(@app, options)
|
75
99
|
end
|
76
|
-
|
77
|
-
def
|
78
|
-
|
100
|
+
|
101
|
+
def call(env)
|
102
|
+
action = Action.for_env(env.clone, @profiler, self)
|
103
|
+
action.act
|
104
|
+
action.response
|
79
105
|
end
|
80
106
|
|
81
|
-
|
107
|
+
def call_app(env)
|
108
|
+
@app.call(env)
|
109
|
+
end
|
82
110
|
|
83
|
-
|
84
|
-
|
85
|
-
def act
|
111
|
+
def force_stop
|
86
112
|
@profiler.stop
|
87
113
|
end
|
88
114
|
|
89
|
-
def
|
90
|
-
|
115
|
+
def profiler_data_response(profiling_data)
|
116
|
+
format, body = profiling_data
|
117
|
+
if format==:none
|
118
|
+
[404, {'Content-Type' => 'text/plain'}, ['No profiling data available.']]
|
119
|
+
else
|
120
|
+
[200, headers(format, body), Array(body)]
|
121
|
+
end
|
91
122
|
end
|
92
123
|
|
93
|
-
|
94
|
-
|
95
|
-
class ProfileOnce < Action
|
96
|
-
|
97
|
-
def self.has_special_param?(request)
|
98
|
-
request.params['profile'] != nil
|
99
|
-
end
|
124
|
+
private
|
100
125
|
|
101
|
-
def
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
126
|
+
def headers(printer, body)
|
127
|
+
headers = {
|
128
|
+
'Content-Type' => PRINTER_CONTENT_TYPE[printer],
|
129
|
+
'Content-Length' => content_length(body)
|
130
|
+
}
|
131
|
+
if printer==:pdf
|
132
|
+
filetype = printer
|
133
|
+
filename='profile_data'
|
134
|
+
headers['Content-Disposition'] = %(attachment; filename="#{filename}.#{filetype}")
|
135
|
+
end
|
136
|
+
headers
|
110
137
|
end
|
111
138
|
|
112
|
-
def
|
113
|
-
|
139
|
+
def content_length(body)
|
140
|
+
body.inject(0) { |len, part| len + bytesize(part) }.to_s
|
114
141
|
end
|
115
142
|
|
116
|
-
def delete_custom_params(env)
|
117
|
-
env.delete('profile')
|
118
|
-
env.delete('times')
|
119
|
-
env.delete('printer')
|
120
|
-
env
|
121
|
-
end
|
122
143
|
|
123
|
-
def parse_printer(printer)
|
124
|
-
if printer.nil?
|
125
|
-
DEFAULT_PRINTER
|
126
|
-
else
|
127
|
-
printer.to_sym
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
144
|
end
|
132
145
|
|
133
|
-
class
|
134
|
-
|
135
|
-
def act
|
136
|
-
@result = @profiler.call_without_profile
|
137
|
-
end
|
146
|
+
class Profiler
|
138
147
|
|
139
|
-
def
|
148
|
+
def self.tmpdir
|
149
|
+
dir = nil
|
150
|
+
Dir.chdir Dir.tmpdir do dir = Dir.pwd end # HACK FOR OSX
|
151
|
+
dir
|
140
152
|
end
|
141
153
|
|
142
|
-
|
143
|
-
|
144
|
-
# Pass the :printer option to pick a different result format.
|
145
|
-
class PerftoolsProfiler
|
146
|
-
MODES = %w(
|
147
|
-
process_time
|
148
|
-
#wall_time
|
149
|
-
)
|
150
|
-
|
151
|
-
PROFILING_DATA_FILE = '/tmp/rack_perftools_profiler.data'
|
154
|
+
PROFILING_DATA_FILE = ::File.join(self.tmpdir, 'rack_perftools_profiler.prof')
|
155
|
+
PROFILING_SETTINGS_FILE = ::File.join(self.tmpdir, 'rack_perftools_profiler.config')
|
152
156
|
DEFAULT_PRINTER = :text
|
157
|
+
DEFAULT_MODE = :cputime
|
158
|
+
UNSET_FREQUENCY = -1
|
153
159
|
|
154
|
-
|
155
|
-
:
|
156
|
-
:
|
157
|
-
:
|
158
|
-
|
159
|
-
|
160
|
-
def self.clear_data
|
161
|
-
::File.delete(PROFILING_DATA_FILE) if ::File.exists?(PROFILING_DATA_FILE)
|
160
|
+
def initialize(app, options)
|
161
|
+
@printer = (options.delete(:default_printer) { DEFAULT_PRINTER }).to_sym
|
162
|
+
@frequency = (options.delete(:frequency) { UNSET_FREQUENCY }).to_s
|
163
|
+
@mode = (options.delete(:mode) { DEFAULT_MODE }).to_sym
|
164
|
+
raise ArgumentError, "Invalid option(s): #{options.keys.join(' ')}" unless options.empty?
|
162
165
|
end
|
163
|
-
|
164
|
-
def
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
def initialize(app, options = {})
|
171
|
-
@app = app
|
172
|
-
@printer = parse_printer(options[:printer])
|
173
|
-
@times = (options[:times] || 1).to_i
|
174
|
-
end
|
175
|
-
|
176
|
-
def printer
|
177
|
-
@printer
|
166
|
+
|
167
|
+
def profile
|
168
|
+
start
|
169
|
+
yield
|
170
|
+
ensure
|
171
|
+
stop
|
178
172
|
end
|
179
173
|
|
180
|
-
def
|
181
|
-
|
182
|
-
if enable_profiling_request?
|
183
|
-
action = Action.for_request(@original_request, self)
|
184
|
-
action.act
|
185
|
-
action.response
|
186
|
-
elsif disable_profiling_request?
|
187
|
-
action = Action.for_request(@original_request, self)
|
188
|
-
action.act
|
189
|
-
action.response
|
190
|
-
elsif profiling_data_request?
|
191
|
-
if in_profiling_mode?
|
192
|
-
[400, {'Content-Type' => 'text/plain'}, 'No profiling data available.']
|
193
|
-
else
|
194
|
-
return_profiling_data(env, @printer)
|
195
|
-
end
|
196
|
-
else
|
197
|
-
if mode = profiling?(env.clone)
|
198
|
-
action = Action.for_request(@original_request, self)
|
199
|
-
action.act
|
200
|
-
action.response
|
201
|
-
else
|
202
|
-
@app.call(env)
|
203
|
-
end
|
204
|
-
end
|
174
|
+
def self.clear_data
|
175
|
+
::File.delete(PROFILING_DATA_FILE) if ::File.exists?(PROFILING_DATA_FILE)
|
205
176
|
end
|
206
177
|
|
207
178
|
def start
|
208
|
-
|
179
|
+
set_env_vars
|
180
|
+
PerfTools::CpuProfiler.start(PROFILING_DATA_FILE)
|
181
|
+
self.profiling = true
|
209
182
|
end
|
210
183
|
|
211
184
|
def stop
|
212
|
-
|
185
|
+
PerfTools::CpuProfiler.stop
|
186
|
+
self.profiling = false
|
187
|
+
unset_env_vars
|
213
188
|
end
|
214
189
|
|
215
|
-
def
|
216
|
-
|
217
|
-
|
190
|
+
def profiling?
|
191
|
+
pstore_transaction(true) do |store|
|
192
|
+
store[:profiling?]
|
218
193
|
end
|
219
194
|
end
|
220
195
|
|
221
|
-
def
|
196
|
+
def data(options = {})
|
197
|
+
printer = (options.fetch('printer') {@printer}).to_sym
|
198
|
+
ignore = options.fetch('ignore') { nil }
|
199
|
+
focus = options.fetch('focus') { nil }
|
222
200
|
if ::File.exists?(PROFILING_DATA_FILE)
|
223
|
-
|
201
|
+
args = "--#{printer}"
|
202
|
+
args += " --ignore=#{ignore}" if ignore
|
203
|
+
args += " --focus=#{focus}" if focus
|
204
|
+
cmd = "pprof.rb #{args} #{PROFILING_DATA_FILE}"
|
205
|
+
[printer, `#{cmd}`]
|
224
206
|
else
|
225
|
-
[
|
207
|
+
[:none, nil]
|
226
208
|
end
|
227
209
|
end
|
228
210
|
|
229
|
-
private
|
211
|
+
private
|
230
212
|
|
231
|
-
def
|
232
|
-
|
233
|
-
|
213
|
+
def set_env_vars
|
214
|
+
ENV['CPUPROFILE_REALTIME'] = '1' if @mode == :walltime
|
215
|
+
ENV['CPUPROFILE_FREQUENCY'] = @frequency if @frequency != UNSET_FREQUENCY
|
234
216
|
end
|
235
217
|
|
236
|
-
def
|
237
|
-
|
238
|
-
|
218
|
+
def unset_env_vars
|
219
|
+
ENV.delete('CPUPROFILE_REALTIME')
|
220
|
+
ENV.delete('CPUPROFILE_FREQUENCY')
|
239
221
|
end
|
240
222
|
|
241
223
|
def profiling=(value)
|
@@ -245,86 +227,149 @@ module Rack
|
|
245
227
|
end
|
246
228
|
|
247
229
|
def pstore_transaction(read_only)
|
248
|
-
pstore = PStore.new(
|
230
|
+
pstore = PStore.new(PROFILING_SETTINGS_FILE)
|
249
231
|
pstore.transaction(read_only) do
|
250
232
|
yield pstore if block_given?
|
251
233
|
end
|
252
234
|
end
|
253
235
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
236
|
+
end
|
237
|
+
|
238
|
+
class Action
|
239
|
+
|
240
|
+
def initialize(env, profiler, middleware)
|
241
|
+
@env = env
|
242
|
+
@request = Request.new(env)
|
243
|
+
@data_params = @request.params.clone
|
244
|
+
@profiler = profiler
|
245
|
+
@middleware = middleware
|
258
246
|
end
|
259
247
|
|
260
|
-
def
|
261
|
-
|
248
|
+
def act
|
249
|
+
# do nothing
|
250
|
+
end
|
251
|
+
|
252
|
+
def self.for_env(env, profiler, middleware)
|
253
|
+
request = Request.new(env)
|
254
|
+
klass =
|
255
|
+
case request.path
|
256
|
+
when '/__start__'
|
257
|
+
StartProfiling
|
258
|
+
when '/__stop__'
|
259
|
+
StopProfiling
|
260
|
+
when '/__data__'
|
261
|
+
ReturnData
|
262
|
+
else
|
263
|
+
if ProfileOnce.has_special_param?(request)
|
264
|
+
ProfileOnce
|
265
|
+
else
|
266
|
+
CallAppDirectly
|
267
|
+
end
|
268
|
+
end
|
269
|
+
klass.new(env, profiler, middleware)
|
262
270
|
end
|
263
271
|
|
264
|
-
|
265
|
-
|
272
|
+
end
|
273
|
+
|
274
|
+
class StartProfiling < Action
|
275
|
+
|
276
|
+
def act
|
277
|
+
@profiler.start
|
266
278
|
end
|
267
279
|
|
268
|
-
def
|
269
|
-
|
280
|
+
def response
|
281
|
+
[200, {'Content-Type' => 'text/plain'},
|
282
|
+
[<<-EOS
|
283
|
+
Profiling is now enabled.
|
284
|
+
Visit the URLS that should be profiled.
|
285
|
+
When you are finished, visit /__stop__, then visit /__data__ to view the results.
|
286
|
+
EOS
|
287
|
+
]]
|
270
288
|
end
|
271
289
|
|
272
|
-
|
273
|
-
|
274
|
-
|
290
|
+
end
|
291
|
+
|
292
|
+
class StopProfiling < Action
|
293
|
+
|
294
|
+
def act
|
295
|
+
@profiler.stop
|
275
296
|
end
|
276
297
|
|
277
|
-
def
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
# mode
|
285
|
-
#else
|
286
|
-
#env['rack.errors'].write "Invalid RubyProf measure_mode: " +
|
287
|
-
# "#{mode}. Use one of #{MODES.to_a.join(', ')}"
|
288
|
-
# false
|
289
|
-
#end
|
290
|
-
end
|
291
|
-
end
|
298
|
+
def response
|
299
|
+
[200, {'Content-Type' => 'text/html'},
|
300
|
+
[<<-EOS
|
301
|
+
Profiling is now disabled.
|
302
|
+
Visit /__data__ to view the results.
|
303
|
+
EOS
|
304
|
+
]]
|
292
305
|
end
|
293
306
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
if @profiling
|
300
|
-
|
307
|
+
end
|
308
|
+
|
309
|
+
class ReturnData < Action
|
310
|
+
|
311
|
+
def response
|
312
|
+
if @profiler.profiling?
|
313
|
+
[400, {'Content-Type' => 'text/plain'}, ['No profiling data available.']]
|
301
314
|
else
|
302
|
-
|
315
|
+
@middleware.profiler_data_response(@profiler.data(@data_params))
|
303
316
|
end
|
304
317
|
end
|
305
318
|
|
306
|
-
|
307
|
-
|
308
|
-
|
319
|
+
end
|
320
|
+
|
321
|
+
class ProfileOnce < Action
|
322
|
+
include Rack::Utils
|
323
|
+
|
324
|
+
def self.has_special_param?(request)
|
325
|
+
request.params['profile'] != nil
|
309
326
|
end
|
310
327
|
|
311
|
-
def
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
328
|
+
def initialize(*args)
|
329
|
+
super
|
330
|
+
@times = (Request.new(@env).params.fetch('times') {1}).to_i
|
331
|
+
@new_env = delete_custom_params(@env)
|
332
|
+
end
|
333
|
+
|
334
|
+
def act
|
335
|
+
@profiler.profile do
|
336
|
+
@times.times { @middleware.call_app(@new_env) }
|
317
337
|
end
|
318
|
-
headers
|
319
338
|
end
|
320
339
|
|
321
|
-
def
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
340
|
+
def response
|
341
|
+
@middleware.profiler_data_response(@profiler.data(@data_params))
|
342
|
+
end
|
343
|
+
|
344
|
+
def delete_custom_params(env)
|
345
|
+
new_env = env.clone
|
346
|
+
|
347
|
+
params = Request.new(new_env).params
|
348
|
+
params.delete('profile')
|
349
|
+
params.delete('times')
|
350
|
+
params.delete('printer')
|
351
|
+
params.delete('ignore')
|
352
|
+
params.delete('focus')
|
353
|
+
|
354
|
+
new_env.delete('rack.request.query_string')
|
355
|
+
new_env.delete('rack.request.query_hash')
|
356
|
+
|
357
|
+
new_env['QUERY_STRING'] = build_query(params)
|
358
|
+
new_env
|
327
359
|
end
|
328
360
|
|
329
361
|
end
|
362
|
+
|
363
|
+
class CallAppDirectly < Action
|
364
|
+
|
365
|
+
def act
|
366
|
+
@result = @middleware.call_app(@env)
|
367
|
+
end
|
368
|
+
|
369
|
+
def response
|
370
|
+
@result
|
371
|
+
end
|
372
|
+
|
373
|
+
end
|
374
|
+
|
330
375
|
end
|
data/rack-contrib.gemspec
CHANGED
@@ -3,7 +3,7 @@ Gem::Specification.new do |s|
|
|
3
3
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
4
4
|
|
5
5
|
s.name = 'devver-rack-contrib'
|
6
|
-
s.version = '0.9.
|
6
|
+
s.version = '0.9.6'
|
7
7
|
s.date = '2010-01-10'
|
8
8
|
|
9
9
|
s.description = "The Devver fork of rack-contrib"
|