blob: 3764d548fa9f8c9a656f1afa77203f66c6a71c86 [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 Jeffreyf61bcd52013-03-14 15:25:57 -070046_WILDCARD = '*'
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080047
48# Values that can be configured through options.
49# TODO(dennisjeffrey): Infer the tip-of-tree milestone dynamically once this
50# issue is addressed: crosbug.com/38564.
Dennis Jeffrey8a305382013-02-28 09:08:57 -080051_TOT_MILESTONE = 27
52_OLDEST_MILESTONE_TO_GRAPH = 25
53_DATA_DIR = _SCRIPT_DIR
54_GRAPH_DIR = _SCRIPT_DIR
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080055
56# Other values that can only be configured here in the code.
57_SYMLINK_LIST = [
58 ('report.html', '../../../../ui/cros_plotter.html'),
59 ('js', '../../../../ui/js'),
60]
61
62
63def set_world_read_permissions(path):
64 """Recursively sets the content of |path| to be world-readable.
65
Dennis Jeffrey8a305382013-02-28 09:08:57 -080066 @param path: The string path.
67
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080068 """
69 logging.debug('Setting world-read permissions recursively on %s', path)
70 os.chmod(path, 0755)
71 for root, dirs, files in os.walk(path):
72 for d in dirs:
73 dname = os.path.join(root, d)
74 if not os.path.islink(dname):
75 os.chmod(dname, 0755)
76 for f in files:
77 fname = os.path.join(root, f)
78 if not os.path.islink(fname):
79 os.chmod(fname, 0755)
80
81
82def remove_path(path):
Dennis Jeffrey8a305382013-02-28 09:08:57 -080083 """Remove the given path (whether file or directory).
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080084
Dennis Jeffrey8a305382013-02-28 09:08:57 -080085 @param path: The string path.
86
87 """
88 if os.path.isdir(path):
89 shutil.rmtree(path)
90 return
91 try:
92 os.remove(path)
93 except OSError:
94 pass
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080095
96
97def symlink_force(link_name, target):
98 """Create a symlink, accounting for different situations.
99
100 @param link_name: The string name of the link to create.
101 @param target: The string destination file to which the link should point.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800102
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800103 """
104 try:
105 os.unlink(link_name)
106 except EnvironmentError:
107 pass
108 try:
109 os.symlink(target, link_name)
110 except OSError:
111 remove_path(link_name)
112 os.symlink(target, link_name)
113
114
115def mean_and_standard_deviation(data):
116 """Compute the mean and standard deviation of a list of numbers.
117
118 @param data: A list of numerica values.
119
120 @return A 2-tuple (mean, standard_deviation) computed from |data|.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800121
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800122 """
123 n = len(data)
124 if n == 0:
125 return 0.0, 0.0
126 mean = float(sum(data)) / n
127 if n == 1:
128 return mean, 0.0
129 # Divide by n-1 to compute "sample standard deviation".
130 variance = sum([(element - mean) ** 2 for element in data]) / (n - 1)
131 return mean, math.sqrt(variance)
132
133
134def get_release_from_jobname(jobname):
135 """Identifies the release number components from an autotest job name.
136
137 For example:
138 'lumpy-release-R21-2384.0.0_pyauto_perf' becomes (21, 2384, 0, 0).
139
140 @param jobname: The string name of an autotest job.
141
142 @return The 4-tuple containing components of the build release number, or
143 None if those components cannot be identifies from the |jobname|.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800144
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800145 """
146 prog = re.compile('r(\d+)-(\d+).(\d+).(\d+)')
147 m = prog.search(jobname.lower())
148 if m:
149 return (int(m.group(1)), int(m.group(2)), int(m.group(3)),
150 int(m.group(4)))
151 return None
152
153
154def is_on_mainline_of_milestone(jobname, milestone):
155 """Determines whether an autotest build is on mainline of a given milestone.
156
157 @param jobname: The string name of an autotest job (containing release
158 number).
159 @param milestone: The integer milestone number to consider.
160
161 @return True, if the given autotest job name is for a release number that
162 is either (1) an ancestor of the specified milestone, or (2) is on the
163 main branch line of the given milestone. Returns False otherwise.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800164
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800165 """
166 r = get_release_from_jobname(jobname)
167 m = milestone
168 # Handle garbage data that might exist.
169 if any(item < 0 for item in r):
170 raise Exception('Unexpected release info in job name: %s' % jobname)
171 if m == r[0]:
172 # Yes for jobs from the specified milestone itself.
173 return True
174 if r[0] < m and r[2] == 0 and r[3] == 0:
175 # Yes for jobs from earlier milestones that were before their respective
176 # branch points.
177 return True
178 return False
179
180
181# TODO(dennisjeffrey): Determine whether or not we need all the values in the
182# config file. Remove unnecessary ones and revised necessary ones as needed.
183def create_config_js_file(path, test_name):
184 """Creates a configuration file used by the performance graphs.
185
186 @param path: The string path to the directory in which to create the file.
187 @param test_name: The string name of the test associated with this config
188 file.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800189
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800190 """
191 config_content = render_to_response(
192 os.path.join(_TEMPLATE_DIR, 'config.js'), locals()).content
193 with open(os.path.join(path, 'config.js'), 'w') as f:
194 f.write(config_content)
195
196
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -0700197def chart_key_matches_actual_key(chart_key, actual_key):
198 """Whether a chart key (with possible wildcard) matches a given actual key.
199
200 A perf key in _CHART_CONFIG_FILE may have wildcards specified to match
201 multiple actual perf keys that a test may measure. For example, the
202 chart key "metric*" could match 3 different actual perf keys: "meticA",
203 "metricB", and "metricC". Wildcards are specified with "*" and may occur
204 in any of these formats:
205 1) *metric: Matches perf keys that end with "metric".
206 2) metric*: Matches perf keys that start with "metric".
207 3) *metric*: Matches perf keys that contain "metric".
208 4) metric: Matches only the perf key "metric" (exact match).
209 5) *: Matches any perf key.
210
211 This function determines whether or not a given chart key (with possible
212 wildcard) matches a given actual key.
213
214 @param chart_key: The chart key string with possible wildcard.
215 @param actual_key: The actual perf key.
216
217 @return True, if the specified chart key matches the actual key, or
218 False otherwise.
219
220 """
221 if chart_key == _WILDCARD:
222 return True
223 elif _WILDCARD not in chart_key:
224 return chart_key == actual_key
225 elif chart_key.startswith(_WILDCARD) and chart_key.endswith(_WILDCARD):
226 return chart_key[len(_WILDCARD):-len(_WILDCARD)] in actual_key
227 elif chart_key.startswith(_WILDCARD):
228 return actual_key.endswith(chart_key[len(_WILDCARD):])
229 elif chart_key.endswith(_WILDCARD):
230 return actual_key.startswith(chart_key[:-len(_WILDCARD)])
231
232 return False
233
234
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800235def output_graph_data_for_entry(test_name, graph_name, job_name, platform,
236 units, better_direction, url, perf_keys,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800237 chart_keys, options, summary_id_to_rev_num,
238 output_data_dir):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800239 """Outputs data for a perf test result into appropriate graph data files.
240
241 @param test_name: The string name of a test.
242 @param graph_name: The string name of the graph associated with this result.
243 @param job_name: The string name of the autotest job associated with this
244 test result.
245 @param platform: The string name of the platform associated with this test
246 result.
247 @param units: The string name of the units displayed on this graph.
248 @param better_direction: A String representing whether better perf results
249 are those that are "higher" or "lower".
250 @param url: The string URL of a webpage docuementing the current graph.
251 @param perf_keys: A list of 2-tuples containing perf keys measured by the
252 test, where the first tuple element is a string key name, and the second
253 tuple element is the associated numeric perf value.
254 @param chart_keys: A list of perf key names that need to be displayed in
255 the current graph.
256 @param options: An optparse.OptionParser options object.
257 @param summary_id_to_rev_num: A dictionary mapping a string (representing
258 a test/platform/release combination), to the next integer revision
259 number to use in the graph data file.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800260 @param output_data_dir: A directory in which to output data files.
261
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800262 """
263 # A string ID that is assumed to be unique across all charts.
264 test_id = test_name + '__' + graph_name
265
266 release_num = get_release_from_jobname(job_name)
267 if not release_num:
268 logging.warning('Could not obtain release number for job name: %s',
269 job_name)
270 return
271 build_num = '%d.%d.%d.%d' % (release_num[0], release_num[1], release_num[2],
272 release_num[3])
273
274 # Filter out particular test runs that we explicitly do not want to
275 # consider.
276 # TODO(dennisjeffrey): Figure out a way to eliminate the need for these
277 # special checks: crosbug.com/36685.
278 if test_name == 'platform_BootPerfServer' and 'perfalerts' not in job_name:
279 # Skip platform_BootPerfServer test results that do not come from the
280 # "perfalerts" runs.
281 return
282
283 # Consider all releases for which this test result may need to be included
284 # on a graph.
285 start_release = max(release_num[0], options.oldest_milestone)
286 for release in xrange(start_release, options.tot_milestone + 1):
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800287 output_path = os.path.join(output_data_dir, 'r%d' % release, platform,
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800288 test_id)
289 summary_file = os.path.join(output_path, graph_name + '-summary.dat')
290
291 # Set up the output directory if it doesn't already exist.
292 if not os.path.exists(output_path):
293 os.makedirs(output_path)
294
295 # Create auxiliary files.
296 create_config_js_file(output_path, test_name)
297 open(summary_file, 'w').close()
298 graphs = [{
299 'name': graph_name,
300 'units': units,
301 'better_direction': better_direction,
302 'info_url': url,
303 'important': False,
304 }]
305 with open(os.path.join(output_path, 'graphs.dat'), 'w') as f:
306 f.write(simplejson.dumps(graphs, indent=2))
307
308 # Add symlinks to the plotting code.
309 for slink, target in _SYMLINK_LIST:
310 slink = os.path.join(output_path, slink)
311 symlink_force(slink, target)
312
313 # Write data to graph data file if it belongs in the current release.
314 if is_on_mainline_of_milestone(job_name, release):
315 entry = {}
316 entry['traces'] = {}
317 entry['ver'] = build_num
318
319 key_to_vals = {}
320 for perf_key in perf_keys:
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -0700321 if any([chart_key_matches_actual_key(c, perf_key[0])
322 for c in chart_keys]):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800323 # Replace dashes with underscores so different lines show
324 # up as different colors in the graphs.
325 key = perf_key[0].replace('-', '_')
326 if key not in key_to_vals:
327 key_to_vals[key] = []
328 # There are some cases where results for
329 # platform_BootPerfServer are negative in reboot/shutdown
330 # times. Ignore these negative values.
331 if float(perf_key[1]) < 0.0:
332 continue
333 key_to_vals[key].append(perf_key[1])
334 for key in key_to_vals:
335 if len(key_to_vals[key]) == 1:
336 entry['traces'][key] = [key_to_vals[key][0], '0.0']
337 else:
338 mean, std_dev = mean_and_standard_deviation(
339 map(float, key_to_vals[key]))
340 entry['traces'][key] = [str(mean), str(std_dev)]
341
342 if entry['traces']:
343 summary_id = '%s|%s|%s' % (test_id, platform, release)
344
345 rev = summary_id_to_rev_num.get(summary_id, 0)
346 summary_id_to_rev_num[summary_id] = rev + 1
347 entry['rev'] = rev
348
349 with open(summary_file, 'a') as f:
350 f.write(simplejson.dumps(entry) + '\n')
351
352
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800353def process_perf_data_files(file_names, test_name, completed_ids,
354 test_name_to_charts, options,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800355 summary_id_to_rev_num, output_data_dir):
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800356 """Processes data files for a single test/platform.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800357
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800358 Multiple data files may exist if the given test name is associated with one
359 or more old test names (i.e., the name of the test has changed over time).
360 In this case, we treat all results from the specified files as if they came
361 from a single test associated with the current test name.
362
363 This function converts the data from the specified data files into new
364 data files formatted in a way that can be graphed.
365
366 @param file_names: A list of perf data files to process.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800367 @param test_name: The string name of the test associated with the file name
368 to process.
369 @param completed_ids: A dictionary of already-processed job IDs.
370 @param test_name_to_charts: A dictionary mapping test names to a list of
371 dictionaries, in which each dictionary contains information about a
372 chart associated with the given test name.
373 @param options: An optparse.OptionParser options object.
374 @param summary_id_to_rev_num: A dictionary mapping a string (representing
375 a test/platform/release combination) to an integer revision number.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800376 @param output_data_dir: A directory in which to output data files.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800377
378 @return The number of newly-added graph data entries.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800379
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800380 """
381 newly_added_count = 0
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800382 for file_name in file_names:
383 with open(file_name, 'r') as fp:
384 for line in fp.readlines():
385 info = simplejson.loads(line.strip())
386 job_id = info[0]
387 job_name = info[1]
388 platform = info[2]
389 perf_keys = info[3]
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800390
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800391 # Skip this job ID if it's already been processed.
392 if job_id in completed_ids:
393 continue
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800394
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800395 # Scan the desired charts and see if we need to output the
396 # current line info to a graph output file.
397 for chart in test_name_to_charts[test_name]:
398 graph_name = chart['graph_name']
399 units = chart['units']
400 better_direction = chart['better_direction']
401 url = chart['info_url']
402 chart_keys = chart['keys']
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800403
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800404 store_entry = False
405 for chart_key in chart_keys:
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -0700406 actual_keys = [x[0] for x in perf_keys]
407 if any([chart_key_matches_actual_key(chart_key, a)
408 for a in actual_keys]):
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800409 store_entry = True
410 break
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800411
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800412 if store_entry:
413 output_graph_data_for_entry(
414 test_name, graph_name, job_name, platform,
415 units, better_direction, url, perf_keys,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800416 chart_keys, options, summary_id_to_rev_num,
417 output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800418
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800419 # Mark this job ID as having been processed.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800420 with open(os.path.join(output_data_dir,
421 _COMPLETED_ID_FILE_NAME), 'a') as fp:
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800422 fp.write(job_id + '\n')
423 completed_ids[job_id] = True
424 newly_added_count += 1
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800425
426 return newly_added_count
427
428
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800429def initialize_graph_dir(options, input_dir, output_data_dir):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800430 """Initialize/populate the directory that will serve the perf graphs.
431
432 @param options: An optparse.OptionParser options object.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800433 @param input_dir: A directory from which to read previously-extracted
434 perf data.
435 @param output_data_dir: A directory in which to output data files.
436
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800437 """
438 charts = simplejson.loads(open(_CHART_CONFIG_FILE, 'r').read())
439
440 # Identify all the job IDs already processed in the graphs, so that we don't
441 # add that data again.
442 completed_ids = {}
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800443 completed_id_file = os.path.join(output_data_dir, _COMPLETED_ID_FILE_NAME)
444 if os.path.exists(completed_id_file):
445 with open(completed_id_file, 'r') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800446 job_ids = map(lambda x: x.strip(), fp.readlines())
447 for job_id in job_ids:
448 completed_ids[job_id] = True
449
450 # Identify the next revision number to use in the graph data files for each
451 # test/platform/release combination.
452 summary_id_to_rev_num = {}
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800453 rev_num_file = os.path.join(output_data_dir, _REV_NUM_FILE_NAME)
454 if os.path.exists(rev_num_file):
455 with open(rev_num_file, 'r') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800456 summary_id_to_rev_num = simplejson.loads(fp.read())
457
458 test_name_to_charts = {}
459 test_names = set()
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800460 test_name_to_old_names = {}
461 # The _CHART_CONFIG_FILE should (and is assumed to) have one entry per
462 # test_name. That entry should declare all graphs associated with the given
463 # test_name.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800464 for chart in charts:
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800465 test_name_to_charts[chart['test_name']] = chart['graphs']
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800466 test_names.add(chart['test_name'])
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800467 test_name_to_old_names[chart['test_name']] = (
468 chart.get('old_test_names', []))
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800469
470 # Scan all database data and format/output only the new data specified in
471 # the graph JSON file.
472 newly_added_count = 0
473 for i, test_name in enumerate(test_names):
474 logging.debug('Analyzing/converting data for test %d of %d: %s',
475 i+1, len(test_names), test_name)
476
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800477 test_data_dir = os.path.join(input_dir, test_name)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800478 if not os.path.exists(test_data_dir):
479 logging.warning('No test data directory for test: %s', test_name)
480 continue
481 files = os.listdir(test_data_dir)
482 for file_name in files:
483 logging.debug('Processing perf platform data file: %s', file_name)
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800484
485 # The current test may be associated with one or more old test
486 # names for which perf results exist for the current platform.
487 # If so, we need to consider those old perf results too, as being
488 # associated with the current test/platform.
489 files_to_process = [os.path.join(test_data_dir, file_name)]
490 for old_test_name in test_name_to_old_names[test_name]:
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800491 old_test_file_name = os.path.join(input_dir, old_test_name,
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800492 file_name)
493 if os.path.exists(old_test_file_name):
494 logging.debug('(also processing this platform for old test '
495 'name "%s")', old_test_name)
496 files_to_process.append(old_test_file_name)
497
498 newly_added_count += process_perf_data_files(
499 files_to_process, test_name, completed_ids,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800500 test_name_to_charts, options, summary_id_to_rev_num,
501 output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800502
503 # Store the latest revision numbers for each test/platform/release
504 # combination, to be used on the next invocation of this script.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800505 with open(rev_num_file, 'w') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800506 fp.write(simplejson.dumps(summary_id_to_rev_num, indent=2))
507
508 logging.info('Added info for %d new jobs to the graphs!', newly_added_count)
509
510
511def create_branch_platform_overview(graph_dir, branch, platform,
512 branch_to_platform_to_test):
513 """Create an overview webpage for the given branch/platform combination.
514
515 @param graph_dir: The string directory containing the graphing files.
516 @param branch: The string name of the milestone (branch).
517 @param platform: The string name of the platform.
518 @param branch_to_platform_to_test: A dictionary mapping branch names to
519 another dictionary, which maps platform names to a list of test names.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800520
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800521 """
522 branches = sorted(branch_to_platform_to_test.keys(), reverse=True)
523 platform_to_tests = branch_to_platform_to_test[branch]
524 platform_list = sorted(platform_to_tests)
525 tests = []
526 for test_id in sorted(platform_to_tests[platform]):
527 has_data = False
528 test_name = ''
529 test_dir = os.path.join(graph_dir, 'data', branch, platform, test_id)
530 data_file_names = fnmatch.filter(os.listdir(test_dir), '*-summary.dat')
531 if len(data_file_names):
532 txt_name = data_file_names[0]
533 # The name of a test is of the form "X: Y", where X is the
534 # autotest name and Y is the graph name. For example:
535 # "platform_BootPerfServer: seconds_from_kernel".
536 test_name = (test_id[:test_id.find('__')] + ': ' +
537 txt_name[:txt_name.find('-summary.dat')])
538 file_name = os.path.join(test_dir, txt_name)
539 has_data = True if os.path.getsize(file_name) > 3 else False
540 test_info = {
541 'id': test_id,
542 'name': test_name,
543 'has_data': has_data
544 }
545 tests.append(test_info)
546
547 # Special check for certain platforms. Will be removed once we remove
548 # all links to the old-style perf graphs.
549 # TODO(dennisjeffrey): Simplify the below code once the following bug
550 # is addressed to standardize the platform names: crosbug.com/38521.
551 platform_converted = 'snow' if platform == 'daisy' else platform
552 platform_converted_2 = ('x86-' + platform if platform in
553 ['alex', 'mario', 'zgb'] else platform)
554
555 # Output the overview page.
556 page_content = render_to_response(
557 os.path.join(_TEMPLATE_DIR, 'branch_platform_overview.html'),
558 locals()).content
559 file_name = os.path.join(graph_dir, '%s-%s.html' % (branch, platform))
560 with open(file_name, 'w') as f:
561 f.write(page_content)
562
563
564def create_comparison_overview(compare_type, graph_dir, test_id, test_dir,
565 branch_to_platform_to_test):
566 """Create an overview webpage to compare a test by platform or by branch.
567
568 @param compare_type: The string type of comaprison graph this is, either
569 "platform" or "branch".
570 @param graph_dir: The string directory containing the graphing files.
571 @param test_id: The string unique ID for a test result.
572 @param test_dir: The string directory name containing the test data.
573 @param branch_to_platform_to_test: A dictionary mapping branch names to
574 another dictionary, which maps platform names to a list of test names.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800575
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800576 """
577 branches = sorted(branch_to_platform_to_test.keys())
578 platforms = [x.keys() for x in branch_to_platform_to_test.values()]
579 platforms = sorted(set([x for sublist in platforms for x in sublist]))
580
581 autotest_name = test_id[:test_id.find('__')]
582
583 text_file_names = fnmatch.filter(os.listdir(test_dir), '*-summary.dat')
584 test_name = '???'
585 if len(text_file_names):
586 txt_name = text_file_names[0]
587 test_name = txt_name[:txt_name.find('-summary.dat')]
588
589 if compare_type == 'branch':
590 outer_list_items = platforms
591 inner_list_items = branches
592 outer_item_type = 'platform'
593 else:
594 outer_list_items = reversed(branches)
595 inner_list_items = platforms
596 outer_item_type = 'branch'
597
598 outer_list = []
599 for outer_item in outer_list_items:
600 inner_list = []
601 for inner_item in inner_list_items:
602 if outer_item_type == 'branch':
603 branch = outer_item
604 platform = inner_item
605 else:
606 branch = inner_item
607 platform = outer_item
608 has_data = False
609 test_dir = os.path.join(graph_dir, 'data', branch, platform,
610 test_id)
611 if os.path.exists(test_dir):
612 data_file_names = fnmatch.filter(os.listdir(test_dir),
613 '*-summary.dat')
614 if len(data_file_names):
615 file_name = os.path.join(test_dir, data_file_names[0])
616 has_data = True if os.path.getsize(file_name) > 3 else False
617 info = {
618 'inner_item': inner_item,
619 'outer_item': outer_item,
620 'branch': branch,
621 'platform': platform,
622 'has_data': has_data
623 }
624 inner_list.append(info)
625 outer_list.append(inner_list)
626
627 # Output the overview page.
628 page_content = render_to_response(
629 os.path.join(_TEMPLATE_DIR, 'compare_by_overview.html'),
630 locals()).content
631 if compare_type == 'branch':
632 file_name = os.path.join(graph_dir, test_id + '_branch.html')
633 else:
634 file_name = os.path.join(graph_dir, test_id + '_platform.html')
635 with open(file_name, 'w') as f:
636 f.write(page_content)
637
638
639def generate_overview_pages(graph_dir, options):
640 """Create static overview webpages for all the perf graphs.
641
642 @param graph_dir: The string directory containing all the graph data.
643 @param options: An optparse.OptionParser options object.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800644
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800645 """
646 # Identify all the milestone names for which we want overview pages.
647 branches_dir = os.path.join(graph_dir, 'data')
648 branches = os.listdir(branches_dir)
649 branches = sorted(branches)
650 branches = [x for x in branches
651 if os.path.isdir(os.path.join(branches_dir, x)) and
652 int(x[1:]) >= options.oldest_milestone]
653
654 unique_tests = set()
655 unique_test_to_dir = {}
656 branch_to_platform_to_test = {}
657
658 for branch in branches:
659 platforms_dir = os.path.join(branches_dir, branch)
660 if not os.path.isdir(platforms_dir):
661 continue
662 platforms = os.listdir(platforms_dir)
663
664 platform_to_tests = {}
665 for platform in platforms:
666 tests_dir = os.path.join(platforms_dir, platform)
667 tests = os.listdir(tests_dir)
668
669 for test in tests:
670 test_dir = os.path.join(tests_dir, test)
671 unique_tests.add(test)
672 unique_test_to_dir[test] = test_dir
673
674 platform_to_tests[platform] = tests
675
676 branch_to_platform_to_test[branch] = platform_to_tests
677
678 for branch in branch_to_platform_to_test:
679 platforms = branch_to_platform_to_test[branch]
680 for platform in platforms:
681 # Create overview page for this branch/platform combination.
682 create_branch_platform_overview(
683 graph_dir, branch, platform, branch_to_platform_to_test)
684
685 # Make index.html a symlink to the most recent branch.
686 latest_branch = branches[-1]
687 first_plat_for_branch = sorted(
688 branch_to_platform_to_test[latest_branch].keys())[0]
689 symlink_force(
690 os.path.join(graph_dir, 'index.html'),
691 '%s-%s.html' % (latest_branch, first_plat_for_branch))
692
693 # Now create overview pages for each test that compare by platform and by
694 # branch.
695 for test_id in unique_tests:
696 for compare_type in ['branch', 'platform']:
697 create_comparison_overview(
698 compare_type, graph_dir, test_id, unique_test_to_dir[test_id],
699 branch_to_platform_to_test)
700
701
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800702def cleanup(dir_name):
703 """Cleans up when this script is done.
704
705 @param dir_name: A directory containing files to clean up.
706
707 """
708 curr_pid_file = os.path.join(dir_name, _CURR_PID_FILE_NAME)
709 if os.path.isfile(curr_pid_file):
710 os.remove(curr_pid_file)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800711
712
713def main():
714 """Main function."""
715 parser = optparse.OptionParser()
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800716 parser.add_option('-i', '--input-dir', metavar='DIR', type='string',
717 default=_DATA_DIR,
718 help='Absolute path to the input directory from which to '
719 'read the raw perf data previously extracted from '
720 'the database. Assumed to contain a subfolder named '
721 '"data". Defaults to "%default".')
722 parser.add_option('-o', '--output-dir', metavar='DIR', type='string',
723 default=_GRAPH_DIR,
724 help='Absolute path to the output directory in which to '
725 'write data files to be displayed on perf graphs. '
726 'Will be written into a subfolder named "graphs". '
727 'Defaults to "%default".')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800728 parser.add_option('-t', '--tot-milestone', metavar='MSTONE', type='int',
729 default=_TOT_MILESTONE,
730 help='Tip-of-tree (most recent) milestone number. '
731 'Defaults to milestone %default (R%default).')
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800732 parser.add_option('-l', '--oldest-milestone', metavar='MSTONE', type='int',
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800733 default=_OLDEST_MILESTONE_TO_GRAPH,
734 help='Oldest milestone number to display in the graphs. '
735 'Defaults to milestone %default (R%default).')
736 parser.add_option('-c', '--clean', action='store_true', default=False,
737 help='Clean/delete existing graph files and then '
738 're-create them from scratch.')
739 parser.add_option('-v', '--verbose', action='store_true', default=False,
740 help='Use verbose logging.')
741 options, _ = parser.parse_args()
742
743 log_level = logging.DEBUG if options.verbose else logging.INFO
744 logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',
745 level=log_level)
746
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800747 input_dir = os.path.join(options.input_dir, 'data')
748 if not os.path.isdir(input_dir):
749 logging.error('Could not find input data directory "%s"', input_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800750 logging.error('Did you forget to run extract_perf.py first?')
751 sys.exit(1)
752
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800753 common.die_if_already_running(
754 os.path.join(input_dir, _CURR_PID_FILE_NAME), logging)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800755
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800756 output_dir = os.path.join(options.output_dir, 'graphs')
757 output_data_dir = os.path.join(output_dir, 'data')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800758 if options.clean:
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800759 remove_path(output_dir)
760 os.makedirs(output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800761
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800762 initialize_graph_dir(options, input_dir, output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800763
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800764 ui_dir = os.path.join(output_dir, 'ui')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800765 if not os.path.exists(ui_dir):
766 logging.debug('Copying "ui" directory to %s', ui_dir)
767 shutil.copytree(os.path.join(_SCRIPT_DIR, 'ui'), ui_dir)
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800768 doc_dir = os.path.join(output_dir, 'doc')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800769 if not os.path.exists(doc_dir):
770 logging.debug('Copying "doc" directory to %s', doc_dir)
771 shutil.copytree(os.path.join(_SCRIPT_DIR, 'doc'), doc_dir)
772
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800773 generate_overview_pages(output_dir, options)
774 set_world_read_permissions(output_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800775
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800776 cleanup(input_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800777 logging.info('All done!')
778
779
780if __name__ == '__main__':
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800781 main()