erkki-production_log_analyzer 2009022401

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,34 @@
1
+ = 1.5.0
2
+
3
+ * Fixed empty log bug. Patch by Tim Lucas.
4
+ * Fixed bug where sometimes lines would be logged before the
5
+ Processing line. Patch by Geoff Grosenbach.
6
+
7
+ = 1.4.0
8
+
9
+ * Switched to Hoe
10
+ * Allowed action_errors to suppress routing errors with > 3 occurances
11
+ * action_grep now works correctly with components
12
+ * pl_analyze now works correctly with components
13
+ * Added action_errors to extract error counts from logs
14
+ * Retabbed to match the rest of the world
15
+
16
+ = 1.3.0
17
+
18
+ * Added action_grep
19
+ * Added support for newer log format
20
+
21
+ = 1.2.0
22
+
23
+ * pl_analyze calculates per-action statistics
24
+ * pl_analyze can send an email with its output
25
+
26
+ = 1.1.0
27
+
28
+ * RDoc
29
+ * Other various fixes lost to time.
30
+
31
+ = 1.0.0
32
+
33
+ * Birthday!
34
+
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,147 @@
1
+ = production_log_analyzer
2
+
3
+ production_log_analyzer lets you find out which actions on a Rails
4
+ site are slowing you down.
5
+
6
+ http://seattlerb.rubyforge.org/production_log_analyzer
7
+
8
+ http://rubyforge.org/projects/seattlerb
9
+
10
+ Bug reports:
11
+
12
+ http://rubyforge.org/tracker/?func=add&group_id=1513&atid=5921
13
+
14
+ == About
15
+
16
+ production_log_analyzer provides three tools to analyze log files
17
+ created by SyslogLogger. pl_analyze for getting daily reports,
18
+ action_grep for pulling log lines for a single action and
19
+ action_errors to summarize errors with counts.
20
+
21
+ The analyzer currently requires the use of SyslogLogger because the
22
+ default Logger doesn't give any way to associate lines logged to a
23
+ request.
24
+
25
+ The PL Analyzer also includes action_grep which lets you grab lines from a log
26
+ that only match a single action.
27
+
28
+ action_grep RssController#uber /var/log/production.log
29
+
30
+ == Installing
31
+
32
+ sudo gem install production_log_analyzer
33
+
34
+ === Setup
35
+
36
+ First:
37
+
38
+ Set up SyslogLogger according to the instructions here:
39
+
40
+ http://seattlerb.rubyforge.org/SyslogLogger/
41
+
42
+ Then:
43
+
44
+ Set up a cronjob (or something like that) to run log files through pl_analyze.
45
+
46
+ == Using pl_analyze
47
+
48
+ To run pl_analyze simply give it the name of a log file to analyze.
49
+
50
+ pl_analyze /var/log/production.log
51
+
52
+ If you want, you can run it from a cron something like this:
53
+
54
+ /usr/bin/gzip -dc /var/log/production.log.0.gz | /usr/local/bin/pl_analyze /dev/stdin
55
+
56
+ Or, have pl_analyze email you (which is preferred, because tabs get preserved):
57
+
58
+ /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"`"
59
+
60
+ In the future, pl_analyze will be able to read from STDIN.
61
+
62
+ == Sample output
63
+
64
+ Request Times Summary: Count Avg Std Dev Min Max
65
+ ALL REQUESTS: 11 0.576 0.508 0.000 1.470
66
+
67
+ ThingsController#view: 3 0.716 0.387 0.396 1.260
68
+ TeamsController#progress: 2 0.841 0.629 0.212 1.470
69
+ RssController#uber: 2 0.035 0.000 0.035 0.035
70
+ PeopleController#progress: 2 0.489 0.489 0.000 0.977
71
+ PeopleController#view: 2 0.731 0.371 0.360 1.102
72
+
73
+ Average Request Time: 0.634
74
+ Request Time Std Dev: 0.498
75
+
76
+ Slowest Request Times:
77
+ TeamsController#progress took 1.470s
78
+ ThingsController#view took 1.260s
79
+ PeopleController#view took 1.102s
80
+ PeopleController#progress took 0.977s
81
+ ThingsController#view took 0.492s
82
+ ThingsController#view took 0.396s
83
+ PeopleController#view took 0.360s
84
+ TeamsController#progress took 0.212s
85
+ RssController#uber took 0.035s
86
+ RssController#uber took 0.035s
87
+
88
+ ------------------------------------------------------------------------
89
+
90
+ DB Times Summary: Count Avg Std Dev Min Max
91
+ ALL REQUESTS: 11 0.366 0.393 0.000 1.144
92
+
93
+ ThingsController#view: 3 0.403 0.362 0.122 0.914
94
+ TeamsController#progress: 2 0.646 0.497 0.149 1.144
95
+ RssController#uber: 2 0.008 0.000 0.008 0.008
96
+ PeopleController#progress: 2 0.415 0.415 0.000 0.830
97
+ PeopleController#view: 2 0.338 0.149 0.189 0.486
98
+
99
+ Average DB Time: 0.402
100
+ DB Time Std Dev: 0.394
101
+
102
+ Slowest Total DB Times:
103
+ TeamsController#progress took 1.144s
104
+ ThingsController#view took 0.914s
105
+ PeopleController#progress took 0.830s
106
+ PeopleController#view took 0.486s
107
+ PeopleController#view took 0.189s
108
+ ThingsController#view took 0.173s
109
+ TeamsController#progress took 0.149s
110
+ ThingsController#view took 0.122s
111
+ RssController#uber took 0.008s
112
+ RssController#uber took 0.008s
113
+
114
+ ------------------------------------------------------------------------
115
+
116
+ Render Times Summary: Count Avg Std Dev Min Max
117
+ ALL REQUESTS: 11 0.219 0.253 0.000 0.695
118
+
119
+ ThingsController#view: 3 0.270 0.171 0.108 0.506
120
+ TeamsController#progress: 2 0.000 0.000 0.000 0.000
121
+ RssController#uber: 2 0.012 0.000 0.012 0.012
122
+ PeopleController#progress: 2 0.302 0.302 0.000 0.604
123
+ PeopleController#view: 2 0.487 0.209 0.278 0.695
124
+
125
+ Average Render Time: 0.302
126
+ Render Time Std Dev: 0.251
127
+
128
+ Slowest Total Render Times:
129
+ PeopleController#view took 0.695s
130
+ PeopleController#progress took 0.604s
131
+ ThingsController#view took 0.506s
132
+ PeopleController#view took 0.278s
133
+ ThingsController#view took 0.197s
134
+ ThingsController#view took 0.108s
135
+ RssController#uber took 0.012s
136
+ RssController#uber took 0.012s
137
+ TeamsController#progress took 0.000s
138
+ TeamsController#progress took 0.000s
139
+
140
+ == What's missing
141
+
142
+ * More reports
143
+ * Command line arguments including:
144
+ * Help
145
+ * What type of log file you've got (if somebody sends patches with tests)
146
+ * Read from STDIN
147
+
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'hoe'
2
+
3
+ $:.unshift './lib'
4
+ require 'production_log/analyzer'
5
+
6
+ Hoe.new 'production_log_analyzer', '2009022401' do |p|
7
+ p.summary = p.paragraphs_of('README.txt', 1).join ' '
8
+ p.description = p.paragraphs_of('README.txt', 7).join ' '
9
+ p.author = 'Eric Hodel'
10
+ p.email = 'drbrain@segment7.net'
11
+ p.url = p.paragraphs_of('README.txt', 2).join ' '
12
+
13
+ p.rubyforge_name = 'seattlerb'
14
+
15
+ p.extra_deps << ['rails_analyzer_tools', '>= 1.4.0']
16
+ end
17
+
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,36 @@
1
+ #!/bin/env ruby -w
2
+
3
+ $:.unshift "#{File.dirname(__FILE__)}/../lib/"
4
+ require "production_log/analyzer"
5
+
6
+ file_name = ARGV.shift
7
+
8
+ if file_name.nil? then
9
+ puts "Usage: #{$0} file_name [-e email_recipient [-s subject]] [count]"
10
+ exit 1
11
+ end
12
+
13
+ email_recipient = nil
14
+ subject = nil
15
+
16
+ if ARGV.first == '-e' then
17
+ ARGV.shift # -e
18
+ email_recipient = ARGV.shift
19
+ end
20
+
21
+ if email_recipient and ARGV.first == '-s' then
22
+ ARGV.shift # -s
23
+ subject = ARGV.shift
24
+ end
25
+
26
+ count = ARGV.shift
27
+ count = count.nil? ? 10 : Integer(count)
28
+
29
+ if email_recipient.nil? then
30
+ analyzer = Analyzer.new file_name
31
+ analyzer.process
32
+ puts analyzer.report(count)
33
+ else
34
+ Analyzer.email file_name, email_recipient, subject, count
35
+ end
36
+
@@ -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,406 @@
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 }.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.0'
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
+ attr_reader :row_counts, :query_counts
141
+
142
+ ##
143
+ # Generates and sends an email report with lots of fun stuff in it. This
144
+ # way, Mail.app will behave when given tabs.
145
+
146
+ def self.email(file_name, recipient, subject, count = 10)
147
+ analyzer = self.new file_name
148
+ analyzer.process
149
+ body = analyzer.report count
150
+
151
+ email = self.envelope(recipient, subject)
152
+ email << nil
153
+ email << "<pre>#{body}</pre>"
154
+ email = email.join($/) << $/
155
+
156
+ return email if $TESTING
157
+
158
+ IO.popen("/usr/sbin/sendmail -i -t", "w+") do |sm|
159
+ sm.print email
160
+ sm.flush
161
+ end
162
+ end
163
+
164
+ def self.envelope(recipient, subject = nil) # :nodoc:
165
+ envelope = {}
166
+ envelope['To'] = recipient
167
+ envelope['Subject'] = subject || "pl_analyze"
168
+ envelope['Content-Type'] = "text/html"
169
+
170
+ return envelope.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
+ @row_counts = Hash.new { |h,k| h[k] = [] }
181
+ @query_counts = Hash.new { |h,k| h[k] = [] }
182
+ @render_times = Hash.new { |h,k| h[k] = [] }
183
+ end
184
+
185
+ ##
186
+ # Processes the log file collecting statistics from each found LogEntry.
187
+
188
+ def process
189
+ File.open @logfile_name do |fp|
190
+ LogParser.parse fp do |entry|
191
+ entry_page = entry.page
192
+ next if entry_page.nil?
193
+ @request_times[entry_page] << entry.request_time
194
+ @db_times[entry_page] << entry.db_time
195
+ @row_counts[entry_page] << entry.row_count
196
+ @query_counts[entry_page] << entry.query_count
197
+ @render_times[entry_page] << entry.render_time
198
+ end
199
+ end
200
+ end
201
+
202
+ ##
203
+ # The average total request time for all requests.
204
+
205
+ def average_request_time
206
+ return time_average(@request_times)
207
+ end
208
+
209
+ ##
210
+ # The standard deviation of the total request time for all requests.
211
+
212
+ def request_time_std_dev
213
+ return time_std_dev(@request_times)
214
+ end
215
+
216
+ ##
217
+ # The +limit+ slowest total request times.
218
+
219
+ def slowest_request_times(limit = 10)
220
+ return slowest_times(@request_times, limit)
221
+ end
222
+
223
+ ##
224
+ # The average total database time for all requests.
225
+
226
+ def average_db_time
227
+ return time_average(@db_times)
228
+ end
229
+
230
+ ##
231
+ # The standard deviation of the total database time for all requests.
232
+
233
+ def db_time_std_dev
234
+ return time_std_dev(@db_times)
235
+ end
236
+
237
+ ##
238
+ # The +limit+ slowest total database times.
239
+
240
+ def slowest_db_times(limit = 10)
241
+ return slowest_times(@db_times, limit)
242
+ end
243
+
244
+ ##
245
+ # The average total render time for all requests.
246
+
247
+ def average_render_time
248
+ return time_average(@render_times)
249
+ end
250
+
251
+ ##
252
+ # The standard deviation of the total render time for all requests.
253
+
254
+ def render_time_std_dev
255
+ return time_std_dev(@render_times)
256
+ end
257
+
258
+ ##
259
+ # The +limit+ slowest total render times for all requests.
260
+
261
+ def slowest_render_times(limit = 10)
262
+ return slowest_times(@render_times, limit)
263
+ end
264
+
265
+ ##
266
+ # A list of count/min/max/avg/std dev for request times.
267
+
268
+ def request_times_summary
269
+ return summarize("Request Times", @request_times)
270
+ end
271
+
272
+ ##
273
+ # A list of count/min/max/avg/std dev for database times.
274
+
275
+ def db_times_summary
276
+ return summarize("DB Times", @db_times)
277
+ end
278
+
279
+ ##
280
+ # A list of count/min/max/avg/std dev for request times.
281
+
282
+ def render_times_summary
283
+ return summarize("Render Times", @render_times)
284
+ end
285
+
286
+ ##
287
+ # Builds a report containing +count+ slow items.
288
+
289
+ def report(count)
290
+ return "No requests to analyze" if request_times.empty?
291
+
292
+ text = []
293
+
294
+ text << request_times_summary
295
+ text << nil
296
+ text << "Slowest Request Times:"
297
+ slowest_request_times(count).each do |time, name|
298
+ text << "\t#{name} took #{'%0.3f' % time}s"
299
+ end
300
+ text << nil
301
+ text << "-" * 72
302
+ text << nil
303
+
304
+ text << db_times_summary
305
+ text << nil
306
+ text << "Slowest Total DB Times:"
307
+ slowest_db_times(count).each do |time, name|
308
+ text << "\t#{name} took #{'%0.3f' % time}s"
309
+ end
310
+ text << nil
311
+ text << "-" * 72
312
+ text << nil
313
+
314
+ text << render_times_summary
315
+ text << nil
316
+ text << "Slowest Total Render Times:"
317
+ slowest_render_times(count).each do |time, name|
318
+ text << "\t#{name} took #{'%0.3f' % time}s"
319
+ end
320
+ text << nil
321
+
322
+ return text.join($/)
323
+ end
324
+
325
+ private unless $TESTING
326
+
327
+ def summarize(title, records) # :nodoc:
328
+ record = nil
329
+ list = []
330
+
331
+ # header
332
+ record = [pad_request_name("#{title} Summary"), 'Count', 'Avg', 'Std Dev',
333
+ 'Min', 'Max', 'Queries', 'Rows']
334
+ list << record.join("\t")
335
+
336
+ # all requests
337
+ times = records.values.flatten
338
+ record = [times.average, times.standard_deviation, times.min, times.max, 0, 0]
339
+ record.map! { |v| "%0.3f" % v }
340
+ record.unshift [pad_request_name('ALL REQUESTS'), times.size]
341
+ list << record.join("\t")
342
+
343
+ # spacer
344
+ list << nil
345
+
346
+ records.sort_by { |k,v| v.size}.reverse_each do |req, times|
347
+ if "DB Times" == title
348
+ average_rows = @row_counts[req].average
349
+ average_queries = @query_counts[req].average
350
+ end
351
+ record = [times.average, times.standard_deviation, times.min, times.max, average_queries || 0, average_rows || 0]
352
+ record.map! { |v| "%0.3f" % v }
353
+ record.unshift ["#{pad_request_name req}", times.size]
354
+ list << record.join("\t")
355
+ end
356
+
357
+ return list.join("\n")
358
+ end
359
+
360
+ def slowest_times(records, limit) # :nodoc:
361
+ slowest_times = SlowestTimes.new limit
362
+
363
+ records.each do |name, times|
364
+ times.each do |time|
365
+ slowest_times << [time, name]
366
+ end
367
+ end
368
+
369
+ return slowest_times.sort_by { |time, name| time }.reverse
370
+ end
371
+
372
+ def time_average(records) # :nodoc:
373
+ times = records.values.flatten
374
+ times.delete 0
375
+ return times.average
376
+ end
377
+
378
+ def time_std_dev(records) # :nodoc:
379
+ times = records.values.flatten
380
+ times.delete 0
381
+ return times.standard_deviation
382
+ end
383
+
384
+ def longest_request_name # :nodoc:
385
+ return @longest_req if defined? @longest_req
386
+
387
+ names = @request_times.keys.map do |name|
388
+ (name||'Unknown').length + 1 # + : - HACK where does nil come from?
389
+ end
390
+
391
+ @longest_req = names.max
392
+
393
+ @longest_req = 'Unknown'.length + 1 if @longest_req.nil?
394
+
395
+ return @longest_req
396
+ end
397
+
398
+ def pad_request_name(name) # :nodoc:
399
+ name = (name||'Unknown') + ':' # HACK where does nil come from?
400
+ padding_width = longest_request_name - name.length
401
+ padding_width = 0 if padding_width < 0
402
+ name += (' ' * padding_width)
403
+ end
404
+
405
+ end
406
+