blob: 02ecbe17c9103aab921c770aee93a7e21bb41acf [file] [log] [blame]
bungeman@google.com85669f92011-06-17 13:58:14 +00001'''
2Created on May 16, 2011
3
4@author: bungeman
5'''
bungeman@google.com85669f92011-06-17 13:58:14 +00006import bench_util
bensong@google.com848fa2b2013-03-07 17:12:43 +00007import getopt
8import httplib
9import itertools
bungeman@google.com85669f92011-06-17 13:58:14 +000010import json
bensong@google.com848fa2b2013-03-07 17:12:43 +000011import os
12import re
13import sys
14import urllib
15import urllib2
bungeman@google.com85669f92011-06-17 13:58:14 +000016import xml.sax.saxutils
17
epoger@google.com3d8cd172012-05-11 18:26:16 +000018# We throw out any measurement outside this range, and log a warning.
19MIN_REASONABLE_TIME = 0
20MAX_REASONABLE_TIME = 99999
21
bensong@google.comad0c5d22012-09-13 21:08:52 +000022# Constants for prefixes in output title used in buildbot.
borenet@google.come6598a02013-04-30 12:02:32 +000023TITLE_PREAMBLE = 'Bench_Performance_for_'
bensong@google.comad0c5d22012-09-13 21:08:52 +000024TITLE_PREAMBLE_LENGTH = len(TITLE_PREAMBLE)
25
bungeman@google.com85669f92011-06-17 13:58:14 +000026def usage():
27 """Prints simple usage information."""
bensong@google.com848fa2b2013-03-07 17:12:43 +000028
29 print '-a <url> the url to use for adding bench values to app engine app.'
30 print ' Example: "https://skiadash.appspot.com/add_point".'
31 print ' If not set, will skip this step.'
bungeman@google.com85669f92011-06-17 13:58:14 +000032 print '-b <bench> the bench to show.'
33 print '-c <config> the config to show (GPU, 8888, 565, etc).'
epoger@google.com5b2e01c2012-06-25 20:29:04 +000034 print '-d <dir> a directory containing bench_r<revision>_<scalar> files.'
bensong@google.com7d9a21b2013-09-25 20:51:16 +000035 print '-e <file> file containing expected bench builder values/ranges.'
bensong@google.comad0c5d22012-09-13 21:08:52 +000036 print ' Will raise exception if actual bench values are out of range.'
bensong@google.com7d9a21b2013-09-25 20:51:16 +000037 print ' See bench_expectations_<builder>.txt for data format / examples.'
bungeman@google.com85669f92011-06-17 13:58:14 +000038 print '-f <revision>[:<revision>] the revisions to use for fitting.'
epoger@google.com37260002011-08-08 20:27:04 +000039 print ' Negative <revision> is taken as offset from most recent revision.'
bensong@google.com8ccfa552012-08-17 21:42:14 +000040 print '-i <time> the time to ignore (w, c, g, etc).'
41 print ' The flag is ignored when -t is set; otherwise we plot all the'
42 print ' times except the one specified here.'
epoger@google.com5b2e01c2012-06-25 20:29:04 +000043 print '-l <title> title to use for the output graph'
bensong@google.com8ccfa552012-08-17 21:42:14 +000044 print '-m <representation> representation of bench value.'
45 print ' See _ListAlgorithm class in bench_util.py.'
borenet@google.com5dc06782013-03-12 17:16:07 +000046 print '-o <path> path to which to write output.'
epoger@google.com5b2e01c2012-06-25 20:29:04 +000047 print '-r <revision>[:<revision>] the revisions to show.'
48 print ' Negative <revision> is taken as offset from most recent revision.'
49 print '-s <setting>[=<value>] a setting to show (alpha, scalar, etc).'
50 print '-t <time> the time to show (w, c, g, etc).'
bungeman@google.com85669f92011-06-17 13:58:14 +000051 print '-x <int> the desired width of the svg.'
52 print '-y <int> the desired height of the svg.'
53 print '--default-setting <setting>[=<value>] setting for those without.'
54
55
56class Label:
57 """The information in a label.
58
59 (str, str, str, str, {str:str})"""
60 def __init__(self, bench, config, time_type, settings):
61 self.bench = bench
62 self.config = config
63 self.time_type = time_type
64 self.settings = settings
65
66 def __repr__(self):
67 return "Label(%s, %s, %s, %s)" % (
68 str(self.bench),
69 str(self.config),
70 str(self.time_type),
71 str(self.settings),
72 )
73
74 def __str__(self):
75 return "%s_%s_%s_%s" % (
76 str(self.bench),
77 str(self.config),
78 str(self.time_type),
79 str(self.settings),
80 )
81
82 def __eq__(self, other):
83 return (self.bench == other.bench and
84 self.config == other.config and
85 self.time_type == other.time_type and
86 self.settings == other.settings)
87
88 def __hash__(self):
89 return (hash(self.bench) ^
90 hash(self.config) ^
91 hash(self.time_type) ^
92 hash(frozenset(self.settings.iteritems())))
93
epoger@google.com37260002011-08-08 20:27:04 +000094def get_latest_revision(directory):
95 """Returns the latest revision number found within this directory.
96 """
97 latest_revision_found = -1
98 for bench_file in os.listdir(directory):
99 file_name_match = re.match('bench_r(\d+)_(\S+)', bench_file)
100 if (file_name_match is None):
101 continue
102 revision = int(file_name_match.group(1))
103 if revision > latest_revision_found:
104 latest_revision_found = revision
105 if latest_revision_found < 0:
106 return None
107 else:
108 return latest_revision_found
109
bensong@google.com87348162012-08-15 17:31:46 +0000110def parse_dir(directory, default_settings, oldest_revision, newest_revision,
111 rep):
bungeman@google.com85669f92011-06-17 13:58:14 +0000112 """Parses bench data from files like bench_r<revision>_<scalar>.
113
114 (str, {str, str}, Number, Number) -> {int:[BenchDataPoints]}"""
115 revision_data_points = {} # {revision : [BenchDataPoints]}
epoger@google.come9b31fa2013-02-14 20:13:32 +0000116 file_list = os.listdir(directory)
117 file_list.sort()
118 for bench_file in file_list:
bungeman@google.com85669f92011-06-17 13:58:14 +0000119 file_name_match = re.match('bench_r(\d+)_(\S+)', bench_file)
120 if (file_name_match is None):
121 continue
epoger@google.com3d8cd172012-05-11 18:26:16 +0000122
bungeman@google.com85669f92011-06-17 13:58:14 +0000123 revision = int(file_name_match.group(1))
124 scalar_type = file_name_match.group(2)
epoger@google.com3d8cd172012-05-11 18:26:16 +0000125
bungeman@google.com85669f92011-06-17 13:58:14 +0000126 if (revision < oldest_revision or revision > newest_revision):
127 continue
epoger@google.com3d8cd172012-05-11 18:26:16 +0000128
bungeman@google.com85669f92011-06-17 13:58:14 +0000129 file_handle = open(directory + '/' + bench_file, 'r')
epoger@google.com3d8cd172012-05-11 18:26:16 +0000130
bungeman@google.com85669f92011-06-17 13:58:14 +0000131 if (revision not in revision_data_points):
132 revision_data_points[revision] = []
133 default_settings['scalar'] = scalar_type
134 revision_data_points[revision].extend(
bensong@google.com87348162012-08-15 17:31:46 +0000135 bench_util.parse(default_settings, file_handle, rep))
bungeman@google.com85669f92011-06-17 13:58:14 +0000136 file_handle.close()
137 return revision_data_points
138
epoger@google.com3d8cd172012-05-11 18:26:16 +0000139def add_to_revision_data_points(new_point, revision, revision_data_points):
140 """Add new_point to set of revision_data_points we are building up.
141 """
142 if (revision not in revision_data_points):
143 revision_data_points[revision] = []
144 revision_data_points[revision].append(new_point)
145
146def filter_data_points(unfiltered_revision_data_points):
147 """Filter out any data points that are utterly bogus.
148
149 Returns (allowed_revision_data_points, ignored_revision_data_points):
150 allowed_revision_data_points: points that survived the filter
151 ignored_revision_data_points: points that did NOT survive the filter
152 """
153 allowed_revision_data_points = {} # {revision : [BenchDataPoints]}
154 ignored_revision_data_points = {} # {revision : [BenchDataPoints]}
155 revisions = unfiltered_revision_data_points.keys()
156 revisions.sort()
157 for revision in revisions:
158 for point in unfiltered_revision_data_points[revision]:
159 if point.time < MIN_REASONABLE_TIME or point.time > MAX_REASONABLE_TIME:
160 add_to_revision_data_points(point, revision, ignored_revision_data_points)
161 else:
162 add_to_revision_data_points(point, revision, allowed_revision_data_points)
163 return (allowed_revision_data_points, ignored_revision_data_points)
164
epoger@google.com1513f6e2012-06-27 13:38:37 +0000165def get_abs_path(relative_path):
166 """My own implementation of os.path.abspath() that better handles paths
167 which approach Window's 260-character limit.
168 See https://code.google.com/p/skia/issues/detail?id=674
169
170 This implementation adds path components one at a time, resolving the
171 absolute path each time, to take advantage of any chdirs into outer
172 directories that will shorten the total path length.
173
174 TODO: share a single implementation with upload_to_bucket.py, instead
175 of pasting this same code into both files."""
176 if os.path.isabs(relative_path):
177 return relative_path
178 path_parts = relative_path.split(os.sep)
179 abs_path = os.path.abspath('.')
180 for path_part in path_parts:
181 abs_path = os.path.abspath(os.path.join(abs_path, path_part))
182 return abs_path
183
epoger@google.com5b2e01c2012-06-25 20:29:04 +0000184def redirect_stdout(output_path):
185 """Redirect all following stdout to a file.
186
187 You may be asking yourself, why redirect stdout within Python rather than
188 redirecting the script's output in the calling shell?
189 The answer lies in https://code.google.com/p/skia/issues/detail?id=674
190 ('buildbot: windows GenerateBenchGraphs step fails due to filename length'):
191 On Windows, we need to generate the absolute path within Python to avoid
192 the operating system's 260-character pathname limit, including chdirs."""
epoger@google.com1513f6e2012-06-27 13:38:37 +0000193 abs_path = get_abs_path(output_path)
epoger@google.com5b2e01c2012-06-25 20:29:04 +0000194 sys.stdout = open(abs_path, 'w')
195
bungeman@google.com85669f92011-06-17 13:58:14 +0000196def create_lines(revision_data_points, settings
bensong@google.com8ccfa552012-08-17 21:42:14 +0000197 , bench_of_interest, config_of_interest, time_of_interest
198 , time_to_ignore):
epoger@google.com2459a392013-02-14 18:58:05 +0000199 """Convert revision data into a dictionary of line data.
bungeman@google.com85669f92011-06-17 13:58:14 +0000200
bensong@google.com848fa2b2013-03-07 17:12:43 +0000201 Args:
202 revision_data_points: a dictionary with integer keys (revision #) and a
203 list of bench data points as values
204 settings: a dictionary of setting names to value
205 bench_of_interest: optional filter parameters: which bench type is of
206 interest. If None, process them all.
207 config_of_interest: optional filter parameters: which config type is of
208 interest. If None, process them all.
209 time_of_interest: optional filter parameters: which timer type is of
210 interest. If None, process them all.
211 time_to_ignore: optional timer type to ignore
212
213 Returns:
214 a dictionary of this form:
215 keys = Label objects
216 values = a list of (x, y) tuples sorted such that x values increase
217 monotonically
218 """
bungeman@google.com85669f92011-06-17 13:58:14 +0000219 revisions = revision_data_points.keys()
220 revisions.sort()
221 lines = {} # {Label:[(x,y)] | x[n] <= x[n+1]}
222 for revision in revisions:
223 for point in revision_data_points[revision]:
224 if (bench_of_interest is not None and
225 not bench_of_interest == point.bench):
226 continue
227
228 if (config_of_interest is not None and
229 not config_of_interest == point.config):
230 continue
231
232 if (time_of_interest is not None and
233 not time_of_interest == point.time_type):
234 continue
bensong@google.com8ccfa552012-08-17 21:42:14 +0000235 elif (time_to_ignore is not None and
236 time_to_ignore == point.time_type):
237 continue
bungeman@google.com85669f92011-06-17 13:58:14 +0000238
239 skip = False
240 for key, value in settings.items():
241 if key in point.settings and point.settings[key] != value:
242 skip = True
243 break
244 if skip:
245 continue
246
247 line_name = Label(point.bench
248 , point.config
249 , point.time_type
250 , point.settings)
251
252 if line_name not in lines:
253 lines[line_name] = []
254
255 lines[line_name].append((revision, point.time))
256
257 return lines
258
259def bounds(lines):
260 """Finds the bounding rectangle for the lines.
261
262 {Label:[(x,y)]} -> ((min_x, min_y),(max_x,max_y))"""
263 min_x = bench_util.Max
264 min_y = bench_util.Max
265 max_x = bench_util.Min
266 max_y = bench_util.Min
267
268 for line in lines.itervalues():
269 for x, y in line:
270 min_x = min(min_x, x)
271 min_y = min(min_y, y)
272 max_x = max(max_x, x)
273 max_y = max(max_y, y)
274
275 return ((min_x, min_y), (max_x, max_y))
276
277def create_regressions(lines, start_x, end_x):
278 """Creates regression data from line segments.
279
280 ({Label:[(x,y)] | [n].x <= [n+1].x}, Number, Number)
281 -> {Label:LinearRegression}"""
282 regressions = {} # {Label : LinearRegression}
283
284 for label, line in lines.iteritems():
285 regression_line = [p for p in line if start_x <= p[0] <= end_x]
286
287 if (len(regression_line) < 2):
288 continue
289 regression = bench_util.LinearRegression(regression_line)
290 regressions[label] = regression
291
292 return regressions
293
294def bounds_slope(regressions):
295 """Finds the extreme up and down slopes of a set of linear regressions.
296
297 ({Label:LinearRegression}) -> (max_up_slope, min_down_slope)"""
298 max_up_slope = 0
299 min_down_slope = 0
300 for regression in regressions.itervalues():
301 min_slope = regression.find_min_slope()
302 max_up_slope = max(max_up_slope, min_slope)
303 min_down_slope = min(min_down_slope, min_slope)
304
305 return (max_up_slope, min_down_slope)
306
307def main():
308 """Parses command line and writes output."""
309
310 try:
311 opts, _ = getopt.getopt(sys.argv[1:]
bensong@google.com848fa2b2013-03-07 17:12:43 +0000312 , "a:b:c:d:e:f:i:l:m:o:r:s:t:x:y:"
bungeman@google.com85669f92011-06-17 13:58:14 +0000313 , "default-setting=")
314 except getopt.GetoptError, err:
315 print str(err)
316 usage()
317 sys.exit(2)
318
319 directory = None
320 config_of_interest = None
321 bench_of_interest = None
322 time_of_interest = None
bensong@google.com8ccfa552012-08-17 21:42:14 +0000323 time_to_ignore = None
borenet@google.com5dc06782013-03-12 17:16:07 +0000324 output_path = None
bensong@google.comad0c5d22012-09-13 21:08:52 +0000325 bench_expectations = {}
bensong@google.com848fa2b2013-03-07 17:12:43 +0000326 appengine_url = None # used for adding data to appengine datastore
bensong@google.comb6204b12012-08-16 20:49:28 +0000327 rep = None # bench representation algorithm
epoger@google.com37260002011-08-08 20:27:04 +0000328 revision_range = '0:'
329 regression_range = '0:'
330 latest_revision = None
bungeman@google.com85669f92011-06-17 13:58:14 +0000331 requested_height = None
332 requested_width = None
epoger@google.com37260002011-08-08 20:27:04 +0000333 title = 'Bench graph'
bungeman@google.com85669f92011-06-17 13:58:14 +0000334 settings = {}
335 default_settings = {}
epoger@google.com37260002011-08-08 20:27:04 +0000336
337 def parse_range(range):
338 """Takes '<old>[:<new>]' as a string and returns (old, new).
339 Any revision numbers that are dependent on the latest revision number
340 will be filled in based on latest_revision.
341 """
342 old, _, new = range.partition(":")
343 old = int(old)
344 if old < 0:
345 old += latest_revision;
bungeman@google.com85669f92011-06-17 13:58:14 +0000346 if not new:
epoger@google.com37260002011-08-08 20:27:04 +0000347 new = latest_revision;
348 new = int(new)
349 if new < 0:
350 new += latest_revision;
351 return (old, new)
352
bungeman@google.com85669f92011-06-17 13:58:14 +0000353 def add_setting(settings, setting):
354 """Takes <key>[=<value>] adds {key:value} or {key:True} to settings."""
355 name, _, value = setting.partition('=')
356 if not value:
357 settings[name] = True
358 else:
359 settings[name] = value
bensong@google.comad0c5d22012-09-13 21:08:52 +0000360
361 def read_expectations(expectations, filename):
362 """Reads expectations data from file and put in expectations dict."""
363 for expectation in open(filename).readlines():
364 elements = expectation.strip().split(',')
365 if not elements[0] or elements[0].startswith('#'):
366 continue
367 if len(elements) != 5:
368 raise Exception("Invalid expectation line format: %s" %
369 expectation)
370 bench_entry = elements[0] + ',' + elements[1]
371 if bench_entry in expectations:
372 raise Exception("Dup entries for bench expectation %s" %
373 bench_entry)
374 # [<Bench_BmpConfig_TimeType>,<Platform-Alg>] -> (LB, UB)
375 expectations[bench_entry] = (float(elements[-2]),
376 float(elements[-1]))
377
378 def check_expectations(lines, expectations, newest_revision, key_suffix):
bensong@google.com43e4f6e2013-07-30 14:47:04 +0000379 """Check if there are benches in latest rev outside expected range.
380 For exceptions, also outputs URL link for the dashboard plot.
381 The link history token format here only works for single-line plots.
382 """
383 # The platform for this bot, to pass to the dashboard plot.
384 platform = key_suffix[ : key_suffix.rfind('-')]
385 # Starting revision for the dashboard plot.
386 start_rev = str(newest_revision - 100) # Displays about 100 revisions.
bensong@google.comad0c5d22012-09-13 21:08:52 +0000387 exceptions = []
388 for line in lines:
389 line_str = str(line)
bensong@google.com43e4f6e2013-07-30 14:47:04 +0000390 line_str = line_str[ : line_str.find('_{')]
391 bench_platform_key = line_str + ',' + key_suffix
bensong@google.comad0c5d22012-09-13 21:08:52 +0000392 this_revision, this_bench_value = lines[line][-1]
393 if (this_revision != newest_revision or
394 bench_platform_key not in expectations):
395 # Skip benches without value for latest revision.
396 continue
397 this_min, this_max = expectations[bench_platform_key]
398 if this_bench_value < this_min or this_bench_value > this_max:
bensong@google.com43e4f6e2013-07-30 14:47:04 +0000399 link = ''
400 # For skp benches out of range, create dashboard plot link.
401 if line_str.find('.skp_') > 0:
402 # Extract bench and config for dashboard plot.
403 bench, config = line_str.strip('_').split('.skp_')
404 link = ' <a href="'
405 link += 'http://go/skpdash/SkpDash.html#%s~%s~%s~%s" ' % (
406 start_rev, bench, platform, config)
407 link += 'target="_blank">graph</a>'
408 exception = 'Bench %s value %s out of range [%s, %s].%s' % (
409 bench_platform_key, this_bench_value, this_min, this_max,
410 link)
411 exceptions.append(exception)
bensong@google.comad0c5d22012-09-13 21:08:52 +0000412 if exceptions:
413 raise Exception('Bench values out of range:\n' +
414 '\n'.join(exceptions))
415
bensong@google.com848fa2b2013-03-07 17:12:43 +0000416 def write_to_appengine(line_data_dict, url, newest_revision, bot):
417 """Writes latest bench values to appengine datastore.
418 line_data_dict: dictionary from create_lines.
419 url: the appengine url used to send bench values to write
420 newest_revision: the latest revision that this script reads
421 bot: the bot platform the bench is run on
422 """
bensong@google.comae6f47e2013-04-08 14:57:40 +0000423 config_data_dic = {}
bensong@google.com848fa2b2013-03-07 17:12:43 +0000424 for label in line_data_dict.iterkeys():
425 if not label.bench.endswith('.skp') or label.time_type:
426 # filter out non-picture and non-walltime benches
427 continue
428 config = label.config
429 rev, val = line_data_dict[label][-1]
430 # This assumes that newest_revision is >= the revision of the last
431 # data point we have for each line.
432 if rev != newest_revision:
433 continue
bensong@google.comae6f47e2013-04-08 14:57:40 +0000434 if config not in config_data_dic:
435 config_data_dic[config] = []
436 config_data_dic[config].append(label.bench.replace('.skp', '') +
437 ':%.2f' % val)
438 for config in config_data_dic:
439 if config_data_dic[config]:
440 data = {'master': 'Skia', 'bot': bot, 'test': config,
441 'revision': newest_revision,
442 'benches': ','.join(config_data_dic[config])}
443 req = urllib2.Request(appengine_url,
444 urllib.urlencode({'data': json.dumps(data)}))
445 try:
446 urllib2.urlopen(req)
447 except urllib2.HTTPError, e:
448 sys.stderr.write("HTTPError for JSON data %s: %s\n" % (
449 data, e))
450 except urllib2.URLError, e:
451 sys.stderr.write("URLError for JSON data %s: %s\n" % (
452 data, e))
453 except httplib.HTTPException, e:
454 sys.stderr.write("HTTPException for JSON data %s: %s\n" % (
455 data, e))
bensong@google.com848fa2b2013-03-07 17:12:43 +0000456
bungeman@google.com85669f92011-06-17 13:58:14 +0000457 try:
458 for option, value in opts:
bensong@google.com848fa2b2013-03-07 17:12:43 +0000459 if option == "-a":
460 appengine_url = value
461 elif option == "-b":
bungeman@google.com85669f92011-06-17 13:58:14 +0000462 bench_of_interest = value
463 elif option == "-c":
464 config_of_interest = value
epoger@google.com5b2e01c2012-06-25 20:29:04 +0000465 elif option == "-d":
466 directory = value
bensong@google.comad0c5d22012-09-13 21:08:52 +0000467 elif option == "-e":
468 read_expectations(bench_expectations, value)
bungeman@google.com85669f92011-06-17 13:58:14 +0000469 elif option == "-f":
epoger@google.com37260002011-08-08 20:27:04 +0000470 regression_range = value
bensong@google.com8ccfa552012-08-17 21:42:14 +0000471 elif option == "-i":
472 time_to_ignore = value
epoger@google.com5b2e01c2012-06-25 20:29:04 +0000473 elif option == "-l":
474 title = value
bensong@google.com87348162012-08-15 17:31:46 +0000475 elif option == "-m":
476 rep = value
epoger@google.com5b2e01c2012-06-25 20:29:04 +0000477 elif option == "-o":
borenet@google.com5dc06782013-03-12 17:16:07 +0000478 output_path = value
479 redirect_stdout(output_path)
epoger@google.com5b2e01c2012-06-25 20:29:04 +0000480 elif option == "-r":
481 revision_range = value
482 elif option == "-s":
483 add_setting(settings, value)
484 elif option == "-t":
485 time_of_interest = value
bungeman@google.com85669f92011-06-17 13:58:14 +0000486 elif option == "-x":
487 requested_width = int(value)
488 elif option == "-y":
489 requested_height = int(value)
490 elif option == "--default-setting":
491 add_setting(default_settings, value)
492 else:
493 usage()
494 assert False, "unhandled option"
495 except ValueError:
496 usage()
497 sys.exit(2)
epoger@google.comc71174d2011-08-08 17:19:23 +0000498
bungeman@google.com85669f92011-06-17 13:58:14 +0000499 if directory is None:
500 usage()
501 sys.exit(2)
epoger@google.comc71174d2011-08-08 17:19:23 +0000502
bensong@google.com8ccfa552012-08-17 21:42:14 +0000503 if time_of_interest:
504 time_to_ignore = None
505
bensong@google.comad0c5d22012-09-13 21:08:52 +0000506 # The title flag (-l) provided in buildbot slave is in the format
borenet@google.come6598a02013-04-30 12:02:32 +0000507 # Bench_Performance_for_<platform>, and we want to extract <platform>
bensong@google.comad0c5d22012-09-13 21:08:52 +0000508 # for use in platform_and_alg to track matching benches later. If title flag
509 # is not in this format, there may be no matching benches in the file
510 # provided by the expectation_file flag (-e).
bensong@google.com848fa2b2013-03-07 17:12:43 +0000511 bot = title # To store the platform as bot name
bensong@google.comad0c5d22012-09-13 21:08:52 +0000512 platform_and_alg = title
513 if platform_and_alg.startswith(TITLE_PREAMBLE):
bensong@google.com848fa2b2013-03-07 17:12:43 +0000514 bot = platform_and_alg[TITLE_PREAMBLE_LENGTH:]
515 platform_and_alg = bot + '-' + rep
bensong@google.com8c1de762012-08-15 18:27:38 +0000516 title += ' [representation: %s]' % rep
517
epoger@google.com37260002011-08-08 20:27:04 +0000518 latest_revision = get_latest_revision(directory)
519 oldest_revision, newest_revision = parse_range(revision_range)
520 oldest_regression, newest_regression = parse_range(regression_range)
521
epoger@google.com3d8cd172012-05-11 18:26:16 +0000522 unfiltered_revision_data_points = parse_dir(directory
bungeman@google.com85669f92011-06-17 13:58:14 +0000523 , default_settings
524 , oldest_revision
bensong@google.com87348162012-08-15 17:31:46 +0000525 , newest_revision
526 , rep)
epoger@google.comc71174d2011-08-08 17:19:23 +0000527
epoger@google.com3d8cd172012-05-11 18:26:16 +0000528 # Filter out any data points that are utterly bogus... make sure to report
529 # that we did so later!
530 (allowed_revision_data_points, ignored_revision_data_points) = filter_data_points(
531 unfiltered_revision_data_points)
532
epoger@google.comc71174d2011-08-08 17:19:23 +0000533 # Update oldest_revision and newest_revision based on the data we could find
epoger@google.com3d8cd172012-05-11 18:26:16 +0000534 all_revision_numbers = allowed_revision_data_points.keys()
epoger@google.comc71174d2011-08-08 17:19:23 +0000535 oldest_revision = min(all_revision_numbers)
536 newest_revision = max(all_revision_numbers)
537
epoger@google.com3d8cd172012-05-11 18:26:16 +0000538 lines = create_lines(allowed_revision_data_points
bungeman@google.com85669f92011-06-17 13:58:14 +0000539 , settings
540 , bench_of_interest
541 , config_of_interest
bensong@google.com8ccfa552012-08-17 21:42:14 +0000542 , time_of_interest
543 , time_to_ignore)
epoger@google.comc71174d2011-08-08 17:19:23 +0000544
bungeman@google.com85669f92011-06-17 13:58:14 +0000545 regressions = create_regressions(lines
546 , oldest_regression
547 , newest_regression)
epoger@google.comc71174d2011-08-08 17:19:23 +0000548
borenet@google.com5dc06782013-03-12 17:16:07 +0000549 if output_path:
550 output_xhtml(lines, oldest_revision, newest_revision,
551 ignored_revision_data_points, regressions, requested_width,
552 requested_height, title)
bungeman@google.com85669f92011-06-17 13:58:14 +0000553
bensong@google.com848fa2b2013-03-07 17:12:43 +0000554 if appengine_url:
555 write_to_appengine(lines, appengine_url, newest_revision, bot)
556
borenet@google.com5dc06782013-03-12 17:16:07 +0000557 if bench_expectations:
558 check_expectations(lines, bench_expectations, newest_revision,
559 platform_and_alg)
bensong@google.comad0c5d22012-09-13 21:08:52 +0000560
bungeman@google.com85669f92011-06-17 13:58:14 +0000561def qa(out):
562 """Stringify input and quote as an xml attribute."""
563 return xml.sax.saxutils.quoteattr(str(out))
564def qe(out):
565 """Stringify input and escape as xml data."""
566 return xml.sax.saxutils.escape(str(out))
567
568def create_select(qualifier, lines, select_id=None):
569 """Output select with options showing lines which qualifier maps to it.
570
571 ((Label) -> str, {Label:_}, str?) -> _"""
572 options = {} #{ option : [Label]}
573 for label in lines.keys():
574 option = qualifier(label)
575 if (option not in options):
576 options[option] = []
577 options[option].append(label)
578 option_list = list(options.keys())
579 option_list.sort()
580 print '<select class="lines"',
581 if select_id is not None:
582 print 'id=%s' % qa(select_id)
583 print 'multiple="true" size="10" onchange="updateSvg();">'
584 for option in option_list:
585 print '<option value=' + qa('[' +
586 reduce(lambda x,y:x+json.dumps(str(y))+',',options[option],"")[0:-1]
587 + ']') + '>'+qe(option)+'</option>'
588 print '</select>'
589
epoger@google.com3d8cd172012-05-11 18:26:16 +0000590def output_ignored_data_points_warning(ignored_revision_data_points):
591 """Write description of ignored_revision_data_points to stdout as xhtml.
592 """
593 num_ignored_points = 0
594 description = ''
595 revisions = ignored_revision_data_points.keys()
596 if revisions:
597 revisions.sort()
598 revisions.reverse()
599 for revision in revisions:
600 num_ignored_points += len(ignored_revision_data_points[revision])
601 points_at_this_revision = []
602 for point in ignored_revision_data_points[revision]:
603 points_at_this_revision.append(point.bench)
604 points_at_this_revision.sort()
605 description += 'r%d: %s\n' % (revision, points_at_this_revision)
606 if num_ignored_points == 0:
607 print 'Did not discard any data points; all were within the range [%d-%d]' % (
608 MIN_REASONABLE_TIME, MAX_REASONABLE_TIME)
609 else:
610 print '<table width="100%" bgcolor="ff0000"><tr><td align="center">'
611 print 'Discarded %d data points outside of range [%d-%d]' % (
612 num_ignored_points, MIN_REASONABLE_TIME, MAX_REASONABLE_TIME)
613 print '</td></tr><tr><td width="100%" align="center">'
614 print ('<textarea rows="4" style="width:97%" readonly="true" wrap="off">'
615 + qe(description) + '</textarea>')
616 print '</td></tr></table>'
617
618def output_xhtml(lines, oldest_revision, newest_revision, ignored_revision_data_points,
epoger@google.com37260002011-08-08 20:27:04 +0000619 regressions, requested_width, requested_height, title):
bungeman@google.com85669f92011-06-17 13:58:14 +0000620 """Outputs an svg/xhtml view of the data."""
621 print '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"',
622 print '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
623 print '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">'
624 print '<head>'
epoger@google.com3d8cd172012-05-11 18:26:16 +0000625 print '<title>%s</title>' % qe(title)
bungeman@google.com85669f92011-06-17 13:58:14 +0000626 print '</head>'
627 print '<body>'
628
629 output_svg(lines, regressions, requested_width, requested_height)
epoger@google.com3d8cd172012-05-11 18:26:16 +0000630
bungeman@google.com85669f92011-06-17 13:58:14 +0000631 #output the manipulation controls
632 print """
633<script type="text/javascript">//<![CDATA[
634 function getElementsByClass(node, searchClass, tag) {
635 var classElements = new Array();
636 var elements = node.getElementsByTagName(tag);
637 var pattern = new RegExp("^|\\s"+searchClass+"\\s|$");
638 for (var i = 0, elementsFound = 0; i < elements.length; ++i) {
639 if (pattern.test(elements[i].className)) {
640 classElements[elementsFound] = elements[i];
641 ++elementsFound;
642 }
643 }
644 return classElements;
645 }
646 function getAllLines() {
647 var selectElem = document.getElementById('benchSelect');
648 var linesObj = {};
649 for (var i = 0; i < selectElem.options.length; ++i) {
650 var lines = JSON.parse(selectElem.options[i].value);
651 for (var j = 0; j < lines.length; ++j) {
652 linesObj[lines[j]] = true;
653 }
654 }
655 return linesObj;
656 }
657 function getOptions(selectElem) {
658 var linesSelectedObj = {};
659 for (var i = 0; i < selectElem.options.length; ++i) {
660 if (!selectElem.options[i].selected) continue;
661
662 var linesSelected = JSON.parse(selectElem.options[i].value);
663 for (var j = 0; j < linesSelected.length; ++j) {
664 linesSelectedObj[linesSelected[j]] = true;
665 }
666 }
667 return linesSelectedObj;
668 }
669 function objectEmpty(obj) {
670 for (var p in obj) {
671 return false;
672 }
673 return true;
674 }
675 function markSelectedLines(selectElem, allLines) {
676 var linesSelected = getOptions(selectElem);
677 if (!objectEmpty(linesSelected)) {
678 for (var line in allLines) {
679 allLines[line] &= (linesSelected[line] == true);
680 }
681 }
682 }
683 function updateSvg() {
684 var allLines = getAllLines();
685
686 var selects = getElementsByClass(document, 'lines', 'select');
687 for (var i = 0; i < selects.length; ++i) {
688 markSelectedLines(selects[i], allLines);
689 }
690
691 for (var line in allLines) {
692 var svgLine = document.getElementById(line);
693 var display = (allLines[line] ? 'inline' : 'none');
694 svgLine.setAttributeNS(null,'display', display);
695 }
696 }
697
698 function mark(markerId) {
699 for (var line in getAllLines()) {
700 var svgLineGroup = document.getElementById(line);
701 var display = svgLineGroup.getAttributeNS(null,'display');
702 if (display == null || display == "" || display != "none") {
703 var svgLine = document.getElementById(line+'_line');
704 if (markerId == null) {
705 svgLine.removeAttributeNS(null,'marker-mid');
706 } else {
707 svgLine.setAttributeNS(null,'marker-mid', markerId);
708 }
709 }
710 }
711 }
712//]]></script>"""
epoger@google.comc71174d2011-08-08 17:19:23 +0000713
bungeman@google.com85669f92011-06-17 13:58:14 +0000714 all_settings = {}
715 variant_settings = set()
716 for label in lines.keys():
717 for key, value in label.settings.items():
718 if key not in all_settings:
719 all_settings[key] = value
720 elif all_settings[key] != value:
721 variant_settings.add(key)
epoger@google.comc71174d2011-08-08 17:19:23 +0000722
bungeman@google.com752acc72012-09-12 19:34:17 +0000723 print '<table border="0" width="%s">' % requested_width
724 #output column headers
725 print """
726<tr valign="top"><td width="50%">
727<table border="0" width="100%">
728<tr><td align="center"><table border="0">
729<form>
730<tr valign="bottom" align="center">
731<td width="1">Bench&nbsp;Type</td>
732<td width="1">Bitmap Config</td>
733<td width="1">Timer&nbsp;Type (Cpu/Gpu/wall)</td>
734"""
735
bungeman@google.com85669f92011-06-17 13:58:14 +0000736 for k in variant_settings:
bungeman@google.com752acc72012-09-12 19:34:17 +0000737 print '<td width="1">%s</td>' % qe(k)
738
739 print '<td width="1"><!--buttons--></td></tr>'
740
741 #output column contents
742 print '<tr valign="top" align="center">'
743 print '<td width="1">'
744 create_select(lambda l: l.bench, lines, 'benchSelect')
745 print '</td><td width="1">'
746 create_select(lambda l: l.config, lines)
747 print '</td><td width="1">'
748 create_select(lambda l: l.time_type, lines)
749
750 for k in variant_settings:
751 print '</td><td width="1">'
752 create_select(lambda l: l.settings.get(k, " "), lines)
epoger@google.comc71174d2011-08-08 17:19:23 +0000753
754 print '</td><td width="1"><button type="button"',
bungeman@google.com85669f92011-06-17 13:58:14 +0000755 print 'onclick=%s' % qa("mark('url(#circleMark)'); return false;"),
epoger@google.comc71174d2011-08-08 17:19:23 +0000756 print '>Mark Points</button>'
757 print '<button type="button" onclick="mark(null);">Clear Points</button>'
epoger@google.com3d8cd172012-05-11 18:26:16 +0000758 print '</td>'
epoger@google.comc71174d2011-08-08 17:19:23 +0000759 print """
epoger@google.com3d8cd172012-05-11 18:26:16 +0000760</tr>
761</form>
762</table></td></tr>
763<tr><td align="center">
764<hr />
765"""
766
767 output_ignored_data_points_warning(ignored_revision_data_points)
768 print '</td></tr></table>'
769 print '</td><td width="2%"><!--gutter--></td>'
770
771 print '<td><table border="0">'
772 print '<tr><td align="center">%s<br></br>revisions r%s - r%s</td></tr>' % (
773 qe(title),
774 bench_util.CreateRevisionLink(oldest_revision),
775 bench_util.CreateRevisionLink(newest_revision))
776 print """
777<tr><td align="left">
epoger@google.comc71174d2011-08-08 17:19:23 +0000778<p>Brighter red indicates tests that have gotten worse; brighter green
779indicates tests that have gotten better.</p>
780<p>To highlight individual tests, hold down CONTROL and mouse over
781graph lines.</p>
782<p>To highlight revision numbers, hold down SHIFT and mouse over
783the graph area.</p>
784<p>To only show certain tests on the graph, select any combination of
785tests in the selectors at left. (To show all, select all.)</p>
786<p>Use buttons at left to mark/clear points on the lines for selected
787benchmarks.</p>
epoger@google.com3d8cd172012-05-11 18:26:16 +0000788</td></tr>
789</table>
790
epoger@google.comc71174d2011-08-08 17:19:23 +0000791</td>
792</tr>
epoger@google.comc71174d2011-08-08 17:19:23 +0000793</table>
794</body>
795</html>"""
bungeman@google.com85669f92011-06-17 13:58:14 +0000796
797def compute_size(requested_width, requested_height, rev_width, time_height):
798 """Converts potentially empty requested size into a concrete size.
799
800 (Number?, Number?) -> (Number, Number)"""
801 pic_width = 0
802 pic_height = 0
803 if (requested_width is not None and requested_height is not None):
804 pic_height = requested_height
805 pic_width = requested_width
806
807 elif (requested_width is not None):
808 pic_width = requested_width
809 pic_height = pic_width * (float(time_height) / rev_width)
810
811 elif (requested_height is not None):
812 pic_height = requested_height
813 pic_width = pic_height * (float(rev_width) / time_height)
814
815 else:
816 pic_height = 800
817 pic_width = max(rev_width*3
818 , pic_height * (float(rev_width) / time_height))
819
820 return (pic_width, pic_height)
821
822def output_svg(lines, regressions, requested_width, requested_height):
823 """Outputs an svg view of the data."""
824
825 (global_min_x, _), (global_max_x, global_max_y) = bounds(lines)
826 max_up_slope, min_down_slope = bounds_slope(regressions)
bensong@google.com741b2e12013-06-13 13:53:16 +0000827
bungeman@google.com85669f92011-06-17 13:58:14 +0000828 #output
829 global_min_y = 0
830 x = global_min_x
831 y = global_min_y
832 w = global_max_x - global_min_x
833 h = global_max_y - global_min_y
834 font_size = 16
835 line_width = 2
bensong@google.com741b2e12013-06-13 13:53:16 +0000836
837 # If there is nothing to see, don't try to draw anything.
838 if w == 0 or h == 0:
839 return
840
bungeman@google.com85669f92011-06-17 13:58:14 +0000841 pic_width, pic_height = compute_size(requested_width, requested_height
842 , w, h)
843
844 def cw(w1):
845 """Converts a revision difference to display width."""
846 return (pic_width / float(w)) * w1
847 def cx(x):
848 """Converts a revision to a horizontal display position."""
849 return cw(x - global_min_x)
850
851 def ch(h1):
852 """Converts a time difference to a display height."""
853 return -(pic_height / float(h)) * h1
854 def cy(y):
855 """Converts a time to a vertical display position."""
856 return pic_height + ch(y - global_min_y)
857
bensong@google.com74267432012-08-30 18:19:02 +0000858 print '<!--Picture height %.2f corresponds to bench value %.2f.-->' % (
859 pic_height, h)
bungeman@google.com85669f92011-06-17 13:58:14 +0000860 print '<svg',
861 print 'width=%s' % qa(str(pic_width)+'px')
862 print 'height=%s' % qa(str(pic_height)+'px')
863 print 'viewBox="0 0 %s %s"' % (str(pic_width), str(pic_height))
864 print 'onclick=%s' % qa(
865 "var event = arguments[0] || window.event;"
866 " if (event.shiftKey) { highlightRevision(null); }"
867 " if (event.ctrlKey) { highlight(null); }"
868 " return false;")
869 print 'xmlns="http://www.w3.org/2000/svg"'
870 print 'xmlns:xlink="http://www.w3.org/1999/xlink">'
871
872 print """
873<defs>
874 <marker id="circleMark"
875 viewBox="0 0 2 2" refX="1" refY="1"
876 markerUnits="strokeWidth"
877 markerWidth="2" markerHeight="2"
878 orient="0">
879 <circle cx="1" cy="1" r="1"/>
880 </marker>
881</defs>"""
882
883 #output the revisions
884 print """
885<script type="text/javascript">//<![CDATA[
886 var previousRevision;
887 var previousRevisionFill;
888 var previousRevisionStroke
889 function highlightRevision(id) {
890 if (previousRevision == id) return;
epoger@google.comc71174d2011-08-08 17:19:23 +0000891
892 document.getElementById('revision').firstChild.nodeValue = 'r' + id;
893 document.getElementById('rev_link').setAttribute('xlink:href',
894 'http://code.google.com/p/skia/source/detail?r=' + id);
bungeman@google.com85669f92011-06-17 13:58:14 +0000895
896 var preRevision = document.getElementById(previousRevision);
897 if (preRevision) {
898 preRevision.setAttributeNS(null,'fill', previousRevisionFill);
899 preRevision.setAttributeNS(null,'stroke', previousRevisionStroke);
900 }
901
902 var revision = document.getElementById(id);
903 previousRevision = id;
904 if (revision) {
905 previousRevisionFill = revision.getAttributeNS(null,'fill');
906 revision.setAttributeNS(null,'fill','rgb(100%, 95%, 95%)');
907
908 previousRevisionStroke = revision.getAttributeNS(null,'stroke');
909 revision.setAttributeNS(null,'stroke','rgb(100%, 90%, 90%)');
910 }
911 }
912//]]></script>"""
913
914 def print_rect(x, y, w, h, revision):
915 """Outputs a revision rectangle in display space,
916 taking arguments in revision space."""
917 disp_y = cy(y)
918 disp_h = ch(h)
919 if disp_h < 0:
920 disp_y += disp_h
921 disp_h = -disp_h
922
923 print '<rect id=%s x=%s y=%s' % (qa(revision), qa(cx(x)), qa(disp_y),),
924 print 'width=%s height=%s' % (qa(cw(w)), qa(disp_h),),
925 print 'fill="white"',
epoger@google.comc71174d2011-08-08 17:19:23 +0000926 print 'stroke="rgb(98%%,98%%,88%%)" stroke-width=%s' % qa(line_width),
bungeman@google.com85669f92011-06-17 13:58:14 +0000927 print 'onmouseover=%s' % qa(
928 "var event = arguments[0] || window.event;"
929 " if (event.shiftKey) {"
930 " highlightRevision('"+str(revision)+"');"
931 " return false;"
932 " }"),
933 print ' />'
934
935 xes = set()
936 for line in lines.itervalues():
937 for point in line:
938 xes.add(point[0])
939 revisions = list(xes)
940 revisions.sort()
941
942 left = x
943 current_revision = revisions[0]
944 for next_revision in revisions[1:]:
945 width = (((next_revision - current_revision) / 2.0)
946 + (current_revision - left))
947 print_rect(left, y, width, h, current_revision)
948 left += width
949 current_revision = next_revision
950 print_rect(left, y, x+w - left, h, current_revision)
epoger@google.comc71174d2011-08-08 17:19:23 +0000951
bungeman@google.com85669f92011-06-17 13:58:14 +0000952 #output the lines
953 print """
954<script type="text/javascript">//<![CDATA[
955 var previous;
956 var previousColor;
957 var previousOpacity;
958 function highlight(id) {
959 if (previous == id) return;
epoger@google.comc71174d2011-08-08 17:19:23 +0000960
bungeman@google.com85669f92011-06-17 13:58:14 +0000961 document.getElementById('label').firstChild.nodeValue = id;
epoger@google.comc71174d2011-08-08 17:19:23 +0000962
bungeman@google.com85669f92011-06-17 13:58:14 +0000963 var preGroup = document.getElementById(previous);
964 if (preGroup) {
965 var preLine = document.getElementById(previous+'_line');
966 preLine.setAttributeNS(null,'stroke', previousColor);
967 preLine.setAttributeNS(null,'opacity', previousOpacity);
epoger@google.comc71174d2011-08-08 17:19:23 +0000968
bungeman@google.com85669f92011-06-17 13:58:14 +0000969 var preSlope = document.getElementById(previous+'_linear');
970 if (preSlope) {
971 preSlope.setAttributeNS(null,'visibility', 'hidden');
972 }
973 }
epoger@google.comc71174d2011-08-08 17:19:23 +0000974
bungeman@google.com85669f92011-06-17 13:58:14 +0000975 var group = document.getElementById(id);
976 previous = id;
977 if (group) {
978 group.parentNode.appendChild(group);
979
980 var line = document.getElementById(id+'_line');
981 previousColor = line.getAttributeNS(null,'stroke');
982 previousOpacity = line.getAttributeNS(null,'opacity');
983 line.setAttributeNS(null,'stroke', 'blue');
984 line.setAttributeNS(null,'opacity', '1');
985
986 var slope = document.getElementById(id+'_linear');
987 if (slope) {
988 slope.setAttributeNS(null,'visibility', 'visible');
989 }
990 }
991 }
992//]]></script>"""
epoger@google.com2459a392013-02-14 18:58:05 +0000993
994 # Add a new element to each item in the 'lines' list: the label in string
995 # form. Then use that element to sort the list.
996 sorted_lines = []
bungeman@google.com85669f92011-06-17 13:58:14 +0000997 for label, line in lines.items():
epoger@google.com2459a392013-02-14 18:58:05 +0000998 sorted_lines.append([str(label), label, line])
999 sorted_lines.sort()
1000
1001 for label_as_string, label, line in sorted_lines:
1002 print '<g id=%s>' % qa(label_as_string)
bungeman@google.com85669f92011-06-17 13:58:14 +00001003 r = 128
1004 g = 128
1005 b = 128
1006 a = .10
1007 if label in regressions:
1008 regression = regressions[label]
1009 min_slope = regression.find_min_slope()
1010 if min_slope < 0:
1011 d = max(0, (min_slope / min_down_slope))
1012 g += int(d*128)
1013 a += d*0.9
1014 elif min_slope > 0:
1015 d = max(0, (min_slope / max_up_slope))
1016 r += int(d*128)
1017 a += d*0.9
1018
1019 slope = regression.slope
1020 intercept = regression.intercept
1021 min_x = regression.min_x
1022 max_x = regression.max_x
1023 print '<polyline id=%s' % qa(str(label)+'_linear'),
1024 print 'fill="none" stroke="yellow"',
1025 print 'stroke-width=%s' % qa(abs(ch(regression.serror*2))),
1026 print 'opacity="0.5" pointer-events="none" visibility="hidden"',
1027 print 'points="',
1028 print '%s,%s' % (str(cx(min_x)), str(cy(slope*min_x + intercept))),
1029 print '%s,%s' % (str(cx(max_x)), str(cy(slope*max_x + intercept))),
1030 print '"/>'
1031
1032 print '<polyline id=%s' % qa(str(label)+'_line'),
1033 print 'onmouseover=%s' % qa(
1034 "var event = arguments[0] || window.event;"
1035 " if (event.ctrlKey) {"
1036 " highlight('"+str(label).replace("'", "\\'")+"');"
1037 " return false;"
1038 " }"),
1039 print 'fill="none" stroke="rgb(%s,%s,%s)"' % (str(r), str(g), str(b)),
1040 print 'stroke-width=%s' % qa(line_width),
1041 print 'opacity=%s' % qa(a),
1042 print 'points="',
1043 for point in line:
1044 print '%s,%s' % (str(cx(point[0])), str(cy(point[1]))),
1045 print '"/>'
epoger@google.comc71174d2011-08-08 17:19:23 +00001046
bungeman@google.com85669f92011-06-17 13:58:14 +00001047 print '</g>'
epoger@google.comc71174d2011-08-08 17:19:23 +00001048
bungeman@google.com85669f92011-06-17 13:58:14 +00001049 #output the labels
1050 print '<text id="label" x="0" y=%s' % qa(font_size),
1051 print 'font-size=%s> </text>' % qa(font_size)
epoger@google.comc71174d2011-08-08 17:19:23 +00001052
1053 print '<a id="rev_link" xlink:href="" target="_top">'
1054 print '<text id="revision" x="0" y=%s style="' % qa(font_size*2)
1055 print 'font-size: %s; ' % qe(font_size)
1056 print 'stroke: #0000dd; text-decoration: underline; '
1057 print '"> </text></a>'
1058
bungeman@google.com85669f92011-06-17 13:58:14 +00001059 print '</svg>'
epoger@google.comc71174d2011-08-08 17:19:23 +00001060
bungeman@google.com85669f92011-06-17 13:58:14 +00001061if __name__ == "__main__":
1062 main()