blob: 63a1adde72cd4e6b158e3708558765e97401caa1 [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 Jeffreyb95ba5a2012-11-12 17:55:18 -080034
35_SETTINGS = 'autotest_lib.frontend.settings'
36os.environ['DJANGO_SETTINGS_MODULE'] = _SETTINGS
37
38import common
39from django.shortcuts import render_to_response
40
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080041_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080042_CHART_CONFIG_FILE = os.path.join(_SCRIPT_DIR, 'croschart_defaults.json')
43_TEMPLATE_DIR = os.path.join(_SCRIPT_DIR, 'templates')
Dennis Jeffrey8a305382013-02-28 09:08:57 -080044_CURR_PID_FILE_NAME = __file__ + '.curr_pid.txt'
45_COMPLETED_ID_FILE_NAME = 'job_id_complete.txt'
46_REV_NUM_FILE_NAME = 'rev_num.txt'
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -070047_WILDCARD = '*'
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080048
49# Values that can be configured through options.
50# TODO(dennisjeffrey): Infer the tip-of-tree milestone dynamically once this
51# issue is addressed: crosbug.com/38564.
Dennis Jeffrey8a305382013-02-28 09:08:57 -080052_TOT_MILESTONE = 27
53_OLDEST_MILESTONE_TO_GRAPH = 25
54_DATA_DIR = _SCRIPT_DIR
55_GRAPH_DIR = _SCRIPT_DIR
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080056
57# Other values that can only be configured here in the code.
58_SYMLINK_LIST = [
59 ('report.html', '../../../../ui/cros_plotter.html'),
60 ('js', '../../../../ui/js'),
61]
62
63
64def set_world_read_permissions(path):
65 """Recursively sets the content of |path| to be world-readable.
66
Dennis Jeffrey8a305382013-02-28 09:08:57 -080067 @param path: The string path.
68
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080069 """
70 logging.debug('Setting world-read permissions recursively on %s', path)
71 os.chmod(path, 0755)
72 for root, dirs, files in os.walk(path):
73 for d in dirs:
74 dname = os.path.join(root, d)
75 if not os.path.islink(dname):
76 os.chmod(dname, 0755)
77 for f in files:
78 fname = os.path.join(root, f)
79 if not os.path.islink(fname):
80 os.chmod(fname, 0755)
81
82
83def remove_path(path):
Dennis Jeffrey8a305382013-02-28 09:08:57 -080084 """Remove the given path (whether file or directory).
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080085
Dennis Jeffrey8a305382013-02-28 09:08:57 -080086 @param path: The string path.
87
88 """
89 if os.path.isdir(path):
90 shutil.rmtree(path)
91 return
92 try:
93 os.remove(path)
94 except OSError:
95 pass
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -080096
97
98def symlink_force(link_name, target):
99 """Create a symlink, accounting for different situations.
100
101 @param link_name: The string name of the link to create.
102 @param target: The string destination file to which the link should point.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800103
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800104 """
105 try:
106 os.unlink(link_name)
107 except EnvironmentError:
108 pass
109 try:
110 os.symlink(target, link_name)
111 except OSError:
112 remove_path(link_name)
113 os.symlink(target, link_name)
114
115
116def mean_and_standard_deviation(data):
117 """Compute the mean and standard deviation of a list of numbers.
118
119 @param data: A list of numerica values.
120
121 @return A 2-tuple (mean, standard_deviation) computed from |data|.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800122
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800123 """
124 n = len(data)
125 if n == 0:
126 return 0.0, 0.0
127 mean = float(sum(data)) / n
128 if n == 1:
129 return mean, 0.0
130 # Divide by n-1 to compute "sample standard deviation".
131 variance = sum([(element - mean) ** 2 for element in data]) / (n - 1)
132 return mean, math.sqrt(variance)
133
134
135def get_release_from_jobname(jobname):
136 """Identifies the release number components from an autotest job name.
137
138 For example:
139 'lumpy-release-R21-2384.0.0_pyauto_perf' becomes (21, 2384, 0, 0).
140
141 @param jobname: The string name of an autotest job.
142
143 @return The 4-tuple containing components of the build release number, or
144 None if those components cannot be identifies from the |jobname|.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800145
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800146 """
147 prog = re.compile('r(\d+)-(\d+).(\d+).(\d+)')
148 m = prog.search(jobname.lower())
149 if m:
150 return (int(m.group(1)), int(m.group(2)), int(m.group(3)),
151 int(m.group(4)))
152 return None
153
154
155def is_on_mainline_of_milestone(jobname, milestone):
156 """Determines whether an autotest build is on mainline of a given milestone.
157
158 @param jobname: The string name of an autotest job (containing release
159 number).
160 @param milestone: The integer milestone number to consider.
161
162 @return True, if the given autotest job name is for a release number that
163 is either (1) an ancestor of the specified milestone, or (2) is on the
164 main branch line of the given milestone. Returns False otherwise.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800165
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800166 """
167 r = get_release_from_jobname(jobname)
168 m = milestone
169 # Handle garbage data that might exist.
170 if any(item < 0 for item in r):
171 raise Exception('Unexpected release info in job name: %s' % jobname)
172 if m == r[0]:
173 # Yes for jobs from the specified milestone itself.
174 return True
175 if r[0] < m and r[2] == 0 and r[3] == 0:
176 # Yes for jobs from earlier milestones that were before their respective
177 # branch points.
178 return True
179 return False
180
181
182# TODO(dennisjeffrey): Determine whether or not we need all the values in the
183# config file. Remove unnecessary ones and revised necessary ones as needed.
184def create_config_js_file(path, test_name):
185 """Creates a configuration file used by the performance graphs.
186
187 @param path: The string path to the directory in which to create the file.
188 @param test_name: The string name of the test associated with this config
189 file.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800190
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800191 """
192 config_content = render_to_response(
193 os.path.join(_TEMPLATE_DIR, 'config.js'), locals()).content
194 with open(os.path.join(path, 'config.js'), 'w') as f:
195 f.write(config_content)
196
197
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -0700198def chart_key_matches_actual_key(chart_key, actual_key):
199 """Whether a chart key (with possible wildcard) matches a given actual key.
200
201 A perf key in _CHART_CONFIG_FILE may have wildcards specified to match
202 multiple actual perf keys that a test may measure. For example, the
203 chart key "metric*" could match 3 different actual perf keys: "meticA",
204 "metricB", and "metricC". Wildcards are specified with "*" and may occur
205 in any of these formats:
206 1) *metric: Matches perf keys that end with "metric".
207 2) metric*: Matches perf keys that start with "metric".
208 3) *metric*: Matches perf keys that contain "metric".
209 4) metric: Matches only the perf key "metric" (exact match).
210 5) *: Matches any perf key.
211
212 This function determines whether or not a given chart key (with possible
213 wildcard) matches a given actual key.
214
215 @param chart_key: The chart key string with possible wildcard.
216 @param actual_key: The actual perf key.
217
218 @return True, if the specified chart key matches the actual key, or
219 False otherwise.
220
221 """
222 if chart_key == _WILDCARD:
223 return True
224 elif _WILDCARD not in chart_key:
225 return chart_key == actual_key
226 elif chart_key.startswith(_WILDCARD) and chart_key.endswith(_WILDCARD):
227 return chart_key[len(_WILDCARD):-len(_WILDCARD)] in actual_key
228 elif chart_key.startswith(_WILDCARD):
229 return actual_key.endswith(chart_key[len(_WILDCARD):])
230 elif chart_key.endswith(_WILDCARD):
231 return actual_key.startswith(chart_key[:-len(_WILDCARD)])
232
233 return False
234
235
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700236def upload_to_chrome_dashboard(data_point_info, platform, test_name,
237 master_name):
238 """Uploads a set of perf values to Chrome's perf dashboard.
239
240 @param data_point_info: A dictionary containing information about perf
241 data points to plot: key names and values, revision number, chromeOS
242 version number.
243 @param platform: The string name of the associated platform.
244 @param test_name: The string name of the associated test.
245 @param master_name: The string name of the "buildbot master" to use
246 (a concept that exists in Chrome's perf dashboard).
247
248 This function is currently a no-op. It will be completed as soon as we're
249 ready to start sending actual data to Chrome's perf dashboard.
250 """
251 traces = data_point_info['traces']
252 for perf_key in traces:
253 perf_val = traces[perf_key][0]
254 perf_err = traces[perf_key][1]
255
256 new_dash_entry = {
257 'master': master_name,
258 'bot': platform,
259 'test': '%s/%s' % (test_name, perf_key),
260 'revision': data_point_info['rev'] + 1, # Don't allow 0.
261 'value': perf_val,
262 'error': perf_err,
263 'supplemental_columns': {
264 'a_cros_build': data_point_info['ver']
265 }
266 }
267 json_string = simplejson.dumps([new_dash_entry], indent=2)
268 params = urllib.urlencode({'data': json_string})
269 # TODO(dennisjeffrey): Upload "params" to Chrome's perf dashboard,
270 # once ready.
271
272
273def output_graph_data_for_entry(test_name, master_name, graph_name, job_name,
274 platform, units, better_direction, url,
275 perf_keys, chart_keys, options,
276 summary_id_to_rev_num, output_data_dir):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800277 """Outputs data for a perf test result into appropriate graph data files.
278
279 @param test_name: The string name of a test.
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700280 @param master_name: The name of the "buildbot master" to use when uploading
281 perf results to chrome's perf dashboard.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800282 @param graph_name: The string name of the graph associated with this result.
283 @param job_name: The string name of the autotest job associated with this
284 test result.
285 @param platform: The string name of the platform associated with this test
286 result.
287 @param units: The string name of the units displayed on this graph.
288 @param better_direction: A String representing whether better perf results
289 are those that are "higher" or "lower".
290 @param url: The string URL of a webpage docuementing the current graph.
291 @param perf_keys: A list of 2-tuples containing perf keys measured by the
292 test, where the first tuple element is a string key name, and the second
293 tuple element is the associated numeric perf value.
294 @param chart_keys: A list of perf key names that need to be displayed in
295 the current graph.
296 @param options: An optparse.OptionParser options object.
297 @param summary_id_to_rev_num: A dictionary mapping a string (representing
298 a test/platform/release combination), to the next integer revision
299 number to use in the graph data file.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800300 @param output_data_dir: A directory in which to output data files.
301
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800302 """
303 # A string ID that is assumed to be unique across all charts.
304 test_id = test_name + '__' + graph_name
305
306 release_num = get_release_from_jobname(job_name)
307 if not release_num:
308 logging.warning('Could not obtain release number for job name: %s',
309 job_name)
310 return
311 build_num = '%d.%d.%d.%d' % (release_num[0], release_num[1], release_num[2],
312 release_num[3])
313
314 # Filter out particular test runs that we explicitly do not want to
315 # consider.
316 # TODO(dennisjeffrey): Figure out a way to eliminate the need for these
317 # special checks: crosbug.com/36685.
318 if test_name == 'platform_BootPerfServer' and 'perfalerts' not in job_name:
319 # Skip platform_BootPerfServer test results that do not come from the
320 # "perfalerts" runs.
321 return
322
323 # Consider all releases for which this test result may need to be included
324 # on a graph.
325 start_release = max(release_num[0], options.oldest_milestone)
326 for release in xrange(start_release, options.tot_milestone + 1):
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800327 output_path = os.path.join(output_data_dir, 'r%d' % release, platform,
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800328 test_id)
329 summary_file = os.path.join(output_path, graph_name + '-summary.dat')
330
331 # Set up the output directory if it doesn't already exist.
332 if not os.path.exists(output_path):
333 os.makedirs(output_path)
334
335 # Create auxiliary files.
336 create_config_js_file(output_path, test_name)
337 open(summary_file, 'w').close()
338 graphs = [{
339 'name': graph_name,
340 'units': units,
341 'better_direction': better_direction,
342 'info_url': url,
343 'important': False,
344 }]
345 with open(os.path.join(output_path, 'graphs.dat'), 'w') as f:
346 f.write(simplejson.dumps(graphs, indent=2))
347
348 # Add symlinks to the plotting code.
349 for slink, target in _SYMLINK_LIST:
350 slink = os.path.join(output_path, slink)
351 symlink_force(slink, target)
352
353 # Write data to graph data file if it belongs in the current release.
354 if is_on_mainline_of_milestone(job_name, release):
355 entry = {}
356 entry['traces'] = {}
357 entry['ver'] = build_num
358
359 key_to_vals = {}
360 for perf_key in perf_keys:
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -0700361 if any([chart_key_matches_actual_key(c, perf_key[0])
362 for c in chart_keys]):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800363 # Replace dashes with underscores so different lines show
364 # up as different colors in the graphs.
365 key = perf_key[0].replace('-', '_')
366 if key not in key_to_vals:
367 key_to_vals[key] = []
368 # There are some cases where results for
369 # platform_BootPerfServer are negative in reboot/shutdown
370 # times. Ignore these negative values.
371 if float(perf_key[1]) < 0.0:
372 continue
373 key_to_vals[key].append(perf_key[1])
374 for key in key_to_vals:
375 if len(key_to_vals[key]) == 1:
376 entry['traces'][key] = [key_to_vals[key][0], '0.0']
377 else:
378 mean, std_dev = mean_and_standard_deviation(
379 map(float, key_to_vals[key]))
380 entry['traces'][key] = [str(mean), str(std_dev)]
381
382 if entry['traces']:
383 summary_id = '%s|%s|%s' % (test_id, platform, release)
384
385 rev = summary_id_to_rev_num.get(summary_id, 0)
386 summary_id_to_rev_num[summary_id] = rev + 1
387 entry['rev'] = rev
388
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700389 upload_to_chrome_dashboard(entry, platform, test_name,
390 master_name)
391
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800392 with open(summary_file, 'a') as f:
393 f.write(simplejson.dumps(entry) + '\n')
394
395
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700396def process_perf_data_files(file_names, test_name, master_name, completed_ids,
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800397 test_name_to_charts, options,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800398 summary_id_to_rev_num, output_data_dir):
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800399 """Processes data files for a single test/platform.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800400
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800401 Multiple data files may exist if the given test name is associated with one
402 or more old test names (i.e., the name of the test has changed over time).
403 In this case, we treat all results from the specified files as if they came
404 from a single test associated with the current test name.
405
406 This function converts the data from the specified data files into new
407 data files formatted in a way that can be graphed.
408
409 @param file_names: A list of perf data files to process.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800410 @param test_name: The string name of the test associated with the file name
411 to process.
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700412 @param master_name: The name of the "buildbot master" to use when uploading
413 perf results to chrome's perf dashboard.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800414 @param completed_ids: A dictionary of already-processed job IDs.
415 @param test_name_to_charts: A dictionary mapping test names to a list of
416 dictionaries, in which each dictionary contains information about a
417 chart associated with the given test name.
418 @param options: An optparse.OptionParser options object.
419 @param summary_id_to_rev_num: A dictionary mapping a string (representing
420 a test/platform/release combination) to an integer revision number.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800421 @param output_data_dir: A directory in which to output data files.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800422
423 @return The number of newly-added graph data entries.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800424
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800425 """
426 newly_added_count = 0
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800427 for file_name in file_names:
428 with open(file_name, 'r') as fp:
429 for line in fp.readlines():
430 info = simplejson.loads(line.strip())
431 job_id = info[0]
432 job_name = info[1]
433 platform = info[2]
434 perf_keys = info[3]
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800435
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800436 # Skip this job ID if it's already been processed.
437 if job_id in completed_ids:
438 continue
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800439
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800440 # Scan the desired charts and see if we need to output the
441 # current line info to a graph output file.
442 for chart in test_name_to_charts[test_name]:
443 graph_name = chart['graph_name']
444 units = chart['units']
445 better_direction = chart['better_direction']
446 url = chart['info_url']
447 chart_keys = chart['keys']
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800448
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800449 store_entry = False
450 for chart_key in chart_keys:
Dennis Jeffreyf61bcd52013-03-14 15:25:57 -0700451 actual_keys = [x[0] for x in perf_keys]
452 if any([chart_key_matches_actual_key(chart_key, a)
453 for a in actual_keys]):
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800454 store_entry = True
455 break
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800456
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800457 if store_entry:
458 output_graph_data_for_entry(
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700459 test_name, master_name, graph_name, job_name,
460 platform, units, better_direction, url, perf_keys,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800461 chart_keys, options, summary_id_to_rev_num,
462 output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800463
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800464 # Mark this job ID as having been processed.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800465 with open(os.path.join(output_data_dir,
466 _COMPLETED_ID_FILE_NAME), 'a') as fp:
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800467 fp.write(job_id + '\n')
468 completed_ids[job_id] = True
469 newly_added_count += 1
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800470
471 return newly_added_count
472
473
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800474def initialize_graph_dir(options, input_dir, output_data_dir):
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800475 """Initialize/populate the directory that will serve the perf graphs.
476
477 @param options: An optparse.OptionParser options object.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800478 @param input_dir: A directory from which to read previously-extracted
479 perf data.
480 @param output_data_dir: A directory in which to output data files.
481
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800482 """
483 charts = simplejson.loads(open(_CHART_CONFIG_FILE, 'r').read())
484
485 # Identify all the job IDs already processed in the graphs, so that we don't
486 # add that data again.
487 completed_ids = {}
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800488 completed_id_file = os.path.join(output_data_dir, _COMPLETED_ID_FILE_NAME)
489 if os.path.exists(completed_id_file):
490 with open(completed_id_file, 'r') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800491 job_ids = map(lambda x: x.strip(), fp.readlines())
492 for job_id in job_ids:
493 completed_ids[job_id] = True
494
495 # Identify the next revision number to use in the graph data files for each
496 # test/platform/release combination.
497 summary_id_to_rev_num = {}
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800498 rev_num_file = os.path.join(output_data_dir, _REV_NUM_FILE_NAME)
499 if os.path.exists(rev_num_file):
500 with open(rev_num_file, 'r') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800501 summary_id_to_rev_num = simplejson.loads(fp.read())
502
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700503 # TODO (dennisjeffrey): If we have to add another "test_name_to_X"
504 # dictionary to the list below, we should simplify this code to create a
505 # single dictionary that maps test names to an object that contains all
506 # the X's as attributes.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800507 test_name_to_charts = {}
508 test_names = set()
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800509 test_name_to_old_names = {}
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700510 test_name_to_master_name = {}
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800511 # The _CHART_CONFIG_FILE should (and is assumed to) have one entry per
512 # test_name. That entry should declare all graphs associated with the given
513 # test_name.
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800514 for chart in charts:
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800515 test_name_to_charts[chart['test_name']] = chart['graphs']
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800516 test_names.add(chart['test_name'])
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800517 test_name_to_old_names[chart['test_name']] = (
518 chart.get('old_test_names', []))
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700519 test_name_to_master_name[chart['test_name']] = (
520 chart.get('master', 'CrosMisc'))
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800521
522 # Scan all database data and format/output only the new data specified in
523 # the graph JSON file.
524 newly_added_count = 0
525 for i, test_name in enumerate(test_names):
526 logging.debug('Analyzing/converting data for test %d of %d: %s',
527 i+1, len(test_names), test_name)
528
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800529 test_data_dir = os.path.join(input_dir, test_name)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800530 if not os.path.exists(test_data_dir):
531 logging.warning('No test data directory for test: %s', test_name)
532 continue
533 files = os.listdir(test_data_dir)
534 for file_name in files:
535 logging.debug('Processing perf platform data file: %s', file_name)
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800536
537 # The current test may be associated with one or more old test
538 # names for which perf results exist for the current platform.
539 # If so, we need to consider those old perf results too, as being
540 # associated with the current test/platform.
541 files_to_process = [os.path.join(test_data_dir, file_name)]
542 for old_test_name in test_name_to_old_names[test_name]:
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800543 old_test_file_name = os.path.join(input_dir, old_test_name,
Dennis Jeffreybbe98a22013-02-13 09:59:38 -0800544 file_name)
545 if os.path.exists(old_test_file_name):
546 logging.debug('(also processing this platform for old test '
547 'name "%s")', old_test_name)
548 files_to_process.append(old_test_file_name)
549
550 newly_added_count += process_perf_data_files(
Dennis Jeffrey900b0692013-03-11 11:15:43 -0700551 files_to_process, test_name,
552 test_name_to_master_name.get(test_name), completed_ids,
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800553 test_name_to_charts, options, summary_id_to_rev_num,
554 output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800555
556 # Store the latest revision numbers for each test/platform/release
557 # combination, to be used on the next invocation of this script.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800558 with open(rev_num_file, 'w') as fp:
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800559 fp.write(simplejson.dumps(summary_id_to_rev_num, indent=2))
560
561 logging.info('Added info for %d new jobs to the graphs!', newly_added_count)
562
563
564def create_branch_platform_overview(graph_dir, branch, platform,
565 branch_to_platform_to_test):
566 """Create an overview webpage for the given branch/platform combination.
567
568 @param graph_dir: The string directory containing the graphing files.
569 @param branch: The string name of the milestone (branch).
570 @param platform: The string name of the platform.
571 @param branch_to_platform_to_test: A dictionary mapping branch names to
572 another dictionary, which maps platform names to a list of test names.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800573
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800574 """
575 branches = sorted(branch_to_platform_to_test.keys(), reverse=True)
576 platform_to_tests = branch_to_platform_to_test[branch]
577 platform_list = sorted(platform_to_tests)
578 tests = []
579 for test_id in sorted(platform_to_tests[platform]):
580 has_data = False
581 test_name = ''
582 test_dir = os.path.join(graph_dir, 'data', branch, platform, test_id)
583 data_file_names = fnmatch.filter(os.listdir(test_dir), '*-summary.dat')
584 if len(data_file_names):
585 txt_name = data_file_names[0]
586 # The name of a test is of the form "X: Y", where X is the
587 # autotest name and Y is the graph name. For example:
588 # "platform_BootPerfServer: seconds_from_kernel".
589 test_name = (test_id[:test_id.find('__')] + ': ' +
590 txt_name[:txt_name.find('-summary.dat')])
591 file_name = os.path.join(test_dir, txt_name)
592 has_data = True if os.path.getsize(file_name) > 3 else False
593 test_info = {
594 'id': test_id,
595 'name': test_name,
596 'has_data': has_data
597 }
598 tests.append(test_info)
599
600 # Special check for certain platforms. Will be removed once we remove
601 # all links to the old-style perf graphs.
602 # TODO(dennisjeffrey): Simplify the below code once the following bug
603 # is addressed to standardize the platform names: crosbug.com/38521.
604 platform_converted = 'snow' if platform == 'daisy' else platform
605 platform_converted_2 = ('x86-' + platform if platform in
606 ['alex', 'mario', 'zgb'] else platform)
607
608 # Output the overview page.
609 page_content = render_to_response(
610 os.path.join(_TEMPLATE_DIR, 'branch_platform_overview.html'),
611 locals()).content
612 file_name = os.path.join(graph_dir, '%s-%s.html' % (branch, platform))
613 with open(file_name, 'w') as f:
614 f.write(page_content)
615
616
617def create_comparison_overview(compare_type, graph_dir, test_id, test_dir,
618 branch_to_platform_to_test):
619 """Create an overview webpage to compare a test by platform or by branch.
620
621 @param compare_type: The string type of comaprison graph this is, either
622 "platform" or "branch".
623 @param graph_dir: The string directory containing the graphing files.
624 @param test_id: The string unique ID for a test result.
625 @param test_dir: The string directory name containing the test data.
626 @param branch_to_platform_to_test: A dictionary mapping branch names to
627 another dictionary, which maps platform names to a list of test names.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800628
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800629 """
630 branches = sorted(branch_to_platform_to_test.keys())
631 platforms = [x.keys() for x in branch_to_platform_to_test.values()]
632 platforms = sorted(set([x for sublist in platforms for x in sublist]))
633
634 autotest_name = test_id[:test_id.find('__')]
635
636 text_file_names = fnmatch.filter(os.listdir(test_dir), '*-summary.dat')
637 test_name = '???'
638 if len(text_file_names):
639 txt_name = text_file_names[0]
640 test_name = txt_name[:txt_name.find('-summary.dat')]
641
642 if compare_type == 'branch':
643 outer_list_items = platforms
644 inner_list_items = branches
645 outer_item_type = 'platform'
646 else:
647 outer_list_items = reversed(branches)
648 inner_list_items = platforms
649 outer_item_type = 'branch'
650
651 outer_list = []
652 for outer_item in outer_list_items:
653 inner_list = []
654 for inner_item in inner_list_items:
655 if outer_item_type == 'branch':
656 branch = outer_item
657 platform = inner_item
658 else:
659 branch = inner_item
660 platform = outer_item
661 has_data = False
662 test_dir = os.path.join(graph_dir, 'data', branch, platform,
663 test_id)
664 if os.path.exists(test_dir):
665 data_file_names = fnmatch.filter(os.listdir(test_dir),
666 '*-summary.dat')
667 if len(data_file_names):
668 file_name = os.path.join(test_dir, data_file_names[0])
669 has_data = True if os.path.getsize(file_name) > 3 else False
670 info = {
671 'inner_item': inner_item,
672 'outer_item': outer_item,
673 'branch': branch,
674 'platform': platform,
675 'has_data': has_data
676 }
677 inner_list.append(info)
678 outer_list.append(inner_list)
679
680 # Output the overview page.
681 page_content = render_to_response(
682 os.path.join(_TEMPLATE_DIR, 'compare_by_overview.html'),
683 locals()).content
684 if compare_type == 'branch':
685 file_name = os.path.join(graph_dir, test_id + '_branch.html')
686 else:
687 file_name = os.path.join(graph_dir, test_id + '_platform.html')
688 with open(file_name, 'w') as f:
689 f.write(page_content)
690
691
692def generate_overview_pages(graph_dir, options):
693 """Create static overview webpages for all the perf graphs.
694
695 @param graph_dir: The string directory containing all the graph data.
696 @param options: An optparse.OptionParser options object.
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800697
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800698 """
699 # Identify all the milestone names for which we want overview pages.
700 branches_dir = os.path.join(graph_dir, 'data')
701 branches = os.listdir(branches_dir)
702 branches = sorted(branches)
703 branches = [x for x in branches
704 if os.path.isdir(os.path.join(branches_dir, x)) and
705 int(x[1:]) >= options.oldest_milestone]
706
707 unique_tests = set()
708 unique_test_to_dir = {}
709 branch_to_platform_to_test = {}
710
711 for branch in branches:
712 platforms_dir = os.path.join(branches_dir, branch)
713 if not os.path.isdir(platforms_dir):
714 continue
715 platforms = os.listdir(platforms_dir)
716
717 platform_to_tests = {}
718 for platform in platforms:
719 tests_dir = os.path.join(platforms_dir, platform)
720 tests = os.listdir(tests_dir)
721
722 for test in tests:
723 test_dir = os.path.join(tests_dir, test)
724 unique_tests.add(test)
725 unique_test_to_dir[test] = test_dir
726
727 platform_to_tests[platform] = tests
728
729 branch_to_platform_to_test[branch] = platform_to_tests
730
731 for branch in branch_to_platform_to_test:
732 platforms = branch_to_platform_to_test[branch]
733 for platform in platforms:
734 # Create overview page for this branch/platform combination.
735 create_branch_platform_overview(
736 graph_dir, branch, platform, branch_to_platform_to_test)
737
738 # Make index.html a symlink to the most recent branch.
739 latest_branch = branches[-1]
740 first_plat_for_branch = sorted(
741 branch_to_platform_to_test[latest_branch].keys())[0]
742 symlink_force(
743 os.path.join(graph_dir, 'index.html'),
744 '%s-%s.html' % (latest_branch, first_plat_for_branch))
745
746 # Now create overview pages for each test that compare by platform and by
747 # branch.
748 for test_id in unique_tests:
749 for compare_type in ['branch', 'platform']:
750 create_comparison_overview(
751 compare_type, graph_dir, test_id, unique_test_to_dir[test_id],
752 branch_to_platform_to_test)
753
754
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800755def cleanup(dir_name):
756 """Cleans up when this script is done.
757
758 @param dir_name: A directory containing files to clean up.
759
760 """
761 curr_pid_file = os.path.join(dir_name, _CURR_PID_FILE_NAME)
762 if os.path.isfile(curr_pid_file):
763 os.remove(curr_pid_file)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800764
765
766def main():
767 """Main function."""
768 parser = optparse.OptionParser()
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800769 parser.add_option('-i', '--input-dir', metavar='DIR', type='string',
770 default=_DATA_DIR,
771 help='Absolute path to the input directory from which to '
772 'read the raw perf data previously extracted from '
773 'the database. Assumed to contain a subfolder named '
774 '"data". Defaults to "%default".')
775 parser.add_option('-o', '--output-dir', metavar='DIR', type='string',
776 default=_GRAPH_DIR,
777 help='Absolute path to the output directory in which to '
778 'write data files to be displayed on perf graphs. '
779 'Will be written into a subfolder named "graphs". '
780 'Defaults to "%default".')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800781 parser.add_option('-t', '--tot-milestone', metavar='MSTONE', type='int',
782 default=_TOT_MILESTONE,
783 help='Tip-of-tree (most recent) milestone number. '
784 'Defaults to milestone %default (R%default).')
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800785 parser.add_option('-l', '--oldest-milestone', metavar='MSTONE', type='int',
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800786 default=_OLDEST_MILESTONE_TO_GRAPH,
787 help='Oldest milestone number to display in the graphs. '
788 'Defaults to milestone %default (R%default).')
789 parser.add_option('-c', '--clean', action='store_true', default=False,
790 help='Clean/delete existing graph files and then '
791 're-create them from scratch.')
792 parser.add_option('-v', '--verbose', action='store_true', default=False,
793 help='Use verbose logging.')
794 options, _ = parser.parse_args()
795
796 log_level = logging.DEBUG if options.verbose else logging.INFO
797 logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',
798 level=log_level)
799
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800800 input_dir = os.path.join(options.input_dir, 'data')
801 if not os.path.isdir(input_dir):
802 logging.error('Could not find input data directory "%s"', input_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800803 logging.error('Did you forget to run extract_perf.py first?')
804 sys.exit(1)
805
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800806 common.die_if_already_running(
807 os.path.join(input_dir, _CURR_PID_FILE_NAME), logging)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800808
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800809 output_dir = os.path.join(options.output_dir, 'graphs')
810 output_data_dir = os.path.join(output_dir, 'data')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800811 if options.clean:
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800812 remove_path(output_dir)
813 os.makedirs(output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800814
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800815 initialize_graph_dir(options, input_dir, output_data_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800816
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800817 ui_dir = os.path.join(output_dir, 'ui')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800818 if not os.path.exists(ui_dir):
819 logging.debug('Copying "ui" directory to %s', ui_dir)
820 shutil.copytree(os.path.join(_SCRIPT_DIR, 'ui'), ui_dir)
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800821 doc_dir = os.path.join(output_dir, 'doc')
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800822 if not os.path.exists(doc_dir):
823 logging.debug('Copying "doc" directory to %s', doc_dir)
824 shutil.copytree(os.path.join(_SCRIPT_DIR, 'doc'), doc_dir)
825
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800826 generate_overview_pages(output_dir, options)
827 set_world_read_permissions(output_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800828
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800829 cleanup(input_dir)
Dennis Jeffreyb95ba5a2012-11-12 17:55:18 -0800830 logging.info('All done!')
831
832
833if __name__ == '__main__':
Dennis Jeffrey8a305382013-02-28 09:08:57 -0800834 main()