erkki-production_log_analyzer 2009022401

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/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
+