blob: 828792bb03427d9dcca8139239d04ad1386107e0 [file] [log] [blame]
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -08001#!/usr/bin/env python
2# Copyright (c) 2013 The Chromium OS 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"""Parses perf data files and creates chrome-based graph files from that data.
7
8This script assumes that extract_perf.py was previously run to extract perf
9test data from a database and then dump it into local text data files. This
10script then parses the extracted perf data files and creates new data files that
11can be directly read in by chrome's perf graphing infrastructure to display
12perf graphs.
13
14This script also generates a set of Javascript/HTML overview pages that present
15birds-eye overviews of multiple perf graphs simultaneously.
16
17Sample usage:
18 python generate_perf_graphs.py -c -v
19
20Run with -h to see the full set of command-line options.
Dennis Jeffrey8a305382013-02-28 09:08:57 -080021
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080022"""
23
24import fnmatch
25import logging
26import math
27import optparse
28import os
29import re
30import shutil
31import simplejson
32import sys
33
34_SETTINGS = 'autotest_lib.frontend.settings'
35os.environ['DJANGO_SETTINGS_MODULE'] = _SETTINGS
36
37import common
38from django.shortcuts import render_to_response
39
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080040_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080041_CHART_CONFIG_FILE = os.path.join(_SCRIPT_DIR, 'croschart_defaults.json')
42_TEMPLATE_DIR = os.path.join(_SCRIPT_DIR, 'templates')
Dennis Jeffrey8a305382013-02-28 09:08:57 -080043_CURR_PID_FILE_NAME = __file__ + '.curr_pid.txt'
44_COMPLETED_ID_FILE_NAME = 'job_id_complete.txt'
45_REV_NUM_FILE_NAME = 'rev_num.txt'
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080046
47# Values that can be configured through options.
48# TODO(dennisjeffrey): Infer the tip-of-tree milestone dynamically once this
49# issue is addressed: crosbug.com/38564.
Dennis Jeffrey8a305382013-02-28 09:08:57 -080050_TOT_MILESTONE = 27
51_OLDEST_MILESTONE_TO_GRAPH = 25
52_DATA_DIR = _SCRIPT_DIR
53_GRAPH_DIR = _SCRIPT_DIR
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080054
55# Other values that can only be configured here in the code.
56_SYMLINK_LIST = [
57 ('report.html', '../../../../ui/cros_plotter.html'),
58 ('js', '../../../../ui/js'),
59]
60
61
62def set_world_read_permissions(path):
63 """Recursively sets the content of |path| to be world-readable.
64
Dennis Jeffrey8a305382013-02-28 09:08:57 -080065 @param path: The string path.
66
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080067 """
68 logging.debug('Setting world-read permissions recursively on %s', path)
69 os.chmod(path, 0755)
70 for root, dirs, files in os.walk(path):
71 for d in dirs:
72 dname = os.path.join(root, d)
73 if not os.path.islink(dname):
74 os.chmod(dname, 0755)
75 for f in files:
76 fname = os.path.join(root, f)
77 if not os.path.islink(fname):
78 os.chmod(fname, 0755)
79
80
81def remove_path(path):
Dennis Jeffrey8a305382013-02-28 09:08:57 -080082 """Remove the given path (whether file or directory).
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080083
Dennis Jeffrey8a305382013-02-28 09:08:57 -080084 @param path: The string path.
85
86 """
87 if os.path.isdir(path):
88 shutil.rmtree(path)
89 return
90 try:
91 os.remove(path)
92 except OSError:
93 pass
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080094
95
96def symlink_force(link_name, target):
97 """Create a symlink, accounting for different situations.
98
99 @param link_name: The string name of the link to create.
100 @param target: The string destination file to which the link should point.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800101
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800102 """
103 try:
104 os.unlink(link_name)
105 except EnvironmentError:
106 pass
107 try:
108 os.symlink(target, link_name)
109 except OSError:
110 remove_path(link_name)
111 os.symlink(target, link_name)
112
113
114def mean_and_standard_deviation(data):
115 """Compute the mean and standard deviation of a list of numbers.
116
117 @param data: A list of numerica values.
118
119 @return A 2-tuple (mean, standard_deviation) computed from |data|.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800120
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800121 """
122 n = len(data)
123 if n == 0:
124 return 0.0, 0.0
125 mean = float(sum(data)) / n
126 if n == 1:
127 return mean, 0.0
128 # Divide by n-1 to compute "sample standard deviation".
129 variance = sum([(element - mean) ** 2 for element in data]) / (n - 1)
130 return mean, math.sqrt(variance)
131
132
133def get_release_from_jobname(jobname):
134 """Identifies the release number components from an autotest job name.
135
136 For example:
137 'lumpy-release-R21-2384.0.0_pyauto_perf' becomes (21, 2384, 0, 0).
138
139 @param jobname: The string name of an autotest job.
140
141 @return The 4-tuple containing components of the build release number, or
142 None if those components cannot be identifies from the |jobname|.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800143
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800144 """
145 prog = re.compile('r(\d+)-(\d+).(\d+).(\d+)')
146 m = prog.search(jobname.lower())
147 if m:
148 return (int(m.group(1)), int(m.group(2)), int(m.group(3)),
149 int(m.group(4)))
150 return None
151
152
153def is_on_mainline_of_milestone(jobname, milestone):
154 """Determines whether an autotest build is on mainline of a given milestone.
155
156 @param jobname: The string name of an autotest job (containing release
157 number).
158 @param milestone: The integer milestone number to consider.
159
160 @return True, if the given autotest job name is for a release number that
161 is either (1) an ancestor of the specified milestone, or (2) is on the
162 main branch line of the given milestone. Returns False otherwise.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800163
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800164 """
165 r = get_release_from_jobname(jobname)
166 m = milestone
167 # Handle garbage data that might exist.
168 if any(item < 0 for item in r):
169 raise Exception('Unexpected release info in job name: %s' % jobname)
170 if m == r[0]:
171 # Yes for jobs from the specified milestone itself.
172 return True
173 if r[0] < m and r[2] == 0 and r[3] == 0:
174 # Yes for jobs from earlier milestones that were before their respective
175 # branch points.
176 return True
177 return False
178
179
180# TODO(dennisjeffrey): Determine whether or not we need all the values in the
181# config file. Remove unnecessary ones and revised necessary ones as needed.
182def create_config_js_file(path, test_name):
183 """Creates a configuration file used by the performance graphs.
184
185 @param path: The string path to the directory in which to create the file.
186 @param test_name: The string name of the test associated with this config
187 file.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800188
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800189 """
190 config_content = render_to_response(
191 os.path.join(_TEMPLATE_DIR, 'config.js'), locals()).content
192 with open(os.path.join(path, 'config.js'), 'w') as f:
193 f.write(config_content)
194
195
196def output_graph_data_for_entry(test_name, graph_name, job_name, platform,
197 units, better_direction, url, perf_keys,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800198 chart_keys, options, summary_id_to_rev_num,
199 output_data_dir):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800200 """Outputs data for a perf test result into appropriate graph data files.
201
202 @param test_name: The string name of a test.
203 @param graph_name: The string name of the graph associated with this result.
204 @param job_name: The string name of the autotest job associated with this
205 test result.
206 @param platform: The string name of the platform associated with this test
207 result.
208 @param units: The string name of the units displayed on this graph.
209 @param better_direction: A String representing whether better perf results
210 are those that are "higher" or "lower".
211 @param url: The string URL of a webpage docuementing the current graph.
212 @param perf_keys: A list of 2-tuples containing perf keys measured by the
213 test, where the first tuple element is a string key name, and the second
214 tuple element is the associated numeric perf value.
215 @param chart_keys: A list of perf key names that need to be displayed in
216 the current graph.
217 @param options: An optparse.OptionParser options object.
218 @param summary_id_to_rev_num: A dictionary mapping a string (representing
219 a test/platform/release combination), to the next integer revision
220 number to use in the graph data file.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800221 @param output_data_dir: A directory in which to output data files.
222
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800223 """
224 # A string ID that is assumed to be unique across all charts.
225 test_id = test_name + '__' + graph_name
226
227 release_num = get_release_from_jobname(job_name)
228 if not release_num:
229 logging.warning('Could not obtain release number for job name: %s',
230 job_name)
231 return
232 build_num = '%d.%d.%d.%d' % (release_num[0], release_num[1], release_num[2],
233 release_num[3])
234
235 # Filter out particular test runs that we explicitly do not want to
236 # consider.
237 # TODO(dennisjeffrey): Figure out a way to eliminate the need for these
238 # special checks: crosbug.com/36685.
239 if test_name == 'platform_BootPerfServer' and 'perfalerts' not in job_name:
240 # Skip platform_BootPerfServer test results that do not come from the
241 # "perfalerts" runs.
242 return
243
244 # Consider all releases for which this test result may need to be included
245 # on a graph.
246 start_release = max(release_num[0], options.oldest_milestone)
247 for release in xrange(start_release, options.tot_milestone + 1):
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800248 output_path = os.path.join(output_data_dir, 'r%d' % release, platform,
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800249 test_id)
250 summary_file = os.path.join(output_path, graph_name + '-summary.dat')
251
252 # Set up the output directory if it doesn't already exist.
253 if not os.path.exists(output_path):
254 os.makedirs(output_path)
255
256 # Create auxiliary files.
257 create_config_js_file(output_path, test_name)
258 open(summary_file, 'w').close()
259 graphs = [{
260 'name': graph_name,
261 'units': units,
262 'better_direction': better_direction,
263 'info_url': url,
264 'important': False,
265 }]
266 with open(os.path.join(output_path, 'graphs.dat'), 'w') as f:
267 f.write(simplejson.dumps(graphs, indent=2))
268
269 # Add symlinks to the plotting code.
270 for slink, target in _SYMLINK_LIST:
271 slink = os.path.join(output_path, slink)
272 symlink_force(slink, target)
273
274 # Write data to graph data file if it belongs in the current release.
275 if is_on_mainline_of_milestone(job_name, release):
276 entry = {}
277 entry['traces'] = {}
278 entry['ver'] = build_num
279
280 key_to_vals = {}
281 for perf_key in perf_keys:
282 if perf_key[0] in chart_keys:
283 # Replace dashes with underscores so different lines show
284 # up as different colors in the graphs.
285 key = perf_key[0].replace('-', '_')
286 if key not in key_to_vals:
287 key_to_vals[key] = []
288 # There are some cases where results for
289 # platform_BootPerfServer are negative in reboot/shutdown
290 # times. Ignore these negative values.
291 if float(perf_key[1]) < 0.0:
292 continue
293 key_to_vals[key].append(perf_key[1])
294 for key in key_to_vals:
295 if len(key_to_vals[key]) == 1:
296 entry['traces'][key] = [key_to_vals[key][0], '0.0']
297 else:
298 mean, std_dev = mean_and_standard_deviation(
299 map(float, key_to_vals[key]))
300 entry['traces'][key] = [str(mean), str(std_dev)]
301
302 if entry['traces']:
303 summary_id = '%s|%s|%s' % (test_id, platform, release)
304
305 rev = summary_id_to_rev_num.get(summary_id, 0)
306 summary_id_to_rev_num[summary_id] = rev + 1
307 entry['rev'] = rev
308
309 with open(summary_file, 'a') as f:
310 f.write(simplejson.dumps(entry) + '\n')
311
312
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800313def process_perf_data_files(file_names, test_name, completed_ids,
314 test_name_to_charts, options,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800315 summary_id_to_rev_num, output_data_dir):
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800316 """Processes data files for a single test/platform.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800317
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800318 Multiple data files may exist if the given test name is associated with one
319 or more old test names (i.e., the name of the test has changed over time).
320 In this case, we treat all results from the specified files as if they came
321 from a single test associated with the current test name.
322
323 This function converts the data from the specified data files into new
324 data files formatted in a way that can be graphed.
325
326 @param file_names: A list of perf data files to process.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800327 @param test_name: The string name of the test associated with the file name
328 to process.
329 @param completed_ids: A dictionary of already-processed job IDs.
330 @param test_name_to_charts: A dictionary mapping test names to a list of
331 dictionaries, in which each dictionary contains information about a
332 chart associated with the given test name.
333 @param options: An optparse.OptionParser options object.
334 @param summary_id_to_rev_num: A dictionary mapping a string (representing
335 a test/platform/release combination) to an integer revision number.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800336 @param output_data_dir: A directory in which to output data files.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800337
338 @return The number of newly-added graph data entries.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800339
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800340 """
341 newly_added_count = 0
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800342 for file_name in file_names:
343 with open(file_name, 'r') as fp:
344 for line in fp.readlines():
345 info = simplejson.loads(line.strip())
346 job_id = info[0]
347 job_name = info[1]
348 platform = info[2]
349 perf_keys = info[3]
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800350
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800351 # Skip this job ID if it's already been processed.
352 if job_id in completed_ids:
353 continue
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800354
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800355 # Scan the desired charts and see if we need to output the
356 # current line info to a graph output file.
357 for chart in test_name_to_charts[test_name]:
358 graph_name = chart['graph_name']
359 units = chart['units']
360 better_direction = chart['better_direction']
361 url = chart['info_url']
362 chart_keys = chart['keys']
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800363
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800364 store_entry = False
365 for chart_key in chart_keys:
366 if chart_key in [x[0] for x in perf_keys]:
367 store_entry = True
368 break
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800369
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800370 if store_entry:
371 output_graph_data_for_entry(
372 test_name, graph_name, job_name, platform,
373 units, better_direction, url, perf_keys,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800374 chart_keys, options, summary_id_to_rev_num,
375 output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800376
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800377 # Mark this job ID as having been processed.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800378 with open(os.path.join(output_data_dir,
379 _COMPLETED_ID_FILE_NAME), 'a') as fp:
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800380 fp.write(job_id + '\n')
381 completed_ids[job_id] = True
382 newly_added_count += 1
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800383
384 return newly_added_count
385
386
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800387def initialize_graph_dir(options, input_dir, output_data_dir):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800388 """Initialize/populate the directory that will serve the perf graphs.
389
390 @param options: An optparse.OptionParser options object.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800391 @param input_dir: A directory from which to read previously-extracted
392 perf data.
393 @param output_data_dir: A directory in which to output data files.
394
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800395 """
396 charts = simplejson.loads(open(_CHART_CONFIG_FILE, 'r').read())
397
398 # Identify all the job IDs already processed in the graphs, so that we don't
399 # add that data again.
400 completed_ids = {}
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800401 completed_id_file = os.path.join(output_data_dir, _COMPLETED_ID_FILE_NAME)
402 if os.path.exists(completed_id_file):
403 with open(completed_id_file, 'r') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800404 job_ids = map(lambda x: x.strip(), fp.readlines())
405 for job_id in job_ids:
406 completed_ids[job_id] = True
407
408 # Identify the next revision number to use in the graph data files for each
409 # test/platform/release combination.
410 summary_id_to_rev_num = {}
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800411 rev_num_file = os.path.join(output_data_dir, _REV_NUM_FILE_NAME)
412 if os.path.exists(rev_num_file):
413 with open(rev_num_file, 'r') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800414 summary_id_to_rev_num = simplejson.loads(fp.read())
415
416 test_name_to_charts = {}
417 test_names = set()
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800418 test_name_to_old_names = {}
419 # The _CHART_CONFIG_FILE should (and is assumed to) have one entry per
420 # test_name. That entry should declare all graphs associated with the given
421 # test_name.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800422 for chart in charts:
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800423 test_name_to_charts[chart['test_name']] = chart['graphs']
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800424 test_names.add(chart['test_name'])
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800425 test_name_to_old_names[chart['test_name']] = (
426 chart.get('old_test_names', []))
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800427
428 # Scan all database data and format/output only the new data specified in
429 # the graph JSON file.
430 newly_added_count = 0
431 for i, test_name in enumerate(test_names):
432 logging.debug('Analyzing/converting data for test %d of %d: %s',
433 i+1, len(test_names), test_name)
434
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800435 test_data_dir = os.path.join(input_dir, test_name)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800436 if not os.path.exists(test_data_dir):
437 logging.warning('No test data directory for test: %s', test_name)
438 continue
439 files = os.listdir(test_data_dir)
440 for file_name in files:
441 logging.debug('Processing perf platform data file: %s', file_name)
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800442
443 # The current test may be associated with one or more old test
444 # names for which perf results exist for the current platform.
445 # If so, we need to consider those old perf results too, as being
446 # associated with the current test/platform.
447 files_to_process = [os.path.join(test_data_dir, file_name)]
448 for old_test_name in test_name_to_old_names[test_name]:
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800449 old_test_file_name = os.path.join(input_dir, old_test_name,
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800450 file_name)
451 if os.path.exists(old_test_file_name):
452 logging.debug('(also processing this platform for old test '
453 'name "%s")', old_test_name)
454 files_to_process.append(old_test_file_name)
455
456 newly_added_count += process_perf_data_files(
457 files_to_process, test_name, completed_ids,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800458 test_name_to_charts, options, summary_id_to_rev_num,
459 output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800460
461 # Store the latest revision numbers for each test/platform/release
462 # combination, to be used on the next invocation of this script.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800463 with open(rev_num_file, 'w') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800464 fp.write(simplejson.dumps(summary_id_to_rev_num, indent=2))
465
466 logging.info('Added info for %d new jobs to the graphs!', newly_added_count)
467
468
469def create_branch_platform_overview(graph_dir, branch, platform,
470 branch_to_platform_to_test):
471 """Create an overview webpage for the given branch/platform combination.
472
473 @param graph_dir: The string directory containing the graphing files.
474 @param branch: The string name of the milestone (branch).
475 @param platform: The string name of the platform.
476 @param branch_to_platform_to_test: A dictionary mapping branch names to
477 another dictionary, which maps platform names to a list of test names.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800478
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800479 """
480 branches = sorted(branch_to_platform_to_test.keys(), reverse=True)
481 platform_to_tests = branch_to_platform_to_test[branch]
482 platform_list = sorted(platform_to_tests)
483 tests = []
484 for test_id in sorted(platform_to_tests[platform]):
485 has_data = False
486 test_name = ''
487 test_dir = os.path.join(graph_dir, 'data', branch, platform, test_id)
488 data_file_names = fnmatch.filter(os.listdir(test_dir), '*-summary.dat')
489 if len(data_file_names):
490 txt_name = data_file_names[0]
491 # The name of a test is of the form "X: Y", where X is the
492 # autotest name and Y is the graph name. For example:
493 # "platform_BootPerfServer: seconds_from_kernel".
494 test_name = (test_id[:test_id.find('__')] + ': ' +
495 txt_name[:txt_name.find('-summary.dat')])
496 file_name = os.path.join(test_dir, txt_name)
497 has_data = True if os.path.getsize(file_name) > 3 else False
498 test_info = {
499 'id': test_id,
500 'name': test_name,
501 'has_data': has_data
502 }
503 tests.append(test_info)
504
505 # Special check for certain platforms. Will be removed once we remove
506 # all links to the old-style perf graphs.
507 # TODO(dennisjeffrey): Simplify the below code once the following bug
508 # is addressed to standardize the platform names: crosbug.com/38521.
509 platform_converted = 'snow' if platform == 'daisy' else platform
510 platform_converted_2 = ('x86-' + platform if platform in
511 ['alex', 'mario', 'zgb'] else platform)
512
513 # Output the overview page.
514 page_content = render_to_response(
515 os.path.join(_TEMPLATE_DIR, 'branch_platform_overview.html'),
516 locals()).content
517 file_name = os.path.join(graph_dir, '%s-%s.html' % (branch, platform))
518 with open(file_name, 'w') as f:
519 f.write(page_content)
520
521
522def create_comparison_overview(compare_type, graph_dir, test_id, test_dir,
523 branch_to_platform_to_test):
524 """Create an overview webpage to compare a test by platform or by branch.
525
526 @param compare_type: The string type of comaprison graph this is, either
527 "platform" or "branch".
528 @param graph_dir: The string directory containing the graphing files.
529 @param test_id: The string unique ID for a test result.
530 @param test_dir: The string directory name containing the test data.
531 @param branch_to_platform_to_test: A dictionary mapping branch names to
532 another dictionary, which maps platform names to a list of test names.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800533
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800534 """
535 branches = sorted(branch_to_platform_to_test.keys())
536 platforms = [x.keys() for x in branch_to_platform_to_test.values()]
537 platforms = sorted(set([x for sublist in platforms for x in sublist]))
538
539 autotest_name = test_id[:test_id.find('__')]
540
541 text_file_names = fnmatch.filter(os.listdir(test_dir), '*-summary.dat')
542 test_name = '???'
543 if len(text_file_names):
544 txt_name = text_file_names[0]
545 test_name = txt_name[:txt_name.find('-summary.dat')]
546
547 if compare_type == 'branch':
548 outer_list_items = platforms
549 inner_list_items = branches
550 outer_item_type = 'platform'
551 else:
552 outer_list_items = reversed(branches)
553 inner_list_items = platforms
554 outer_item_type = 'branch'
555
556 outer_list = []
557 for outer_item in outer_list_items:
558 inner_list = []
559 for inner_item in inner_list_items:
560 if outer_item_type == 'branch':
561 branch = outer_item
562 platform = inner_item
563 else:
564 branch = inner_item
565 platform = outer_item
566 has_data = False
567 test_dir = os.path.join(graph_dir, 'data', branch, platform,
568 test_id)
569 if os.path.exists(test_dir):
570 data_file_names = fnmatch.filter(os.listdir(test_dir),
571 '*-summary.dat')
572 if len(data_file_names):
573 file_name = os.path.join(test_dir, data_file_names[0])
574 has_data = True if os.path.getsize(file_name) > 3 else False
575 info = {
576 'inner_item': inner_item,
577 'outer_item': outer_item,
578 'branch': branch,
579 'platform': platform,
580 'has_data': has_data
581 }
582 inner_list.append(info)
583 outer_list.append(inner_list)
584
585 # Output the overview page.
586 page_content = render_to_response(
587 os.path.join(_TEMPLATE_DIR, 'compare_by_overview.html'),
588 locals()).content
589 if compare_type == 'branch':
590 file_name = os.path.join(graph_dir, test_id + '_branch.html')
591 else:
592 file_name = os.path.join(graph_dir, test_id + '_platform.html')
593 with open(file_name, 'w') as f:
594 f.write(page_content)
595
596
597def generate_overview_pages(graph_dir, options):
598 """Create static overview webpages for all the perf graphs.
599
600 @param graph_dir: The string directory containing all the graph data.
601 @param options: An optparse.OptionParser options object.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800602
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800603 """
604 # Identify all the milestone names for which we want overview pages.
605 branches_dir = os.path.join(graph_dir, 'data')
606 branches = os.listdir(branches_dir)
607 branches = sorted(branches)
608 branches = [x for x in branches
609 if os.path.isdir(os.path.join(branches_dir, x)) and
610 int(x[1:]) >= options.oldest_milestone]
611
612 unique_tests = set()
613 unique_test_to_dir = {}
614 branch_to_platform_to_test = {}
615
616 for branch in branches:
617 platforms_dir = os.path.join(branches_dir, branch)
618 if not os.path.isdir(platforms_dir):
619 continue
620 platforms = os.listdir(platforms_dir)
621
622 platform_to_tests = {}
623 for platform in platforms:
624 tests_dir = os.path.join(platforms_dir, platform)
625 tests = os.listdir(tests_dir)
626
627 for test in tests:
628 test_dir = os.path.join(tests_dir, test)
629 unique_tests.add(test)
630 unique_test_to_dir[test] = test_dir
631
632 platform_to_tests[platform] = tests
633
634 branch_to_platform_to_test[branch] = platform_to_tests
635
636 for branch in branch_to_platform_to_test:
637 platforms = branch_to_platform_to_test[branch]
638 for platform in platforms:
639 # Create overview page for this branch/platform combination.
640 create_branch_platform_overview(
641 graph_dir, branch, platform, branch_to_platform_to_test)
642
643 # Make index.html a symlink to the most recent branch.
644 latest_branch = branches[-1]
645 first_plat_for_branch = sorted(
646 branch_to_platform_to_test[latest_branch].keys())[0]
647 symlink_force(
648 os.path.join(graph_dir, 'index.html'),
649 '%s-%s.html' % (latest_branch, first_plat_for_branch))
650
651 # Now create overview pages for each test that compare by platform and by
652 # branch.
653 for test_id in unique_tests:
654 for compare_type in ['branch', 'platform']:
655 create_comparison_overview(
656 compare_type, graph_dir, test_id, unique_test_to_dir[test_id],
657 branch_to_platform_to_test)
658
659
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800660def cleanup(dir_name):
661 """Cleans up when this script is done.
662
663 @param dir_name: A directory containing files to clean up.
664
665 """
666 curr_pid_file = os.path.join(dir_name, _CURR_PID_FILE_NAME)
667 if os.path.isfile(curr_pid_file):
668 os.remove(curr_pid_file)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800669
670
671def main():
672 """Main function."""
673 parser = optparse.OptionParser()
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800674 parser.add_option('-i', '--input-dir', metavar='DIR', type='string',
675 default=_DATA_DIR,
676 help='Absolute path to the input directory from which to '
677 'read the raw perf data previously extracted from '
678 'the database. Assumed to contain a subfolder named '
679 '"data". Defaults to "%default".')
680 parser.add_option('-o', '--output-dir', metavar='DIR', type='string',
681 default=_GRAPH_DIR,
682 help='Absolute path to the output directory in which to '
683 'write data files to be displayed on perf graphs. '
684 'Will be written into a subfolder named "graphs". '
685 'Defaults to "%default".')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800686 parser.add_option('-t', '--tot-milestone', metavar='MSTONE', type='int',
687 default=_TOT_MILESTONE,
688 help='Tip-of-tree (most recent) milestone number. '
689 'Defaults to milestone %default (R%default).')
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800690 parser.add_option('-l', '--oldest-milestone', metavar='MSTONE', type='int',
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800691 default=_OLDEST_MILESTONE_TO_GRAPH,
692 help='Oldest milestone number to display in the graphs. '
693 'Defaults to milestone %default (R%default).')
694 parser.add_option('-c', '--clean', action='store_true', default=False,
695 help='Clean/delete existing graph files and then '
696 're-create them from scratch.')
697 parser.add_option('-v', '--verbose', action='store_true', default=False,
698 help='Use verbose logging.')
699 options, _ = parser.parse_args()
700
701 log_level = logging.DEBUG if options.verbose else logging.INFO
702 logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',
703 level=log_level)
704
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800705 input_dir = os.path.join(options.input_dir, 'data')
706 if not os.path.isdir(input_dir):
707 logging.error('Could not find input data directory "%s"', input_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800708 logging.error('Did you forget to run extract_perf.py first?')
709 sys.exit(1)
710
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800711 common.die_if_already_running(
712 os.path.join(input_dir, _CURR_PID_FILE_NAME), logging)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800713
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800714 output_dir = os.path.join(options.output_dir, 'graphs')
715 output_data_dir = os.path.join(output_dir, 'data')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800716 if options.clean:
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800717 remove_path(output_dir)
718 os.makedirs(output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800719
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800720 initialize_graph_dir(options, input_dir, output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800721
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800722 ui_dir = os.path.join(output_dir, 'ui')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800723 if not os.path.exists(ui_dir):
724 logging.debug('Copying "ui" directory to %s', ui_dir)
725 shutil.copytree(os.path.join(_SCRIPT_DIR, 'ui'), ui_dir)
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800726 doc_dir = os.path.join(output_dir, 'doc')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800727 if not os.path.exists(doc_dir):
728 logging.debug('Copying "doc" directory to %s', doc_dir)
729 shutil.copytree(os.path.join(_SCRIPT_DIR, 'doc'), doc_dir)
730
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800731 generate_overview_pages(output_dir, options)
732 set_world_read_permissions(output_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800733
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800734 cleanup(input_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800735 logging.info('All done!')
736
737
738if __name__ == '__main__':
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800739 main()