Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1331)

Side by Side Diff: chrome/test/functional/perf/endure_result_parser.py

Issue 10944003: Add standalone script to parse Chrome Endure test results and dump to graph files. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Addressed first round of review comments. Created 8 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """Script to parse perf data from Chrome Endure test executions, to be graphed.
7
8 This script connects via HTTP to a buildbot master in order to scrape and parse
9 perf data from Chrome Endure tests that have been run. The perf data is then
10 stored in local text files to be graphed by the Chrome Endure graphing code.
11
12 It is assumed that any Chrome Endure tests that show up on the waterfall have
13 names that are of the following form:
14
15 "endure_<webapp_name>_test <test_name>" (non-Web Page Replay tests)
16
17 or
18
19 "endure_<webapp_name>_wpr_test <test_name>" (Web Page Replay tests)
20
21 For example: "endure_gmail_wpr_test testGmailComposeDiscard"
22 """
23
24 import getpass
25 import logging
26 import optparse
27 import os
28 import re
29 import simplejson
30 import socket
31 import sys
32 import time
33 import urllib
34 import urllib2
35
36
37 CHROME_ENDURE_SLAVE_NAMES = [
38 'Linux (perf0)',
39 'Linux (perf1)',
40 'Linux (perf2)',
41 'Linux (perf3)',
42 'Linux (perf4)',
43 ]
44
45 BUILDER_URL_BASE = 'http://chromegw.corp.google.com/i/chromium.pyauto/builders/'
Nirnimesh 2012/09/25 20:31:28 we should not refer to internal URLs and paths in
dennis_jeffrey 2012/09/25 21:47:40 Changed to the external URL.
46 LAST_BUILD_NUM_PROCESSED_FILE = os.path.join(os.path.dirname(__file__),
47 '_parser_last_processed.txt')
48 LOCAL_GRAPH_DIR = '/home/%s/www/chrome_endure_clean' % getpass.getuser()
49
50
51 def SetupBaseGraphDirIfNeeded(webapp_name, test_name, dest_dir):
52 """Sets up the directory containing results for a particular test, if needed.
53
54 Args:
55 webapp_name: The string name of the webapp associated with the given test.
56 test_name: The string name of the test.
57 dest_dir: The name of the destination directory that needs to be set up.
58 """
59 if not os.path.exists(dest_dir):
60 os.mkdir(dest_dir) # Test name directory.
61 os.chmod(dest_dir, 0755)
62
63 # Create config file.
64 config_file = os.path.join(dest_dir, 'config.js')
65 if not os.path.exists(config_file):
66 with open(config_file, 'w') as f:
67 f.write('var Config = {\n')
68 f.write('buildslave: "Chrome Endure Bots",\n')
69 f.write('title: "Chrome Endure %s Test: %s",\n' % (webapp_name.upper(),
70 test_name))
71 f.write('};\n')
72 os.chmod(config_file, 0755)
73
74 # Set up symbolic links to the real graphing files.
75 link_file = os.path.join(dest_dir, 'index.html')
76 if not os.path.exists(link_file):
77 command = 'ln -s ../../endure_plotter.html ' + link_file
Nirnimesh 2012/09/25 20:31:28 use os.symlink
dennis_jeffrey 2012/09/25 21:47:40 Done.
78 os.system(command)
79 os.chmod(link_file, 0755)
80 link_file = os.path.join(dest_dir, 'endure_plotter.js')
81 if not os.path.exists(link_file):
82 command = 'ln -s ../../endure_plotter.js ' + link_file
83 os.system(command)
84 os.chmod(link_file, 0755)
85 link_file = os.path.join(dest_dir, 'js')
86 if not os.path.exists(link_file):
87 command = 'ln -s ../../js ' + link_file
88 os.system(command)
89 os.chmod(link_file, 0755)
90
91
92 def WriteToDataFile(new_line, existing_lines, revision, data_file):
93 """Writes a new entry to an existing perf data file to be graphed.
94
95 If there's an existing line with the same revision number, overwrite its data
96 with the new line. Else, prepend the info for the new revision.
97
98 Args:
99 new_line: A dictionary representing perf information for the new entry.
100 existing_lines: A list of string lines from the existing perf data file.
101 revision: The string revision number associated with the new perf entry.
102 data_file: The string name of the perf data file to which to write.
103 """
104 overwritten = False
105 for i, line in enumerate(existing_lines):
106 line_dict = simplejson.loads(line)
107 if line_dict['rev'] == revision:
108 existing_lines[i] = simplejson.dumps(new_line)
109 overwritten = True
110 break
111 elif int(line_dict['rev']) < int(revision):
112 break
113 if not overwritten:
114 existing_lines.insert(0, simplejson.dumps(new_line))
115
116 with open(data_file, 'w') as f:
117 f.write('\n'.join(existing_lines))
118 os.chmod(data_file, 0755)
119
120
121 def OutputPerfData(revision, graph_name, description, value, units, units_x,
122 dest_dir):
123 """Outputs perf data to a local text file to be graphed.
124
125 Args:
126 revision: The string revision number associated with the perf data.
127 graph_name: The string name of the graph on which to plot the data.
128 description: A string description of the perf value to be graphed.
129 value: Either a single data value to be graphed, or a list of 2-tuples
130 representing (x, y) points to be graphed for long-running tests.
131 units: The string description for the y-axis units on the graph.
132 units_x: The string description for the x-axis units on the graph. Should
133 be set to None if the results are not for long-running graphs.
134 dest_dir: The name of the destination directory to which to write.
135 """
136 # Update graphs.dat, which contains metadata associated with each graph.
137 existing_graphs = []
138 graphs_file = os.path.join(dest_dir, 'graphs.dat')
139 if os.path.exists(graphs_file):
140 with open(graphs_file, 'r') as f:
141 existing_graphs = simplejson.loads(f.read())
142 is_new_graph = True
143 for graph in existing_graphs:
144 if graph['name'] == graph_name:
145 is_new_graph = False
146 break
147 if is_new_graph:
148 new_graph = {
149 'name': graph_name,
150 'units': units,
151 'important': False,
152 }
153 if units_x:
154 new_graph['units_x'] = units_x
155 existing_graphs.append(new_graph)
156 existing_graphs = sorted(existing_graphs, key=lambda x: x['name'])
157 with open(graphs_file, 'w') as f:
158 f.write(simplejson.dumps(existing_graphs, indent=2))
159 os.chmod(graphs_file, 0755)
160
161 # Update summary data file, containing the actual data to be graphed.
162 data_file_name = graph_name + '-summary.dat'
163 existing_lines = []
164 data_file = os.path.join(dest_dir, data_file_name)
165 if os.path.exists(data_file):
166 with open(data_file, 'r') as f:
167 existing_lines = f.readlines()
168 existing_lines = map(lambda x: x.strip(), existing_lines)
169 if units_x:
170 points = []
171 for point in value:
172 points.append([str(point[0]), str(point[1])])
173 new_traces = {
174 description: points
175 }
176 else:
177 new_traces = {
178 description: [str(value), str(0.0)]
179 }
180 new_line = {
181 'traces': new_traces,
182 'rev': revision
183 }
184
185 WriteToDataFile(new_line, existing_lines, revision, data_file)
186
187
188 def OutputEventData(revision, description, event_list, dest_dir):
189 """Outputs event data to a local text file to be graphed.
190
191 Args:
192 revision: The string revision number associated with the event data.
193 description: A string description of the event values to be graphed.
194 event_list: An array of tuples representing event data to be graphed.
195 dest_dir: The name of the destination directory to which to write.
196 """
197 data_file_name = '_EVENT_-summary.dat'
198 existing_lines = []
199 data_file = os.path.join(dest_dir, data_file_name)
200 if os.path.exists(data_file):
201 with open(data_file, 'r') as f:
202 existing_lines = f.readlines()
203 existing_lines = map(lambda x: x.strip(), existing_lines)
204
205 value_list = []
206 for event_time, event_data in event_list:
207 value_list.append([str(event_time), event_data])
208 new_events = {
209 description: value_list
210 }
211
212 new_line = {
213 'rev': revision,
214 'events': new_events
215 }
216
217 WriteToDataFile(new_line, existing_lines, revision, data_file)
218
219
220 def UpdatePerfDataForSlaveAndBuild(slave_info, build_num):
221 """Process updated perf data for a particular slave and build number.
222
223 Args:
224 slave_info: A dictionary containing information about the slave to process.
225 build_num: The particular build number on the slave to process.
226
227 Returns:
228 True if the perf data for the given slave/build is updated properly, or
229 False if any critical error occurred.
230 """
231 logging.debug(' %s, build %d.', slave_info['slave_name'], build_num)
232 build_url = (BUILDER_URL_BASE + urllib.quote(slave_info['slave_name']) +
233 '/builds/' + str(build_num))
234
235 url_contents = ''
236 fp = None
237 try:
238 fp = urllib2.urlopen(build_url, timeout=60)
239 url_contents = fp.read()
240 except urllib2.URLError, e:
241 logging.exception('Error reading build URL "%s": %s', build_url, str(e))
242 return False
243 finally:
244 if fp:
245 fp.close()
246
247 # Extract the revision number for this build.
248 revision = re.findall(
249 r'<td class="left">got_revision</td>\s+<td>(\d+)</td>\s+<td>Source</td>',
250 url_contents)
251 if not revision:
252 logging.warning('Could not get revision number. Assuming build is too new '
253 'or was cancelled.')
254 return True # Do not fail the script in this case; continue with next one.
255 revision = revision[0]
256
257 # Extract any Chrome Endure stdio links for this build.
258 stdio_urls = []
259 links = re.findall(r'(/steps/endure[^/]+/logs/stdio)', url_contents)
260 for link in links:
261 link_unquoted = urllib.unquote(link)
262 found_wpr_result = False
263 match = re.findall(r'endure_([^_]+)_test ([^/]+)/', link_unquoted)
264 if not match:
265 match = re.findall(r'endure_([^_]+)_wpr_test ([^/]+)/', link_unquoted)
266 if match:
267 found_wpr_result = True
268 else:
269 logging.error('Test name not in expected format in link: ' +
270 link_unquoted)
271 return False
272 match = match[0]
273 webapp_name = match[0] + '_wpr' if found_wpr_result else match[0]
274 test_name = match[1]
275 stdio_urls.append({
276 'link': build_url + link + '/text',
277 'webapp_name': webapp_name,
278 'test_name': test_name,
279 })
280
281 # For each test stdio link, parse it and look for new perf data to be graphed.
282 for stdio_url_data in stdio_urls:
283 stdio_url = stdio_url_data['link']
284 url_contents = ''
285 fp = None
286 try:
287 fp = urllib2.urlopen(stdio_url, timeout=60)
288 # Since in-progress test output is sent chunked, there's no EOF. We need
289 # to specially handle this case so we don't hang here waiting for the
290 # test to complete.
291 start_time = time.time()
292 while True:
293 data = fp.read(1024)
294 if not data:
295 break
296 url_contents += data
297 if time.time() - start_time >= 30: # Read for at most 30 seconds.
298 break
299 except (urllib2.URLError, socket.error), e:
300 # Issue warning but continue to the next stdio link.
301 logging.warning('Error reading test stdio URL "%s": %s', stdio_url,
302 str(e))
303 finally:
304 if fp:
305 fp.close()
306
307 perf_data_raw = []
308
309 def AppendRawPerfData(graph_name, description, value, units, units_x,
310 webapp_name, test_name):
311 perf_data_raw.append({
312 'graph_name': graph_name,
313 'description': description,
314 'value': value,
315 'units': units,
316 'units_x': units_x,
317 'webapp_name': webapp_name,
318 'test_name': test_name,
319 })
320
321 # First scan for short-running perf test results.
322 for match in re.findall(
323 r'RESULT ([^:]+): ([^=]+)= ([-\d\.]+) (\S+)', url_contents):
324 AppendRawPerfData(match[0], match[1], eval(match[2]), match[3], None,
325 stdio_url_data['webapp_name'],
326 stdio_url_data['webapp_name'])
327
328 # Next scan for long-running perf test results.
329 for match in re.findall(
330 r'RESULT ([^:]+): ([^=]+)= (\[[^\]]+\]) (\S+) (\S+)', url_contents):
331 AppendRawPerfData(match[0], match[1], eval(match[2]), match[3], match[4],
332 stdio_url_data['webapp_name'],
333 stdio_url_data['test_name'])
334
335 # Next scan for events in the test results.
336 for match in re.findall(
337 r'RESULT _EVENT_: ([^=]+)= (\[[^\]]+\])', url_contents):
338 AppendRawPerfData('_EVENT_', match[0], eval(match[1]), None, None,
339 stdio_url_data['webapp_name'],
340 stdio_url_data['test_name'])
341
342 # For each graph_name/description pair that refers to a long-running test
343 # result or an event, concatenate all the results together (assume results
344 # in the input file are in the correct order). For short-running test
345 # results, keep just one if more than one is specified.
346 perf_data = {} # Maps a graph-line key to a perf data dictionary.
347 for data in perf_data_raw:
348 key = data['graph_name'] + '|' + data['description']
349 if data['graph_name'] != '_EVENT_' and not data['units_x']:
350 # Short-running test result.
351 perf_data[key] = data
352 else:
353 # Long-running test result or event.
354 if key in perf_data:
355 perf_data[key]['value'] += data['value']
356 else:
357 perf_data[key] = data
358
359 # Finally, for each graph-line in |perf_data|, update the associated local
360 # graph data files if necessary.
361 for perf_data_key in perf_data:
362 perf_data_dict = perf_data[perf_data_key]
363
364 dest_dir = os.path.join(LOCAL_GRAPH_DIR, perf_data_dict['webapp_name'])
365 if not os.path.exists(dest_dir):
366 os.mkdir(dest_dir) # Webapp name directory.
367 os.chmod(dest_dir, 0755)
368 dest_dir = os.path.join(dest_dir, perf_data_dict['test_name'])
369
370 SetupBaseGraphDirIfNeeded(perf_data_dict['webapp_name'],
371 perf_data_dict['test_name'], dest_dir)
372 if perf_data_dict['graph_name'] == '_EVENT_':
373 OutputEventData(revision, perf_data_dict['description'],
374 perf_data_dict['value'], dest_dir)
375 else:
376 OutputPerfData(revision, perf_data_dict['graph_name'],
377 perf_data_dict['description'], perf_data_dict['value'],
378 perf_data_dict['units'], perf_data_dict['units_x'],
379 dest_dir)
380
381 return True
382
383
384 def UpdatePerfDataFiles():
385 """Updates the Chrome Endure graph data files with the latest test results.
386
387 For each known Chrome Endure slave, we scan its latest test results looking
388 for any new test data. Any new data that is found is then appended to the
389 data files used to display the Chrome Endure graphs.
390
391 Returns:
392 True if all graph data files are updated properly, or
393 False if any error occurred.
394 """
395 slave_list = []
396 for slave_name in CHROME_ENDURE_SLAVE_NAMES:
397 slave_info = {}
398 slave_info['slave_name'] = slave_name
399 slave_info['most_recent_build_num'] = None
400 slave_info['last_processed_build_num'] = None
401 slave_list.append(slave_info)
402
403 # Identify the most recent build number for each slave.
404 logging.debug('Searching for latest build numbers for each slave...')
405 for slave in slave_list:
406 slave_name = slave['slave_name']
407 slave_url = BUILDER_URL_BASE + urllib.quote(slave_name)
408
409 url_contents = ''
410 fp = None
411 try:
412 fp = urllib2.urlopen(slave_url, timeout=60)
413 url_contents = fp.read()
414 except urllib2.URLError, e:
415 logging.exception('Error reading builder URL: %s', str(e))
416 return False
417 finally:
418 if fp:
419 fp.close()
420
421 matches = re.findall(r'/(\d+)/stop', url_contents)
422 if matches:
423 slave['most_recent_build_num'] = int(matches[0])
424 else:
425 matches = re.findall(r'#(\d+)</a></td>', url_contents)
426 if matches:
427 slave['most_recent_build_num'] = sorted(map(int, matches),
428 reverse=True)[0]
429 else:
430 logging.error('Could not identify latest build number for slave %s.',
431 slave_name)
432 return False
433
434 logging.debug('%s most recent build number: %s', slave_name,
435 slave['most_recent_build_num'])
436
437 # Identify the last-processed build number for each slave.
438 logging.debug('Identifying last processed build numbers...')
439 if not os.path.exists(LAST_BUILD_NUM_PROCESSED_FILE):
440 for slave_info in slave_list:
441 slave_info['last_processed_build_num'] = 0
442 else:
443 with open(LAST_BUILD_NUM_PROCESSED_FILE, 'r') as fp:
444 file_contents = fp.read()
445 for match in re.findall(r'([^:]+):(\d+)', file_contents):
446 slave_name = match[0].strip()
447 last_processed_build_num = match[1].strip()
448 for slave_info in slave_list:
449 if slave_info['slave_name'] == slave_name:
450 slave_info['last_processed_build_num'] = int(
451 last_processed_build_num)
452 for slave_info in slave_list:
453 if not slave_info['last_processed_build_num']:
454 slave_info['last_processed_build_num'] = 0
455 logging.debug('Done identifying last processed build numbers.')
456
457 # For each Chrome Endure slave, process each build in-between the last
458 # processed build num and the most recent build num, inclusive. To process
459 # each one, first get the revision number for that build, then scan the test
460 # result stdio for any performance data, and add any new performance data to
461 # local files to be graphed.
462 for slave_info in slave_list:
463 logging.debug('Processing %s, builds %d-%d...',
464 slave_info['slave_name'],
465 slave_info['last_processed_build_num'],
466 slave_info['most_recent_build_num'])
467 curr_build_num = slave_info['last_processed_build_num']
468 while curr_build_num <= slave_info['most_recent_build_num']:
469 if not UpdatePerfDataForSlaveAndBuild(slave_info, curr_build_num):
470 return False
471 curr_build_num += 1
472
473 # Log the newly-processed build numbers.
474 logging.debug('Logging the newly-processed build numbers...')
475 with open(LAST_BUILD_NUM_PROCESSED_FILE, 'w') as f:
476 for slave_info in slave_list:
477 f.write('%s:%s\n' % (slave_info['slave_name'],
478 slave_info['most_recent_build_num']))
479
480 return True
481
482
483 def GenerateIndexPage():
484 """Generates a summary (landing) page for the Chrome Endure graphs."""
485 logging.debug('Generating new index.html page...')
486
487 # Page header.
488 page = """
489 <html>
490
491 <head>
492 <title>Chrome Endure Overview</title>
493 <script language="javascript">
494 function DisplayGraph(name, graph) {
495 document.write(
496 '<td><iframe scrolling="no" height="438" width="700" src="');
497 document.write(name);
498 document.write('"></iframe></td>');
499 }
500 </script>
501 </head>
502
503 <body>
504 <center>
505
506 <h1>
507 Chrome Endure
508 </h1>
509 """
510 # Print current time.
511 page += '<p>Updated: %s</p>\n' % (
512 time.strftime('%A, %B %d, %Y at %I:%M:%S %p %Z'))
513
514 # Links for each webapp.
515 webapp_names = [x for x in os.listdir(LOCAL_GRAPH_DIR) if
516 x not in ['js', 'old_data'] and
517 os.path.isdir(os.path.join(LOCAL_GRAPH_DIR, x))]
518 webapp_names = sorted(webapp_names)
519
520 page += '<p> ['
521 for i, name in enumerate(webapp_names):
522 page += '<a href="#%s">%s</a>' % (name.upper(), name.upper())
523 if i < len(webapp_names) - 1:
524 page += ' | '
525 page += '] </p>\n'
526
527 # Print out the data for each webapp.
528 for webapp_name in webapp_names:
529 page += '\n<h1 id="%s">%s</h1>\n' % (webapp_name.upper(),
530 webapp_name.upper())
531
532 # Links for each test for this webapp.
533 test_names = [x for x in
534 os.listdir(os.path.join(LOCAL_GRAPH_DIR, webapp_name))]
535 test_names = sorted(test_names)
536
537 page += '<p> ['
538 for i, name in enumerate(test_names):
539 page += '<a href="#%s">%s</a>' % (name, name)
540 if i < len(test_names) - 1:
541 page += ' | '
542 page += '] </p>\n'
543
544 # Print out the data for each test for this webapp.
545 for test_name in test_names:
546 # Get the set of graph names for this test.
547 graph_names = [x[:x.find('-summary.dat')] for x in
548 os.listdir(os.path.join(LOCAL_GRAPH_DIR,
549 webapp_name, test_name))
550 if '-summary.dat' in x and '_EVENT_' not in x]
551 graph_names = sorted(graph_names)
552
553 page += '<h2 id="%s">%s</h2>\n' % (test_name, test_name)
554 page += '<table>\n'
555
556 for i, graph_name in enumerate(graph_names):
557 if i % 2 == 0:
558 page += ' <tr>\n'
559 page += (' <script>DisplayGraph("%s/%s?graph=%s&lookout=1");'
560 '</script>\n' % (webapp_name, test_name, graph_name))
561 if i % 2 == 1:
562 page += ' </tr>\n'
563 if len(graph_names) % 2 == 1:
564 page += ' </tr>\n'
565 page += '</table>\n'
566
567 # Page footer.
568 page += """
569 </center>
570 </body>
571
572 </html>
573 """
574
575 index_file = os.path.join(LOCAL_GRAPH_DIR, 'index.html')
576 with open(index_file, 'w') as f:
577 f.write(page)
578 os.chmod(index_file, 0755)
579
580
581 def main():
582 parser = optparse.OptionParser()
583 parser.add_option(
584 '-v', '--verbose', action='store_true', default=False,
585 help='Use verbose logging.')
586 options, _ = parser.parse_args(sys.argv)
587
588 logging_level = logging.DEBUG if options.verbose else logging.INFO
589 logging.basicConfig(level=logging_level,
590 format='[%(asctime)s] %(levelname)s: %(message)s')
591
592 success = UpdatePerfDataFiles()
593 if not success:
594 logging.error('Failed to update perf data files.')
595 sys.exit(0)
596
597 GenerateIndexPage()
598 logging.debug('All done!')
599
600
601 if __name__ == '__main__':
602 main()
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698