Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(278)

Side by Side Diff: tools/run_benchmarks.py

Issue 564373003: Remove benchmark runner. (Closed) Base URL: https://v8.googlecode.com/svn/branches/bleeding_edge
Patch Set: Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | tools/unittests/run_benchmarks_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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 math
96 import optparse
97 import os
98 import re
99 import sys
100
101 from testrunner.local import commands
102 from testrunner.local import utils
103
104 ARCH_GUESS = utils.DefaultArch()
105 SUPPORTED_ARCHS = ["android_arm",
106 "android_arm64",
107 "android_ia32",
108 "arm",
109 "ia32",
110 "mips",
111 "mipsel",
112 "nacl_ia32",
113 "nacl_x64",
114 "x64",
115 "arm64"]
116
117 GENERIC_RESULTS_RE = re.compile(
118 r"^Trace\(([^\)]+)\), Result\(([^\)]+)\), StdDev\(([^\)]+)\)$")
119
120
121 def GeometricMean(values):
122 """Returns the geometric mean of a list of values.
123
124 The mean is calculated using log to avoid overflow.
125 """
126 values = map(float, values)
127 return str(math.exp(sum(map(math.log, values)) / len(values)))
128
129
130 class Results(object):
131 """Place holder for result traces."""
132 def __init__(self, traces=None, errors=None):
133 self.traces = traces or []
134 self.errors = errors or []
135
136 def ToDict(self):
137 return {"traces": self.traces, "errors": self.errors}
138
139 def WriteToFile(self, file_name):
140 with open(file_name, "w") as f:
141 f.write(json.dumps(self.ToDict()))
142
143 def __add__(self, other):
144 self.traces += other.traces
145 self.errors += other.errors
146 return self
147
148 def __str__(self): # pragma: no cover
149 return str(self.ToDict())
150
151
152 class Node(object):
153 """Represents a node in the benchmark suite tree structure."""
154 def __init__(self, *args):
155 self._children = []
156
157 def AppendChild(self, child):
158 self._children.append(child)
159
160
161 class DefaultSentinel(Node):
162 """Fake parent node with all default values."""
163 def __init__(self):
164 super(DefaultSentinel, self).__init__()
165 self.binary = "d8"
166 self.run_count = 10
167 self.path = []
168 self.graphs = []
169 self.flags = []
170 self.resources = []
171 self.results_regexp = None
172 self.stddev_regexp = None
173 self.units = "score"
174 self.total = False
175
176
177 class Graph(Node):
178 """Represents a benchmark suite definition.
179
180 Can either be a leaf or an inner node that provides default values.
181 """
182 def __init__(self, suite, parent, arch):
183 super(Graph, self).__init__()
184 self._suite = suite
185
186 assert isinstance(suite.get("path", []), list)
187 assert isinstance(suite["name"], basestring)
188 assert isinstance(suite.get("flags", []), list)
189 assert isinstance(suite.get("resources", []), list)
190
191 # Accumulated values.
192 self.path = parent.path[:] + suite.get("path", [])
193 self.graphs = parent.graphs[:] + [suite["name"]]
194 self.flags = parent.flags[:] + suite.get("flags", [])
195 self.resources = parent.resources[:] + suite.get("resources", [])
196
197 # Descrete values (with parent defaults).
198 self.binary = suite.get("binary", parent.binary)
199 self.run_count = suite.get("run_count", parent.run_count)
200 self.run_count = suite.get("run_count_%s" % arch, self.run_count)
201 self.units = suite.get("units", parent.units)
202 self.total = suite.get("total", parent.total)
203
204 # A regular expression for results. If the parent graph provides a
205 # regexp and the current suite has none, a string place holder for the
206 # suite name is expected.
207 # TODO(machenbach): Currently that makes only sense for the leaf level.
208 # Multiple place holders for multiple levels are not supported.
209 if parent.results_regexp:
210 regexp_default = parent.results_regexp % re.escape(suite["name"])
211 else:
212 regexp_default = None
213 self.results_regexp = suite.get("results_regexp", regexp_default)
214
215 # A similar regular expression for the standard deviation (optional).
216 if parent.stddev_regexp:
217 stddev_default = parent.stddev_regexp % re.escape(suite["name"])
218 else:
219 stddev_default = None
220 self.stddev_regexp = suite.get("stddev_regexp", stddev_default)
221
222
223 class Trace(Graph):
224 """Represents a leaf in the benchmark suite tree structure.
225
226 Handles collection of measurements.
227 """
228 def __init__(self, suite, parent, arch):
229 super(Trace, self).__init__(suite, parent, arch)
230 assert self.results_regexp
231 self.results = []
232 self.errors = []
233 self.stddev = ""
234
235 def ConsumeOutput(self, stdout):
236 try:
237 self.results.append(
238 re.search(self.results_regexp, stdout, re.M).group(1))
239 except:
240 self.errors.append("Regexp \"%s\" didn't match for benchmark %s."
241 % (self.results_regexp, self.graphs[-1]))
242
243 try:
244 if self.stddev_regexp and self.stddev:
245 self.errors.append("Benchmark %s should only run once since a stddev "
246 "is provided by the benchmark." % self.graphs[-1])
247 if self.stddev_regexp:
248 self.stddev = re.search(self.stddev_regexp, stdout, re.M).group(1)
249 except:
250 self.errors.append("Regexp \"%s\" didn't match for benchmark %s."
251 % (self.stddev_regexp, self.graphs[-1]))
252
253 def GetResults(self):
254 return Results([{
255 "graphs": self.graphs,
256 "units": self.units,
257 "results": self.results,
258 "stddev": self.stddev,
259 }], self.errors)
260
261
262 class Runnable(Graph):
263 """Represents a runnable benchmark suite definition (i.e. has a main file).
264 """
265 @property
266 def main(self):
267 return self._suite.get("main", "")
268
269 def ChangeCWD(self, suite_path):
270 """Changes the cwd to to path defined in the current graph.
271
272 The benchmarks are supposed to be relative to the suite configuration.
273 """
274 suite_dir = os.path.abspath(os.path.dirname(suite_path))
275 bench_dir = os.path.normpath(os.path.join(*self.path))
276 os.chdir(os.path.join(suite_dir, bench_dir))
277
278 def GetCommand(self, shell_dir):
279 # TODO(machenbach): This requires +.exe if run on windows.
280 return (
281 [os.path.join(shell_dir, self.binary)] +
282 self.flags +
283 self.resources +
284 [self.main]
285 )
286
287 def Run(self, runner):
288 """Iterates over several runs and handles the output for all traces."""
289 for stdout in runner():
290 for trace in self._children:
291 trace.ConsumeOutput(stdout)
292 res = reduce(lambda r, t: r + t.GetResults(), self._children, Results())
293
294 if not res.traces or not self.total:
295 return res
296
297 # Assume all traces have the same structure.
298 if len(set(map(lambda t: len(t["results"]), res.traces))) != 1:
299 res.errors.append("Not all traces have the same number of results.")
300 return res
301
302 # Calculate the geometric means for all traces. Above we made sure that
303 # there is at least one trace and that the number of results is the same
304 # for each trace.
305 n_results = len(res.traces[0]["results"])
306 total_results = [GeometricMean(t["results"][i] for t in res.traces)
307 for i in range(0, n_results)]
308 res.traces.append({
309 "graphs": self.graphs + ["Total"],
310 "units": res.traces[0]["units"],
311 "results": total_results,
312 "stddev": "",
313 })
314 return res
315
316 class RunnableTrace(Trace, Runnable):
317 """Represents a runnable benchmark suite definition that is a leaf."""
318 def __init__(self, suite, parent, arch):
319 super(RunnableTrace, self).__init__(suite, parent, arch)
320
321 def Run(self, runner):
322 """Iterates over several runs and handles the output."""
323 for stdout in runner():
324 self.ConsumeOutput(stdout)
325 return self.GetResults()
326
327
328 class RunnableGeneric(Runnable):
329 """Represents a runnable benchmark suite definition with generic traces."""
330 def __init__(self, suite, parent, arch):
331 super(RunnableGeneric, self).__init__(suite, parent, arch)
332
333 def Run(self, runner):
334 """Iterates over several runs and handles the output."""
335 traces = {}
336 for stdout in runner():
337 for line in stdout.strip().splitlines():
338 match = GENERIC_RESULTS_RE.match(line)
339 if match:
340 trace = match.group(1)
341 result = match.group(2)
342 stddev = match.group(3)
343 trace_result = traces.setdefault(trace, Results([{
344 "graphs": self.graphs + [trace],
345 "units": self.units,
346 "results": [],
347 "stddev": "",
348 }], []))
349 trace_result.traces[0]["results"].append(result)
350 trace_result.traces[0]["stddev"] = stddev
351
352 return reduce(lambda r, t: r + t, traces.itervalues(), Results())
353
354
355 def MakeGraph(suite, arch, parent):
356 """Factory method for making graph objects."""
357 if isinstance(parent, Runnable):
358 # Below a runnable can only be traces.
359 return Trace(suite, parent, arch)
360 elif suite.get("main"):
361 # A main file makes this graph runnable.
362 if suite.get("benchmarks"):
363 # This graph has subbenchmarks (traces).
364 return Runnable(suite, parent, arch)
365 else:
366 # This graph has no subbenchmarks, it's a leaf.
367 return RunnableTrace(suite, parent, arch)
368 elif suite.get("generic"):
369 # This is a generic suite definition. It is either a runnable executable
370 # or has a main js file.
371 return RunnableGeneric(suite, parent, arch)
372 elif suite.get("benchmarks"):
373 # This is neither a leaf nor a runnable.
374 return Graph(suite, parent, arch)
375 else: # pragma: no cover
376 raise Exception("Invalid benchmark suite configuration.")
377
378
379 def BuildGraphs(suite, arch, parent=None):
380 """Builds a tree structure of graph objects that corresponds to the suite
381 configuration.
382 """
383 parent = parent or DefaultSentinel()
384
385 # TODO(machenbach): Implement notion of cpu type?
386 if arch not in suite.get("archs", ["ia32", "x64"]):
387 return None
388
389 graph = MakeGraph(suite, arch, parent)
390 for subsuite in suite.get("benchmarks", []):
391 BuildGraphs(subsuite, arch, graph)
392 parent.AppendChild(graph)
393 return graph
394
395
396 def FlattenRunnables(node):
397 """Generator that traverses the tree structure and iterates over all
398 runnables.
399 """
400 if isinstance(node, Runnable):
401 yield node
402 elif isinstance(node, Node):
403 for child in node._children:
404 for result in FlattenRunnables(child):
405 yield result
406 else: # pragma: no cover
407 raise Exception("Invalid benchmark suite configuration.")
408
409
410 # TODO: Implement results_processor.
411 def Main(args):
412 parser = optparse.OptionParser()
413 parser.add_option("--arch",
414 help=("The architecture to run tests for, "
415 "'auto' or 'native' for auto-detect"),
416 default="x64")
417 parser.add_option("--buildbot",
418 help="Adapt to path structure used on buildbots",
419 default=False, action="store_true")
420 parser.add_option("--json-test-results",
421 help="Path to a file for storing json results.")
422 parser.add_option("--outdir", help="Base directory with compile output",
423 default="out")
424 (options, args) = parser.parse_args(args)
425
426 if len(args) == 0: # pragma: no cover
427 parser.print_help()
428 return 1
429
430 if options.arch in ["auto", "native"]: # pragma: no cover
431 options.arch = ARCH_GUESS
432
433 if not options.arch in SUPPORTED_ARCHS: # pragma: no cover
434 print "Unknown architecture %s" % options.arch
435 return 1
436
437 workspace = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
438
439 if options.buildbot:
440 shell_dir = os.path.join(workspace, options.outdir, "Release")
441 else:
442 shell_dir = os.path.join(workspace, options.outdir,
443 "%s.release" % options.arch)
444
445 results = Results()
446 for path in args:
447 path = os.path.abspath(path)
448
449 if not os.path.exists(path): # pragma: no cover
450 results.errors.append("Benchmark file %s does not exist." % path)
451 continue
452
453 with open(path) as f:
454 suite = json.loads(f.read())
455
456 # If no name is given, default to the file name without .json.
457 suite.setdefault("name", os.path.splitext(os.path.basename(path))[0])
458
459 for runnable in FlattenRunnables(BuildGraphs(suite, options.arch)):
460 print ">>> Running suite: %s" % "/".join(runnable.graphs)
461 runnable.ChangeCWD(path)
462
463 def Runner():
464 """Output generator that reruns several times."""
465 for i in xrange(0, max(1, runnable.run_count)):
466 # TODO(machenbach): Make timeout configurable in the suite definition.
467 # Allow timeout per arch like with run_count per arch.
468 output = commands.Execute(runnable.GetCommand(shell_dir), timeout=60)
469 print ">>> Stdout (#%d):" % (i + 1)
470 print output.stdout
471 if output.stderr: # pragma: no cover
472 # Print stderr for debugging.
473 print ">>> Stderr (#%d):" % (i + 1)
474 print output.stderr
475 yield output.stdout
476
477 # Let runnable iterate over all runs and handle output.
478 results += runnable.Run(Runner)
479
480 if options.json_test_results:
481 results.WriteToFile(options.json_test_results)
482 else: # pragma: no cover
483 print results
484
485 return min(1, len(results.errors))
486
487 if __name__ == "__main__": # pragma: no cover
488 sys.exit(Main(sys.argv[1:]))
OLDNEW
« no previous file with comments | « no previous file | tools/unittests/run_benchmarks_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698