Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / v8 / tools / run_perf.py
1 #!/usr/bin/env python
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.
5
6 """
7 Performance runner for d8.
8
9 Call e.g. with tools/run-perf.py --arch ia32 some_suite.json
10
11 The suite json format is expected to be:
12 {
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>,
25   "tests": [
26     {
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>,
31     }, ...
32   ]
33 }
34
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
37 tests.
38
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
41 defaults.
42
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>
47
48 The <output> is a temporary file containing d8 output. The results_regexp will
49 be applied to the output of this script.
50
51 A suite without "tests" is considered a performance test itself.
52
53 Full example (suite with one runner):
54 {
55   "path": ["."],
56   "flags": ["--expose-gc"],
57   "archs": ["ia32", "x64"],
58   "run_count": 5,
59   "run_count_ia32": 3,
60   "main": "run.js",
61   "results_regexp": "^%s: (.+)$",
62   "units": "score",
63   "tests": [
64     {"name": "Richards"},
65     {"name": "DeltaBlue"},
66     {"name": "NavierStokes",
67      "results_regexp": "^NavierStokes: (.+)$"}
68   ]
69 }
70
71 Full example (suite with several runners):
72 {
73   "path": ["."],
74   "flags": ["--expose-gc"],
75   "archs": ["ia32", "x64"],
76   "run_count": 5,
77   "units": "score",
78   "tests": [
79     {"name": "Richards",
80      "path": ["richards"],
81      "main": "run.js",
82      "run_count": 3,
83      "results_regexp": "^Richards: (.+)$"},
84     {"name": "NavierStokes",
85      "path": ["navier_stokes"],
86      "main": "run.js",
87      "results_regexp": "^NavierStokes: (.+)$"}
88   ]
89 }
90
91 Path pieces are concatenated. D8 is always run with the suite's path as cwd.
92 """
93
94 from collections import OrderedDict
95 import json
96 import math
97 import optparse
98 import os
99 import re
100 import sys
101
102 from testrunner.local import commands
103 from testrunner.local import utils
104
105 ARCH_GUESS = utils.DefaultArch()
106 SUPPORTED_ARCHS = ["android_arm",
107                    "android_arm64",
108                    "android_ia32",
109                    "arm",
110                    "ia32",
111                    "mips",
112                    "mipsel",
113                    "nacl_ia32",
114                    "nacl_x64",
115                    "x64",
116                    "arm64"]
117
118 GENERIC_RESULTS_RE = re.compile(r"^RESULT ([^:]+): ([^=]+)= ([^ ]+) ([^ ]*)$")
119 RESULT_STDDEV_RE = re.compile(r"^\{([^\}]+)\}$")
120 RESULT_LIST_RE = re.compile(r"^\[([^\]]+)\]$")
121
122
123
124 def GeometricMean(values):
125   """Returns the geometric mean of a list of values.
126
127   The mean is calculated using log to avoid overflow.
128   """
129   values = map(float, values)
130   return str(math.exp(sum(map(math.log, values)) / len(values)))
131
132
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 []
138
139   def ToDict(self):
140     return {"traces": self.traces, "errors": self.errors}
141
142   def WriteToFile(self, file_name):
143     with open(file_name, "w") as f:
144       f.write(json.dumps(self.ToDict()))
145
146   def __add__(self, other):
147     self.traces += other.traces
148     self.errors += other.errors
149     return self
150
151   def __str__(self):  # pragma: no cover
152     return str(self.ToDict())
153
154
155 class Node(object):
156   """Represents a node in the suite tree structure."""
157   def __init__(self, *args):
158     self._children = []
159
160   def AppendChild(self, child):
161     self._children.append(child)
162
163
164 class DefaultSentinel(Node):
165   """Fake parent node with all default values."""
166   def __init__(self):
167     super(DefaultSentinel, self).__init__()
168     self.binary = "d8"
169     self.run_count = 10
170     self.timeout = 60
171     self.path = []
172     self.graphs = []
173     self.flags = []
174     self.resources = []
175     self.results_regexp = None
176     self.stddev_regexp = None
177     self.units = "score"
178     self.total = False
179
180
181 class Graph(Node):
182   """Represents a suite definition.
183
184   Can either be a leaf or an inner node that provides default values.
185   """
186   def __init__(self, suite, parent, arch):
187     super(Graph, self).__init__()
188     self._suite = suite
189
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)
194
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", [])
200
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)
208
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"])
216     else:
217       regexp_default = None
218     self.results_regexp = suite.get("results_regexp", regexp_default)
219
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"])
223     else:
224       stddev_default = None
225     self.stddev_regexp = suite.get("stddev_regexp", stddev_default)
226
227
228 class Trace(Graph):
229   """Represents a leaf in the suite tree structure.
230
231   Handles collection of measurements.
232   """
233   def __init__(self, suite, parent, arch):
234     super(Trace, self).__init__(suite, parent, arch)
235     assert self.results_regexp
236     self.results = []
237     self.errors = []
238     self.stddev = ""
239
240   def ConsumeOutput(self, stdout):
241     try:
242       self.results.append(
243           re.search(self.results_regexp, stdout, re.M).group(1))
244     except:
245       self.errors.append("Regexp \"%s\" didn't match for test %s."
246                          % (self.results_regexp, self.graphs[-1]))
247
248     try:
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)
254     except:
255       self.errors.append("Regexp \"%s\" didn't match for test %s."
256                          % (self.stddev_regexp, self.graphs[-1]))
257
258   def GetResults(self):
259     return Results([{
260       "graphs": self.graphs,
261       "units": self.units,
262       "results": self.results,
263       "stddev": self.stddev,
264     }], self.errors)
265
266
267 class Runnable(Graph):
268   """Represents a runnable suite definition (i.e. has a main file).
269   """
270   @property
271   def main(self):
272     return self._suite.get("main", "")
273
274   def ChangeCWD(self, suite_path):
275     """Changes the cwd to to path defined in the current graph.
276
277     The tests are supposed to be relative to the suite configuration.
278     """
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))
282
283   def GetCommand(self, shell_dir):
284     # TODO(machenbach): This requires +.exe if run on windows.
285     return (
286       [os.path.join(shell_dir, self.binary)] +
287       self.flags +
288       self.resources +
289       [self.main]
290     )
291
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())
298
299     if not res.traces or not self.total:
300       return res
301
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.")
305       return res
306
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
309     # for each trace.
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)]
313     res.traces.append({
314       "graphs": self.graphs + ["Total"],
315       "units": res.traces[0]["units"],
316       "results": total_results,
317       "stddev": "",
318     })
319     return res
320
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)
325
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()
331
332
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)
337
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)
344         if match:
345           stddev = ""
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)
352           if match_stddev:
353             result, stddev = map(str.strip, match_stddev.group(1).split(","))
354             results = [result]
355           elif match_list:
356             results = map(str.strip, match_list.group(1).split(","))
357           else:
358             results = [body.strip()]
359
360           trace_result = traces.setdefault(trace, Results([{
361             "graphs": self.graphs + [graph, trace],
362             "units": (units or self.units).strip(),
363             "results": [],
364             "stddev": "",
365           }], []))
366           trace_result.traces[0]["results"].extend(results)
367           trace_result.traces[0]["stddev"] = stddev
368
369     return reduce(lambda r, t: r + t, traces.itervalues(), Results())
370
371
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)
382     else:
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.")
394
395
396 def BuildGraphs(suite, arch, parent=None):
397   """Builds a tree structure of graph objects that corresponds to the suite
398   configuration.
399   """
400   parent = parent or DefaultSentinel()
401
402   # TODO(machenbach): Implement notion of cpu type?
403   if arch not in suite.get("archs", ["ia32", "x64"]):
404     return None
405
406   graph = MakeGraph(suite, arch, parent)
407   for subsuite in suite.get("tests", []):
408     BuildGraphs(subsuite, arch, graph)
409   parent.AppendChild(graph)
410   return graph
411
412
413 def FlattenRunnables(node):
414   """Generator that traverses the tree structure and iterates over all
415   runnables.
416   """
417   if isinstance(node, Runnable):
418     yield node
419   elif isinstance(node, Node):
420     for child in node._children:
421       for result in FlattenRunnables(child):
422         yield result
423   else:  # pragma: no cover
424     raise Exception("Invalid suite configuration.")
425
426
427 # TODO: Implement results_processor.
428 def Main(args):
429   parser = optparse.OptionParser()
430   parser.add_option("--arch",
431                     help=("The architecture to run tests for, "
432                           "'auto' or 'native' for auto-detect"),
433                     default="x64")
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",
440                     default="out")
441   (options, args) = parser.parse_args(args)
442
443   if len(args) == 0:  # pragma: no cover
444     parser.print_help()
445     return 1
446
447   if options.arch in ["auto", "native"]:  # pragma: no cover
448     options.arch = ARCH_GUESS
449
450   if not options.arch in SUPPORTED_ARCHS:  # pragma: no cover
451     print "Unknown architecture %s" % options.arch
452     return 1
453
454   workspace = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
455
456   if options.buildbot:
457     shell_dir = os.path.join(workspace, options.outdir, "Release")
458   else:
459     shell_dir = os.path.join(workspace, options.outdir,
460                              "%s.release" % options.arch)
461
462   results = Results()
463   for path in args:
464     path = os.path.abspath(path)
465
466     if not os.path.exists(path):  # pragma: no cover
467       results.errors.append("Configuration file %s does not exist." % path)
468       continue
469
470     with open(path) as f:
471       suite = json.loads(f.read())
472
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])
475
476     for runnable in FlattenRunnables(BuildGraphs(suite, options.arch)):
477       print ">>> Running suite: %s" % "/".join(runnable.graphs)
478       runnable.ChangeCWD(path)
479
480       def Runner():
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
484           # arch.
485           output = commands.Execute(runnable.GetCommand(shell_dir),
486                                     timeout=runnable.timeout)
487           print ">>> Stdout (#%d):" % (i + 1)
488           print output.stdout
489           if output.stderr:  # pragma: no cover
490             # Print stderr for debugging.
491             print ">>> Stderr (#%d):" % (i + 1)
492             print output.stderr
493           if output.timed_out:
494             print ">>> Test timed out after %ss." % runnable.timeout
495           yield output.stdout
496
497       # Let runnable iterate over all runs and handle output.
498       results += runnable.Run(Runner)
499
500   if options.json_test_results:
501     results.WriteToFile(options.json_test_results)
502   else:  # pragma: no cover
503     print results
504
505   return min(1, len(results.errors))
506
507 if __name__ == "__main__":  # pragma: no cover
508   sys.exit(Main(sys.argv[1:]))