devver-rack-contrib 0.9.5 → 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,14 +1,39 @@
1
+ require 'rack'
1
2
  require 'perftools'
2
3
  require 'rbconfig'
3
4
  require 'pstore'
4
5
 
5
- # You'll need graphviz to generate call graphs using dot:
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 (comes with ghostscript)
11
- # sudo port install ghostscript
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
- module Rack
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
- class Action
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
- DEFAULT_PRINTER = :text
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 initialize(request, profiler)
51
- @request = request
52
- @profiler = profiler
85
+
86
+ def self.clear_data
87
+ Profiler.clear_data
53
88
  end
54
89
 
55
- def self.for_request(request, profiler)
56
- return ProfileOnce.new(request, profiler) if ProfileOnce.has_special_param?(request)
57
- case request.path
58
- when '/__start__'
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
- end
70
-
71
- class StartProfiling < Action
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 response
78
- Response.new("Profiling is enabled\n", 200, {'Content-Type' => 'text/plain'}).finish
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
- end
107
+ def call_app(env)
108
+ @app.call(env)
109
+ end
82
110
 
83
- class StopProfiling < Action
84
-
85
- def act
111
+ def force_stop
86
112
  @profiler.stop
87
113
  end
88
114
 
89
- def response
90
- Response.new("Profiling is disabled\n", 200, {'Content-Type' => 'text/plain'}).finish
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
- end
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 initialize(request, profiler)
102
- super
103
- @times = (request.params.fetch('times') {1}).to_i
104
- @printer = parse_printer(profiler.printer)
105
- @env = delete_custom_params(request.params)
106
- end
107
-
108
- def act
109
- @profiler.profile_call(@env, @times)
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 response
113
- @profiler.return_profiling_data(@request.params, @printer)
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 CallAppDirectly < Action
134
-
135
- def act
136
- @result = @profiler.call_without_profile
137
- end
146
+ class Profiler
138
147
 
139
- def reponse
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
- end
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
- PRINTER_CONTENT_TYPE = {
155
- :text => 'text/plain',
156
- :gif => 'image/gif',
157
- :pdf => 'application/pdf'
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 self.with_profiling_off(app, options = {})
165
- instance = self.new(app, options)
166
- instance.send(:profiling=, false)
167
- instance
168
- end
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 call(env)
181
- @original_request = Request.new(env)
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
- start_profiling
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
- stop_profiling
185
+ PerfTools::CpuProfiler.stop
186
+ self.profiling = false
187
+ unset_env_vars
213
188
  end
214
189
 
215
- def profile_call(env, times)
216
- PerfTools::CpuProfiler.start(PROFILING_DATA_FILE) do
217
- times.times { result = @app.call(env) }
190
+ def profiling?
191
+ pstore_transaction(true) do |store|
192
+ store[:profiling?]
218
193
  end
219
194
  end
220
195
 
221
- def return_profiling_data(env, printer)
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
- [200, headers(printer, env), print(printer)]
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
- [404, {'Content-Type' => 'text/plain'}, 'No profiling data available.']
207
+ [:none, nil]
226
208
  end
227
209
  end
228
210
 
229
- private
211
+ private
230
212
 
231
- def start_profiling
232
- PerfTools::CpuProfiler.start(PROFILING_DATA_FILE)
233
- self.profiling = true
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 stop_profiling
237
- PerfTools::CpuProfiler.stop
238
- self.profiling = false
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('/tmp/rack_perftools_profiler.config')
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
- def in_profiling_mode?
255
- pstore_transaction(true) do |store|
256
- store[:profiling?]
257
- end
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 enable_profiling_request?
261
- @original_request.path == '/__start__'
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
- def disable_profiling_request?
265
- @original_request.path == '/__stop__'
272
+ end
273
+
274
+ class StartProfiling < Action
275
+
276
+ def act
277
+ @profiler.start
266
278
  end
267
279
 
268
- def profiling_data_request?
269
- @original_request.path == '/__data__'
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
- def simple_profiling_mode?(env)
273
- request = Rack::Request.new(env)
274
- request.params.delete('profile') != nil
290
+ end
291
+
292
+ class StopProfiling < Action
293
+
294
+ def act
295
+ @profiler.stop
275
296
  end
276
297
 
277
- def profiling?(env)
278
- unless PerfTools::CpuProfiler.running?
279
- request = Rack::Request.new(env)
280
- @profiling = request.params.delete('profile')
281
- if @profiling #|| get_profiling
282
- true
283
- #if RubyProf.const_defined?(mode.upcase)
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
- def profile(env, mode)
295
- result = nil
296
- PerfTools::CpuProfiler.start(PROFILING_DATA_FILE) do
297
- @times.times { result = @app.call(env) }
298
- end
299
- if @profiling
300
- return_profiling_data(env, @printer)
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
- result
315
+ @middleware.profiler_data_response(@profiler.data(@data_params))
303
316
  end
304
317
  end
305
318
 
306
- def print(printer)
307
- cmd = "pprof.rb --#{printer} #{PROFILING_DATA_FILE}"
308
- `#{cmd}`
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 headers(printer, env)
312
- headers = { 'Content-Type' => PRINTER_CONTENT_TYPE[printer] }
313
- if printer==:pdf
314
- filetype = printer
315
- filename='profile_data'
316
- headers['Content-Disposition'] = %(attachment; filename="#{filename}.#{filetype}")
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 parse_printer(printer)
322
- if printer.nil?
323
- DEFAULT_PRINTER
324
- else
325
- printer.to_sym
326
- end
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.5'
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"
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 9
8
- - 5
9
- version: 0.9.5
8
+ - 6
9
+ version: 0.9.6
10
10
  platform: ruby
11
11
  authors:
12
12
  - rack-devel