63c9148515c9380604bc2ee973fe3f0ddb27078e
[platform/upstream/nodejs.git] / deps / 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   "test_flags": [<flag to the test file>, ...],
19   "run_count": <how often will this suite run (optional)>,
20   "run_count_XXX": <how often will this suite run for arch XXX (optional)>,
21   "resources": [<js file to be moved to android device>, ...]
22   "main": <main js perf runner file>,
23   "results_regexp": <optional regexp>,
24   "results_processor": <optional python results processor script>,
25   "units": <the unit specification for the performance dashboard>,
26   "tests": [
27     {
28       "name": <name of the trace>,
29       "results_regexp": <optional more specific regexp>,
30       "results_processor": <optional python results processor script>,
31       "units": <the unit specification for the performance dashboard>,
32     }, ...
33   ]
34 }
35
36 The tests field can also nest other suites in arbitrary depth. A suite
37 with a "main" file is a leaf suite that can contain one more level of
38 tests.
39
40 A suite's results_regexp is expected to have one string place holder
41 "%s" for the trace name. A trace's results_regexp overwrites suite
42 defaults.
43
44 A suite's results_processor may point to an optional python script. If
45 specified, it is called after running the tests like this (with a path
46 relatve to the suite level's path):
47 <results_processor file> <same flags as for d8> <suite level name> <output>
48
49 The <output> is a temporary file containing d8 output. The results_regexp will
50 be applied to the output of this script.
51
52 A suite without "tests" is considered a performance test itself.
53
54 Full example (suite with one runner):
55 {
56   "path": ["."],
57   "flags": ["--expose-gc"],
58   "test_flags": ["5"],
59   "archs": ["ia32", "x64"],
60   "run_count": 5,
61   "run_count_ia32": 3,
62   "main": "run.js",
63   "results_regexp": "^%s: (.+)$",
64   "units": "score",
65   "tests": [
66     {"name": "Richards"},
67     {"name": "DeltaBlue"},
68     {"name": "NavierStokes",
69      "results_regexp": "^NavierStokes: (.+)$"}
70   ]
71 }
72
73 Full example (suite with several runners):
74 {
75   "path": ["."],
76   "flags": ["--expose-gc"],
77   "archs": ["ia32", "x64"],
78   "run_count": 5,
79   "units": "score",
80   "tests": [
81     {"name": "Richards",
82      "path": ["richards"],
83      "main": "run.js",
84      "run_count": 3,
85      "results_regexp": "^Richards: (.+)$"},
86     {"name": "NavierStokes",
87      "path": ["navier_stokes"],
88      "main": "run.js",
89      "results_regexp": "^NavierStokes: (.+)$"}
90   ]
91 }
92
93 Path pieces are concatenated. D8 is always run with the suite's path as cwd.
94
95 The test flags are passed to the js test file after '--'.
96 """
97
98 from collections import OrderedDict
99 import json
100 import logging
101 import math
102 import optparse
103 import os
104 import re
105 import sys
106
107 from testrunner.local import commands
108 from testrunner.local import utils
109
110 ARCH_GUESS = utils.DefaultArch()
111 SUPPORTED_ARCHS = ["android_arm",
112                    "android_arm64",
113                    "android_ia32",
114                    "arm",
115                    "ia32",
116                    "mips",
117                    "mipsel",
118                    "nacl_ia32",
119                    "nacl_x64",
120                    "x64",
121                    "arm64"]
122
123 GENERIC_RESULTS_RE = re.compile(r"^RESULT ([^:]+): ([^=]+)= ([^ ]+) ([^ ]*)$")
124 RESULT_STDDEV_RE = re.compile(r"^\{([^\}]+)\}$")
125 RESULT_LIST_RE = re.compile(r"^\[([^\]]+)\]$")
126
127
128 def LoadAndroidBuildTools(path):  # pragma: no cover
129   assert os.path.exists(path)
130   sys.path.insert(0, path)
131
132   from pylib.device import device_utils  # pylint: disable=F0401
133   from pylib.device import device_errors  # pylint: disable=F0401
134   from pylib.perf import cache_control  # pylint: disable=F0401
135   from pylib.perf import perf_control  # pylint: disable=F0401
136   import pylib.android_commands  # pylint: disable=F0401
137   global cache_control
138   global device_errors
139   global device_utils
140   global perf_control
141   global pylib
142
143
144 def GeometricMean(values):
145   """Returns the geometric mean of a list of values.
146
147   The mean is calculated using log to avoid overflow.
148   """
149   values = map(float, values)
150   return str(math.exp(sum(map(math.log, values)) / len(values)))
151
152
153 class Results(object):
154   """Place holder for result traces."""
155   def __init__(self, traces=None, errors=None):
156     self.traces = traces or []
157     self.errors = errors or []
158
159   def ToDict(self):
160     return {"traces": self.traces, "errors": self.errors}
161
162   def WriteToFile(self, file_name):
163     with open(file_name, "w") as f:
164       f.write(json.dumps(self.ToDict()))
165
166   def __add__(self, other):
167     self.traces += other.traces
168     self.errors += other.errors
169     return self
170
171   def __str__(self):  # pragma: no cover
172     return str(self.ToDict())
173
174
175 class Node(object):
176   """Represents a node in the suite tree structure."""
177   def __init__(self, *args):
178     self._children = []
179
180   def AppendChild(self, child):
181     self._children.append(child)
182
183
184 class DefaultSentinel(Node):
185   """Fake parent node with all default values."""
186   def __init__(self):
187     super(DefaultSentinel, self).__init__()
188     self.binary = "d8"
189     self.run_count = 10
190     self.timeout = 60
191     self.path = []
192     self.graphs = []
193     self.flags = []
194     self.test_flags = []
195     self.resources = []
196     self.results_regexp = None
197     self.stddev_regexp = None
198     self.units = "score"
199     self.total = False
200
201
202 class Graph(Node):
203   """Represents a suite definition.
204
205   Can either be a leaf or an inner node that provides default values.
206   """
207   def __init__(self, suite, parent, arch):
208     super(Graph, self).__init__()
209     self._suite = suite
210
211     assert isinstance(suite.get("path", []), list)
212     assert isinstance(suite["name"], basestring)
213     assert isinstance(suite.get("flags", []), list)
214     assert isinstance(suite.get("test_flags", []), list)
215     assert isinstance(suite.get("resources", []), list)
216
217     # Accumulated values.
218     self.path = parent.path[:] + suite.get("path", [])
219     self.graphs = parent.graphs[:] + [suite["name"]]
220     self.flags = parent.flags[:] + suite.get("flags", [])
221     self.test_flags = parent.test_flags[:] + suite.get("test_flags", [])
222
223     # Values independent of parent node.
224     self.resources = suite.get("resources", [])
225
226     # Descrete values (with parent defaults).
227     self.binary = suite.get("binary", parent.binary)
228     self.run_count = suite.get("run_count", parent.run_count)
229     self.run_count = suite.get("run_count_%s" % arch, self.run_count)
230     self.timeout = suite.get("timeout", parent.timeout)
231     self.timeout = suite.get("timeout_%s" % arch, self.timeout)
232     self.units = suite.get("units", parent.units)
233     self.total = suite.get("total", parent.total)
234
235     # A regular expression for results. If the parent graph provides a
236     # regexp and the current suite has none, a string place holder for the
237     # suite name is expected.
238     # TODO(machenbach): Currently that makes only sense for the leaf level.
239     # Multiple place holders for multiple levels are not supported.
240     if parent.results_regexp:
241       regexp_default = parent.results_regexp % re.escape(suite["name"])
242     else:
243       regexp_default = None
244     self.results_regexp = suite.get("results_regexp", regexp_default)
245
246     # A similar regular expression for the standard deviation (optional).
247     if parent.stddev_regexp:
248       stddev_default = parent.stddev_regexp % re.escape(suite["name"])
249     else:
250       stddev_default = None
251     self.stddev_regexp = suite.get("stddev_regexp", stddev_default)
252
253
254 class Trace(Graph):
255   """Represents a leaf in the suite tree structure.
256
257   Handles collection of measurements.
258   """
259   def __init__(self, suite, parent, arch):
260     super(Trace, self).__init__(suite, parent, arch)
261     assert self.results_regexp
262     self.results = []
263     self.errors = []
264     self.stddev = ""
265
266   def ConsumeOutput(self, stdout):
267     try:
268       result = re.search(self.results_regexp, stdout, re.M).group(1)
269       self.results.append(str(float(result)))
270     except ValueError:
271       self.errors.append("Regexp \"%s\" returned a non-numeric for test %s."
272                          % (self.results_regexp, self.graphs[-1]))
273     except:
274       self.errors.append("Regexp \"%s\" didn't match for test %s."
275                          % (self.results_regexp, self.graphs[-1]))
276
277     try:
278       if self.stddev_regexp and self.stddev:
279         self.errors.append("Test %s should only run once since a stddev "
280                            "is provided by the test." % self.graphs[-1])
281       if self.stddev_regexp:
282         self.stddev = re.search(self.stddev_regexp, stdout, re.M).group(1)
283     except:
284       self.errors.append("Regexp \"%s\" didn't match for test %s."
285                          % (self.stddev_regexp, self.graphs[-1]))
286
287   def GetResults(self):
288     return Results([{
289       "graphs": self.graphs,
290       "units": self.units,
291       "results": self.results,
292       "stddev": self.stddev,
293     }], self.errors)
294
295
296 class Runnable(Graph):
297   """Represents a runnable suite definition (i.e. has a main file).
298   """
299   @property
300   def main(self):
301     return self._suite.get("main", "")
302
303   def ChangeCWD(self, suite_path):
304     """Changes the cwd to to path defined in the current graph.
305
306     The tests are supposed to be relative to the suite configuration.
307     """
308     suite_dir = os.path.abspath(os.path.dirname(suite_path))
309     bench_dir = os.path.normpath(os.path.join(*self.path))
310     os.chdir(os.path.join(suite_dir, bench_dir))
311
312   def GetCommandFlags(self):
313     suffix = ["--"] + self.test_flags if self.test_flags else []
314     return self.flags + [self.main] + suffix
315
316   def GetCommand(self, shell_dir):
317     # TODO(machenbach): This requires +.exe if run on windows.
318     return [os.path.join(shell_dir, self.binary)] + self.GetCommandFlags()
319
320   def Run(self, runner):
321     """Iterates over several runs and handles the output for all traces."""
322     for stdout in runner():
323       for trace in self._children:
324         trace.ConsumeOutput(stdout)
325     res = reduce(lambda r, t: r + t.GetResults(), self._children, Results())
326
327     if not res.traces or not self.total:
328       return res
329
330     # Assume all traces have the same structure.
331     if len(set(map(lambda t: len(t["results"]), res.traces))) != 1:
332       res.errors.append("Not all traces have the same number of results.")
333       return res
334
335     # Calculate the geometric means for all traces. Above we made sure that
336     # there is at least one trace and that the number of results is the same
337     # for each trace.
338     n_results = len(res.traces[0]["results"])
339     total_results = [GeometricMean(t["results"][i] for t in res.traces)
340                      for i in range(0, n_results)]
341     res.traces.append({
342       "graphs": self.graphs + ["Total"],
343       "units": res.traces[0]["units"],
344       "results": total_results,
345       "stddev": "",
346     })
347     return res
348
349 class RunnableTrace(Trace, Runnable):
350   """Represents a runnable suite definition that is a leaf."""
351   def __init__(self, suite, parent, arch):
352     super(RunnableTrace, self).__init__(suite, parent, arch)
353
354   def Run(self, runner):
355     """Iterates over several runs and handles the output."""
356     for stdout in runner():
357       self.ConsumeOutput(stdout)
358     return self.GetResults()
359
360
361 class RunnableGeneric(Runnable):
362   """Represents a runnable suite definition with generic traces."""
363   def __init__(self, suite, parent, arch):
364     super(RunnableGeneric, self).__init__(suite, parent, arch)
365
366   def Run(self, runner):
367     """Iterates over several runs and handles the output."""
368     traces = OrderedDict()
369     for stdout in runner():
370       for line in stdout.strip().splitlines():
371         match = GENERIC_RESULTS_RE.match(line)
372         if match:
373           stddev = ""
374           graph = match.group(1)
375           trace = match.group(2)
376           body = match.group(3)
377           units = match.group(4)
378           match_stddev = RESULT_STDDEV_RE.match(body)
379           match_list = RESULT_LIST_RE.match(body)
380           errors = []
381           if match_stddev:
382             result, stddev = map(str.strip, match_stddev.group(1).split(","))
383             results = [result]
384           elif match_list:
385             results = map(str.strip, match_list.group(1).split(","))
386           else:
387             results = [body.strip()]
388
389           try:
390             results = map(lambda r: str(float(r)), results)
391           except ValueError:
392             results = []
393             errors = ["Found non-numeric in %s" %
394                       "/".join(self.graphs + [graph, trace])]
395
396           trace_result = traces.setdefault(trace, Results([{
397             "graphs": self.graphs + [graph, trace],
398             "units": (units or self.units).strip(),
399             "results": [],
400             "stddev": "",
401           }], errors))
402           trace_result.traces[0]["results"].extend(results)
403           trace_result.traces[0]["stddev"] = stddev
404
405     return reduce(lambda r, t: r + t, traces.itervalues(), Results())
406
407
408 def MakeGraph(suite, arch, parent):
409   """Factory method for making graph objects."""
410   if isinstance(parent, Runnable):
411     # Below a runnable can only be traces.
412     return Trace(suite, parent, arch)
413   elif suite.get("main"):
414     # A main file makes this graph runnable.
415     if suite.get("tests"):
416       # This graph has subgraphs (traces).
417       return Runnable(suite, parent, arch)
418     else:
419       # This graph has no subgraphs, it's a leaf.
420       return RunnableTrace(suite, parent, arch)
421   elif suite.get("generic"):
422     # This is a generic suite definition. It is either a runnable executable
423     # or has a main js file.
424     return RunnableGeneric(suite, parent, arch)
425   elif suite.get("tests"):
426     # This is neither a leaf nor a runnable.
427     return Graph(suite, parent, arch)
428   else:  # pragma: no cover
429     raise Exception("Invalid suite configuration.")
430
431
432 def BuildGraphs(suite, arch, parent=None):
433   """Builds a tree structure of graph objects that corresponds to the suite
434   configuration.
435   """
436   parent = parent or DefaultSentinel()
437
438   # TODO(machenbach): Implement notion of cpu type?
439   if arch not in suite.get("archs", SUPPORTED_ARCHS):
440     return None
441
442   graph = MakeGraph(suite, arch, parent)
443   for subsuite in suite.get("tests", []):
444     BuildGraphs(subsuite, arch, graph)
445   parent.AppendChild(graph)
446   return graph
447
448
449 def FlattenRunnables(node, node_cb):
450   """Generator that traverses the tree structure and iterates over all
451   runnables.
452   """
453   node_cb(node)
454   if isinstance(node, Runnable):
455     yield node
456   elif isinstance(node, Node):
457     for child in node._children:
458       for result in FlattenRunnables(child, node_cb):
459         yield result
460   else:  # pragma: no cover
461     raise Exception("Invalid suite configuration.")
462
463
464 class Platform(object):
465   @staticmethod
466   def GetPlatform(options):
467     if options.arch.startswith("android"):
468       return AndroidPlatform(options)
469     else:
470       return DesktopPlatform(options)
471
472
473 class DesktopPlatform(Platform):
474   def __init__(self, options):
475     self.shell_dir = options.shell_dir
476
477   def PreExecution(self):
478     pass
479
480   def PostExecution(self):
481     pass
482
483   def PreTests(self, node, path):
484     if isinstance(node, Runnable):
485       node.ChangeCWD(path)
486
487   def Run(self, runnable, count):
488     output = commands.Execute(runnable.GetCommand(self.shell_dir),
489                               timeout=runnable.timeout)
490     print ">>> Stdout (#%d):" % (count + 1)
491     print output.stdout
492     if output.stderr:  # pragma: no cover
493       # Print stderr for debugging.
494       print ">>> Stderr (#%d):" % (count + 1)
495       print output.stderr
496     if output.timed_out:
497       print ">>> Test timed out after %ss." % runnable.timeout
498     return output.stdout
499
500
501 class AndroidPlatform(Platform):  # pragma: no cover
502   DEVICE_DIR = "/data/local/tmp/v8/"
503
504   def __init__(self, options):
505     self.shell_dir = options.shell_dir
506     LoadAndroidBuildTools(options.android_build_tools)
507
508     if not options.device:
509       # Detect attached device if not specified.
510       devices = pylib.android_commands.GetAttachedDevices(
511           hardware=True, emulator=False, offline=False)
512       assert devices and len(devices) == 1, (
513           "None or multiple devices detected. Please specify the device on "
514           "the command-line with --device")
515       options.device = devices[0]
516     adb_wrapper = pylib.android_commands.AndroidCommands(options.device)
517     self.device = device_utils.DeviceUtils(adb_wrapper)
518     self.adb = adb_wrapper.Adb()
519
520   def PreExecution(self):
521     perf = perf_control.PerfControl(self.device)
522     perf.SetHighPerfMode()
523
524     # Remember what we have already pushed to the device.
525     self.pushed = set()
526
527   def PostExecution(self):
528     perf = perf_control.PerfControl(self.device)
529     perf.SetDefaultPerfMode()
530     self.device.RunShellCommand(["rm", "-rf", AndroidPlatform.DEVICE_DIR])
531
532   def _SendCommand(self, cmd):
533     logging.info("adb -s %s %s" % (str(self.device), cmd))
534     return self.adb.SendCommand(cmd, timeout_time=60)
535
536   def _PushFile(self, host_dir, file_name, target_rel="."):
537     file_on_host = os.path.join(host_dir, file_name)
538     file_on_device_tmp = os.path.join(
539         AndroidPlatform.DEVICE_DIR, "_tmp_", file_name)
540     file_on_device = os.path.join(
541         AndroidPlatform.DEVICE_DIR, target_rel, file_name)
542     folder_on_device = os.path.dirname(file_on_device)
543
544     # Only push files not yet pushed in one execution.
545     if file_on_host in self.pushed:
546       return
547     else:
548       self.pushed.add(file_on_host)
549
550     # Work-around for "text file busy" errors. Push the files to a temporary
551     # location and then copy them with a shell command.
552     output = self._SendCommand(
553         "push %s %s" % (file_on_host, file_on_device_tmp))
554     # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)".
555     # Errors look like this: "failed to copy  ... ".
556     if output and not re.search('^[0-9]', output.splitlines()[-1]):
557       logging.critical('PUSH FAILED: ' + output)
558     self._SendCommand("shell mkdir -p %s" % folder_on_device)
559     self._SendCommand("shell cp %s %s" % (file_on_device_tmp, file_on_device))
560
561   def PreTests(self, node, path):
562     suite_dir = os.path.abspath(os.path.dirname(path))
563     if node.path:
564       bench_rel = os.path.normpath(os.path.join(*node.path))
565       bench_abs = os.path.join(suite_dir, bench_rel)
566     else:
567       bench_rel = "."
568       bench_abs = suite_dir
569
570     self._PushFile(self.shell_dir, node.binary)
571     if isinstance(node, Runnable):
572       self._PushFile(bench_abs, node.main, bench_rel)
573     for resource in node.resources:
574       self._PushFile(bench_abs, resource, bench_rel)
575
576   def Run(self, runnable, count):
577     cache = cache_control.CacheControl(self.device)
578     cache.DropRamCaches()
579     binary_on_device = AndroidPlatform.DEVICE_DIR + runnable.binary
580     cmd = [binary_on_device] + runnable.GetCommandFlags()
581
582     # Relative path to benchmark directory.
583     if runnable.path:
584       bench_rel = os.path.normpath(os.path.join(*runnable.path))
585     else:
586       bench_rel = "."
587
588     try:
589       output = self.device.RunShellCommand(
590           cmd,
591           cwd=os.path.join(AndroidPlatform.DEVICE_DIR, bench_rel),
592           timeout=runnable.timeout,
593           retries=0,
594       )
595       stdout = "\n".join(output)
596       print ">>> Stdout (#%d):" % (count + 1)
597       print stdout
598     except device_errors.CommandTimeoutError:
599       print ">>> Test timed out after %ss." % runnable.timeout
600       stdout = ""
601     return stdout
602
603
604 # TODO: Implement results_processor.
605 def Main(args):
606   logging.getLogger().setLevel(logging.INFO)
607   parser = optparse.OptionParser()
608   parser.add_option("--android-build-tools",
609                     help="Path to chromium's build/android.")
610   parser.add_option("--arch",
611                     help=("The architecture to run tests for, "
612                           "'auto' or 'native' for auto-detect"),
613                     default="x64")
614   parser.add_option("--buildbot",
615                     help="Adapt to path structure used on buildbots",
616                     default=False, action="store_true")
617   parser.add_option("--device",
618                     help="The device ID to run Android tests on. If not given "
619                          "it will be autodetected.")
620   parser.add_option("--json-test-results",
621                     help="Path to a file for storing json results.")
622   parser.add_option("--outdir", help="Base directory with compile output",
623                     default="out")
624   (options, args) = parser.parse_args(args)
625
626   if len(args) == 0:  # pragma: no cover
627     parser.print_help()
628     return 1
629
630   if options.arch in ["auto", "native"]:  # pragma: no cover
631     options.arch = ARCH_GUESS
632
633   if not options.arch in SUPPORTED_ARCHS:  # pragma: no cover
634     print "Unknown architecture %s" % options.arch
635     return 1
636
637   if (bool(options.arch.startswith("android")) !=
638       bool(options.android_build_tools)):  # pragma: no cover
639     print ("Android architectures imply setting --android-build-tools and the "
640            "other way around.")
641     return 1
642
643   if (options.device and not
644       options.arch.startswith("android")):  # pragma: no cover
645     print "Specifying a device requires an Android architecture to be used."
646     return 1
647
648   workspace = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
649
650   if options.buildbot:
651     options.shell_dir = os.path.join(workspace, options.outdir, "Release")
652   else:
653     options.shell_dir = os.path.join(workspace, options.outdir,
654                                      "%s.release" % options.arch)
655
656   platform = Platform.GetPlatform(options)
657
658   results = Results()
659   for path in args:
660     path = os.path.abspath(path)
661
662     if not os.path.exists(path):  # pragma: no cover
663       results.errors.append("Configuration file %s does not exist." % path)
664       continue
665
666     with open(path) as f:
667       suite = json.loads(f.read())
668
669     # If no name is given, default to the file name without .json.
670     suite.setdefault("name", os.path.splitext(os.path.basename(path))[0])
671
672     # Setup things common to one test suite.
673     platform.PreExecution()
674
675     # Build the graph/trace tree structure.
676     root = BuildGraphs(suite, options.arch)
677
678     # Callback to be called on each node on traversal.
679     def NodeCB(node):
680       platform.PreTests(node, path)
681
682     # Traverse graph/trace tree and interate over all runnables.
683     for runnable in FlattenRunnables(root, NodeCB):
684       print ">>> Running suite: %s" % "/".join(runnable.graphs)
685
686       def Runner():
687         """Output generator that reruns several times."""
688         for i in xrange(0, max(1, runnable.run_count)):
689           # TODO(machenbach): Allow timeout per arch like with run_count per
690           # arch.
691           yield platform.Run(runnable, i)
692
693       # Let runnable iterate over all runs and handle output.
694       results += runnable.Run(Runner)
695
696     platform.PostExecution()
697
698   if options.json_test_results:
699     results.WriteToFile(options.json_test_results)
700   else:  # pragma: no cover
701     print results
702
703   return min(1, len(results.errors))
704
705 if __name__ == "__main__":  # pragma: no cover
706   sys.exit(Main(sys.argv[1:]))