blob: da18dc9a87581020540a2f2798e1087e93305ec7 [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.comad0c5d22012-09-13 21:08:52 +000035 print '-e <file> file containing expected bench values/ranges.'
36 print ' Will raise exception if actual bench values are out of range.'
37 print ' See bench_expectations.txt for data format and 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
borenet@google.com5dc06782013-03-12 17:16:07 +0000503 if not output_path:
504 print 'Warning: No output path provided. No graphs will be written.'
505
bensong@google.com8ccfa552012-08-17 21:42:14 +0000506 if time_of_interest:
507 time_to_ignore = None
508
bensong@google.comad0c5d22012-09-13 21:08:52 +0000509 # The title flag (-l) provided in buildbot slave is in the format
borenet@google.come6598a02013-04-30 12:02:32 +0000510 # Bench_Performance_for_<platform>, and we want to extract <platform>
bensong@google.comad0c5d22012-09-13 21:08:52 +0000511 # for use in platform_and_alg to track matching benches later. If title flag
512 # is not in this format, there may be no matching benches in the file
513 # provided by the expectation_file flag (-e).
bensong@google.com848fa2b2013-03-07 17:12:43 +0000514 bot = title # To store the platform as bot name
bensong@google.comad0c5d22012-09-13 21:08:52 +0000515 platform_and_alg = title
516 if platform_and_alg.startswith(TITLE_PREAMBLE):
bensong@google.com848fa2b2013-03-07 17:12:43 +0000517 bot = platform_and_alg[TITLE_PREAMBLE_LENGTH:]
518 platform_and_alg = bot + '-' + rep
bensong@google.com8c1de762012-08-15 18:27:38 +0000519 title += ' [representation: %s]' % rep
520
epoger@google.com37260002011-08-08 20:27:04 +0000521 latest_revision = get_latest_revision(directory)
522 oldest_revision, newest_revision = parse_range(revision_range)
523 oldest_regression, newest_regression = parse_range(regression_range)
524
epoger@google.com3d8cd172012-05-11 18:26:16 +0000525 unfiltered_revision_data_points = parse_dir(directory
bungeman@google.com85669f92011-06-17 13:58:14 +0000526 , default_settings
527 , oldest_revision
bensong@google.com87348162012-08-15 17:31:46 +0000528 , newest_revision
529 , rep)
epoger@google.comc71174d2011-08-08 17:19:23 +0000530
epoger@google.com3d8cd172012-05-11 18:26:16 +0000531 # Filter out any data points that are utterly bogus... make sure to report
532 # that we did so later!
533 (allowed_revision_data_points, ignored_revision_data_points) = filter_data_points(
534 unfiltered_revision_data_points)
535
epoger@google.comc71174d2011-08-08 17:19:23 +0000536 # Update oldest_revision and newest_revision based on the data we could find
epoger@google.com3d8cd172012-05-11 18:26:16 +0000537 all_revision_numbers = allowed_revision_data_points.keys()
epoger@google.comc71174d2011-08-08 17:19:23 +0000538 oldest_revision = min(all_revision_numbers)
539 newest_revision = max(all_revision_numbers)
540
epoger@google.com3d8cd172012-05-11 18:26:16 +0000541 lines = create_lines(allowed_revision_data_points
bungeman@google.com85669f92011-06-17 13:58:14 +0000542 , settings
543 , bench_of_interest
544 , config_of_interest
bensong@google.com8ccfa552012-08-17 21:42:14 +0000545 , time_of_interest
546 , time_to_ignore)
epoger@google.comc71174d2011-08-08 17:19:23 +0000547
bungeman@google.com85669f92011-06-17 13:58:14 +0000548 regressions = create_regressions(lines
549 , oldest_regression
550 , newest_regression)
epoger@google.comc71174d2011-08-08 17:19:23 +0000551
borenet@google.com5dc06782013-03-12 17:16:07 +0000552 if output_path:
553 output_xhtml(lines, oldest_revision, newest_revision,
554 ignored_revision_data_points, regressions, requested_width,
555 requested_height, title)
bungeman@google.com85669f92011-06-17 13:58:14 +0000556
bensong@google.com848fa2b2013-03-07 17:12:43 +0000557 if appengine_url:
558 write_to_appengine(lines, appengine_url, newest_revision, bot)
559
borenet@google.com5dc06782013-03-12 17:16:07 +0000560 if bench_expectations:
561 check_expectations(lines, bench_expectations, newest_revision,
562 platform_and_alg)
bensong@google.comad0c5d22012-09-13 21:08:52 +0000563
bungeman@google.com85669f92011-06-17 13:58:14 +0000564def qa(out):
565 """Stringify input and quote as an xml attribute."""
566 return xml.sax.saxutils.quoteattr(str(out))
567def qe(out):
568 """Stringify input and escape as xml data."""
569 return xml.sax.saxutils.escape(str(out))
570
571def create_select(qualifier, lines, select_id=None):
572 """Output select with options showing lines which qualifier maps to it.
573
574 ((Label) -> str, {Label:_}, str?) -> _"""
575 options = {} #{ option : [Label]}
576 for label in lines.keys():
577 option = qualifier(label)
578 if (option not in options):
579 options[option] = []
580 options[option].append(label)
581 option_list = list(options.keys())
582 option_list.sort()
583 print '<select class="lines"',
584 if select_id is not None:
585 print 'id=%s' % qa(select_id)
586 print 'multiple="true" size="10" onchange="updateSvg();">'
587 for option in option_list:
588 print '<option value=' + qa('[' +
589 reduce(lambda x,y:x+json.dumps(str(y))+',',options[option],"")[0:-1]
590 + ']') + '>'+qe(option)+'</option>'
591 print '</select>'
592
epoger@google.com3d8cd172012-05-11 18:26:16 +0000593def output_ignored_data_points_warning(ignored_revision_data_points):
594 """Write description of ignored_revision_data_points to stdout as xhtml.
595 """
596 num_ignored_points = 0
597 description = ''
598 revisions = ignored_revision_data_points.keys()
599 if revisions:
600 revisions.sort()
601 revisions.reverse()
602 for revision in revisions:
603 num_ignored_points += len(ignored_revision_data_points[revision])
604 points_at_this_revision = []
605 for point in ignored_revision_data_points[revision]:
606 points_at_this_revision.append(point.bench)
607 points_at_this_revision.sort()
608 description += 'r%d: %s\n' % (revision, points_at_this_revision)
609 if num_ignored_points == 0:
610 print 'Did not discard any data points; all were within the range [%d-%d]' % (
611 MIN_REASONABLE_TIME, MAX_REASONABLE_TIME)
612 else:
613 print '<table width="100%" bgcolor="ff0000"><tr><td align="center">'
614 print 'Discarded %d data points outside of range [%d-%d]' % (
615 num_ignored_points, MIN_REASONABLE_TIME, MAX_REASONABLE_TIME)
616 print '</td></tr><tr><td width="100%" align="center">'
617 print ('<textarea rows="4" style="width:97%" readonly="true" wrap="off">'
618 + qe(description) + '</textarea>')
619 print '</td></tr></table>'
620
621def output_xhtml(lines, oldest_revision, newest_revision, ignored_revision_data_points,
epoger@google.com37260002011-08-08 20:27:04 +0000622 regressions, requested_width, requested_height, title):
bungeman@google.com85669f92011-06-17 13:58:14 +0000623 """Outputs an svg/xhtml view of the data."""
624 print '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"',
625 print '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
626 print '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">'
627 print '<head>'
epoger@google.com3d8cd172012-05-11 18:26:16 +0000628 print '<title>%s</title>' % qe(title)
bungeman@google.com85669f92011-06-17 13:58:14 +0000629 print '</head>'
630 print '<body>'
631
632 output_svg(lines, regressions, requested_width, requested_height)
epoger@google.com3d8cd172012-05-11 18:26:16 +0000633
bungeman@google.com85669f92011-06-17 13:58:14 +0000634 #output the manipulation controls
635 print """
636<script type="text/javascript">//<![CDATA[
637 function getElementsByClass(node, searchClass, tag) {
638 var classElements = new Array();
639 var elements = node.getElementsByTagName(tag);
640 var pattern = new RegExp("^|\\s"+searchClass+"\\s|$");
641 for (var i = 0, elementsFound = 0; i < elements.length; ++i) {
642 if (pattern.test(elements[i].className)) {
643 classElements[elementsFound] = elements[i];
644 ++elementsFound;
645 }
646 }
647 return classElements;
648 }
649 function getAllLines() {
650 var selectElem = document.getElementById('benchSelect');
651 var linesObj = {};
652 for (var i = 0; i < selectElem.options.length; ++i) {
653 var lines = JSON.parse(selectElem.options[i].value);
654 for (var j = 0; j < lines.length; ++j) {
655 linesObj[lines[j]] = true;
656 }
657 }
658 return linesObj;
659 }
660 function getOptions(selectElem) {
661 var linesSelectedObj = {};
662 for (var i = 0; i < selectElem.options.length; ++i) {
663 if (!selectElem.options[i].selected) continue;
664
665 var linesSelected = JSON.parse(selectElem.options[i].value);
666 for (var j = 0; j < linesSelected.length; ++j) {
667 linesSelectedObj[linesSelected[j]] = true;
668 }
669 }
670 return linesSelectedObj;
671 }
672 function objectEmpty(obj) {
673 for (var p in obj) {
674 return false;
675 }
676 return true;
677 }
678 function markSelectedLines(selectElem, allLines) {
679 var linesSelected = getOptions(selectElem);
680 if (!objectEmpty(linesSelected)) {
681 for (var line in allLines) {
682 allLines[line] &= (linesSelected[line] == true);
683 }
684 }
685 }
686 function updateSvg() {
687 var allLines = getAllLines();
688
689 var selects = getElementsByClass(document, 'lines', 'select');
690 for (var i = 0; i < selects.length; ++i) {
691 markSelectedLines(selects[i], allLines);
692 }
693
694 for (var line in allLines) {
695 var svgLine = document.getElementById(line);
696 var display = (allLines[line] ? 'inline' : 'none');
697 svgLine.setAttributeNS(null,'display', display);
698 }
699 }
700
701 function mark(markerId) {
702 for (var line in getAllLines()) {
703 var svgLineGroup = document.getElementById(line);
704 var display = svgLineGroup.getAttributeNS(null,'display');
705 if (display == null || display == "" || display != "none") {
706 var svgLine = document.getElementById(line+'_line');
707 if (markerId == null) {
708 svgLine.removeAttributeNS(null,'marker-mid');
709 } else {
710 svgLine.setAttributeNS(null,'marker-mid', markerId);
711 }
712 }
713 }
714 }
715//]]></script>"""
epoger@google.comc71174d2011-08-08 17:19:23 +0000716
bungeman@google.com85669f92011-06-17 13:58:14 +0000717 all_settings = {}
718 variant_settings = set()
719 for label in lines.keys():
720 for key, value in label.settings.items():
721 if key not in all_settings:
722 all_settings[key] = value
723 elif all_settings[key] != value:
724 variant_settings.add(key)
epoger@google.comc71174d2011-08-08 17:19:23 +0000725
bungeman@google.com752acc72012-09-12 19:34:17 +0000726 print '<table border="0" width="%s">' % requested_width
727 #output column headers
728 print """
729<tr valign="top"><td width="50%">
730<table border="0" width="100%">
731<tr><td align="center"><table border="0">
732<form>
733<tr valign="bottom" align="center">
734<td width="1">Bench&nbsp;Type</td>
735<td width="1">Bitmap Config</td>
736<td width="1">Timer&nbsp;Type (Cpu/Gpu/wall)</td>
737"""
738
bungeman@google.com85669f92011-06-17 13:58:14 +0000739 for k in variant_settings:
bungeman@google.com752acc72012-09-12 19:34:17 +0000740 print '<td width="1">%s</td>' % qe(k)
741
742 print '<td width="1"><!--buttons--></td></tr>'
743
744 #output column contents
745 print '<tr valign="top" align="center">'
746 print '<td width="1">'
747 create_select(lambda l: l.bench, lines, 'benchSelect')
748 print '</td><td width="1">'
749 create_select(lambda l: l.config, lines)
750 print '</td><td width="1">'
751 create_select(lambda l: l.time_type, lines)
752
753 for k in variant_settings:
754 print '</td><td width="1">'
755 create_select(lambda l: l.settings.get(k, " "), lines)
epoger@google.comc71174d2011-08-08 17:19:23 +0000756
757 print '</td><td width="1"><button type="button"',
bungeman@google.com85669f92011-06-17 13:58:14 +0000758 print 'onclick=%s' % qa("mark('url(#circleMark)'); return false;"),
epoger@google.comc71174d2011-08-08 17:19:23 +0000759 print '>Mark Points</button>'
760 print '<button type="button" onclick="mark(null);">Clear Points</button>'
epoger@google.com3d8cd172012-05-11 18:26:16 +0000761 print '</td>'
epoger@google.comc71174d2011-08-08 17:19:23 +0000762 print """
epoger@google.com3d8cd172012-05-11 18:26:16 +0000763</tr>
764</form>
765</table></td></tr>
766<tr><td align="center">
767<hr />
768"""
769
770 output_ignored_data_points_warning(ignored_revision_data_points)
771 print '</td></tr></table>'
772 print '</td><td width="2%"><!--gutter--></td>'
773
774 print '<td><table border="0">'
775 print '<tr><td align="center">%s<br></br>revisions r%s - r%s</td></tr>' % (
776 qe(title),
777 bench_util.CreateRevisionLink(oldest_revision),
778 bench_util.CreateRevisionLink(newest_revision))
779 print """
780<tr><td align="left">
epoger@google.comc71174d2011-08-08 17:19:23 +0000781<p>Brighter red indicates tests that have gotten worse; brighter green
782indicates tests that have gotten better.</p>
783<p>To highlight individual tests, hold down CONTROL and mouse over
784graph lines.</p>
785<p>To highlight revision numbers, hold down SHIFT and mouse over
786the graph area.</p>
787<p>To only show certain tests on the graph, select any combination of
788tests in the selectors at left. (To show all, select all.)</p>
789<p>Use buttons at left to mark/clear points on the lines for selected
790benchmarks.</p>
epoger@google.com3d8cd172012-05-11 18:26:16 +0000791</td></tr>
792</table>
793
epoger@google.comc71174d2011-08-08 17:19:23 +0000794</td>
795</tr>
epoger@google.comc71174d2011-08-08 17:19:23 +0000796</table>
797</body>
798</html>"""
bungeman@google.com85669f92011-06-17 13:58:14 +0000799
800def compute_size(requested_width, requested_height, rev_width, time_height):
801 """Converts potentially empty requested size into a concrete size.
802
803 (Number?, Number?) -> (Number, Number)"""
804 pic_width = 0
805 pic_height = 0
806 if (requested_width is not None and requested_height is not None):
807 pic_height = requested_height
808 pic_width = requested_width
809
810 elif (requested_width is not None):
811 pic_width = requested_width
812 pic_height = pic_width * (float(time_height) / rev_width)
813
814 elif (requested_height is not None):
815 pic_height = requested_height
816 pic_width = pic_height * (float(rev_width) / time_height)
817
818 else:
819 pic_height = 800
820 pic_width = max(rev_width*3
821 , pic_height * (float(rev_width) / time_height))
822
823 return (pic_width, pic_height)
824
825def output_svg(lines, regressions, requested_width, requested_height):
826 """Outputs an svg view of the data."""
827
828 (global_min_x, _), (global_max_x, global_max_y) = bounds(lines)
829 max_up_slope, min_down_slope = bounds_slope(regressions)
bensong@google.com741b2e12013-06-13 13:53:16 +0000830
bungeman@google.com85669f92011-06-17 13:58:14 +0000831 #output
832 global_min_y = 0
833 x = global_min_x
834 y = global_min_y
835 w = global_max_x - global_min_x
836 h = global_max_y - global_min_y
837 font_size = 16
838 line_width = 2
bensong@google.com741b2e12013-06-13 13:53:16 +0000839
840 # If there is nothing to see, don't try to draw anything.
841 if w == 0 or h == 0:
842 return
843
bungeman@google.com85669f92011-06-17 13:58:14 +0000844 pic_width, pic_height = compute_size(requested_width, requested_height
845 , w, h)
846
847 def cw(w1):
848 """Converts a revision difference to display width."""
849 return (pic_width / float(w)) * w1
850 def cx(x):
851 """Converts a revision to a horizontal display position."""
852 return cw(x - global_min_x)
853
854 def ch(h1):
855 """Converts a time difference to a display height."""
856 return -(pic_height / float(h)) * h1
857 def cy(y):
858 """Converts a time to a vertical display position."""
859 return pic_height + ch(y - global_min_y)
860
bensong@google.com74267432012-08-30 18:19:02 +0000861 print '<!--Picture height %.2f corresponds to bench value %.2f.-->' % (
862 pic_height, h)
bungeman@google.com85669f92011-06-17 13:58:14 +0000863 print '<svg',
864 print 'width=%s' % qa(str(pic_width)+'px')
865 print 'height=%s' % qa(str(pic_height)+'px')
866 print 'viewBox="0 0 %s %s"' % (str(pic_width), str(pic_height))
867 print 'onclick=%s' % qa(
868 "var event = arguments[0] || window.event;"
869 " if (event.shiftKey) { highlightRevision(null); }"
870 " if (event.ctrlKey) { highlight(null); }"
871 " return false;")
872 print 'xmlns="http://www.w3.org/2000/svg"'
873 print 'xmlns:xlink="http://www.w3.org/1999/xlink">'
874
875 print """
876<defs>
877 <marker id="circleMark"
878 viewBox="0 0 2 2" refX="1" refY="1"
879 markerUnits="strokeWidth"
880 markerWidth="2" markerHeight="2"
881 orient="0">
882 <circle cx="1" cy="1" r="1"/>
883 </marker>
884</defs>"""
885
886 #output the revisions
887 print """
888<script type="text/javascript">//<![CDATA[
889 var previousRevision;
890 var previousRevisionFill;
891 var previousRevisionStroke
892 function highlightRevision(id) {
893 if (previousRevision == id) return;
epoger@google.comc71174d2011-08-08 17:19:23 +0000894
895 document.getElementById('revision').firstChild.nodeValue = 'r' + id;
896 document.getElementById('rev_link').setAttribute('xlink:href',
897 'http://code.google.com/p/skia/source/detail?r=' + id);
bungeman@google.com85669f92011-06-17 13:58:14 +0000898
899 var preRevision = document.getElementById(previousRevision);
900 if (preRevision) {
901 preRevision.setAttributeNS(null,'fill', previousRevisionFill);
902 preRevision.setAttributeNS(null,'stroke', previousRevisionStroke);
903 }
904
905 var revision = document.getElementById(id);
906 previousRevision = id;
907 if (revision) {
908 previousRevisionFill = revision.getAttributeNS(null,'fill');
909 revision.setAttributeNS(null,'fill','rgb(100%, 95%, 95%)');
910
911 previousRevisionStroke = revision.getAttributeNS(null,'stroke');
912 revision.setAttributeNS(null,'stroke','rgb(100%, 90%, 90%)');
913 }
914 }
915//]]></script>"""
916
917 def print_rect(x, y, w, h, revision):
918 """Outputs a revision rectangle in display space,
919 taking arguments in revision space."""
920 disp_y = cy(y)
921 disp_h = ch(h)
922 if disp_h < 0:
923 disp_y += disp_h
924 disp_h = -disp_h
925
926 print '<rect id=%s x=%s y=%s' % (qa(revision), qa(cx(x)), qa(disp_y),),
927 print 'width=%s height=%s' % (qa(cw(w)), qa(disp_h),),
928 print 'fill="white"',
epoger@google.comc71174d2011-08-08 17:19:23 +0000929 print 'stroke="rgb(98%%,98%%,88%%)" stroke-width=%s' % qa(line_width),
bungeman@google.com85669f92011-06-17 13:58:14 +0000930 print 'onmouseover=%s' % qa(
931 "var event = arguments[0] || window.event;"
932 " if (event.shiftKey) {"
933 " highlightRevision('"+str(revision)+"');"
934 " return false;"
935 " }"),
936 print ' />'
937
938 xes = set()
939 for line in lines.itervalues():
940 for point in line:
941 xes.add(point[0])
942 revisions = list(xes)
943 revisions.sort()
944
945 left = x
946 current_revision = revisions[0]
947 for next_revision in revisions[1:]:
948 width = (((next_revision - current_revision) / 2.0)
949 + (current_revision - left))
950 print_rect(left, y, width, h, current_revision)
951 left += width
952 current_revision = next_revision
953 print_rect(left, y, x+w - left, h, current_revision)
epoger@google.comc71174d2011-08-08 17:19:23 +0000954
bungeman@google.com85669f92011-06-17 13:58:14 +0000955 #output the lines
956 print """
957<script type="text/javascript">//<![CDATA[
958 var previous;
959 var previousColor;
960 var previousOpacity;
961 function highlight(id) {
962 if (previous == id) return;
epoger@google.comc71174d2011-08-08 17:19:23 +0000963
bungeman@google.com85669f92011-06-17 13:58:14 +0000964 document.getElementById('label').firstChild.nodeValue = id;
epoger@google.comc71174d2011-08-08 17:19:23 +0000965
bungeman@google.com85669f92011-06-17 13:58:14 +0000966 var preGroup = document.getElementById(previous);
967 if (preGroup) {
968 var preLine = document.getElementById(previous+'_line');
969 preLine.setAttributeNS(null,'stroke', previousColor);
970 preLine.setAttributeNS(null,'opacity', previousOpacity);
epoger@google.comc71174d2011-08-08 17:19:23 +0000971
bungeman@google.com85669f92011-06-17 13:58:14 +0000972 var preSlope = document.getElementById(previous+'_linear');
973 if (preSlope) {
974 preSlope.setAttributeNS(null,'visibility', 'hidden');
975 }
976 }
epoger@google.comc71174d2011-08-08 17:19:23 +0000977
bungeman@google.com85669f92011-06-17 13:58:14 +0000978 var group = document.getElementById(id);
979 previous = id;
980 if (group) {
981 group.parentNode.appendChild(group);
982
983 var line = document.getElementById(id+'_line');
984 previousColor = line.getAttributeNS(null,'stroke');
985 previousOpacity = line.getAttributeNS(null,'opacity');
986 line.setAttributeNS(null,'stroke', 'blue');
987 line.setAttributeNS(null,'opacity', '1');
988
989 var slope = document.getElementById(id+'_linear');
990 if (slope) {
991 slope.setAttributeNS(null,'visibility', 'visible');
992 }
993 }
994 }
995//]]></script>"""
epoger@google.com2459a392013-02-14 18:58:05 +0000996
997 # Add a new element to each item in the 'lines' list: the label in string
998 # form. Then use that element to sort the list.
999 sorted_lines = []
bungeman@google.com85669f92011-06-17 13:58:14 +00001000 for label, line in lines.items():
epoger@google.com2459a392013-02-14 18:58:05 +00001001 sorted_lines.append([str(label), label, line])
1002 sorted_lines.sort()
1003
1004 for label_as_string, label, line in sorted_lines:
1005 print '<g id=%s>' % qa(label_as_string)
bungeman@google.com85669f92011-06-17 13:58:14 +00001006 r = 128
1007 g = 128
1008 b = 128
1009 a = .10
1010 if label in regressions:
1011 regression = regressions[label]
1012 min_slope = regression.find_min_slope()
1013 if min_slope < 0:
1014 d = max(0, (min_slope / min_down_slope))
1015 g += int(d*128)
1016 a += d*0.9
1017 elif min_slope > 0:
1018 d = max(0, (min_slope / max_up_slope))
1019 r += int(d*128)
1020 a += d*0.9
1021
1022 slope = regression.slope
1023 intercept = regression.intercept
1024 min_x = regression.min_x
1025 max_x = regression.max_x
1026 print '<polyline id=%s' % qa(str(label)+'_linear'),
1027 print 'fill="none" stroke="yellow"',
1028 print 'stroke-width=%s' % qa(abs(ch(regression.serror*2))),
1029 print 'opacity="0.5" pointer-events="none" visibility="hidden"',
1030 print 'points="',
1031 print '%s,%s' % (str(cx(min_x)), str(cy(slope*min_x + intercept))),
1032 print '%s,%s' % (str(cx(max_x)), str(cy(slope*max_x + intercept))),
1033 print '"/>'
1034
1035 print '<polyline id=%s' % qa(str(label)+'_line'),
1036 print 'onmouseover=%s' % qa(
1037 "var event = arguments[0] || window.event;"
1038 " if (event.ctrlKey) {"
1039 " highlight('"+str(label).replace("'", "\\'")+"');"
1040 " return false;"
1041 " }"),
1042 print 'fill="none" stroke="rgb(%s,%s,%s)"' % (str(r), str(g), str(b)),
1043 print 'stroke-width=%s' % qa(line_width),
1044 print 'opacity=%s' % qa(a),
1045 print 'points="',
1046 for point in line:
1047 print '%s,%s' % (str(cx(point[0])), str(cy(point[1]))),
1048 print '"/>'
epoger@google.comc71174d2011-08-08 17:19:23 +00001049
bungeman@google.com85669f92011-06-17 13:58:14 +00001050 print '</g>'
epoger@google.comc71174d2011-08-08 17:19:23 +00001051
bungeman@google.com85669f92011-06-17 13:58:14 +00001052 #output the labels
1053 print '<text id="label" x="0" y=%s' % qa(font_size),
1054 print 'font-size=%s> </text>' % qa(font_size)
epoger@google.comc71174d2011-08-08 17:19:23 +00001055
1056 print '<a id="rev_link" xlink:href="" target="_top">'
1057 print '<text id="revision" x="0" y=%s style="' % qa(font_size*2)
1058 print 'font-size: %s; ' % qe(font_size)
1059 print 'stroke: #0000dd; text-decoration: underline; '
1060 print '"> </text></a>'
1061
bungeman@google.com85669f92011-06-17 13:58:14 +00001062 print '</svg>'
epoger@google.comc71174d2011-08-08 17:19:23 +00001063
bungeman@google.com85669f92011-06-17 13:58:14 +00001064if __name__ == "__main__":
1065 main()