Merge lp:~flacoste/launchpad/ppr-constant-memory into lp:launchpad
- ppr-constant-memory
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Robert Collins |
Approved revision: | no longer in the source branch. |
Merged at revision: | 11795 |
Proposed branch: | lp:~flacoste/launchpad/ppr-constant-memory |
Merge into: | lp:launchpad |
Diff against target: |
628 lines (+246/-169) 1 file modified
lib/lp/scripts/utilities/pageperformancereport.py (+246/-169) |
To merge this branch: | bzr merge lp:~flacoste/launchpad/ppr-constant-memory |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Collins (community) | Approve | ||
Review via email: mp+39324@code.launchpad.net |
Commit message
Refactor page-performanc
Description of the change
This branch changes the algorithm used by the Page Performance Report to be
able to reduce memory usage.
The current algorithm builds the statistics as it parses the logs
all-in-memory. This uses a great amount of memory because it maintains
multiple array of request times in memory for all the keys (categories, page
ids, urls) it wants to report on. It currently fails to generate any weekly or
monthly report and has trouble with some daily report too.
The new algorithm parses all the logs into a SQLite3 database and then
generates statistics for one key at a time. It still does the statistics
computation in memory. This means that the amount of memory still grows
linearly with the number of requests, as the all category will require an
array that has all the request times.
Other changes:
* I've dropped the variance column for the report. We include standard deviation
which is its square root and more useful anyway.
* I've used numpy.clip instead of doing it using list comprehension for the
input to the histogram.
Locally on a 300 000 request file here are the performance diff:
Old New
User time 1m33 1m52
Sys time 0m1.6 0m5
RSS 483M 229M
QA
I've compared the reports generated using the old algorithm with the new one
and the reports are identical (apart the removed column).
On sodium, I've been able to generate the problematic daily reports. It peaked
at 2.2G for 4 million requests. I'm not sure that the weekly and monthly
reports will be able to be computed still. Trying that now.
Francis J. Lacoste (flacoste) wrote : | # |
Robert Collins (lifeless) : | # |
Robert Collins (lifeless) wrote : | # |
Seems plausible; it might be better to not put the time and sql time in the same table.
If you used different tables, you could avoid all the masking stuff entirely.
Francis J. Lacoste (flacoste) wrote : | # |
Yeah, computing one statistics at a time would also reduce the peak amount of memory used at the cost of more processing time. I'll see how it goes for weekly and monthly and assess if another round is needed.
Preview Diff
1 | === modified file 'lib/lp/scripts/utilities/pageperformancereport.py' | |||
2 | --- lib/lp/scripts/utilities/pageperformancereport.py 2010-08-20 20:31:18 +0000 | |||
3 | +++ lib/lp/scripts/utilities/pageperformancereport.py 2010-10-25 21:49:08 +0000 | |||
4 | @@ -13,7 +13,10 @@ | |||
5 | 13 | import re | 13 | import re |
6 | 14 | import subprocess | 14 | import subprocess |
7 | 15 | from textwrap import dedent | 15 | from textwrap import dedent |
8 | 16 | import sqlite3 | ||
9 | 17 | import tempfile | ||
10 | 16 | import time | 18 | import time |
11 | 19 | import warnings | ||
12 | 17 | 20 | ||
13 | 18 | import numpy | 21 | import numpy |
14 | 19 | import simplejson as json | 22 | import simplejson as json |
15 | @@ -24,6 +27,9 @@ | |||
16 | 24 | from canonical.launchpad.scripts.logger import log | 27 | from canonical.launchpad.scripts.logger import log |
17 | 25 | from lp.scripts.helpers import LPOptionParser | 28 | from lp.scripts.helpers import LPOptionParser |
18 | 26 | 29 | ||
19 | 30 | # We don't care about conversion to nan, they are expected. | ||
20 | 31 | warnings.filterwarnings( | ||
21 | 32 | 'ignore', '.*converting a masked element to nan.', UserWarning) | ||
22 | 27 | 33 | ||
23 | 28 | class Request(zc.zservertracelog.tracereport.Request): | 34 | class Request(zc.zservertracelog.tracereport.Request): |
24 | 29 | url = None | 35 | url = None |
25 | @@ -52,19 +58,14 @@ | |||
26 | 52 | 58 | ||
27 | 53 | Requests belong to a Category if the URL matches a regular expression. | 59 | Requests belong to a Category if the URL matches a regular expression. |
28 | 54 | """ | 60 | """ |
30 | 55 | def __init__(self, title, regexp, timeout): | 61 | def __init__(self, title, regexp): |
31 | 56 | self.title = title | 62 | self.title = title |
32 | 57 | self.regexp = regexp | 63 | self.regexp = regexp |
33 | 58 | self._compiled_regexp = re.compile(regexp, re.I | re.X) | 64 | self._compiled_regexp = re.compile(regexp, re.I | re.X) |
43 | 59 | self.times = Times(timeout) | 65 | |
44 | 60 | 66 | def match(self, request): | |
45 | 61 | def add(self, request): | 67 | """Return true when the request match this category.""" |
46 | 62 | """Add a request to a Category if it belongs. | 68 | return self._compiled_regexp.search(request.url) is not None |
38 | 63 | |||
39 | 64 | Does nothing if the request does not belong in this Category. | ||
40 | 65 | """ | ||
41 | 66 | if self._compiled_regexp.search(request.url) is not None: | ||
42 | 67 | self.times.add(request) | ||
47 | 68 | 69 | ||
48 | 69 | def __cmp__(self, other): | 70 | def __cmp__(self, other): |
49 | 70 | return cmp(self.title.lower(), other.title.lower()) | 71 | return cmp(self.title.lower(), other.title.lower()) |
50 | @@ -81,7 +82,6 @@ | |||
51 | 81 | mean = 0 # Mean time per hit. | 82 | mean = 0 # Mean time per hit. |
52 | 82 | median = 0 # Median time per hit. | 83 | median = 0 # Median time per hit. |
53 | 83 | std = 0 # Standard deviation per hit. | 84 | std = 0 # Standard deviation per hit. |
54 | 84 | var = 0 # Variance per hit. | ||
55 | 85 | ninetyninth_percentile_time = 0 | 85 | ninetyninth_percentile_time = 0 |
56 | 86 | histogram = None # # Request times histogram. | 86 | histogram = None # # Request times histogram. |
57 | 87 | 87 | ||
58 | @@ -89,46 +89,16 @@ | |||
59 | 89 | mean_sqltime = 0 # Mean time spend waiting for SQL to process. | 89 | mean_sqltime = 0 # Mean time spend waiting for SQL to process. |
60 | 90 | median_sqltime = 0 # Median time spend waiting for SQL to process. | 90 | median_sqltime = 0 # Median time spend waiting for SQL to process. |
61 | 91 | std_sqltime = 0 # Standard deviation of SQL time. | 91 | std_sqltime = 0 # Standard deviation of SQL time. |
62 | 92 | var_sqltime = 0 # Variance of SQL time | ||
63 | 93 | 92 | ||
64 | 94 | total_sqlstatements = 0 # Total number of SQL statements issued. | 93 | total_sqlstatements = 0 # Total number of SQL statements issued. |
65 | 95 | mean_sqlstatements = 0 | 94 | mean_sqlstatements = 0 |
66 | 96 | median_sqlstatements = 0 | 95 | median_sqlstatements = 0 |
67 | 97 | std_sqlstatements = 0 | 96 | std_sqlstatements = 0 |
102 | 98 | var_sqlstatements = 0 | 97 | |
103 | 99 | 98 | def __init__(self, times, timeout): | |
104 | 100 | empty_stats = Stats() # Singleton. | 99 | """Compute the stats based on times. |
105 | 101 | 100 | ||
106 | 102 | 101 | Times is a list of (app_time, sql_statements, sql_times). | |
73 | 103 | class Times: | ||
74 | 104 | """Collection of request times.""" | ||
75 | 105 | def __init__(self, timeout): | ||
76 | 106 | self.total_hits = 0 | ||
77 | 107 | self.total_time = 0 | ||
78 | 108 | self.request_times = [] | ||
79 | 109 | self.sql_statements = [] | ||
80 | 110 | self.sql_times = [] | ||
81 | 111 | self.ticks = [] | ||
82 | 112 | self.histogram_width = int(1.5*timeout) | ||
83 | 113 | |||
84 | 114 | def add(self, request): | ||
85 | 115 | """Add the application time from the request to the collection.""" | ||
86 | 116 | self.total_hits += 1 | ||
87 | 117 | self.total_time += request.app_seconds | ||
88 | 118 | self.request_times.append(request.app_seconds) | ||
89 | 119 | if request.sql_statements is not None: | ||
90 | 120 | self.sql_statements.append(request.sql_statements) | ||
91 | 121 | if request.sql_seconds is not None: | ||
92 | 122 | self.sql_times.append(request.sql_seconds) | ||
93 | 123 | if request.ticks is not None: | ||
94 | 124 | self.ticks.append(request.ticks) | ||
95 | 125 | |||
96 | 126 | _stats = None | ||
97 | 127 | |||
98 | 128 | def stats(self): | ||
99 | 129 | """Generate statistics about our request times. | ||
100 | 130 | |||
101 | 131 | Returns a `Stats` instance. | ||
107 | 132 | 102 | ||
108 | 133 | The histogram is a list of request counts per 1 second bucket. | 103 | The histogram is a list of request counts per 1 second bucket. |
109 | 134 | ie. histogram[0] contains the number of requests taking between 0 and | 104 | ie. histogram[0] contains the number of requests taking between 0 and |
110 | @@ -136,67 +106,201 @@ | |||
111 | 136 | 1 and 2 seconds etc. histogram is None if there are no requests in | 106 | 1 and 2 seconds etc. histogram is None if there are no requests in |
112 | 137 | this Category. | 107 | this Category. |
113 | 138 | """ | 108 | """ |
131 | 139 | if not self.total_hits: | 109 | if not times: |
132 | 140 | return empty_stats | 110 | return |
133 | 141 | 111 | ||
134 | 142 | if self._stats is not None: | 112 | self.total_hits = len(times) |
135 | 143 | return self._stats | 113 | |
136 | 144 | 114 | # Ignore missing values (-1) in computation. | |
137 | 145 | stats = Stats() | 115 | times_array = numpy.ma.masked_values( |
138 | 146 | 116 | numpy.asarray(times, dtype=numpy.float32), -1.) | |
139 | 147 | stats.total_hits = self.total_hits | 117 | |
140 | 148 | 118 | self.total_time, self.total_sqlstatements, self.total_sqltime = ( | |
141 | 149 | # Time stats | 119 | times_array.sum(axis=0)) |
142 | 150 | array = numpy.asarray(self.request_times, numpy.float32) | 120 | |
143 | 151 | stats.total_time = numpy.sum(array) | 121 | self.mean, self.mean_sqlstatements, self.mean_sqltime = ( |
144 | 152 | stats.mean = numpy.mean(array) | 122 | times_array.mean(axis=0)) |
145 | 153 | stats.median = numpy.median(array) | 123 | |
146 | 154 | stats.std = numpy.std(array) | 124 | self.median, self.median_sqlstatements, self.median_sqltime = ( |
147 | 155 | stats.var = numpy.var(array) | 125 | numpy.median(times_array, axis=0)) |
148 | 126 | |||
149 | 127 | self.std, self.std_sqlstatements, self.std_sqltime = ( | ||
150 | 128 | numpy.std(times_array, axis=0)) | ||
151 | 129 | |||
152 | 156 | # This is an approximation which may not be true: we don't know if we | 130 | # This is an approximation which may not be true: we don't know if we |
153 | 157 | # have a std distribution or not. We could just find the 99th | 131 | # have a std distribution or not. We could just find the 99th |
154 | 158 | # percentile by counting. Shock. Horror; however this appears pretty | 132 | # percentile by counting. Shock. Horror; however this appears pretty |
155 | 159 | # good based on eyeballing things so far - once we're down in the 2-3 | 133 | # good based on eyeballing things so far - once we're down in the 2-3 |
156 | 160 | # second range for everything we may want to revisit. | 134 | # second range for everything we may want to revisit. |
162 | 161 | stats.ninetyninth_percentile_time = stats.mean + stats.std*3 | 135 | self.ninetyninth_percentile_time = self.mean + self.std*3 |
163 | 162 | capped_times = (min(a_time, self.histogram_width) for a_time in | 136 | |
164 | 163 | self.request_times) | 137 | histogram_width = int(timeout*1.5) |
165 | 164 | array = numpy.fromiter(capped_times, numpy.float32, | 138 | histogram_times = numpy.clip(times_array[:,0], 0, histogram_width) |
161 | 165 | len(self.request_times)) | ||
166 | 166 | histogram = numpy.histogram( | 139 | histogram = numpy.histogram( |
200 | 167 | array, normed=True, | 140 | histogram_times, normed=True, range=(0, histogram_width), |
201 | 168 | range=(0, self.histogram_width), bins=self.histogram_width) | 141 | bins=histogram_width) |
202 | 169 | stats.histogram = zip(histogram[1], histogram[0]) | 142 | self.histogram = zip(histogram[1], histogram[0]) |
203 | 170 | 143 | ||
204 | 171 | # SQL time stats. | 144 | |
205 | 172 | array = numpy.asarray(self.sql_times, numpy.float32) | 145 | class SQLiteRequestTimes: |
206 | 173 | stats.total_sqltime = numpy.sum(array) | 146 | """SQLite-based request times computation.""" |
207 | 174 | stats.mean_sqltime = numpy.mean(array) | 147 | |
208 | 175 | stats.median_sqltime = numpy.median(array) | 148 | def __init__(self, categories, options): |
209 | 176 | stats.std_sqltime = numpy.std(array) | 149 | if options.db_file is None: |
210 | 177 | stats.var_sqltime = numpy.var(array) | 150 | fd, self.filename = tempfile.mkstemp(suffix='.db', prefix='ppr') |
211 | 178 | 151 | os.close(fd) | |
212 | 179 | # SQL query count. | 152 | else: |
213 | 180 | array = numpy.asarray(self.sql_statements, numpy.int) | 153 | self.filename = options.db_file |
214 | 181 | stats.total_sqlstatements = int(numpy.sum(array)) | 154 | self.con = sqlite3.connect(self.filename, isolation_level='EXCLUSIVE') |
215 | 182 | stats.mean_sqlstatements = numpy.mean(array) | 155 | log.debug('Using request database %s' % self.filename) |
216 | 183 | stats.median_sqlstatements = numpy.median(array) | 156 | # Some speed optimization. |
217 | 184 | stats.std_sqlstatements = numpy.std(array) | 157 | self.con.execute('PRAGMA synchronous = off') |
218 | 185 | stats.var_sqlstatements = numpy.var(array) | 158 | self.con.execute('PRAGMA journal_mode = off') |
219 | 186 | 159 | ||
220 | 187 | # Cache for next invocation. | 160 | self.categories = categories |
221 | 188 | self._stats = stats | 161 | self.store_all_request = options.pageids or options.top_urls |
222 | 189 | return stats | 162 | self.timeout = options.timeout |
223 | 190 | 163 | self.cur = self.con.cursor() | |
224 | 191 | def __str__(self): | 164 | |
225 | 192 | results = self.stats() | 165 | # Create the tables, ignore errors about them being already present. |
226 | 193 | total, mean, median, std, histogram = results | 166 | try: |
227 | 194 | hstr = " ".join("%2d" % v for v in histogram) | 167 | self.cur.execute(''' |
228 | 195 | return "%2.2f %2.2f %2.2f %s" % ( | 168 | CREATE TABLE category_request ( |
229 | 196 | total, mean, median, std, hstr) | 169 | category INTEGER, |
230 | 197 | 170 | time REAL, | |
231 | 198 | def __cmp__(self, b): | 171 | sql_statements INTEGER, |
232 | 199 | return cmp(self.total_time, b.total_time) | 172 | sql_time REAL) |
233 | 173 | '''); | ||
234 | 174 | except sqlite3.OperationalError, e: | ||
235 | 175 | if 'already exists' in str(e): | ||
236 | 176 | pass | ||
237 | 177 | else: | ||
238 | 178 | raise | ||
239 | 179 | |||
240 | 180 | if self.store_all_request: | ||
241 | 181 | try: | ||
242 | 182 | self.cur.execute(''' | ||
243 | 183 | CREATE TABLE request ( | ||
244 | 184 | pageid TEXT, | ||
245 | 185 | url TEXT, | ||
246 | 186 | time REAL, | ||
247 | 187 | sql_statements INTEGER, | ||
248 | 188 | sql_time REAL) | ||
249 | 189 | '''); | ||
250 | 190 | except sqlite3.OperationalError, e: | ||
251 | 191 | if 'already exists' in str(e): | ||
252 | 192 | pass | ||
253 | 193 | else: | ||
254 | 194 | raise | ||
255 | 195 | |||
256 | 196 | def add_request(self, request): | ||
257 | 197 | """Add a request to the cache.""" | ||
258 | 198 | sql_statements = request.sql_statements | ||
259 | 199 | sql_seconds = request.sql_seconds | ||
260 | 200 | |||
261 | 201 | # Store missing value as -1, as it makes dealing with those | ||
262 | 202 | # easier with numpy. | ||
263 | 203 | if sql_statements is None: | ||
264 | 204 | sql_statements = -1 | ||
265 | 205 | if sql_seconds is None: | ||
266 | 206 | sql_seconds = -1 | ||
267 | 207 | for idx, category in enumerate(self.categories): | ||
268 | 208 | if category.match(request): | ||
269 | 209 | self.con.execute( | ||
270 | 210 | "INSERT INTO category_request VALUES (?,?,?,?)", | ||
271 | 211 | (idx, request.app_seconds, sql_statements, sql_seconds)) | ||
272 | 212 | |||
273 | 213 | if self.store_all_request: | ||
274 | 214 | pageid = request.pageid or 'Unknown' | ||
275 | 215 | self.con.execute( | ||
276 | 216 | "INSERT INTO request VALUES (?,?,?,?,?)", | ||
277 | 217 | (pageid, request.url, request.app_seconds, sql_statements, | ||
278 | 218 | sql_seconds)) | ||
279 | 219 | |||
280 | 220 | def commit(self): | ||
281 | 221 | """Call commit on the underlying connection.""" | ||
282 | 222 | self.con.commit() | ||
283 | 223 | |||
284 | 224 | def get_category_times(self): | ||
285 | 225 | """Return the times for each category.""" | ||
286 | 226 | category_query = 'SELECT * FROM category_request ORDER BY category' | ||
287 | 227 | |||
288 | 228 | empty_stats = Stats([], 0) | ||
289 | 229 | categories = dict(self.get_times(category_query)) | ||
290 | 230 | return [ | ||
291 | 231 | (category, categories.get(idx, empty_stats)) | ||
292 | 232 | for idx, category in enumerate(self.categories)] | ||
293 | 233 | |||
294 | 234 | def get_top_urls_times(self, top_n): | ||
295 | 235 | """Return the times for the Top URL by total time""" | ||
296 | 236 | top_url_query = ''' | ||
297 | 237 | SELECT url, time, sql_statements, sql_time | ||
298 | 238 | FROM request WHERE url IN ( | ||
299 | 239 | SELECT url FROM (SELECT url, sum(time) FROM request | ||
300 | 240 | GROUP BY url | ||
301 | 241 | ORDER BY sum(time) DESC | ||
302 | 242 | LIMIT %d)) | ||
303 | 243 | ORDER BY url | ||
304 | 244 | ''' % top_n | ||
305 | 245 | # Sort the result by total time | ||
306 | 246 | return sorted( | ||
307 | 247 | self.get_times(top_url_query), key=lambda x: x[1].total_time, | ||
308 | 248 | reverse=True) | ||
309 | 249 | |||
310 | 250 | def get_pageid_times(self): | ||
311 | 251 | """Return the times for the pageids.""" | ||
312 | 252 | pageid_query = ''' | ||
313 | 253 | SELECT pageid, time, sql_statements, sql_time | ||
314 | 254 | FROM request | ||
315 | 255 | ORDER BY pageid | ||
316 | 256 | ''' | ||
317 | 257 | return self.get_times(pageid_query) | ||
318 | 258 | |||
319 | 259 | def get_times(self, query): | ||
320 | 260 | """Return a list of key, stats based on the query. | ||
321 | 261 | |||
322 | 262 | The query should return rows of the form: | ||
323 | 263 | [key, app_time, sql_statements, sql_times] | ||
324 | 264 | |||
325 | 265 | And should be sorted on key. | ||
326 | 266 | """ | ||
327 | 267 | times = [] | ||
328 | 268 | current_key = None | ||
329 | 269 | results = [] | ||
330 | 270 | self.cur.execute(query) | ||
331 | 271 | while True: | ||
332 | 272 | rows = self.cur.fetchmany() | ||
333 | 273 | if len(rows) == 0: | ||
334 | 274 | break | ||
335 | 275 | for row in rows: | ||
336 | 276 | # We are encountering a new group... | ||
337 | 277 | if row[0] != current_key: | ||
338 | 278 | # Compute the stats of the previous group | ||
339 | 279 | if current_key != None: | ||
340 | 280 | results.append( | ||
341 | 281 | (current_key, Stats(times, self.timeout))) | ||
342 | 282 | # Initialize the new group. | ||
343 | 283 | current_key = row[0] | ||
344 | 284 | times = [] | ||
345 | 285 | |||
346 | 286 | times.append(row[1:]) | ||
347 | 287 | # Compute the stats of the last group | ||
348 | 288 | if current_key != None: | ||
349 | 289 | results.append((current_key, Stats(times, self.timeout))) | ||
350 | 290 | |||
351 | 291 | return results | ||
352 | 292 | |||
353 | 293 | def close(self, remove=False): | ||
354 | 294 | """Close the SQLite connection. | ||
355 | 295 | |||
356 | 296 | :param remove: If true, the DB file will be removed. | ||
357 | 297 | """ | ||
358 | 298 | self.con.close() | ||
359 | 299 | if remove: | ||
360 | 300 | log.debug('Deleting request database.') | ||
361 | 301 | os.unlink(self.filename) | ||
362 | 302 | else: | ||
363 | 303 | log.debug('Keeping request database %s.' % self.filename) | ||
364 | 200 | 304 | ||
365 | 201 | 305 | ||
366 | 202 | def main(): | 306 | def main(): |
367 | @@ -235,13 +339,17 @@ | |||
368 | 235 | # Default to 12: the staging timeout. | 339 | # Default to 12: the staging timeout. |
369 | 236 | default=12, type="int", | 340 | default=12, type="int", |
370 | 237 | help="The configured timeout value : determines high risk page ids.") | 341 | help="The configured timeout value : determines high risk page ids.") |
371 | 342 | parser.add_option( | ||
372 | 343 | "--db-file", dest="db_file", | ||
373 | 344 | default=None, metavar="FILE", | ||
374 | 345 | help="Do not parse the records, generate reports from the DB file.") | ||
375 | 238 | 346 | ||
376 | 239 | options, args = parser.parse_args() | 347 | options, args = parser.parse_args() |
377 | 240 | 348 | ||
378 | 241 | if not os.path.isdir(options.directory): | 349 | if not os.path.isdir(options.directory): |
379 | 242 | parser.error("Directory %s does not exist" % options.directory) | 350 | parser.error("Directory %s does not exist" % options.directory) |
380 | 243 | 351 | ||
382 | 244 | if len(args) == 0: | 352 | if len(args) == 0 and options.db_file is None: |
383 | 245 | parser.error("At least one zserver tracelog file must be provided") | 353 | parser.error("At least one zserver tracelog file must be provided") |
384 | 246 | 354 | ||
385 | 247 | if options.from_ts is not None and options.until_ts is not None: | 355 | if options.from_ts is not None and options.until_ts is not None: |
386 | @@ -266,7 +374,7 @@ | |||
387 | 266 | for option in script_config.options('categories'): | 374 | for option in script_config.options('categories'): |
388 | 267 | regexp = script_config.get('categories', option) | 375 | regexp = script_config.get('categories', option) |
389 | 268 | try: | 376 | try: |
391 | 269 | categories.append(Category(option, regexp, options.timeout)) | 377 | categories.append(Category(option, regexp)) |
392 | 270 | except sre_constants.error, x: | 378 | except sre_constants.error, x: |
393 | 271 | log.fatal("Unable to compile regexp %r (%s)" % (regexp, x)) | 379 | log.fatal("Unable to compile regexp %r (%s)" % (regexp, x)) |
394 | 272 | return 1 | 380 | return 1 |
395 | @@ -275,18 +383,23 @@ | |||
396 | 275 | if len(categories) == 0: | 383 | if len(categories) == 0: |
397 | 276 | parser.error("No data in [categories] section of configuration.") | 384 | parser.error("No data in [categories] section of configuration.") |
398 | 277 | 385 | ||
405 | 278 | pageid_times = {} | 386 | times = SQLiteRequestTimes(categories, options) |
406 | 279 | url_times = {} | 387 | |
407 | 280 | 388 | if len(args) > 0: | |
408 | 281 | parse(args, categories, pageid_times, url_times, options) | 389 | parse(args, times, options) |
409 | 282 | 390 | times.commit() | |
410 | 283 | # Truncate the URL times to the top N. | 391 | |
411 | 392 | log.debug('Generating category statistics...') | ||
412 | 393 | category_times = times.get_category_times() | ||
413 | 394 | |||
414 | 395 | pageid_times = [] | ||
415 | 396 | url_times= [] | ||
416 | 284 | if options.top_urls: | 397 | if options.top_urls: |
422 | 285 | sorted_urls = sorted( | 398 | log.debug('Generating top %d urls statistics...' % options.top_urls) |
423 | 286 | ((times, url) for url, times in url_times.items() | 399 | url_times = times.get_top_urls_times(options.top_urls) |
424 | 287 | if times.total_hits > 0), reverse=True) | 400 | if options.pageids: |
425 | 288 | url_times = [(url, times) | 401 | log.debug('Generating pageid statistics...') |
426 | 289 | for times, url in sorted_urls[:options.top_urls]] | 402 | pageid_times = times.get_pageid_times() |
427 | 290 | 403 | ||
428 | 291 | def _report_filename(filename): | 404 | def _report_filename(filename): |
429 | 292 | return os.path.join(options.directory, filename) | 405 | return os.path.join(options.directory, filename) |
430 | @@ -295,7 +408,7 @@ | |||
431 | 295 | if options.categories: | 408 | if options.categories: |
432 | 296 | report_filename = _report_filename('categories.html') | 409 | report_filename = _report_filename('categories.html') |
433 | 297 | log.info("Generating %s", report_filename) | 410 | log.info("Generating %s", report_filename) |
435 | 298 | html_report(open(report_filename, 'w'), categories, None, None) | 411 | html_report(open(report_filename, 'w'), category_times, None, None) |
436 | 299 | 412 | ||
437 | 300 | # Pageid only report. | 413 | # Pageid only report. |
438 | 301 | if options.pageids: | 414 | if options.pageids: |
439 | @@ -313,7 +426,8 @@ | |||
440 | 313 | if options.categories and options.pageids: | 426 | if options.categories and options.pageids: |
441 | 314 | report_filename = _report_filename('combined.html') | 427 | report_filename = _report_filename('combined.html') |
442 | 315 | html_report( | 428 | html_report( |
444 | 316 | open(report_filename, 'w'), categories, pageid_times, url_times) | 429 | open(report_filename, 'w'), |
445 | 430 | category_times, pageid_times, url_times) | ||
446 | 317 | 431 | ||
447 | 318 | # Report of likely timeout candidates | 432 | # Report of likely timeout candidates |
448 | 319 | report_filename = _report_filename('timeout-candidates.html') | 433 | report_filename = _report_filename('timeout-candidates.html') |
449 | @@ -322,6 +436,7 @@ | |||
450 | 322 | open(report_filename, 'w'), None, pageid_times, None, | 436 | open(report_filename, 'w'), None, pageid_times, None, |
451 | 323 | options.timeout - 2) | 437 | options.timeout - 2) |
452 | 324 | 438 | ||
453 | 439 | times.close(options.db_file is None) | ||
454 | 325 | return 0 | 440 | return 0 |
455 | 326 | 441 | ||
456 | 327 | 442 | ||
457 | @@ -363,7 +478,7 @@ | |||
458 | 363 | *(int(elem) for elem in match.groups() if elem is not None)) | 478 | *(int(elem) for elem in match.groups() if elem is not None)) |
459 | 364 | 479 | ||
460 | 365 | 480 | ||
462 | 366 | def parse(tracefiles, categories, pageid_times, url_times, options): | 481 | def parse(tracefiles, times, options): |
463 | 367 | requests = {} | 482 | requests = {} |
464 | 368 | total_requests = 0 | 483 | total_requests = 0 |
465 | 369 | for tracefile in tracefiles: | 484 | for tracefile in tracefiles: |
466 | @@ -444,35 +559,7 @@ | |||
467 | 444 | log.debug("Parsed %d requests", total_requests) | 559 | log.debug("Parsed %d requests", total_requests) |
468 | 445 | 560 | ||
469 | 446 | # Add the request to any matching categories. | 561 | # Add the request to any matching categories. |
499 | 447 | if options.categories: | 562 | times.add_request(request) |
471 | 448 | for category in categories: | ||
472 | 449 | category.add(request) | ||
473 | 450 | |||
474 | 451 | # Add the request to the times for that pageid. | ||
475 | 452 | if options.pageids: | ||
476 | 453 | pageid = request.pageid | ||
477 | 454 | try: | ||
478 | 455 | times = pageid_times[pageid] | ||
479 | 456 | except KeyError: | ||
480 | 457 | times = Times(options.timeout) | ||
481 | 458 | pageid_times[pageid] = times | ||
482 | 459 | times.add(request) | ||
483 | 460 | |||
484 | 461 | # Add the request to the times for that URL. | ||
485 | 462 | if options.top_urls: | ||
486 | 463 | url = request.url | ||
487 | 464 | # Hack to remove opstats from top N report. This | ||
488 | 465 | # should go into a config file if we end up with | ||
489 | 466 | # more pages that need to be ignored because | ||
490 | 467 | # they are just noise. | ||
491 | 468 | if not (url is None or url.endswith('+opstats')): | ||
492 | 469 | try: | ||
493 | 470 | times = url_times[url] | ||
494 | 471 | except KeyError: | ||
495 | 472 | times = Times(options.timeout) | ||
496 | 473 | url_times[url] = times | ||
497 | 474 | times.add(request) | ||
498 | 475 | |||
500 | 476 | else: | 563 | else: |
501 | 477 | raise MalformedLine('Unknown record type %s', record_type) | 564 | raise MalformedLine('Unknown record type %s', record_type) |
502 | 478 | except MalformedLine, x: | 565 | except MalformedLine, x: |
503 | @@ -491,7 +578,6 @@ | |||
504 | 491 | elif prefix == 't': | 578 | elif prefix == 't': |
505 | 492 | if len(args) != 4: | 579 | if len(args) != 4: |
506 | 493 | raise MalformedLine("Wrong number of arguments %s" % (args,)) | 580 | raise MalformedLine("Wrong number of arguments %s" % (args,)) |
507 | 494 | request.ticks = int(args[1]) | ||
508 | 495 | request.sql_statements = int(args[2]) | 581 | request.sql_statements = int(args[2]) |
509 | 496 | request.sql_seconds = float(args[3]) / 1000 | 582 | request.sql_seconds = float(args[3]) / 1000 |
510 | 497 | else: | 583 | else: |
511 | @@ -500,12 +586,12 @@ | |||
512 | 500 | 586 | ||
513 | 501 | 587 | ||
514 | 502 | def html_report( | 588 | def html_report( |
516 | 503 | outf, categories, pageid_times, url_times, | 589 | outf, category_times, pageid_times, url_times, |
517 | 504 | ninetyninth_percentile_threshold=None): | 590 | ninetyninth_percentile_threshold=None): |
518 | 505 | """Write an html report to outf. | 591 | """Write an html report to outf. |
519 | 506 | 592 | ||
520 | 507 | :param outf: A file object to write the report to. | 593 | :param outf: A file object to write the report to. |
522 | 508 | :param categories: Categories to report. | 594 | :param category_times: The time statistics for categories. |
523 | 509 | :param pageid_times: The time statistics for pageids. | 595 | :param pageid_times: The time statistics for pageids. |
524 | 510 | :param url_times: The time statistics for the top XXX urls. | 596 | :param url_times: The time statistics for the top XXX urls. |
525 | 511 | :param ninetyninth_percentile_threshold: Lower threshold for inclusion of | 597 | :param ninetyninth_percentile_threshold: Lower threshold for inclusion of |
526 | @@ -575,20 +661,17 @@ | |||
527 | 575 | 661 | ||
528 | 576 | <th class="clickable">Mean Time (secs)</th> | 662 | <th class="clickable">Mean Time (secs)</th> |
529 | 577 | <th class="clickable">Time Standard Deviation</th> | 663 | <th class="clickable">Time Standard Deviation</th> |
530 | 578 | <th class="clickable">Time Variance</th> | ||
531 | 579 | <th class="clickable">Median Time (secs)</th> | 664 | <th class="clickable">Median Time (secs)</th> |
532 | 580 | <th class="sorttable_nosort">Time Distribution</th> | 665 | <th class="sorttable_nosort">Time Distribution</th> |
533 | 581 | 666 | ||
534 | 582 | <th class="clickable">Total SQL Time (secs)</th> | 667 | <th class="clickable">Total SQL Time (secs)</th> |
535 | 583 | <th class="clickable">Mean SQL Time (secs)</th> | 668 | <th class="clickable">Mean SQL Time (secs)</th> |
536 | 584 | <th class="clickable">SQL Time Standard Deviation</th> | 669 | <th class="clickable">SQL Time Standard Deviation</th> |
537 | 585 | <th class="clickable">SQL Time Variance</th> | ||
538 | 586 | <th class="clickable">Median SQL Time (secs)</th> | 670 | <th class="clickable">Median SQL Time (secs)</th> |
539 | 587 | 671 | ||
540 | 588 | <th class="clickable">Total SQL Statements</th> | 672 | <th class="clickable">Total SQL Statements</th> |
541 | 589 | <th class="clickable">Mean SQL Statements</th> | 673 | <th class="clickable">Mean SQL Statements</th> |
542 | 590 | <th class="clickable">SQL Statement Standard Deviation</th> | 674 | <th class="clickable">SQL Statement Standard Deviation</th> |
543 | 591 | <th class="clickable">SQL Statement Variance</th> | ||
544 | 592 | <th class="clickable">Median SQL Statements</th> | 675 | <th class="clickable">Median SQL Statements</th> |
545 | 593 | 676 | ||
546 | 594 | </tr> | 677 | </tr> |
547 | @@ -600,8 +683,7 @@ | |||
548 | 600 | # Store our generated histograms to output Javascript later. | 683 | # Store our generated histograms to output Javascript later. |
549 | 601 | histograms = [] | 684 | histograms = [] |
550 | 602 | 685 | ||
553 | 603 | def handle_times(html_title, times): | 686 | def handle_times(html_title, stats): |
552 | 604 | stats = times.stats() | ||
554 | 605 | histograms.append(stats.histogram) | 687 | histograms.append(stats.histogram) |
555 | 606 | print >> outf, dedent("""\ | 688 | print >> outf, dedent("""\ |
556 | 607 | <tr> | 689 | <tr> |
557 | @@ -611,7 +693,6 @@ | |||
558 | 611 | <td class="numeric 99pc_under">%.2f</td> | 693 | <td class="numeric 99pc_under">%.2f</td> |
559 | 612 | <td class="numeric mean_time">%.2f</td> | 694 | <td class="numeric mean_time">%.2f</td> |
560 | 613 | <td class="numeric std_time">%.2f</td> | 695 | <td class="numeric std_time">%.2f</td> |
561 | 614 | <td class="numeric var_time">%.2f</td> | ||
562 | 615 | <td class="numeric median_time">%.2f</td> | 696 | <td class="numeric median_time">%.2f</td> |
563 | 616 | <td> | 697 | <td> |
564 | 617 | <div class="histogram" id="histogram%d"></div> | 698 | <div class="histogram" id="histogram%d"></div> |
565 | @@ -619,30 +700,27 @@ | |||
566 | 619 | <td class="numeric total_sqltime">%.2f</td> | 700 | <td class="numeric total_sqltime">%.2f</td> |
567 | 620 | <td class="numeric mean_sqltime">%.2f</td> | 701 | <td class="numeric mean_sqltime">%.2f</td> |
568 | 621 | <td class="numeric std_sqltime">%.2f</td> | 702 | <td class="numeric std_sqltime">%.2f</td> |
569 | 622 | <td class="numeric var_sqltime">%.2f</td> | ||
570 | 623 | <td class="numeric median_sqltime">%.2f</td> | 703 | <td class="numeric median_sqltime">%.2f</td> |
571 | 624 | 704 | ||
573 | 625 | <td class="numeric total_sqlstatements">%d</td> | 705 | <td class="numeric total_sqlstatements">%.f</td> |
574 | 626 | <td class="numeric mean_sqlstatements">%.2f</td> | 706 | <td class="numeric mean_sqlstatements">%.2f</td> |
575 | 627 | <td class="numeric std_sqlstatements">%.2f</td> | 707 | <td class="numeric std_sqlstatements">%.2f</td> |
576 | 628 | <td class="numeric var_sqlstatements">%.2f</td> | ||
577 | 629 | <td class="numeric median_sqlstatements">%.2f</td> | 708 | <td class="numeric median_sqlstatements">%.2f</td> |
578 | 630 | </tr> | 709 | </tr> |
579 | 631 | """ % ( | 710 | """ % ( |
580 | 632 | html_title, | 711 | html_title, |
581 | 633 | stats.total_hits, stats.total_time, | 712 | stats.total_hits, stats.total_time, |
582 | 634 | stats.ninetyninth_percentile_time, | 713 | stats.ninetyninth_percentile_time, |
584 | 635 | stats.mean, stats.std, stats.var, stats.median, | 714 | stats.mean, stats.std, stats.median, |
585 | 636 | len(histograms) - 1, | 715 | len(histograms) - 1, |
586 | 637 | stats.total_sqltime, stats.mean_sqltime, | 716 | stats.total_sqltime, stats.mean_sqltime, |
588 | 638 | stats.std_sqltime, stats.var_sqltime, stats.median_sqltime, | 717 | stats.std_sqltime, stats.median_sqltime, |
589 | 639 | stats.total_sqlstatements, stats.mean_sqlstatements, | 718 | stats.total_sqlstatements, stats.mean_sqlstatements, |
592 | 640 | stats.std_sqlstatements, stats.var_sqlstatements, | 719 | stats.std_sqlstatements, stats.median_sqlstatements)) |
591 | 641 | stats.median_sqlstatements)) | ||
593 | 642 | 720 | ||
594 | 643 | # Table of contents | 721 | # Table of contents |
595 | 644 | print >> outf, '<ol>' | 722 | print >> outf, '<ol>' |
597 | 645 | if categories: | 723 | if category_times: |
598 | 646 | print >> outf, '<li><a href="#catrep">Category Report</a></li>' | 724 | print >> outf, '<li><a href="#catrep">Category Report</a></li>' |
599 | 647 | if pageid_times: | 725 | if pageid_times: |
600 | 648 | print >> outf, '<li><a href="#pageidrep">Pageid Report</a></li>' | 726 | print >> outf, '<li><a href="#pageidrep">Pageid Report</a></li>' |
601 | @@ -650,22 +728,21 @@ | |||
602 | 650 | print >> outf, '<li><a href="#topurlrep">Top URL Report</a></li>' | 728 | print >> outf, '<li><a href="#topurlrep">Top URL Report</a></li>' |
603 | 651 | print >> outf, '</ol>' | 729 | print >> outf, '</ol>' |
604 | 652 | 730 | ||
606 | 653 | if categories: | 731 | if category_times: |
607 | 654 | print >> outf, '<h2 id="catrep">Category Report</h2>' | 732 | print >> outf, '<h2 id="catrep">Category Report</h2>' |
608 | 655 | print >> outf, table_header | 733 | print >> outf, table_header |
610 | 656 | for category in categories: | 734 | for category, times in category_times: |
611 | 657 | html_title = '%s<br/><span class="regexp">%s</span>' % ( | 735 | html_title = '%s<br/><span class="regexp">%s</span>' % ( |
612 | 658 | html_quote(category.title), html_quote(category.regexp)) | 736 | html_quote(category.title), html_quote(category.regexp)) |
614 | 659 | handle_times(html_title, category.times) | 737 | handle_times(html_title, times) |
615 | 660 | print >> outf, table_footer | 738 | print >> outf, table_footer |
616 | 661 | 739 | ||
617 | 662 | if pageid_times: | 740 | if pageid_times: |
618 | 663 | print >> outf, '<h2 id="pageidrep">Pageid Report</h2>' | 741 | print >> outf, '<h2 id="pageidrep">Pageid Report</h2>' |
619 | 664 | print >> outf, table_header | 742 | print >> outf, table_header |
622 | 665 | for pageid, times in sorted(pageid_times.items()): | 743 | for pageid, times in pageid_times: |
621 | 666 | pageid = pageid or 'None' | ||
623 | 667 | if (ninetyninth_percentile_threshold is not None and | 744 | if (ninetyninth_percentile_threshold is not None and |
625 | 668 | (times.stats().ninetyninth_percentile_time < | 745 | (times.ninetyninth_percentile_time < |
626 | 669 | ninetyninth_percentile_threshold)): | 746 | ninetyninth_percentile_threshold)): |
627 | 670 | continue | 747 | continue |
628 | 671 | handle_times(html_quote(pageid), times) | 748 | handle_times(html_quote(pageid), times) |
As far as stats goes, I forgot to report that the SQLite3 DB size was 55M for 300000 requests and 776M for 4.1M.