blob: 0b30951b549173465f250d7d26f7f08cb8254c05 [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
Dennis Jeffrey900b0692013-03-11 11:15:43 -070033import urllib
Dennis Jeffrey9fece302013-03-26 12:08:06 -070034import urllib2
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080035
36_SETTINGS = 'autotest_lib.frontend.settings'
37os.environ['DJANGO_SETTINGS_MODULE'] = _SETTINGS
38
39import common
40from django.shortcuts import render_to_response
41
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080042_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080043_CHART_CONFIG_FILE = os.path.join(_SCRIPT_DIR, 'croschart_defaults.json')
44_TEMPLATE_DIR = os.path.join(_SCRIPT_DIR, 'templates')
Dennis Jeffrey8a305382013-02-28 09:08:57 -080045_CURR_PID_FILE_NAME = __file__ + '.curr_pid.txt'
46_COMPLETED_ID_FILE_NAME = 'job_id_complete.txt'
47_REV_NUM_FILE_NAME = 'rev_num.txt'
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -070048_WILDCARD = '*'
Dennis Jeffrey9fece302013-03-26 12:08:06 -070049_TELEMETRY_PERF_KEY_IDENTIFIER = 'TELEMETRY'
50_TELEMETRY_PERF_KEY_DELIMITER = '--'
Dennis Jeffrey00ee98b2013-04-30 15:19:06 -070051_NEW_DASH_UPLOAD_URL = 'https://chromeperf.appspot.com/add_point'
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080052
53# Values that can be configured through options.
54# TODO(dennisjeffrey): Infer the tip-of-tree milestone dynamically once this
55# issue is addressed: crosbug.com/38564.
Dennis Jeffrey8a305382013-02-28 09:08:57 -080056_TOT_MILESTONE = 27
57_OLDEST_MILESTONE_TO_GRAPH = 25
58_DATA_DIR = _SCRIPT_DIR
59_GRAPH_DIR = _SCRIPT_DIR
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080060
61# Other values that can only be configured here in the code.
62_SYMLINK_LIST = [
63 ('report.html', '../../../../ui/cros_plotter.html'),
64 ('js', '../../../../ui/js'),
65]
66
67
68def set_world_read_permissions(path):
69 """Recursively sets the content of |path| to be world-readable.
70
Dennis Jeffrey8a305382013-02-28 09:08:57 -080071 @param path: The string path.
72
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080073 """
74 logging.debug('Setting world-read permissions recursively on %s', path)
75 os.chmod(path, 0755)
76 for root, dirs, files in os.walk(path):
77 for d in dirs:
78 dname = os.path.join(root, d)
79 if not os.path.islink(dname):
80 os.chmod(dname, 0755)
81 for f in files:
82 fname = os.path.join(root, f)
83 if not os.path.islink(fname):
84 os.chmod(fname, 0755)
85
86
87def remove_path(path):
Dennis Jeffrey8a305382013-02-28 09:08:57 -080088 """Remove the given path (whether file or directory).
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080089
Dennis Jeffrey8a305382013-02-28 09:08:57 -080090 @param path: The string path.
91
92 """
93 if os.path.isdir(path):
94 shutil.rmtree(path)
95 return
96 try:
97 os.remove(path)
98 except OSError:
99 pass
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800100
101
102def symlink_force(link_name, target):
103 """Create a symlink, accounting for different situations.
104
105 @param link_name: The string name of the link to create.
106 @param target: The string destination file to which the link should point.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800107
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800108 """
109 try:
110 os.unlink(link_name)
111 except EnvironmentError:
112 pass
113 try:
114 os.symlink(target, link_name)
115 except OSError:
116 remove_path(link_name)
117 os.symlink(target, link_name)
118
119
120def mean_and_standard_deviation(data):
121 """Compute the mean and standard deviation of a list of numbers.
122
123 @param data: A list of numerica values.
124
125 @return A 2-tuple (mean, standard_deviation) computed from |data|.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800126
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800127 """
128 n = len(data)
129 if n == 0:
130 return 0.0, 0.0
131 mean = float(sum(data)) / n
132 if n == 1:
133 return mean, 0.0
134 # Divide by n-1 to compute "sample standard deviation".
135 variance = sum([(element - mean) ** 2 for element in data]) / (n - 1)
136 return mean, math.sqrt(variance)
137
138
139def get_release_from_jobname(jobname):
140 """Identifies the release number components from an autotest job name.
141
142 For example:
143 'lumpy-release-R21-2384.0.0_pyauto_perf' becomes (21, 2384, 0, 0).
144
145 @param jobname: The string name of an autotest job.
146
147 @return The 4-tuple containing components of the build release number, or
148 None if those components cannot be identifies from the |jobname|.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800149
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800150 """
151 prog = re.compile('r(\d+)-(\d+).(\d+).(\d+)')
152 m = prog.search(jobname.lower())
153 if m:
154 return (int(m.group(1)), int(m.group(2)), int(m.group(3)),
155 int(m.group(4)))
156 return None
157
158
159def is_on_mainline_of_milestone(jobname, milestone):
160 """Determines whether an autotest build is on mainline of a given milestone.
161
162 @param jobname: The string name of an autotest job (containing release
163 number).
164 @param milestone: The integer milestone number to consider.
165
166 @return True, if the given autotest job name is for a release number that
167 is either (1) an ancestor of the specified milestone, or (2) is on the
168 main branch line of the given milestone. Returns False otherwise.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800169
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800170 """
171 r = get_release_from_jobname(jobname)
172 m = milestone
173 # Handle garbage data that might exist.
174 if any(item < 0 for item in r):
175 raise Exception('Unexpected release info in job name: %s' % jobname)
176 if m == r[0]:
177 # Yes for jobs from the specified milestone itself.
178 return True
179 if r[0] < m and r[2] == 0 and r[3] == 0:
180 # Yes for jobs from earlier milestones that were before their respective
181 # branch points.
182 return True
183 return False
184
185
186# TODO(dennisjeffrey): Determine whether or not we need all the values in the
187# config file. Remove unnecessary ones and revised necessary ones as needed.
188def create_config_js_file(path, test_name):
189 """Creates a configuration file used by the performance graphs.
190
191 @param path: The string path to the directory in which to create the file.
192 @param test_name: The string name of the test associated with this config
193 file.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800194
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800195 """
196 config_content = render_to_response(
197 os.path.join(_TEMPLATE_DIR, 'config.js'), locals()).content
198 with open(os.path.join(path, 'config.js'), 'w') as f:
199 f.write(config_content)
200
201
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -0700202def chart_key_matches_actual_key(chart_key, actual_key):
203 """Whether a chart key (with possible wildcard) matches a given actual key.
204
205 A perf key in _CHART_CONFIG_FILE may have wildcards specified to match
206 multiple actual perf keys that a test may measure. For example, the
207 chart key "metric*" could match 3 different actual perf keys: "meticA",
208 "metricB", and "metricC". Wildcards are specified with "*" and may occur
209 in any of these formats:
210 1) *metric: Matches perf keys that end with "metric".
211 2) metric*: Matches perf keys that start with "metric".
212 3) *metric*: Matches perf keys that contain "metric".
213 4) metric: Matches only the perf key "metric" (exact match).
214 5) *: Matches any perf key.
215
216 This function determines whether or not a given chart key (with possible
217 wildcard) matches a given actual key.
218
219 @param chart_key: The chart key string with possible wildcard.
220 @param actual_key: The actual perf key.
221
222 @return True, if the specified chart key matches the actual key, or
223 False otherwise.
224
225 """
226 if chart_key == _WILDCARD:
227 return True
228 elif _WILDCARD not in chart_key:
229 return chart_key == actual_key
230 elif chart_key.startswith(_WILDCARD) and chart_key.endswith(_WILDCARD):
231 return chart_key[len(_WILDCARD):-len(_WILDCARD)] in actual_key
232 elif chart_key.startswith(_WILDCARD):
233 return actual_key.endswith(chart_key[len(_WILDCARD):])
234 elif chart_key.endswith(_WILDCARD):
235 return actual_key.startswith(chart_key[:-len(_WILDCARD)])
236
237 return False
238
239
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700240def upload_to_chrome_dashboard(data_point_info, platform, test_name,
241 master_name):
242 """Uploads a set of perf values to Chrome's perf dashboard.
243
244 @param data_point_info: A dictionary containing information about perf
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700245 data points to plot: key names and values, chrome(OS) version numbers.
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700246 @param platform: The string name of the associated platform.
247 @param test_name: The string name of the associated test.
248 @param master_name: The string name of the "buildbot master" to use
249 (a concept that exists in Chrome's perf dashboard).
250
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700251 """
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700252
253 # Start slow - only upload results for one bot, one test right now.
254 # TODO(dennisjeffrey): Expand this to include other platforms/tests once
255 # we've proven ourselves with a single bot/test. Longer-term, the
256 # check below will be completely removed as soon as we're ready to upload
257 # the full suite of perf results to the new dashboard. The check is only
258 # in place temporarily to allow a subset of results to be uploaded until
259 # we're ready to upload everything.
260 if platform != 'lumpy' or test_name != 'telemetry_Benchmarks.octane':
261 return
262
263 # Generate a warning and return if any expected values in |data_point_info|
264 # are missing.
265 for expected_val in ['chrome_ver', 'traces', 'ver']:
266 if (expected_val not in data_point_info or
267 not data_point_info[expected_val]):
268 logging.warning('Did not upload data point for test "%s", '
269 'platform "%s": missing value for "%s"',
270 test_name, platform, expected_val)
271 return
272
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700273 traces = data_point_info['traces']
274 for perf_key in traces:
275 perf_val = traces[perf_key][0]
276 perf_err = traces[perf_key][1]
277
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700278 units = None
279 if perf_key.startswith(_TELEMETRY_PERF_KEY_IDENTIFIER):
280 # The perf key is associated with a Telemetry test, and has a
281 # specially-formatted perf key that encodes a graph_name,
282 # trace_name, and units. Example Telemetry perf key:
283 # "TELEMETRY--DeltaBlue--DeltaBlue--score__bigger_is_better_"
284 graph_name, trace_name, units = (
285 perf_key.split(_TELEMETRY_PERF_KEY_DELIMITER)[1:])
286 # The Telemetry test name is the name of the tag that has been
287 # appended to |test_name|. For example, autotest name
288 # "telemetry_Benchmarks.octane" corresponds to Telemetry test name
289 # "octane" on chrome's new perf dashboard.
290 test_name = test_name[test_name.find('.') + 1:]
291
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700292 new_dash_entry = {
293 'master': master_name,
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700294 'bot': 'cros-' + platform, # Prefix to make clear it's chromeOS.
295 'test': '%s/%s/%s' % (test_name, graph_name, trace_name),
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700296 'value': perf_val,
297 'error': perf_err,
298 'supplemental_columns': {
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700299 'r_cros_version': data_point_info['ver'],
300 'r_chrome_version': data_point_info['chrome_ver'],
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700301 }
302 }
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700303 if units:
304 new_dash_entry['units'] = units
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700305 json_string = simplejson.dumps([new_dash_entry], indent=2)
306 params = urllib.urlencode({'data': json_string})
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700307 fp = None
308 try:
309 fp = urllib2.urlopen(_NEW_DASH_UPLOAD_URL, params)
310 errors = fp.read().strip()
311 if errors:
312 raise urllib2.URLError(errors)
313 except urllib2.URLError, e:
314 # TODO(dennisjeffrey): If the live dashboard is currently down,
315 # cache results and retry them later when the live dashboard is
316 # back up. For now we skip the current upload if the live
317 # dashboard is down.
318 logging.exception('Error uploading to new dashboard, skipping '
319 'upload attempt: %s', e)
320 return
321 finally:
322 if fp:
323 fp.close()
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700324
325
326def output_graph_data_for_entry(test_name, master_name, graph_name, job_name,
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700327 platform, chrome_ver, units, better_direction,
328 url, perf_keys, chart_keys, options,
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700329 summary_id_to_rev_num, output_data_dir):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800330 """Outputs data for a perf test result into appropriate graph data files.
331
332 @param test_name: The string name of a test.
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700333 @param master_name: The name of the "buildbot master" to use when uploading
334 perf results to chrome's perf dashboard.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800335 @param graph_name: The string name of the graph associated with this result.
336 @param job_name: The string name of the autotest job associated with this
337 test result.
338 @param platform: The string name of the platform associated with this test
339 result.
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700340 @param chrome_ver: The string Chrome version number associated with this
341 test result.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800342 @param units: The string name of the units displayed on this graph.
343 @param better_direction: A String representing whether better perf results
344 are those that are "higher" or "lower".
345 @param url: The string URL of a webpage docuementing the current graph.
346 @param perf_keys: A list of 2-tuples containing perf keys measured by the
347 test, where the first tuple element is a string key name, and the second
348 tuple element is the associated numeric perf value.
349 @param chart_keys: A list of perf key names that need to be displayed in
350 the current graph.
351 @param options: An optparse.OptionParser options object.
352 @param summary_id_to_rev_num: A dictionary mapping a string (representing
353 a test/platform/release combination), to the next integer revision
354 number to use in the graph data file.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800355 @param output_data_dir: A directory in which to output data files.
356
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800357 """
358 # A string ID that is assumed to be unique across all charts.
359 test_id = test_name + '__' + graph_name
360
361 release_num = get_release_from_jobname(job_name)
362 if not release_num:
363 logging.warning('Could not obtain release number for job name: %s',
364 job_name)
365 return
366 build_num = '%d.%d.%d.%d' % (release_num[0], release_num[1], release_num[2],
367 release_num[3])
368
369 # Filter out particular test runs that we explicitly do not want to
370 # consider.
371 # TODO(dennisjeffrey): Figure out a way to eliminate the need for these
372 # special checks: crosbug.com/36685.
373 if test_name == 'platform_BootPerfServer' and 'perfalerts' not in job_name:
374 # Skip platform_BootPerfServer test results that do not come from the
375 # "perfalerts" runs.
376 return
377
378 # Consider all releases for which this test result may need to be included
379 # on a graph.
380 start_release = max(release_num[0], options.oldest_milestone)
381 for release in xrange(start_release, options.tot_milestone + 1):
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800382 output_path = os.path.join(output_data_dir, 'r%d' % release, platform,
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800383 test_id)
384 summary_file = os.path.join(output_path, graph_name + '-summary.dat')
385
386 # Set up the output directory if it doesn't already exist.
387 if not os.path.exists(output_path):
388 os.makedirs(output_path)
389
390 # Create auxiliary files.
391 create_config_js_file(output_path, test_name)
392 open(summary_file, 'w').close()
393 graphs = [{
394 'name': graph_name,
395 'units': units,
396 'better_direction': better_direction,
397 'info_url': url,
398 'important': False,
399 }]
400 with open(os.path.join(output_path, 'graphs.dat'), 'w') as f:
401 f.write(simplejson.dumps(graphs, indent=2))
402
403 # Add symlinks to the plotting code.
404 for slink, target in _SYMLINK_LIST:
405 slink = os.path.join(output_path, slink)
406 symlink_force(slink, target)
407
408 # Write data to graph data file if it belongs in the current release.
409 if is_on_mainline_of_milestone(job_name, release):
410 entry = {}
411 entry['traces'] = {}
412 entry['ver'] = build_num
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700413 entry['chrome_ver'] = chrome_ver
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800414
415 key_to_vals = {}
416 for perf_key in perf_keys:
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -0700417 if any([chart_key_matches_actual_key(c, perf_key[0])
418 for c in chart_keys]):
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700419 key = perf_key[0]
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800420 if key not in key_to_vals:
421 key_to_vals[key] = []
422 # There are some cases where results for
423 # platform_BootPerfServer are negative in reboot/shutdown
424 # times. Ignore these negative values.
425 if float(perf_key[1]) < 0.0:
426 continue
427 key_to_vals[key].append(perf_key[1])
428 for key in key_to_vals:
429 if len(key_to_vals[key]) == 1:
430 entry['traces'][key] = [key_to_vals[key][0], '0.0']
431 else:
432 mean, std_dev = mean_and_standard_deviation(
433 map(float, key_to_vals[key]))
434 entry['traces'][key] = [str(mean), str(std_dev)]
435
436 if entry['traces']:
437 summary_id = '%s|%s|%s' % (test_id, platform, release)
438
439 rev = summary_id_to_rev_num.get(summary_id, 0)
440 summary_id_to_rev_num[summary_id] = rev + 1
441 entry['rev'] = rev
442
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700443 # Upload data point to the new performance dashboard (only
444 # for the tip-of-tree branch).
445 if release == options.tot_milestone:
446 upload_to_chrome_dashboard(entry, platform, test_name,
447 master_name)
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700448
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700449 # For each perf key, replace dashes with underscores so
450 # different lines show up as different colors in the graphs.
451 for orig_key in entry['traces'].keys():
452 new_key = orig_key.replace('-', '_')
453 entry['traces'][new_key] = entry['traces'].pop(orig_key)
454
455 # Output data point to be displayed on the current (deprecated)
456 # dashboard.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800457 with open(summary_file, 'a') as f:
458 f.write(simplejson.dumps(entry) + '\n')
459
460
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700461def process_perf_data_files(file_names, test_name, master_name, completed_ids,
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800462 test_name_to_charts, options,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800463 summary_id_to_rev_num, output_data_dir):
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800464 """Processes data files for a single test/platform.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800465
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800466 Multiple data files may exist if the given test name is associated with one
467 or more old test names (i.e., the name of the test has changed over time).
468 In this case, we treat all results from the specified files as if they came
469 from a single test associated with the current test name.
470
471 This function converts the data from the specified data files into new
472 data files formatted in a way that can be graphed.
473
474 @param file_names: A list of perf data files to process.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800475 @param test_name: The string name of the test associated with the file name
476 to process.
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700477 @param master_name: The name of the "buildbot master" to use when uploading
478 perf results to chrome's perf dashboard.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800479 @param completed_ids: A dictionary of already-processed job IDs.
480 @param test_name_to_charts: A dictionary mapping test names to a list of
481 dictionaries, in which each dictionary contains information about a
482 chart associated with the given test name.
483 @param options: An optparse.OptionParser options object.
484 @param summary_id_to_rev_num: A dictionary mapping a string (representing
485 a test/platform/release combination) to an integer revision number.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800486 @param output_data_dir: A directory in which to output data files.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800487
488 @return The number of newly-added graph data entries.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800489
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800490 """
491 newly_added_count = 0
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800492 for file_name in file_names:
493 with open(file_name, 'r') as fp:
494 for line in fp.readlines():
495 info = simplejson.loads(line.strip())
496 job_id = info[0]
497 job_name = info[1]
498 platform = info[2]
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700499 chrome_ver = info[3]
500 perf_keys = info[4]
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800501
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800502 # Skip this job ID if it's already been processed.
503 if job_id in completed_ids:
504 continue
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800505
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800506 # Scan the desired charts and see if we need to output the
507 # current line info to a graph output file.
508 for chart in test_name_to_charts[test_name]:
509 graph_name = chart['graph_name']
510 units = chart['units']
511 better_direction = chart['better_direction']
512 url = chart['info_url']
513 chart_keys = chart['keys']
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800514
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800515 store_entry = False
516 for chart_key in chart_keys:
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -0700517 actual_keys = [x[0] for x in perf_keys]
518 if any([chart_key_matches_actual_key(chart_key, a)
519 for a in actual_keys]):
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800520 store_entry = True
521 break
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800522
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800523 if store_entry:
524 output_graph_data_for_entry(
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700525 test_name, master_name, graph_name, job_name,
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700526 platform, chrome_ver, units, better_direction, url,
527 perf_keys, chart_keys, options,
528 summary_id_to_rev_num, output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800529
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800530 # Mark this job ID as having been processed.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800531 with open(os.path.join(output_data_dir,
532 _COMPLETED_ID_FILE_NAME), 'a') as fp:
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800533 fp.write(job_id + '\n')
534 completed_ids[job_id] = True
535 newly_added_count += 1
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800536
537 return newly_added_count
538
539
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800540def initialize_graph_dir(options, input_dir, output_data_dir):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800541 """Initialize/populate the directory that will serve the perf graphs.
542
543 @param options: An optparse.OptionParser options object.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800544 @param input_dir: A directory from which to read previously-extracted
545 perf data.
546 @param output_data_dir: A directory in which to output data files.
547
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800548 """
549 charts = simplejson.loads(open(_CHART_CONFIG_FILE, 'r').read())
550
551 # Identify all the job IDs already processed in the graphs, so that we don't
552 # add that data again.
553 completed_ids = {}
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800554 completed_id_file = os.path.join(output_data_dir, _COMPLETED_ID_FILE_NAME)
555 if os.path.exists(completed_id_file):
556 with open(completed_id_file, 'r') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800557 job_ids = map(lambda x: x.strip(), fp.readlines())
558 for job_id in job_ids:
559 completed_ids[job_id] = True
560
561 # Identify the next revision number to use in the graph data files for each
562 # test/platform/release combination.
563 summary_id_to_rev_num = {}
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800564 rev_num_file = os.path.join(output_data_dir, _REV_NUM_FILE_NAME)
565 if os.path.exists(rev_num_file):
566 with open(rev_num_file, 'r') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800567 summary_id_to_rev_num = simplejson.loads(fp.read())
568
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700569 # TODO (dennisjeffrey): If we have to add another "test_name_to_X"
570 # dictionary to the list below, we should simplify this code to create a
571 # single dictionary that maps test names to an object that contains all
572 # the X's as attributes.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800573 test_name_to_charts = {}
574 test_names = set()
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800575 test_name_to_old_names = {}
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700576 test_name_to_master_name = {}
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800577 # The _CHART_CONFIG_FILE should (and is assumed to) have one entry per
578 # test_name. That entry should declare all graphs associated with the given
579 # test_name.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800580 for chart in charts:
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800581 test_name_to_charts[chart['test_name']] = chart['graphs']
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800582 test_names.add(chart['test_name'])
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800583 test_name_to_old_names[chart['test_name']] = (
584 chart.get('old_test_names', []))
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700585 test_name_to_master_name[chart['test_name']] = (
586 chart.get('master', 'CrosMisc'))
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800587
588 # Scan all database data and format/output only the new data specified in
589 # the graph JSON file.
590 newly_added_count = 0
591 for i, test_name in enumerate(test_names):
592 logging.debug('Analyzing/converting data for test %d of %d: %s',
593 i+1, len(test_names), test_name)
594
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800595 test_data_dir = os.path.join(input_dir, test_name)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800596 if not os.path.exists(test_data_dir):
597 logging.warning('No test data directory for test: %s', test_name)
598 continue
599 files = os.listdir(test_data_dir)
600 for file_name in files:
601 logging.debug('Processing perf platform data file: %s', file_name)
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800602
603 # The current test may be associated with one or more old test
604 # names for which perf results exist for the current platform.
605 # If so, we need to consider those old perf results too, as being
606 # associated with the current test/platform.
607 files_to_process = [os.path.join(test_data_dir, file_name)]
608 for old_test_name in test_name_to_old_names[test_name]:
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800609 old_test_file_name = os.path.join(input_dir, old_test_name,
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800610 file_name)
611 if os.path.exists(old_test_file_name):
612 logging.debug('(also processing this platform for old test '
613 'name "%s")', old_test_name)
614 files_to_process.append(old_test_file_name)
615
616 newly_added_count += process_perf_data_files(
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700617 files_to_process, test_name,
618 test_name_to_master_name.get(test_name), completed_ids,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800619 test_name_to_charts, options, summary_id_to_rev_num,
620 output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800621
622 # Store the latest revision numbers for each test/platform/release
623 # combination, to be used on the next invocation of this script.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800624 with open(rev_num_file, 'w') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800625 fp.write(simplejson.dumps(summary_id_to_rev_num, indent=2))
626
627 logging.info('Added info for %d new jobs to the graphs!', newly_added_count)
628
629
630def create_branch_platform_overview(graph_dir, branch, platform,
631 branch_to_platform_to_test):
632 """Create an overview webpage for the given branch/platform combination.
633
634 @param graph_dir: The string directory containing the graphing files.
635 @param branch: The string name of the milestone (branch).
636 @param platform: The string name of the platform.
637 @param branch_to_platform_to_test: A dictionary mapping branch names to
638 another dictionary, which maps platform names to a list of test names.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800639
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800640 """
641 branches = sorted(branch_to_platform_to_test.keys(), reverse=True)
642 platform_to_tests = branch_to_platform_to_test[branch]
643 platform_list = sorted(platform_to_tests)
644 tests = []
645 for test_id in sorted(platform_to_tests[platform]):
646 has_data = False
647 test_name = ''
648 test_dir = os.path.join(graph_dir, 'data', branch, platform, test_id)
649 data_file_names = fnmatch.filter(os.listdir(test_dir), '*-summary.dat')
650 if len(data_file_names):
651 txt_name = data_file_names[0]
652 # The name of a test is of the form "X: Y", where X is the
653 # autotest name and Y is the graph name. For example:
654 # "platform_BootPerfServer: seconds_from_kernel".
655 test_name = (test_id[:test_id.find('__')] + ': ' +
656 txt_name[:txt_name.find('-summary.dat')])
657 file_name = os.path.join(test_dir, txt_name)
658 has_data = True if os.path.getsize(file_name) > 3 else False
659 test_info = {
660 'id': test_id,
661 'name': test_name,
662 'has_data': has_data
663 }
664 tests.append(test_info)
665
666 # Special check for certain platforms. Will be removed once we remove
667 # all links to the old-style perf graphs.
668 # TODO(dennisjeffrey): Simplify the below code once the following bug
669 # is addressed to standardize the platform names: crosbug.com/38521.
670 platform_converted = 'snow' if platform == 'daisy' else platform
671 platform_converted_2 = ('x86-' + platform if platform in
672 ['alex', 'mario', 'zgb'] else platform)
673
674 # Output the overview page.
675 page_content = render_to_response(
676 os.path.join(_TEMPLATE_DIR, 'branch_platform_overview.html'),
677 locals()).content
678 file_name = os.path.join(graph_dir, '%s-%s.html' % (branch, platform))
679 with open(file_name, 'w') as f:
680 f.write(page_content)
681
682
683def create_comparison_overview(compare_type, graph_dir, test_id, test_dir,
684 branch_to_platform_to_test):
685 """Create an overview webpage to compare a test by platform or by branch.
686
687 @param compare_type: The string type of comaprison graph this is, either
688 "platform" or "branch".
689 @param graph_dir: The string directory containing the graphing files.
690 @param test_id: The string unique ID for a test result.
691 @param test_dir: The string directory name containing the test data.
692 @param branch_to_platform_to_test: A dictionary mapping branch names to
693 another dictionary, which maps platform names to a list of test names.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800694
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800695 """
696 branches = sorted(branch_to_platform_to_test.keys())
697 platforms = [x.keys() for x in branch_to_platform_to_test.values()]
698 platforms = sorted(set([x for sublist in platforms for x in sublist]))
699
700 autotest_name = test_id[:test_id.find('__')]
701
702 text_file_names = fnmatch.filter(os.listdir(test_dir), '*-summary.dat')
703 test_name = '???'
704 if len(text_file_names):
705 txt_name = text_file_names[0]
706 test_name = txt_name[:txt_name.find('-summary.dat')]
707
708 if compare_type == 'branch':
709 outer_list_items = platforms
710 inner_list_items = branches
711 outer_item_type = 'platform'
712 else:
713 outer_list_items = reversed(branches)
714 inner_list_items = platforms
715 outer_item_type = 'branch'
716
717 outer_list = []
718 for outer_item in outer_list_items:
719 inner_list = []
720 for inner_item in inner_list_items:
721 if outer_item_type == 'branch':
722 branch = outer_item
723 platform = inner_item
724 else:
725 branch = inner_item
726 platform = outer_item
727 has_data = False
728 test_dir = os.path.join(graph_dir, 'data', branch, platform,
729 test_id)
730 if os.path.exists(test_dir):
731 data_file_names = fnmatch.filter(os.listdir(test_dir),
732 '*-summary.dat')
733 if len(data_file_names):
734 file_name = os.path.join(test_dir, data_file_names[0])
735 has_data = True if os.path.getsize(file_name) > 3 else False
736 info = {
737 'inner_item': inner_item,
738 'outer_item': outer_item,
739 'branch': branch,
740 'platform': platform,
741 'has_data': has_data
742 }
743 inner_list.append(info)
744 outer_list.append(inner_list)
745
746 # Output the overview page.
747 page_content = render_to_response(
748 os.path.join(_TEMPLATE_DIR, 'compare_by_overview.html'),
749 locals()).content
750 if compare_type == 'branch':
751 file_name = os.path.join(graph_dir, test_id + '_branch.html')
752 else:
753 file_name = os.path.join(graph_dir, test_id + '_platform.html')
754 with open(file_name, 'w') as f:
755 f.write(page_content)
756
757
758def generate_overview_pages(graph_dir, options):
759 """Create static overview webpages for all the perf graphs.
760
761 @param graph_dir: The string directory containing all the graph data.
762 @param options: An optparse.OptionParser options object.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800763
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800764 """
765 # Identify all the milestone names for which we want overview pages.
766 branches_dir = os.path.join(graph_dir, 'data')
767 branches = os.listdir(branches_dir)
768 branches = sorted(branches)
769 branches = [x for x in branches
770 if os.path.isdir(os.path.join(branches_dir, x)) and
771 int(x[1:]) >= options.oldest_milestone]
772
773 unique_tests = set()
774 unique_test_to_dir = {}
775 branch_to_platform_to_test = {}
776
777 for branch in branches:
778 platforms_dir = os.path.join(branches_dir, branch)
779 if not os.path.isdir(platforms_dir):
780 continue
781 platforms = os.listdir(platforms_dir)
782
783 platform_to_tests = {}
784 for platform in platforms:
785 tests_dir = os.path.join(platforms_dir, platform)
786 tests = os.listdir(tests_dir)
787
788 for test in tests:
789 test_dir = os.path.join(tests_dir, test)
790 unique_tests.add(test)
791 unique_test_to_dir[test] = test_dir
792
793 platform_to_tests[platform] = tests
794
795 branch_to_platform_to_test[branch] = platform_to_tests
796
797 for branch in branch_to_platform_to_test:
798 platforms = branch_to_platform_to_test[branch]
799 for platform in platforms:
800 # Create overview page for this branch/platform combination.
801 create_branch_platform_overview(
802 graph_dir, branch, platform, branch_to_platform_to_test)
803
804 # Make index.html a symlink to the most recent branch.
805 latest_branch = branches[-1]
806 first_plat_for_branch = sorted(
807 branch_to_platform_to_test[latest_branch].keys())[0]
808 symlink_force(
809 os.path.join(graph_dir, 'index.html'),
810 '%s-%s.html' % (latest_branch, first_plat_for_branch))
811
812 # Now create overview pages for each test that compare by platform and by
813 # branch.
814 for test_id in unique_tests:
815 for compare_type in ['branch', 'platform']:
816 create_comparison_overview(
817 compare_type, graph_dir, test_id, unique_test_to_dir[test_id],
818 branch_to_platform_to_test)
819
820
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800821def cleanup(dir_name):
822 """Cleans up when this script is done.
823
824 @param dir_name: A directory containing files to clean up.
825
826 """
827 curr_pid_file = os.path.join(dir_name, _CURR_PID_FILE_NAME)
828 if os.path.isfile(curr_pid_file):
829 os.remove(curr_pid_file)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800830
831
832def main():
833 """Main function."""
834 parser = optparse.OptionParser()
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800835 parser.add_option('-i', '--input-dir', metavar='DIR', type='string',
836 default=_DATA_DIR,
837 help='Absolute path to the input directory from which to '
838 'read the raw perf data previously extracted from '
839 'the database. Assumed to contain a subfolder named '
840 '"data". Defaults to "%default".')
841 parser.add_option('-o', '--output-dir', metavar='DIR', type='string',
842 default=_GRAPH_DIR,
843 help='Absolute path to the output directory in which to '
844 'write data files to be displayed on perf graphs. '
845 'Will be written into a subfolder named "graphs". '
846 'Defaults to "%default".')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800847 parser.add_option('-t', '--tot-milestone', metavar='MSTONE', type='int',
848 default=_TOT_MILESTONE,
849 help='Tip-of-tree (most recent) milestone number. '
850 'Defaults to milestone %default (R%default).')
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800851 parser.add_option('-l', '--oldest-milestone', metavar='MSTONE', type='int',
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800852 default=_OLDEST_MILESTONE_TO_GRAPH,
853 help='Oldest milestone number to display in the graphs. '
854 'Defaults to milestone %default (R%default).')
855 parser.add_option('-c', '--clean', action='store_true', default=False,
856 help='Clean/delete existing graph files and then '
857 're-create them from scratch.')
858 parser.add_option('-v', '--verbose', action='store_true', default=False,
859 help='Use verbose logging.')
860 options, _ = parser.parse_args()
861
862 log_level = logging.DEBUG if options.verbose else logging.INFO
863 logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',
864 level=log_level)
865
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800866 input_dir = os.path.join(options.input_dir, 'data')
867 if not os.path.isdir(input_dir):
868 logging.error('Could not find input data directory "%s"', input_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800869 logging.error('Did you forget to run extract_perf.py first?')
870 sys.exit(1)
871
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800872 common.die_if_already_running(
873 os.path.join(input_dir, _CURR_PID_FILE_NAME), logging)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800874
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800875 output_dir = os.path.join(options.output_dir, 'graphs')
876 output_data_dir = os.path.join(output_dir, 'data')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800877 if options.clean:
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800878 remove_path(output_dir)
879 os.makedirs(output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800880
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800881 initialize_graph_dir(options, input_dir, output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800882
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800883 ui_dir = os.path.join(output_dir, 'ui')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800884 if not os.path.exists(ui_dir):
885 logging.debug('Copying "ui" directory to %s', ui_dir)
886 shutil.copytree(os.path.join(_SCRIPT_DIR, 'ui'), ui_dir)
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800887 doc_dir = os.path.join(output_dir, 'doc')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800888 if not os.path.exists(doc_dir):
889 logging.debug('Copying "doc" directory to %s', doc_dir)
890 shutil.copytree(os.path.join(_SCRIPT_DIR, 'doc'), doc_dir)
891
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800892 generate_overview_pages(output_dir, options)
893 set_world_read_permissions(output_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800894
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800895 cleanup(input_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800896 logging.info('All done!')
897
898
899if __name__ == '__main__':
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800900 main()