nicholashubbard-production_log_analyzer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,38 @@
1
+ === 1.5.1
2
+
3
+ * 1.9 and 1.8.7 compatibility.
4
+
5
+ === 1.5.0
6
+
7
+ * Fixed empty log bug. Patch by Tim Lucas.
8
+ * Fixed bug where sometimes lines would be logged before the
9
+ Processing line. Patch by Geoff Grosenbach.
10
+
11
+ === 1.4.0
12
+
13
+ * Switched to Hoe
14
+ * Allowed action_errors to suppress routing errors with > 3 occurances
15
+ * action_grep now works correctly with components
16
+ * pl_analyze now works correctly with components
17
+ * Added action_errors to extract error counts from logs
18
+ * Retabbed to match the rest of the world
19
+
20
+ === 1.3.0
21
+
22
+ * Added action_grep
23
+ * Added support for newer log format
24
+
25
+ === 1.2.0
26
+
27
+ * pl_analyze calculates per-action statistics
28
+ * pl_analyze can send an email with its output
29
+
30
+ === 1.1.0
31
+
32
+ * RDoc
33
+ * Other various fixes lost to time.
34
+
35
+ === 1.0.0
36
+
37
+ * Birthday!
38
+
data/LICENSE.txt ADDED
@@ -0,0 +1,27 @@
1
+ Copyright 2005, 2007 Eric Hodel, The Robot Co-op. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions
5
+ are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright
10
+ notice, this list of conditions and the following disclaimer in the
11
+ documentation and/or other materials provided with the distribution.
12
+ 3. Neither the names of the authors nor the names of their contributors
13
+ may be used to endorse or promote products derived from this software
14
+ without specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
17
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
20
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
21
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
22
+ OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
23
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
24
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
25
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
26
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+
data/Manifest.txt ADDED
@@ -0,0 +1,18 @@
1
+ History.txt
2
+ LICENSE.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/action_errors
7
+ bin/action_grep
8
+ bin/pl_analyze
9
+ lib/production_log/action_grep.rb
10
+ lib/production_log/analyzer.rb
11
+ lib/production_log/parser.rb
12
+ test/test.syslog.0.14.x.log
13
+ test/test.syslog.1.2.shortname.log
14
+ test/test.syslog.empty.log
15
+ test/test.syslog.log
16
+ test/test_action_grep.rb
17
+ test/test_analyzer.rb
18
+ test/test_parser.rb
data/README.txt ADDED
@@ -0,0 +1,148 @@
1
+ = production_log_analyzer
2
+
3
+ * http://seattlerb.rubyforge.org/production_log_analyzer
4
+ * http://rubyforge.org/projects/seattlerb
5
+
6
+ == DESCRIPTION
7
+
8
+ production_log_analyzer lets you find out which actions on a Rails
9
+ site are slowing you down.
10
+
11
+ Bug reports:
12
+
13
+ http://rubyforge.org/tracker/?func=add&group_id=1513&atid=5921
14
+
15
+ == About
16
+
17
+ production_log_analyzer provides three tools to analyze log files
18
+ created by SyslogLogger. pl_analyze for getting daily reports,
19
+ action_grep for pulling log lines for a single action and
20
+ action_errors to summarize errors with counts.
21
+
22
+ The analyzer currently requires the use of SyslogLogger because the
23
+ default Logger doesn't give any way to associate lines logged to a
24
+ request.
25
+
26
+ The PL Analyzer also includes action_grep which lets you grab lines from a log
27
+ that only match a single action.
28
+
29
+ action_grep RssController#uber /var/log/production.log
30
+
31
+ == Installing
32
+
33
+ sudo gem install production_log_analyzer
34
+
35
+ === Setup
36
+
37
+ First:
38
+
39
+ Set up SyslogLogger according to the instructions here:
40
+
41
+ http://seattlerb.rubyforge.org/SyslogLogger/
42
+
43
+ Then:
44
+
45
+ Set up a cronjob (or something like that) to run log files through pl_analyze.
46
+
47
+ == Using pl_analyze
48
+
49
+ To run pl_analyze simply give it the name of a log file to analyze.
50
+
51
+ pl_analyze /var/log/production.log
52
+
53
+ If you want, you can run it from a cron something like this:
54
+
55
+ /usr/bin/gzip -dc /var/log/production.log.0.gz | /usr/local/bin/pl_analyze /dev/stdin
56
+
57
+ Or, have pl_analyze email you (which is preferred, because tabs get preserved):
58
+
59
+ /usr/bin/gzip -dc /var/log/production.log.0.gz | /usr/local/bin/pl_analyze /dev/stdin -e devnull@robotcoop.com -s "pl_analyze for `date -v-1d "+%D"`"
60
+
61
+ In the future, pl_analyze will be able to read from STDIN.
62
+
63
+ == Sample output
64
+
65
+ Request Times Summary: Count Avg Std Dev Min Max
66
+ ALL REQUESTS: 11 0.576 0.508 0.000 1.470
67
+
68
+ ThingsController#view: 3 0.716 0.387 0.396 1.260
69
+ TeamsController#progress: 2 0.841 0.629 0.212 1.470
70
+ RssController#uber: 2 0.035 0.000 0.035 0.035
71
+ PeopleController#progress: 2 0.489 0.489 0.000 0.977
72
+ PeopleController#view: 2 0.731 0.371 0.360 1.102
73
+
74
+ Average Request Time: 0.634
75
+ Request Time Std Dev: 0.498
76
+
77
+ Slowest Request Times:
78
+ TeamsController#progress took 1.470s
79
+ ThingsController#view took 1.260s
80
+ PeopleController#view took 1.102s
81
+ PeopleController#progress took 0.977s
82
+ ThingsController#view took 0.492s
83
+ ThingsController#view took 0.396s
84
+ PeopleController#view took 0.360s
85
+ TeamsController#progress took 0.212s
86
+ RssController#uber took 0.035s
87
+ RssController#uber took 0.035s
88
+
89
+ ------------------------------------------------------------------------
90
+
91
+ DB Times Summary: Count Avg Std Dev Min Max
92
+ ALL REQUESTS: 11 0.366 0.393 0.000 1.144
93
+
94
+ ThingsController#view: 3 0.403 0.362 0.122 0.914
95
+ TeamsController#progress: 2 0.646 0.497 0.149 1.144
96
+ RssController#uber: 2 0.008 0.000 0.008 0.008
97
+ PeopleController#progress: 2 0.415 0.415 0.000 0.830
98
+ PeopleController#view: 2 0.338 0.149 0.189 0.486
99
+
100
+ Average DB Time: 0.402
101
+ DB Time Std Dev: 0.394
102
+
103
+ Slowest Total DB Times:
104
+ TeamsController#progress took 1.144s
105
+ ThingsController#view took 0.914s
106
+ PeopleController#progress took 0.830s
107
+ PeopleController#view took 0.486s
108
+ PeopleController#view took 0.189s
109
+ ThingsController#view took 0.173s
110
+ TeamsController#progress took 0.149s
111
+ ThingsController#view took 0.122s
112
+ RssController#uber took 0.008s
113
+ RssController#uber took 0.008s
114
+
115
+ ------------------------------------------------------------------------
116
+
117
+ Render Times Summary: Count Avg Std Dev Min Max
118
+ ALL REQUESTS: 11 0.219 0.253 0.000 0.695
119
+
120
+ ThingsController#view: 3 0.270 0.171 0.108 0.506
121
+ TeamsController#progress: 2 0.000 0.000 0.000 0.000
122
+ RssController#uber: 2 0.012 0.000 0.012 0.012
123
+ PeopleController#progress: 2 0.302 0.302 0.000 0.604
124
+ PeopleController#view: 2 0.487 0.209 0.278 0.695
125
+
126
+ Average Render Time: 0.302
127
+ Render Time Std Dev: 0.251
128
+
129
+ Slowest Total Render Times:
130
+ PeopleController#view took 0.695s
131
+ PeopleController#progress took 0.604s
132
+ ThingsController#view took 0.506s
133
+ PeopleController#view took 0.278s
134
+ ThingsController#view took 0.197s
135
+ ThingsController#view took 0.108s
136
+ RssController#uber took 0.012s
137
+ RssController#uber took 0.012s
138
+ TeamsController#progress took 0.000s
139
+ TeamsController#progress took 0.000s
140
+
141
+ == What's missing
142
+
143
+ * More reports
144
+ * Command line arguments including:
145
+ * Help
146
+ * What type of log file you've got (if somebody sends patches with tests)
147
+ * Read from STDIN
148
+
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "nicholashubbard-production_log_analyzer"
5
+ gemspec.summary = "production_log_analyzer with support for Rails 2.3 logging"
6
+ gemspec.description = "nothing really more to say"
7
+ gemspec.email = "nicholas.e.hubbard@gmail.com"
8
+ gemspec.homepage = "http://github.com/nicholashubbard/production_log_analyzer"
9
+ gemspec.authors = ["Nicholas Hubbard"]
10
+ end
11
+ Jeweler::GemcutterTasks.new
12
+ rescue LoadError
13
+ puts "Jeweler not available. Install it with: gem install jeweler"
14
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/bin/action_errors ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/local/bin/ruby -ws
2
+
3
+ $h ||= false
4
+ $r ||= false
5
+ $o ||= false
6
+
7
+ $r = $r ? ($r.to_i rescue false) : false
8
+
9
+ if $h then
10
+ $stderr.puts "Usage: #{$0} [-r=N] LOGFILE"
11
+ $stderr.puts "\t-r=N\tShow routing errors with N or more occurances"
12
+ $stderr.puts "\t-o\tShow errors with one occurance"
13
+ exit
14
+ end
15
+
16
+ errors = {}
17
+ counts = Hash.new 0
18
+
19
+ ARGF.each_line do |line|
20
+ line =~ /\]: (.*?) (.*)/
21
+ next if $1.nil?
22
+ msg = $1
23
+ trace = $2
24
+ key = msg.gsub(/\d/, '#')
25
+ counts[key] += 1
26
+ next if counts[key] > 1
27
+ trace = trace.split(' ')[0..-2].map { |l| l.strip }.join("\n\t")
28
+ error = "#{msg}\n\t#{trace}"
29
+ errors[key] = error
30
+ end
31
+
32
+ counts.sort_by { |_,c| -c }.each do |key, count|
33
+ next if count == 1 and not $o
34
+ error = errors[key]
35
+
36
+ if error =~ /^ActionController::RoutingError/ then
37
+ next unless $r
38
+ next if $r and count < $r
39
+ end
40
+
41
+ puts "count: #{count}"
42
+ puts "{{{"
43
+ puts error
44
+ puts "}}}"
45
+ end
46
+
data/bin/action_grep ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ require 'production_log/action_grep'
4
+
5
+ action_name = ARGV.shift
6
+ file_name = ARGV.shift
7
+
8
+ if action_name.nil? or file_name.nil? then
9
+ puts "Usage: #{$0} action_name file_name"
10
+ exit 1
11
+ end
12
+
13
+ begin
14
+ ActionGrep.grep action_name, file_name
15
+ rescue ArgumentError => e
16
+ puts e
17
+ exit 1
18
+ end
19
+
data/bin/pl_analyze ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ require 'production_log/analyzer'
4
+
5
+ file_name = ARGV.shift
6
+
7
+ if file_name.nil? then
8
+ puts "Usage: #{$0} file_name [-e email_recipient [-s subject]] [count]"
9
+ exit 1
10
+ end
11
+
12
+ email_recipient = nil
13
+ subject = nil
14
+
15
+ if ARGV.first == '-e' then
16
+ ARGV.shift # -e
17
+ email_recipient = ARGV.shift
18
+ end
19
+
20
+ if email_recipient and ARGV.first == '-s' then
21
+ ARGV.shift # -s
22
+ subject = ARGV.shift
23
+ end
24
+
25
+ count = ARGV.shift
26
+ count = count.nil? ? 10 : Integer(count)
27
+
28
+ if email_recipient.nil? then
29
+ analyzer = Analyzer.new file_name
30
+ analyzer.process
31
+ puts analyzer.report(count)
32
+ else
33
+ Analyzer.email file_name, email_recipient, subject, count
34
+ end
35
+
@@ -0,0 +1,42 @@
1
+ module ActionGrep; end
2
+
3
+ class << ActionGrep
4
+
5
+ def grep(action_name, file_name)
6
+ unless action_name =~ /\A([A-Z][A-Za-z\d]*)(?:#([A-Za-z]\w*))?\Z/ then
7
+ raise ArgumentError, "Invalid action name #{action_name} expected something like SomeController#action"
8
+ end
9
+
10
+ unless File.file? file_name and File.readable? file_name then
11
+ raise ArgumentError, "Unable to read #{file_name}"
12
+ end
13
+
14
+ buckets = Hash.new { |h,k| h[k] = [] }
15
+ comp_count = Hash.new 0
16
+
17
+ File.open file_name do |fp|
18
+ fp.each_line do |line|
19
+ line =~ / ([^ ]+) ([^ ]+)\[(\d+)\]: (.*)/
20
+ next if $2.nil? or $2 == 'newsyslog'
21
+ bucket = [$1, $2, $3].join '-'
22
+ data = $4
23
+
24
+ buckets[bucket] << line
25
+
26
+ case data
27
+ when /^Start rendering component / then
28
+ comp_count[bucket] += 1
29
+ when /^End of component rendering$/ then
30
+ comp_count[bucket] -= 1
31
+ when /^Completed/ then
32
+ next unless comp_count[bucket] == 0
33
+ action = buckets.delete bucket
34
+ next unless action.any? { |l| l =~ /: Processing #{action_name}/ }
35
+ puts action.join
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ end
42
+
@@ -0,0 +1,401 @@
1
+ $TESTING = false unless defined? $TESTING
2
+
3
+ require 'production_log/parser'
4
+
5
+ module Enumerable
6
+
7
+ ##
8
+ # Sum of all the elements of the Enumerable
9
+
10
+ def sum
11
+ return self.inject(0) { |acc, i| acc + i }
12
+ end
13
+
14
+ ##
15
+ # Average of all the elements of the Enumerable
16
+ #
17
+ # The Enumerable must respond to #length
18
+
19
+ def average
20
+ return self.sum / self.length.to_f
21
+ end
22
+
23
+ ##
24
+ # Sample variance of all the elements of the Enumerable
25
+ #
26
+ # The Enumerable must respond to #length
27
+
28
+ def sample_variance
29
+ avg = self.average
30
+ sum = self.inject(0) { |acc, i| acc + (i - avg) ** 2 }
31
+ return (1 / self.length.to_f * sum)
32
+ end
33
+
34
+ ##
35
+ # Standard deviation of all the elements of the Enumerable
36
+ #
37
+ # The Enumerable must respond to #length
38
+
39
+ def standard_deviation
40
+ return Math.sqrt(self.sample_variance)
41
+ end
42
+
43
+ end
44
+
45
+ ##
46
+ # A list that only stores +limit+ items.
47
+
48
+ class SizedList < Array
49
+
50
+ ##
51
+ # Creates a new SizedList that can hold up to +limit+ items. Whenever
52
+ # adding a new item to the SizedList would make the list larger than
53
+ # +limit+, +delete_block+ is called.
54
+ #
55
+ # +delete_block+ is passed the list and the item being added.
56
+ # +delete_block+ must take action to remove an item and return true or
57
+ # return false if the item should not be added to the list.
58
+
59
+ def initialize(limit, &delete_block)
60
+ @limit = limit
61
+ @delete_block = delete_block
62
+ end
63
+
64
+ ##
65
+ # Attempts to add +obj+ to the list.
66
+
67
+ def <<(obj)
68
+ return super if self.length < @limit
69
+ return super if @delete_block.call self, obj
70
+ end
71
+
72
+ end
73
+
74
+ ##
75
+ # Stores +limit+ time/object pairs, keeping only the largest +limit+ items.
76
+ #
77
+ # Sample usage:
78
+ #
79
+ # l = SlowestTimes.new 5
80
+ #
81
+ # l << [Time.now, 'one']
82
+ # l << [Time.now, 'two']
83
+ # l << [Time.now, 'three']
84
+ # l << [Time.now, 'four']
85
+ # l << [Time.now, 'five']
86
+ # l << [Time.now, 'six']
87
+ #
88
+ # p l.map { |i| i.last }
89
+
90
+ class SlowestTimes < SizedList
91
+
92
+ ##
93
+ # Creates a new SlowestTimes SizedList that holds only +limit+ time/object
94
+ # pairs.
95
+
96
+ def initialize(limit)
97
+ super limit do |arr, new_item|
98
+ fastest_time = arr.sort_by { |time, name| [time, name] }.first
99
+ if fastest_time.first < new_item.first then
100
+ arr.delete_at index(fastest_time)
101
+ true
102
+ else
103
+ false
104
+ end
105
+ end
106
+ end
107
+
108
+ end
109
+
110
+ ##
111
+ # Calculates statistics for production logs.
112
+
113
+ class Analyzer
114
+
115
+ ##
116
+ # The version of the production log analyzer you are using.
117
+
118
+ VERSION = '1.5.1'
119
+
120
+ ##
121
+ # The logfile being read by the Analyzer.
122
+
123
+ attr_reader :logfile_name
124
+
125
+ ##
126
+ # An Array of all the request total times for the log file.
127
+
128
+ attr_reader :request_times
129
+
130
+ ##
131
+ # An Array of all the request database times for the log file.
132
+
133
+ attr_reader :db_times
134
+
135
+ ##
136
+ # An Array of all the request render times for the log file.
137
+
138
+ attr_reader :render_times
139
+
140
+ ##
141
+ # Generates and sends an email report with lots of fun stuff in it. This
142
+ # way, Mail.app will behave when given tabs.
143
+
144
+ def self.email(file_name, recipient, subject, count = 10)
145
+ analyzer = self.new file_name
146
+ analyzer.process
147
+ body = analyzer.report count
148
+
149
+ email = self.envelope(recipient, subject)
150
+ email << nil
151
+ email << "<pre>#{body}</pre>"
152
+ email = email.join($/) << $/
153
+
154
+ return email if $TESTING
155
+
156
+ IO.popen("/usr/sbin/sendmail -i -t", "w+") do |sm|
157
+ sm.print email
158
+ sm.flush
159
+ end
160
+ end
161
+
162
+ def self.envelope(recipient, subject = nil) # :nodoc:
163
+ envelope = {}
164
+
165
+ # HACK: this is a hack and the tests should be made order independent
166
+ envelope['Subject'] = subject || "pl_analyze"
167
+ envelope['To'] = recipient
168
+ envelope['Content-Type'] = "text/html"
169
+
170
+ envelope.sort.map { |(k,v)| "#{k}: #{v}" }
171
+ end
172
+
173
+ ##
174
+ # Creates a new Analyzer that will read data from +logfile_name+.
175
+
176
+ def initialize(logfile_name)
177
+ @logfile_name = logfile_name
178
+ @request_times = Hash.new { |h,k| h[k] = [] }
179
+ @db_times = Hash.new { |h,k| h[k] = [] }
180
+ @render_times = Hash.new { |h,k| h[k] = [] }
181
+ end
182
+
183
+ ##
184
+ # Processes the log file collecting statistics from each found LogEntry.
185
+
186
+ def process
187
+ File.open @logfile_name do |fp|
188
+ LogParser.parse fp do |entry|
189
+ entry_page = entry.page
190
+ next if entry_page.nil?
191
+ @request_times[entry_page] << entry.request_time
192
+ @db_times[entry_page] << entry.db_time
193
+ @render_times[entry_page] << entry.render_time
194
+ end
195
+ end
196
+ end
197
+
198
+ ##
199
+ # The average total request time for all requests.
200
+
201
+ def average_request_time
202
+ return time_average(@request_times)
203
+ end
204
+
205
+ ##
206
+ # The standard deviation of the total request time for all requests.
207
+
208
+ def request_time_std_dev
209
+ return time_std_dev(@request_times)
210
+ end
211
+
212
+ ##
213
+ # The +limit+ slowest total request times.
214
+
215
+ def slowest_request_times(limit = 10)
216
+ return slowest_times(@request_times, limit)
217
+ end
218
+
219
+ ##
220
+ # The average total database time for all requests.
221
+
222
+ def average_db_time
223
+ return time_average(@db_times)
224
+ end
225
+
226
+ ##
227
+ # The standard deviation of the total database time for all requests.
228
+
229
+ def db_time_std_dev
230
+ return time_std_dev(@db_times)
231
+ end
232
+
233
+ ##
234
+ # The +limit+ slowest total database times.
235
+
236
+ def slowest_db_times(limit = 10)
237
+ return slowest_times(@db_times, limit)
238
+ end
239
+
240
+ ##
241
+ # The average total render time for all requests.
242
+
243
+ def average_render_time
244
+ return time_average(@render_times)
245
+ end
246
+
247
+ ##
248
+ # The standard deviation of the total render time for all requests.
249
+
250
+ def render_time_std_dev
251
+ return time_std_dev(@render_times)
252
+ end
253
+
254
+ ##
255
+ # The +limit+ slowest total render times for all requests.
256
+
257
+ def slowest_render_times(limit = 10)
258
+ return slowest_times(@render_times, limit)
259
+ end
260
+
261
+ ##
262
+ # A list of count/min/max/avg/std dev for request times.
263
+
264
+ def request_times_summary
265
+ return summarize("Request Times", @request_times)
266
+ end
267
+
268
+ ##
269
+ # A list of count/min/max/avg/std dev for database times.
270
+
271
+ def db_times_summary
272
+ return summarize("DB Times", @db_times)
273
+ end
274
+
275
+ ##
276
+ # A list of count/min/max/avg/std dev for request times.
277
+
278
+ def render_times_summary
279
+ return summarize("Render Times", @render_times)
280
+ end
281
+
282
+ ##
283
+ # Builds a report containing +count+ slow items.
284
+
285
+ def report(count)
286
+ return "No requests to analyze" if request_times.empty?
287
+
288
+ text = []
289
+
290
+ text << request_times_summary
291
+ text << nil
292
+ text << "Slowest Request Times:"
293
+ slowest_request_times(count).each do |time, name|
294
+ text << "\t#{name} took #{'%0.3f' % time}s"
295
+ end
296
+ text << nil
297
+ text << "-" * 72
298
+ text << nil
299
+
300
+ text << db_times_summary
301
+ text << nil
302
+ text << "Slowest Total DB Times:"
303
+ slowest_db_times(count).each do |time, name|
304
+ text << "\t#{name} took #{'%0.3f' % time}s"
305
+ end
306
+ text << nil
307
+ text << "-" * 72
308
+ text << nil
309
+
310
+ text << render_times_summary
311
+ text << nil
312
+ text << "Slowest Total Render Times:"
313
+ slowest_render_times(count).each do |time, name|
314
+ text << "\t#{name} took #{'%0.3f' % time}s"
315
+ end
316
+ text << nil
317
+
318
+ return text.join($/)
319
+ end
320
+
321
+ private unless $TESTING
322
+
323
+ def summarize(title, records) # :nodoc:
324
+ record = nil
325
+ list = []
326
+
327
+ # header
328
+ record = [pad_request_name("#{title} Summary"), 'Count', 'Avg', 'Std Dev',
329
+ 'Min', 'Max']
330
+ list << record.join("\t")
331
+
332
+ # all requests
333
+ all_times = records.values.flatten
334
+ record = [
335
+ all_times.average, all_times.standard_deviation, all_times.min,
336
+ all_times.max
337
+ ]
338
+ record.map! { |v| "%0.3f" % v }
339
+ record.unshift [pad_request_name('ALL REQUESTS'), all_times.size]
340
+ list << record.join("\t")
341
+
342
+ # spacer
343
+ list << nil
344
+
345
+ records.sort_by { |k,v| [-v.size, k] }.each do |req, times|
346
+ record = [times.average, times.standard_deviation, times.min, times.max]
347
+ record.map! { |v| "%0.3f" % v }
348
+ record.unshift ["#{pad_request_name req}", times.size]
349
+ list << record.join("\t")
350
+ end
351
+
352
+ return list.join("\n")
353
+ end
354
+
355
+ def slowest_times(records, limit) # :nodoc:
356
+ slowest_times = SlowestTimes.new limit
357
+
358
+ records.each do |name, times|
359
+ times.each do |time|
360
+ slowest_times << [time, name]
361
+ end
362
+ end
363
+
364
+ return slowest_times.sort_by { |time, name| [-time, name] }
365
+ end
366
+
367
+ def time_average(records) # :nodoc:
368
+ times = records.values.flatten
369
+ times.delete 0
370
+ return times.average
371
+ end
372
+
373
+ def time_std_dev(records) # :nodoc:
374
+ times = records.values.flatten
375
+ times.delete 0
376
+ return times.standard_deviation
377
+ end
378
+
379
+ def longest_request_name # :nodoc:
380
+ return @longest_req if defined? @longest_req
381
+
382
+ names = @request_times.keys.map do |name|
383
+ (name||'Unknown').length + 1 # + : - HACK where does nil come from?
384
+ end
385
+
386
+ @longest_req = names.max
387
+
388
+ @longest_req = 'Unknown'.length + 1 if @longest_req.nil?
389
+
390
+ return @longest_req
391
+ end
392
+
393
+ def pad_request_name(name) # :nodoc:
394
+ name = (name||'Unknown') + ':' # HACK where does nil come from?
395
+ padding_width = longest_request_name - name.length
396
+ padding_width = 0 if padding_width < 0
397
+ name += (' ' * padding_width)
398
+ end
399
+
400
+ end
401
+