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.
@@ -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