1a07025f07d61a9890bfd146625e982fb8f340f0
[platform/framework/web/crosswalk.git] / src / v8 / tools / run_benchmarks.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-benchmarks.py --arch ia32 some_suite.json
10
11 The suite json format is expected to be:
12 {
13   "path": <relative path chunks to benchmark 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 benchmark 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   "benchmarks": [
26     {
27       "name": <name of the benchmark>,
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 benchmarks 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 benchmarks.
38
39 A suite's results_regexp is expected to have one string place holder
40 "%s" for the benchmark name. A benchmark'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 benchmarks 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 "benchmarks" is considered a benchmark 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   "benchmarks": [
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   "benchmarks": [
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 import json
95 import optparse
96 import os
97 import re
98 import sys
99
100 from testrunner.local import commands
101 from testrunner.local import utils
102
103 ARCH_GUESS = utils.DefaultArch()
104 SUPPORTED_ARCHS = ["android_arm",
105                    "android_arm64",
106                    "android_ia32",
107                    "arm",
108                    "ia32",
109                    "mips",
110                    "mipsel",
111                    "nacl_ia32",
112                    "nacl_x64",
113                    "x64",
114                    "arm64"]
115
116
117 class Results(object):
118   """Place holder for result traces."""
119   def __init__(self, traces=None, errors=None):
120     self.traces = traces or []
121     self.errors = errors or []
122
123   def ToDict(self):
124     return {"traces": self.traces, "errors": self.errors}
125
126   def WriteToFile(self, file_name):
127     with open(file_name, "w") as f:
128       f.write(json.dumps(self.ToDict()))
129
130   def __add__(self, other):
131     self.traces += other.traces
132     self.errors += other.errors
133     return self
134
135   def __str__(self):  # pragma: no cover
136     return str(self.ToDict())
137
138
139 class Node(object):
140   """Represents a node in the benchmark suite tree structure."""
141   def __init__(self, *args):
142     self._children = []
143
144   def AppendChild(self, child):
145     self._children.append(child)
146
147
148 class DefaultSentinel(Node):
149   """Fake parent node with all default values."""
150   def __init__(self):
151     super(DefaultSentinel, self).__init__()
152     self.binary = "d8"
153     self.run_count = 10
154     self.path = []
155     self.graphs = []
156     self.flags = []
157     self.resources = []
158     self.results_regexp = None
159     self.units = "score"
160
161
162 class Graph(Node):
163   """Represents a benchmark suite definition.
164
165   Can either be a leaf or an inner node that provides default values.
166   """
167   def __init__(self, suite, parent, arch):
168     super(Graph, self).__init__()
169     self._suite = suite
170
171     assert isinstance(suite.get("path", []), list)
172     assert isinstance(suite["name"], basestring)
173     assert isinstance(suite.get("flags", []), list)
174     assert isinstance(suite.get("resources", []), list)
175
176     # Accumulated values.
177     self.path = parent.path[:] + suite.get("path", [])
178     self.graphs = parent.graphs[:] + [suite["name"]]
179     self.flags = parent.flags[:] + suite.get("flags", [])
180     self.resources = parent.resources[:] + suite.get("resources", [])
181
182     # Descrete values (with parent defaults).
183     self.binary = suite.get("binary", parent.binary)
184     self.run_count = suite.get("run_count", parent.run_count)
185     self.run_count = suite.get("run_count_%s" % arch, self.run_count)
186     self.units = suite.get("units", parent.units)
187
188     # A regular expression for results. If the parent graph provides a
189     # regexp and the current suite has none, a string place holder for the
190     # suite name is expected.
191     # TODO(machenbach): Currently that makes only sense for the leaf level.
192     # Multiple place holders for multiple levels are not supported.
193     if parent.results_regexp:
194       regexp_default = parent.results_regexp % suite["name"]
195     else:
196       regexp_default = None
197     self.results_regexp = suite.get("results_regexp", regexp_default)
198
199
200 class Trace(Graph):
201   """Represents a leaf in the benchmark suite tree structure.
202
203   Handles collection of measurements.
204   """
205   def __init__(self, suite, parent, arch):
206     super(Trace, self).__init__(suite, parent, arch)
207     assert self.results_regexp
208     self.results = []
209     self.errors = []
210
211   def ConsumeOutput(self, stdout):
212     try:
213       self.results.append(
214           re.search(self.results_regexp, stdout, re.M).group(1))
215     except:
216       self.errors.append("Regexp \"%s\" didn't match for benchmark %s."
217                          % (self.results_regexp, self.graphs[-1]))
218
219   def GetResults(self):
220     return Results([{
221       "graphs": self.graphs,
222       "units": self.units,
223       "results": self.results,
224     }], self.errors)
225
226
227 class Runnable(Graph):
228   """Represents a runnable benchmark suite definition (i.e. has a main file).
229   """
230   @property
231   def main(self):
232     return self._suite["main"]
233
234   def ChangeCWD(self, suite_path):
235     """Changes the cwd to to path defined in the current graph.
236
237     The benchmarks are supposed to be relative to the suite configuration.
238     """
239     suite_dir = os.path.abspath(os.path.dirname(suite_path))
240     bench_dir = os.path.normpath(os.path.join(*self.path))
241     os.chdir(os.path.join(suite_dir, bench_dir))
242
243   def GetCommand(self, shell_dir):
244     # TODO(machenbach): This requires +.exe if run on windows.
245     return (
246       [os.path.join(shell_dir, self.binary)] +
247       self.flags +
248       self.resources +
249       [self.main]
250     )
251
252   def Run(self, runner):
253     """Iterates over several runs and handles the output for all traces."""
254     for stdout in runner():
255       for trace in self._children:
256         trace.ConsumeOutput(stdout)
257     return reduce(lambda r, t: r + t.GetResults(), self._children, Results())
258
259
260 class RunnableTrace(Trace, Runnable):
261   """Represents a runnable benchmark suite definition that is a leaf."""
262   def __init__(self, suite, parent, arch):
263     super(RunnableTrace, self).__init__(suite, parent, arch)
264
265   def Run(self, runner):
266     """Iterates over several runs and handles the output."""
267     for stdout in runner():
268       self.ConsumeOutput(stdout)
269     return self.GetResults()
270
271
272 def MakeGraph(suite, arch, parent):
273   """Factory method for making graph objects."""
274   if isinstance(parent, Runnable):
275     # Below a runnable can only be traces.
276     return Trace(suite, parent, arch)
277   elif suite.get("main"):
278     # A main file makes this graph runnable.
279     if suite.get("benchmarks"):
280       # This graph has subbenchmarks (traces).
281       return Runnable(suite, parent, arch)
282     else:
283       # This graph has no subbenchmarks, it's a leaf.
284       return RunnableTrace(suite, parent, arch)
285   elif suite.get("benchmarks"):
286     # This is neither a leaf nor a runnable.
287     return Graph(suite, parent, arch)
288   else:  # pragma: no cover
289     raise Exception("Invalid benchmark suite configuration.")
290
291
292 def BuildGraphs(suite, arch, parent=None):
293   """Builds a tree structure of graph objects that corresponds to the suite
294   configuration.
295   """
296   parent = parent or DefaultSentinel()
297
298   # TODO(machenbach): Implement notion of cpu type?
299   if arch not in suite.get("archs", ["ia32", "x64"]):
300     return None
301
302   graph = MakeGraph(suite, arch, parent)
303   for subsuite in suite.get("benchmarks", []):
304     BuildGraphs(subsuite, arch, graph)
305   parent.AppendChild(graph)
306   return graph
307
308
309 def FlattenRunnables(node):
310   """Generator that traverses the tree structure and iterates over all
311   runnables.
312   """
313   if isinstance(node, Runnable):
314     yield node
315   elif isinstance(node, Node):
316     for child in node._children:
317       for result in FlattenRunnables(child):
318         yield result
319   else:  # pragma: no cover
320     raise Exception("Invalid benchmark suite configuration.")
321
322
323 # TODO: Implement results_processor.
324 def Main(args):
325   parser = optparse.OptionParser()
326   parser.add_option("--arch",
327                     help=("The architecture to run tests for, "
328                           "'auto' or 'native' for auto-detect"),
329                     default="x64")
330   parser.add_option("--buildbot",
331                     help="Adapt to path structure used on buildbots",
332                     default=False, action="store_true")
333   parser.add_option("--json-test-results",
334                     help="Path to a file for storing json results.")
335   parser.add_option("--outdir", help="Base directory with compile output",
336                     default="out")
337   (options, args) = parser.parse_args(args)
338
339   if len(args) == 0:  # pragma: no cover
340     parser.print_help()
341     return 1
342
343   if options.arch in ["auto", "native"]:  # pragma: no cover
344     options.arch = ARCH_GUESS
345
346   if not options.arch in SUPPORTED_ARCHS:  # pragma: no cover
347     print "Unknown architecture %s" % options.arch
348     return 1
349
350   workspace = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
351
352   if options.buildbot:
353     shell_dir = os.path.join(workspace, options.outdir, "Release")
354   else:
355     shell_dir = os.path.join(workspace, options.outdir,
356                              "%s.release" % options.arch)
357
358   results = Results()
359   for path in args:
360     path = os.path.abspath(path)
361
362     if not os.path.exists(path):  # pragma: no cover
363       results.errors.append("Benchmark file %s does not exist." % path)
364       continue
365
366     with open(path) as f:
367       suite = json.loads(f.read())
368
369     # If no name is given, default to the file name without .json.
370     suite.setdefault("name", os.path.splitext(os.path.basename(path))[0])
371
372     for runnable in FlattenRunnables(BuildGraphs(suite, options.arch)):
373       print ">>> Running suite: %s" % "/".join(runnable.graphs)
374       runnable.ChangeCWD(path)
375
376       def Runner():
377         """Output generator that reruns several times."""
378         for i in xrange(0, max(1, runnable.run_count)):
379           # TODO(machenbach): Make timeout configurable in the suite definition.
380           # Allow timeout per arch like with run_count per arch.
381           output = commands.Execute(runnable.GetCommand(shell_dir), timeout=60)
382           print ">>> Stdout (#%d):" % (i + 1)
383           print output.stdout
384           if output.stderr:  # pragma: no cover
385             # Print stderr for debugging.
386             print ">>> Stderr (#%d):" % (i + 1)
387             print output.stderr
388           yield output.stdout
389
390       # Let runnable iterate over all runs and handle output.
391       results += runnable.Run(Runner)
392
393   if options.json_test_results:
394     results.WriteToFile(options.json_test_results)
395   else:  # pragma: no cover
396     print results
397
398   return min(1, len(results.errors))
399
400 if __name__ == "__main__":  # pragma: no cover
401   sys.exit(Main(sys.argv[1:]))