2 # Copyright 2014 the V8 project 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.
7 Performance runner for d8.
9 Call e.g. with tools/run-perf.py --arch ia32 some_suite.json
11 The suite json format is expected to be:
13 "path": <relative path chunks to perf resources and main file>,
14 "name": <optional suite name, file name is default>,
15 "archs": [<architecture name for which this suite is run>, ...],
16 "binary": <name of binary to run, default "d8">,
17 "flags": [<flag to d8>, ...],
18 "run_count": <how often will this suite run (optional)>,
19 "run_count_XXX": <how often will this suite run for arch XXX (optional)>,
20 "resources": [<js file to be loaded before main>, ...]
21 "main": <main js perf runner file>,
22 "results_regexp": <optional regexp>,
23 "results_processor": <optional python results processor script>,
24 "units": <the unit specification for the performance dashboard>,
27 "name": <name of the trace>,
28 "results_regexp": <optional more specific regexp>,
29 "results_processor": <optional python results processor script>,
30 "units": <the unit specification for the performance dashboard>,
35 The tests field can also nest other suites in arbitrary depth. A suite
36 with a "main" file is a leaf suite that can contain one more level of
39 A suite's results_regexp is expected to have one string place holder
40 "%s" for the trace name. A trace's results_regexp overwrites suite
43 A suite's results_processor may point to an optional python script. If
44 specified, it is called after running the tests like this (with a path
45 relatve to the suite level's path):
46 <results_processor file> <same flags as for d8> <suite level name> <output>
48 The <output> is a temporary file containing d8 output. The results_regexp will
49 be applied to the output of this script.
51 A suite without "tests" is considered a performance test itself.
53 Full example (suite with one runner):
56 "flags": ["--expose-gc"],
57 "archs": ["ia32", "x64"],
61 "results_regexp": "^%s: (.+)$",
65 {"name": "DeltaBlue"},
66 {"name": "NavierStokes",
67 "results_regexp": "^NavierStokes: (.+)$"}
71 Full example (suite with several runners):
74 "flags": ["--expose-gc"],
75 "archs": ["ia32", "x64"],
83 "results_regexp": "^Richards: (.+)$"},
84 {"name": "NavierStokes",
85 "path": ["navier_stokes"],
87 "results_regexp": "^NavierStokes: (.+)$"}
91 Path pieces are concatenated. D8 is always run with the suite's path as cwd.
94 from collections import OrderedDict
102 from testrunner.local import commands
103 from testrunner.local import utils
105 ARCH_GUESS = utils.DefaultArch()
106 SUPPORTED_ARCHS = ["android_arm",
118 GENERIC_RESULTS_RE = re.compile(r"^RESULT ([^:]+): ([^=]+)= ([^ ]+) ([^ ]*)$")
119 RESULT_STDDEV_RE = re.compile(r"^\{([^\}]+)\}$")
120 RESULT_LIST_RE = re.compile(r"^\[([^\]]+)\]$")
124 def GeometricMean(values):
125 """Returns the geometric mean of a list of values.
127 The mean is calculated using log to avoid overflow.
129 values = map(float, values)
130 return str(math.exp(sum(map(math.log, values)) / len(values)))
133 class Results(object):
134 """Place holder for result traces."""
135 def __init__(self, traces=None, errors=None):
136 self.traces = traces or []
137 self.errors = errors or []
140 return {"traces": self.traces, "errors": self.errors}
142 def WriteToFile(self, file_name):
143 with open(file_name, "w") as f:
144 f.write(json.dumps(self.ToDict()))
146 def __add__(self, other):
147 self.traces += other.traces
148 self.errors += other.errors
151 def __str__(self): # pragma: no cover
152 return str(self.ToDict())
156 """Represents a node in the suite tree structure."""
157 def __init__(self, *args):
160 def AppendChild(self, child):
161 self._children.append(child)
164 class DefaultSentinel(Node):
165 """Fake parent node with all default values."""
167 super(DefaultSentinel, self).__init__()
175 self.results_regexp = None
176 self.stddev_regexp = None
182 """Represents a suite definition.
184 Can either be a leaf or an inner node that provides default values.
186 def __init__(self, suite, parent, arch):
187 super(Graph, self).__init__()
190 assert isinstance(suite.get("path", []), list)
191 assert isinstance(suite["name"], basestring)
192 assert isinstance(suite.get("flags", []), list)
193 assert isinstance(suite.get("resources", []), list)
195 # Accumulated values.
196 self.path = parent.path[:] + suite.get("path", [])
197 self.graphs = parent.graphs[:] + [suite["name"]]
198 self.flags = parent.flags[:] + suite.get("flags", [])
199 self.resources = parent.resources[:] + suite.get("resources", [])
201 # Descrete values (with parent defaults).
202 self.binary = suite.get("binary", parent.binary)
203 self.run_count = suite.get("run_count", parent.run_count)
204 self.run_count = suite.get("run_count_%s" % arch, self.run_count)
205 self.timeout = suite.get("timeout", parent.timeout)
206 self.units = suite.get("units", parent.units)
207 self.total = suite.get("total", parent.total)
209 # A regular expression for results. If the parent graph provides a
210 # regexp and the current suite has none, a string place holder for the
211 # suite name is expected.
212 # TODO(machenbach): Currently that makes only sense for the leaf level.
213 # Multiple place holders for multiple levels are not supported.
214 if parent.results_regexp:
215 regexp_default = parent.results_regexp % re.escape(suite["name"])
217 regexp_default = None
218 self.results_regexp = suite.get("results_regexp", regexp_default)
220 # A similar regular expression for the standard deviation (optional).
221 if parent.stddev_regexp:
222 stddev_default = parent.stddev_regexp % re.escape(suite["name"])
224 stddev_default = None
225 self.stddev_regexp = suite.get("stddev_regexp", stddev_default)
229 """Represents a leaf in the suite tree structure.
231 Handles collection of measurements.
233 def __init__(self, suite, parent, arch):
234 super(Trace, self).__init__(suite, parent, arch)
235 assert self.results_regexp
240 def ConsumeOutput(self, stdout):
243 re.search(self.results_regexp, stdout, re.M).group(1))
245 self.errors.append("Regexp \"%s\" didn't match for test %s."
246 % (self.results_regexp, self.graphs[-1]))
249 if self.stddev_regexp and self.stddev:
250 self.errors.append("Test %s should only run once since a stddev "
251 "is provided by the test." % self.graphs[-1])
252 if self.stddev_regexp:
253 self.stddev = re.search(self.stddev_regexp, stdout, re.M).group(1)
255 self.errors.append("Regexp \"%s\" didn't match for test %s."
256 % (self.stddev_regexp, self.graphs[-1]))
258 def GetResults(self):
260 "graphs": self.graphs,
262 "results": self.results,
263 "stddev": self.stddev,
267 class Runnable(Graph):
268 """Represents a runnable suite definition (i.e. has a main file).
272 return self._suite.get("main", "")
274 def ChangeCWD(self, suite_path):
275 """Changes the cwd to to path defined in the current graph.
277 The tests are supposed to be relative to the suite configuration.
279 suite_dir = os.path.abspath(os.path.dirname(suite_path))
280 bench_dir = os.path.normpath(os.path.join(*self.path))
281 os.chdir(os.path.join(suite_dir, bench_dir))
283 def GetCommand(self, shell_dir):
284 # TODO(machenbach): This requires +.exe if run on windows.
286 [os.path.join(shell_dir, self.binary)] +
292 def Run(self, runner):
293 """Iterates over several runs and handles the output for all traces."""
294 for stdout in runner():
295 for trace in self._children:
296 trace.ConsumeOutput(stdout)
297 res = reduce(lambda r, t: r + t.GetResults(), self._children, Results())
299 if not res.traces or not self.total:
302 # Assume all traces have the same structure.
303 if len(set(map(lambda t: len(t["results"]), res.traces))) != 1:
304 res.errors.append("Not all traces have the same number of results.")
307 # Calculate the geometric means for all traces. Above we made sure that
308 # there is at least one trace and that the number of results is the same
310 n_results = len(res.traces[0]["results"])
311 total_results = [GeometricMean(t["results"][i] for t in res.traces)
312 for i in range(0, n_results)]
314 "graphs": self.graphs + ["Total"],
315 "units": res.traces[0]["units"],
316 "results": total_results,
321 class RunnableTrace(Trace, Runnable):
322 """Represents a runnable suite definition that is a leaf."""
323 def __init__(self, suite, parent, arch):
324 super(RunnableTrace, self).__init__(suite, parent, arch)
326 def Run(self, runner):
327 """Iterates over several runs and handles the output."""
328 for stdout in runner():
329 self.ConsumeOutput(stdout)
330 return self.GetResults()
333 class RunnableGeneric(Runnable):
334 """Represents a runnable suite definition with generic traces."""
335 def __init__(self, suite, parent, arch):
336 super(RunnableGeneric, self).__init__(suite, parent, arch)
338 def Run(self, runner):
339 """Iterates over several runs and handles the output."""
340 traces = OrderedDict()
341 for stdout in runner():
342 for line in stdout.strip().splitlines():
343 match = GENERIC_RESULTS_RE.match(line)
346 graph = match.group(1)
347 trace = match.group(2)
348 body = match.group(3)
349 units = match.group(4)
350 match_stddev = RESULT_STDDEV_RE.match(body)
351 match_list = RESULT_LIST_RE.match(body)
353 result, stddev = map(str.strip, match_stddev.group(1).split(","))
356 results = map(str.strip, match_list.group(1).split(","))
358 results = [body.strip()]
360 trace_result = traces.setdefault(trace, Results([{
361 "graphs": self.graphs + [graph, trace],
362 "units": (units or self.units).strip(),
366 trace_result.traces[0]["results"].extend(results)
367 trace_result.traces[0]["stddev"] = stddev
369 return reduce(lambda r, t: r + t, traces.itervalues(), Results())
372 def MakeGraph(suite, arch, parent):
373 """Factory method for making graph objects."""
374 if isinstance(parent, Runnable):
375 # Below a runnable can only be traces.
376 return Trace(suite, parent, arch)
377 elif suite.get("main"):
378 # A main file makes this graph runnable.
379 if suite.get("tests"):
380 # This graph has subgraphs (traces).
381 return Runnable(suite, parent, arch)
383 # This graph has no subgraphs, it's a leaf.
384 return RunnableTrace(suite, parent, arch)
385 elif suite.get("generic"):
386 # This is a generic suite definition. It is either a runnable executable
387 # or has a main js file.
388 return RunnableGeneric(suite, parent, arch)
389 elif suite.get("tests"):
390 # This is neither a leaf nor a runnable.
391 return Graph(suite, parent, arch)
392 else: # pragma: no cover
393 raise Exception("Invalid suite configuration.")
396 def BuildGraphs(suite, arch, parent=None):
397 """Builds a tree structure of graph objects that corresponds to the suite
400 parent = parent or DefaultSentinel()
402 # TODO(machenbach): Implement notion of cpu type?
403 if arch not in suite.get("archs", ["ia32", "x64"]):
406 graph = MakeGraph(suite, arch, parent)
407 for subsuite in suite.get("tests", []):
408 BuildGraphs(subsuite, arch, graph)
409 parent.AppendChild(graph)
413 def FlattenRunnables(node):
414 """Generator that traverses the tree structure and iterates over all
417 if isinstance(node, Runnable):
419 elif isinstance(node, Node):
420 for child in node._children:
421 for result in FlattenRunnables(child):
423 else: # pragma: no cover
424 raise Exception("Invalid suite configuration.")
427 # TODO: Implement results_processor.
429 parser = optparse.OptionParser()
430 parser.add_option("--arch",
431 help=("The architecture to run tests for, "
432 "'auto' or 'native' for auto-detect"),
434 parser.add_option("--buildbot",
435 help="Adapt to path structure used on buildbots",
436 default=False, action="store_true")
437 parser.add_option("--json-test-results",
438 help="Path to a file for storing json results.")
439 parser.add_option("--outdir", help="Base directory with compile output",
441 (options, args) = parser.parse_args(args)
443 if len(args) == 0: # pragma: no cover
447 if options.arch in ["auto", "native"]: # pragma: no cover
448 options.arch = ARCH_GUESS
450 if not options.arch in SUPPORTED_ARCHS: # pragma: no cover
451 print "Unknown architecture %s" % options.arch
454 workspace = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
457 shell_dir = os.path.join(workspace, options.outdir, "Release")
459 shell_dir = os.path.join(workspace, options.outdir,
460 "%s.release" % options.arch)
464 path = os.path.abspath(path)
466 if not os.path.exists(path): # pragma: no cover
467 results.errors.append("Configuration file %s does not exist." % path)
470 with open(path) as f:
471 suite = json.loads(f.read())
473 # If no name is given, default to the file name without .json.
474 suite.setdefault("name", os.path.splitext(os.path.basename(path))[0])
476 for runnable in FlattenRunnables(BuildGraphs(suite, options.arch)):
477 print ">>> Running suite: %s" % "/".join(runnable.graphs)
478 runnable.ChangeCWD(path)
481 """Output generator that reruns several times."""
482 for i in xrange(0, max(1, runnable.run_count)):
483 # TODO(machenbach): Allow timeout per arch like with run_count per
485 output = commands.Execute(runnable.GetCommand(shell_dir),
486 timeout=runnable.timeout)
487 print ">>> Stdout (#%d):" % (i + 1)
489 if output.stderr: # pragma: no cover
490 # Print stderr for debugging.
491 print ">>> Stderr (#%d):" % (i + 1)
494 print ">>> Test timed out after %ss." % runnable.timeout
497 # Let runnable iterate over all runs and handle output.
498 results += runnable.Run(Runner)
500 if options.json_test_results:
501 results.WriteToFile(options.json_test_results)
502 else: # pragma: no cover
505 return min(1, len(results.errors))
507 if __name__ == "__main__": # pragma: no cover
508 sys.exit(Main(sys.argv[1:]))