blob: 44c09ae468a7fa52bc6fa93836ea73bb634a680c [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
Dennis Jeffrey9316bb22013-05-01 12:39:36 -0700279 test_path = test_name
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700280 if perf_key.startswith(_TELEMETRY_PERF_KEY_IDENTIFIER):
281 # The perf key is associated with a Telemetry test, and has a
282 # specially-formatted perf key that encodes a graph_name,
283 # trace_name, and units. Example Telemetry perf key:
284 # "TELEMETRY--DeltaBlue--DeltaBlue--score__bigger_is_better_"
285 graph_name, trace_name, units = (
286 perf_key.split(_TELEMETRY_PERF_KEY_DELIMITER)[1:])
Dennis Jeffrey9316bb22013-05-01 12:39:36 -0700287
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700288 # The Telemetry test name is the name of the tag that has been
289 # appended to |test_name|. For example, autotest name
290 # "telemetry_Benchmarks.octane" corresponds to Telemetry test name
291 # "octane" on chrome's new perf dashboard.
292 test_name = test_name[test_name.find('.') + 1:]
293
Dennis Jeffrey9316bb22013-05-01 12:39:36 -0700294 # Transform the names according to rules set by the Chrome team,
295 # as implemented in:
296 # chromium/tools/build/scripts/slave/results_dashboard.py
297 if trace_name == graph_name + '_ref':
298 trace_name = 'ref'
299 graph_name = graph_name.replace('_by_url', '')
300 trace_name = trace_name.replace('/', '_')
301 test_path = '%s/%s/%s' % (test_name, graph_name, trace_name)
302 if graph_name == trace_name:
303 test_path = '%s/%s' % (test_name, graph_name)
304
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700305 new_dash_entry = {
306 'master': master_name,
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700307 'bot': 'cros-' + platform, # Prefix to make clear it's chromeOS.
Dennis Jeffrey9316bb22013-05-01 12:39:36 -0700308 'test': test_path,
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700309 'value': perf_val,
310 'error': perf_err,
311 'supplemental_columns': {
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700312 'r_cros_version': data_point_info['ver'],
313 'r_chrome_version': data_point_info['chrome_ver'],
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700314 }
315 }
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700316 if units:
317 new_dash_entry['units'] = units
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700318 json_string = simplejson.dumps([new_dash_entry], indent=2)
319 params = urllib.urlencode({'data': json_string})
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700320 fp = None
321 try:
322 fp = urllib2.urlopen(_NEW_DASH_UPLOAD_URL, params)
323 errors = fp.read().strip()
324 if errors:
325 raise urllib2.URLError(errors)
326 except urllib2.URLError, e:
327 # TODO(dennisjeffrey): If the live dashboard is currently down,
328 # cache results and retry them later when the live dashboard is
329 # back up. For now we skip the current upload if the live
330 # dashboard is down.
331 logging.exception('Error uploading to new dashboard, skipping '
332 'upload attempt: %s', e)
333 return
334 finally:
335 if fp:
336 fp.close()
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700337
338
339def output_graph_data_for_entry(test_name, master_name, graph_name, job_name,
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700340 platform, chrome_ver, units, better_direction,
341 url, perf_keys, chart_keys, options,
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700342 summary_id_to_rev_num, output_data_dir):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800343 """Outputs data for a perf test result into appropriate graph data files.
344
345 @param test_name: The string name of a test.
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700346 @param master_name: The name of the "buildbot master" to use when uploading
347 perf results to chrome's perf dashboard.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800348 @param graph_name: The string name of the graph associated with this result.
349 @param job_name: The string name of the autotest job associated with this
350 test result.
351 @param platform: The string name of the platform associated with this test
352 result.
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700353 @param chrome_ver: The string Chrome version number associated with this
354 test result.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800355 @param units: The string name of the units displayed on this graph.
356 @param better_direction: A String representing whether better perf results
357 are those that are "higher" or "lower".
358 @param url: The string URL of a webpage docuementing the current graph.
359 @param perf_keys: A list of 2-tuples containing perf keys measured by the
360 test, where the first tuple element is a string key name, and the second
361 tuple element is the associated numeric perf value.
362 @param chart_keys: A list of perf key names that need to be displayed in
363 the current graph.
364 @param options: An optparse.OptionParser options object.
365 @param summary_id_to_rev_num: A dictionary mapping a string (representing
366 a test/platform/release combination), to the next integer revision
367 number to use in the graph data file.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800368 @param output_data_dir: A directory in which to output data files.
369
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800370 """
371 # A string ID that is assumed to be unique across all charts.
372 test_id = test_name + '__' + graph_name
373
374 release_num = get_release_from_jobname(job_name)
375 if not release_num:
376 logging.warning('Could not obtain release number for job name: %s',
377 job_name)
378 return
379 build_num = '%d.%d.%d.%d' % (release_num[0], release_num[1], release_num[2],
380 release_num[3])
381
382 # Filter out particular test runs that we explicitly do not want to
383 # consider.
384 # TODO(dennisjeffrey): Figure out a way to eliminate the need for these
385 # special checks: crosbug.com/36685.
386 if test_name == 'platform_BootPerfServer' and 'perfalerts' not in job_name:
387 # Skip platform_BootPerfServer test results that do not come from the
388 # "perfalerts" runs.
389 return
390
391 # Consider all releases for which this test result may need to be included
392 # on a graph.
393 start_release = max(release_num[0], options.oldest_milestone)
394 for release in xrange(start_release, options.tot_milestone + 1):
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800395 output_path = os.path.join(output_data_dir, 'r%d' % release, platform,
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800396 test_id)
397 summary_file = os.path.join(output_path, graph_name + '-summary.dat')
398
399 # Set up the output directory if it doesn't already exist.
400 if not os.path.exists(output_path):
401 os.makedirs(output_path)
402
403 # Create auxiliary files.
404 create_config_js_file(output_path, test_name)
405 open(summary_file, 'w').close()
406 graphs = [{
407 'name': graph_name,
408 'units': units,
409 'better_direction': better_direction,
410 'info_url': url,
411 'important': False,
412 }]
413 with open(os.path.join(output_path, 'graphs.dat'), 'w') as f:
414 f.write(simplejson.dumps(graphs, indent=2))
415
416 # Add symlinks to the plotting code.
417 for slink, target in _SYMLINK_LIST:
418 slink = os.path.join(output_path, slink)
419 symlink_force(slink, target)
420
421 # Write data to graph data file if it belongs in the current release.
422 if is_on_mainline_of_milestone(job_name, release):
423 entry = {}
424 entry['traces'] = {}
425 entry['ver'] = build_num
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700426 entry['chrome_ver'] = chrome_ver
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800427
428 key_to_vals = {}
429 for perf_key in perf_keys:
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -0700430 if any([chart_key_matches_actual_key(c, perf_key[0])
431 for c in chart_keys]):
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700432 key = perf_key[0]
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800433 if key not in key_to_vals:
434 key_to_vals[key] = []
435 # There are some cases where results for
436 # platform_BootPerfServer are negative in reboot/shutdown
437 # times. Ignore these negative values.
438 if float(perf_key[1]) < 0.0:
439 continue
440 key_to_vals[key].append(perf_key[1])
441 for key in key_to_vals:
442 if len(key_to_vals[key]) == 1:
443 entry['traces'][key] = [key_to_vals[key][0], '0.0']
444 else:
445 mean, std_dev = mean_and_standard_deviation(
446 map(float, key_to_vals[key]))
447 entry['traces'][key] = [str(mean), str(std_dev)]
448
449 if entry['traces']:
450 summary_id = '%s|%s|%s' % (test_id, platform, release)
451
452 rev = summary_id_to_rev_num.get(summary_id, 0)
453 summary_id_to_rev_num[summary_id] = rev + 1
454 entry['rev'] = rev
455
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700456 # Upload data point to the new performance dashboard (only
457 # for the tip-of-tree branch).
458 if release == options.tot_milestone:
459 upload_to_chrome_dashboard(entry, platform, test_name,
460 master_name)
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700461
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700462 # For each perf key, replace dashes with underscores so
463 # different lines show up as different colors in the graphs.
464 for orig_key in entry['traces'].keys():
465 new_key = orig_key.replace('-', '_')
466 entry['traces'][new_key] = entry['traces'].pop(orig_key)
467
468 # Output data point to be displayed on the current (deprecated)
469 # dashboard.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800470 with open(summary_file, 'a') as f:
471 f.write(simplejson.dumps(entry) + '\n')
472
473
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700474def process_perf_data_files(file_names, test_name, master_name, completed_ids,
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800475 test_name_to_charts, options,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800476 summary_id_to_rev_num, output_data_dir):
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800477 """Processes data files for a single test/platform.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800478
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800479 Multiple data files may exist if the given test name is associated with one
480 or more old test names (i.e., the name of the test has changed over time).
481 In this case, we treat all results from the specified files as if they came
482 from a single test associated with the current test name.
483
484 This function converts the data from the specified data files into new
485 data files formatted in a way that can be graphed.
486
487 @param file_names: A list of perf data files to process.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800488 @param test_name: The string name of the test associated with the file name
489 to process.
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700490 @param master_name: The name of the "buildbot master" to use when uploading
491 perf results to chrome's perf dashboard.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800492 @param completed_ids: A dictionary of already-processed job IDs.
493 @param test_name_to_charts: A dictionary mapping test names to a list of
494 dictionaries, in which each dictionary contains information about a
495 chart associated with the given test name.
496 @param options: An optparse.OptionParser options object.
497 @param summary_id_to_rev_num: A dictionary mapping a string (representing
498 a test/platform/release combination) to an integer revision number.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800499 @param output_data_dir: A directory in which to output data files.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800500
501 @return The number of newly-added graph data entries.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800502
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800503 """
504 newly_added_count = 0
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800505 for file_name in file_names:
506 with open(file_name, 'r') as fp:
507 for line in fp.readlines():
508 info = simplejson.loads(line.strip())
509 job_id = info[0]
510 job_name = info[1]
511 platform = info[2]
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700512 chrome_ver = info[3]
513 perf_keys = info[4]
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800514
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800515 # Skip this job ID if it's already been processed.
516 if job_id in completed_ids:
517 continue
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800518
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800519 # Scan the desired charts and see if we need to output the
520 # current line info to a graph output file.
521 for chart in test_name_to_charts[test_name]:
522 graph_name = chart['graph_name']
523 units = chart['units']
524 better_direction = chart['better_direction']
525 url = chart['info_url']
526 chart_keys = chart['keys']
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800527
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800528 store_entry = False
529 for chart_key in chart_keys:
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -0700530 actual_keys = [x[0] for x in perf_keys]
531 if any([chart_key_matches_actual_key(chart_key, a)
532 for a in actual_keys]):
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800533 store_entry = True
534 break
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800535
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800536 if store_entry:
537 output_graph_data_for_entry(
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700538 test_name, master_name, graph_name, job_name,
Dennis Jeffrey9fece302013-03-26 12:08:06 -0700539 platform, chrome_ver, units, better_direction, url,
540 perf_keys, chart_keys, options,
541 summary_id_to_rev_num, output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800542
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800543 # Mark this job ID as having been processed.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800544 with open(os.path.join(output_data_dir,
545 _COMPLETED_ID_FILE_NAME), 'a') as fp:
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800546 fp.write(job_id + '\n')
547 completed_ids[job_id] = True
548 newly_added_count += 1
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800549
550 return newly_added_count
551
552
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800553def initialize_graph_dir(options, input_dir, output_data_dir):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800554 """Initialize/populate the directory that will serve the perf graphs.
555
556 @param options: An optparse.OptionParser options object.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800557 @param input_dir: A directory from which to read previously-extracted
558 perf data.
559 @param output_data_dir: A directory in which to output data files.
560
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800561 """
562 charts = simplejson.loads(open(_CHART_CONFIG_FILE, 'r').read())
563
564 # Identify all the job IDs already processed in the graphs, so that we don't
565 # add that data again.
566 completed_ids = {}
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800567 completed_id_file = os.path.join(output_data_dir, _COMPLETED_ID_FILE_NAME)
568 if os.path.exists(completed_id_file):
569 with open(completed_id_file, 'r') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800570 job_ids = map(lambda x: x.strip(), fp.readlines())
571 for job_id in job_ids:
572 completed_ids[job_id] = True
573
574 # Identify the next revision number to use in the graph data files for each
575 # test/platform/release combination.
576 summary_id_to_rev_num = {}
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800577 rev_num_file = os.path.join(output_data_dir, _REV_NUM_FILE_NAME)
578 if os.path.exists(rev_num_file):
579 with open(rev_num_file, 'r') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800580 summary_id_to_rev_num = simplejson.loads(fp.read())
581
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700582 # TODO (dennisjeffrey): If we have to add another "test_name_to_X"
583 # dictionary to the list below, we should simplify this code to create a
584 # single dictionary that maps test names to an object that contains all
585 # the X's as attributes.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800586 test_name_to_charts = {}
587 test_names = set()
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800588 test_name_to_old_names = {}
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700589 test_name_to_master_name = {}
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800590 # The _CHART_CONFIG_FILE should (and is assumed to) have one entry per
591 # test_name. That entry should declare all graphs associated with the given
592 # test_name.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800593 for chart in charts:
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800594 test_name_to_charts[chart['test_name']] = chart['graphs']
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800595 test_names.add(chart['test_name'])
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800596 test_name_to_old_names[chart['test_name']] = (
597 chart.get('old_test_names', []))
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700598 test_name_to_master_name[chart['test_name']] = (
599 chart.get('master', 'CrosMisc'))
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800600
601 # Scan all database data and format/output only the new data specified in
602 # the graph JSON file.
603 newly_added_count = 0
604 for i, test_name in enumerate(test_names):
605 logging.debug('Analyzing/converting data for test %d of %d: %s',
606 i+1, len(test_names), test_name)
607
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800608 test_data_dir = os.path.join(input_dir, test_name)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800609 if not os.path.exists(test_data_dir):
610 logging.warning('No test data directory for test: %s', test_name)
611 continue
612 files = os.listdir(test_data_dir)
613 for file_name in files:
614 logging.debug('Processing perf platform data file: %s', file_name)
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800615
616 # The current test may be associated with one or more old test
617 # names for which perf results exist for the current platform.
618 # If so, we need to consider those old perf results too, as being
619 # associated with the current test/platform.
620 files_to_process = [os.path.join(test_data_dir, file_name)]
621 for old_test_name in test_name_to_old_names[test_name]:
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800622 old_test_file_name = os.path.join(input_dir, old_test_name,
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800623 file_name)
624 if os.path.exists(old_test_file_name):
625 logging.debug('(also processing this platform for old test '
626 'name "%s")', old_test_name)
627 files_to_process.append(old_test_file_name)
628
629 newly_added_count += process_perf_data_files(
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700630 files_to_process, test_name,
631 test_name_to_master_name.get(test_name), completed_ids,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800632 test_name_to_charts, options, summary_id_to_rev_num,
633 output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800634
635 # Store the latest revision numbers for each test/platform/release
636 # combination, to be used on the next invocation of this script.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800637 with open(rev_num_file, 'w') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800638 fp.write(simplejson.dumps(summary_id_to_rev_num, indent=2))
639
640 logging.info('Added info for %d new jobs to the graphs!', newly_added_count)
641
642
643def create_branch_platform_overview(graph_dir, branch, platform,
644 branch_to_platform_to_test):
645 """Create an overview webpage for the given branch/platform combination.
646
647 @param graph_dir: The string directory containing the graphing files.
648 @param branch: The string name of the milestone (branch).
649 @param platform: The string name of the platform.
650 @param branch_to_platform_to_test: A dictionary mapping branch names to
651 another dictionary, which maps platform names to a list of test names.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800652
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800653 """
654 branches = sorted(branch_to_platform_to_test.keys(), reverse=True)
655 platform_to_tests = branch_to_platform_to_test[branch]
656 platform_list = sorted(platform_to_tests)
657 tests = []
658 for test_id in sorted(platform_to_tests[platform]):
659 has_data = False
660 test_name = ''
661 test_dir = os.path.join(graph_dir, 'data', branch, platform, test_id)
662 data_file_names = fnmatch.filter(os.listdir(test_dir), '*-summary.dat')
663 if len(data_file_names):
664 txt_name = data_file_names[0]
665 # The name of a test is of the form "X: Y", where X is the
666 # autotest name and Y is the graph name. For example:
667 # "platform_BootPerfServer: seconds_from_kernel".
668 test_name = (test_id[:test_id.find('__')] + ': ' +
669 txt_name[:txt_name.find('-summary.dat')])
670 file_name = os.path.join(test_dir, txt_name)
671 has_data = True if os.path.getsize(file_name) > 3 else False
672 test_info = {
673 'id': test_id,
674 'name': test_name,
675 'has_data': has_data
676 }
677 tests.append(test_info)
678
679 # Special check for certain platforms. Will be removed once we remove
680 # all links to the old-style perf graphs.
681 # TODO(dennisjeffrey): Simplify the below code once the following bug
682 # is addressed to standardize the platform names: crosbug.com/38521.
683 platform_converted = 'snow' if platform == 'daisy' else platform
684 platform_converted_2 = ('x86-' + platform if platform in
685 ['alex', 'mario', 'zgb'] else platform)
686
687 # Output the overview page.
688 page_content = render_to_response(
689 os.path.join(_TEMPLATE_DIR, 'branch_platform_overview.html'),
690 locals()).content
691 file_name = os.path.join(graph_dir, '%s-%s.html' % (branch, platform))
692 with open(file_name, 'w') as f:
693 f.write(page_content)
694
695
696def create_comparison_overview(compare_type, graph_dir, test_id, test_dir,
697 branch_to_platform_to_test):
698 """Create an overview webpage to compare a test by platform or by branch.
699
700 @param compare_type: The string type of comaprison graph this is, either
701 "platform" or "branch".
702 @param graph_dir: The string directory containing the graphing files.
703 @param test_id: The string unique ID for a test result.
704 @param test_dir: The string directory name containing the test data.
705 @param branch_to_platform_to_test: A dictionary mapping branch names to
706 another dictionary, which maps platform names to a list of test names.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800707
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800708 """
709 branches = sorted(branch_to_platform_to_test.keys())
710 platforms = [x.keys() for x in branch_to_platform_to_test.values()]
711 platforms = sorted(set([x for sublist in platforms for x in sublist]))
712
713 autotest_name = test_id[:test_id.find('__')]
714
715 text_file_names = fnmatch.filter(os.listdir(test_dir), '*-summary.dat')
716 test_name = '???'
717 if len(text_file_names):
718 txt_name = text_file_names[0]
719 test_name = txt_name[:txt_name.find('-summary.dat')]
720
721 if compare_type == 'branch':
722 outer_list_items = platforms
723 inner_list_items = branches
724 outer_item_type = 'platform'
725 else:
726 outer_list_items = reversed(branches)
727 inner_list_items = platforms
728 outer_item_type = 'branch'
729
730 outer_list = []
731 for outer_item in outer_list_items:
732 inner_list = []
733 for inner_item in inner_list_items:
734 if outer_item_type == 'branch':
735 branch = outer_item
736 platform = inner_item
737 else:
738 branch = inner_item
739 platform = outer_item
740 has_data = False
741 test_dir = os.path.join(graph_dir, 'data', branch, platform,
742 test_id)
743 if os.path.exists(test_dir):
744 data_file_names = fnmatch.filter(os.listdir(test_dir),
745 '*-summary.dat')
746 if len(data_file_names):
747 file_name = os.path.join(test_dir, data_file_names[0])
748 has_data = True if os.path.getsize(file_name) > 3 else False
749 info = {
750 'inner_item': inner_item,
751 'outer_item': outer_item,
752 'branch': branch,
753 'platform': platform,
754 'has_data': has_data
755 }
756 inner_list.append(info)
757 outer_list.append(inner_list)
758
759 # Output the overview page.
760 page_content = render_to_response(
761 os.path.join(_TEMPLATE_DIR, 'compare_by_overview.html'),
762 locals()).content
763 if compare_type == 'branch':
764 file_name = os.path.join(graph_dir, test_id + '_branch.html')
765 else:
766 file_name = os.path.join(graph_dir, test_id + '_platform.html')
767 with open(file_name, 'w') as f:
768 f.write(page_content)
769
770
771def generate_overview_pages(graph_dir, options):
772 """Create static overview webpages for all the perf graphs.
773
774 @param graph_dir: The string directory containing all the graph data.
775 @param options: An optparse.OptionParser options object.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800776
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800777 """
778 # Identify all the milestone names for which we want overview pages.
779 branches_dir = os.path.join(graph_dir, 'data')
780 branches = os.listdir(branches_dir)
781 branches = sorted(branches)
782 branches = [x for x in branches
783 if os.path.isdir(os.path.join(branches_dir, x)) and
784 int(x[1:]) >= options.oldest_milestone]
785
786 unique_tests = set()
787 unique_test_to_dir = {}
788 branch_to_platform_to_test = {}
789
790 for branch in branches:
791 platforms_dir = os.path.join(branches_dir, branch)
792 if not os.path.isdir(platforms_dir):
793 continue
794 platforms = os.listdir(platforms_dir)
795
796 platform_to_tests = {}
797 for platform in platforms:
798 tests_dir = os.path.join(platforms_dir, platform)
799 tests = os.listdir(tests_dir)
800
801 for test in tests:
802 test_dir = os.path.join(tests_dir, test)
803 unique_tests.add(test)
804 unique_test_to_dir[test] = test_dir
805
806 platform_to_tests[platform] = tests
807
808 branch_to_platform_to_test[branch] = platform_to_tests
809
810 for branch in branch_to_platform_to_test:
811 platforms = branch_to_platform_to_test[branch]
812 for platform in platforms:
813 # Create overview page for this branch/platform combination.
814 create_branch_platform_overview(
815 graph_dir, branch, platform, branch_to_platform_to_test)
816
817 # Make index.html a symlink to the most recent branch.
818 latest_branch = branches[-1]
819 first_plat_for_branch = sorted(
820 branch_to_platform_to_test[latest_branch].keys())[0]
821 symlink_force(
822 os.path.join(graph_dir, 'index.html'),
823 '%s-%s.html' % (latest_branch, first_plat_for_branch))
824
825 # Now create overview pages for each test that compare by platform and by
826 # branch.
827 for test_id in unique_tests:
828 for compare_type in ['branch', 'platform']:
829 create_comparison_overview(
830 compare_type, graph_dir, test_id, unique_test_to_dir[test_id],
831 branch_to_platform_to_test)
832
833
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800834def cleanup(dir_name):
835 """Cleans up when this script is done.
836
837 @param dir_name: A directory containing files to clean up.
838
839 """
840 curr_pid_file = os.path.join(dir_name, _CURR_PID_FILE_NAME)
841 if os.path.isfile(curr_pid_file):
842 os.remove(curr_pid_file)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800843
844
845def main():
846 """Main function."""
847 parser = optparse.OptionParser()
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800848 parser.add_option('-i', '--input-dir', metavar='DIR', type='string',
849 default=_DATA_DIR,
850 help='Absolute path to the input directory from which to '
851 'read the raw perf data previously extracted from '
852 'the database. Assumed to contain a subfolder named '
853 '"data". Defaults to "%default".')
854 parser.add_option('-o', '--output-dir', metavar='DIR', type='string',
855 default=_GRAPH_DIR,
856 help='Absolute path to the output directory in which to '
857 'write data files to be displayed on perf graphs. '
858 'Will be written into a subfolder named "graphs". '
859 'Defaults to "%default".')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800860 parser.add_option('-t', '--tot-milestone', metavar='MSTONE', type='int',
861 default=_TOT_MILESTONE,
862 help='Tip-of-tree (most recent) milestone number. '
863 'Defaults to milestone %default (R%default).')
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800864 parser.add_option('-l', '--oldest-milestone', metavar='MSTONE', type='int',
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800865 default=_OLDEST_MILESTONE_TO_GRAPH,
866 help='Oldest milestone number to display in the graphs. '
867 'Defaults to milestone %default (R%default).')
868 parser.add_option('-c', '--clean', action='store_true', default=False,
869 help='Clean/delete existing graph files and then '
870 're-create them from scratch.')
871 parser.add_option('-v', '--verbose', action='store_true', default=False,
872 help='Use verbose logging.')
873 options, _ = parser.parse_args()
874
875 log_level = logging.DEBUG if options.verbose else logging.INFO
876 logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',
877 level=log_level)
878
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800879 input_dir = os.path.join(options.input_dir, 'data')
880 if not os.path.isdir(input_dir):
881 logging.error('Could not find input data directory "%s"', input_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800882 logging.error('Did you forget to run extract_perf.py first?')
883 sys.exit(1)
884
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800885 common.die_if_already_running(
886 os.path.join(input_dir, _CURR_PID_FILE_NAME), logging)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800887
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800888 output_dir = os.path.join(options.output_dir, 'graphs')
889 output_data_dir = os.path.join(output_dir, 'data')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800890 if options.clean:
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800891 remove_path(output_dir)
892 os.makedirs(output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800893
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800894 initialize_graph_dir(options, input_dir, output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800895
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800896 ui_dir = os.path.join(output_dir, 'ui')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800897 if not os.path.exists(ui_dir):
898 logging.debug('Copying "ui" directory to %s', ui_dir)
899 shutil.copytree(os.path.join(_SCRIPT_DIR, 'ui'), ui_dir)
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800900 doc_dir = os.path.join(output_dir, 'doc')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800901 if not os.path.exists(doc_dir):
902 logging.debug('Copying "doc" directory to %s', doc_dir)
903 shutil.copytree(os.path.join(_SCRIPT_DIR, 'doc'), doc_dir)
904
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800905 generate_overview_pages(output_dir, options)
906 set_world_read_permissions(output_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800907
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800908 cleanup(input_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800909 logging.info('All done!')
910
911
912if __name__ == '__main__':
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800913 main()