Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2014 the V8 project authors. All rights reserved. | 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 | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """ | 6 """ |
| 7 Performance runner for d8. | 7 Performance runner for d8. |
| 8 | 8 |
| 9 Call e.g. with tools/run-perf.py --arch ia32 some_suite.json | 9 Call e.g. with tools/run-perf.py --arch ia32 some_suite.json |
| 10 | 10 |
| (...skipping 151 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 162 | 162 |
| 163 def __add__(self, other): | 163 def __add__(self, other): |
| 164 self.traces += other.traces | 164 self.traces += other.traces |
| 165 self.errors += other.errors | 165 self.errors += other.errors |
| 166 return self | 166 return self |
| 167 | 167 |
| 168 def __str__(self): # pragma: no cover | 168 def __str__(self): # pragma: no cover |
| 169 return str(self.ToDict()) | 169 return str(self.ToDict()) |
| 170 | 170 |
| 171 | 171 |
| 172 class Measurement(object): | |
| 173 """Represents a series of results of one trace. | |
|
Michael Achenbach
2015/07/08 10:03:19
Basically copy from line 259.
| |
| 174 | |
| 175 The results are from repetitive runs of the same executable. They are | |
| 176 gathered by repeated calls to ConsumeOutput. | |
| 177 """ | |
| 178 def __init__(self, graphs, units, results_regexp, stddev_regexp): | |
| 179 self.name = graphs[-1] | |
| 180 self.graphs = graphs | |
| 181 self.units = units | |
| 182 self.results_regexp = results_regexp | |
| 183 self.stddev_regexp = stddev_regexp | |
| 184 self.results = [] | |
| 185 self.errors = [] | |
| 186 self.stddev = "" | |
| 187 | |
| 188 def ConsumeOutput(self, stdout): | |
| 189 try: | |
| 190 result = re.search(self.results_regexp, stdout, re.M).group(1) | |
| 191 self.results.append(str(float(result))) | |
| 192 except ValueError: | |
| 193 self.errors.append("Regexp \"%s\" returned a non-numeric for test %s." | |
| 194 % (self.results_regexp, self.name)) | |
| 195 except: | |
| 196 self.errors.append("Regexp \"%s\" didn't match for test %s." | |
| 197 % (self.results_regexp, self.name)) | |
| 198 | |
| 199 try: | |
| 200 if self.stddev_regexp and self.stddev: | |
| 201 self.errors.append("Test %s should only run once since a stddev " | |
| 202 "is provided by the test." % self.name) | |
| 203 if self.stddev_regexp: | |
| 204 self.stddev = re.search(self.stddev_regexp, stdout, re.M).group(1) | |
| 205 except: | |
| 206 self.errors.append("Regexp \"%s\" didn't match for test %s." | |
| 207 % (self.stddev_regexp, self.name)) | |
| 208 | |
| 209 def GetResults(self): | |
| 210 return Results([{ | |
| 211 "graphs": self.graphs, | |
| 212 "units": self.units, | |
| 213 "results": self.results, | |
| 214 "stddev": self.stddev, | |
| 215 }], self.errors) | |
| 216 | |
| 217 | |
| 218 def AccumulateResults(graph_names, traces, iter_output, calc_total): | |
| 219 """Iterates over the output of multiple benchmark reruns and accumulates | |
|
Michael Achenbach
2015/07/08 10:03:19
Basically copy from line 320.
| |
| 220 results for a configured list of traces. | |
| 221 | |
| 222 Args: | |
| 223 graph_names: List of names that configure the base path of the traces. E.g. | |
| 224 ['v8', 'Octane']. | |
| 225 traces: List of "Trace" instances. Each trace defines how to perform a | |
| 226 measurement. | |
| 227 iter_output: Iterator over the standard output of each test run. | |
| 228 calc_total: Boolean flag to speficy the calculation of a summary trace. | |
| 229 Returns: A "Results" object. | |
| 230 """ | |
| 231 measurements = [trace.CreateMeasurement() for trace in traces] | |
| 232 for stdout in iter_output(): | |
| 233 for measurement in measurements: | |
| 234 measurement.ConsumeOutput(stdout) | |
| 235 | |
| 236 res = reduce(lambda r, t: r + t.GetResults(), measurements, Results()) | |
| 237 | |
| 238 if not res.traces or not calc_total: | |
| 239 return res | |
| 240 | |
| 241 # Assume all traces have the same structure. | |
| 242 if len(set(map(lambda t: len(t["results"]), res.traces))) != 1: | |
| 243 res.errors.append("Not all traces have the same number of results.") | |
| 244 return res | |
| 245 | |
| 246 # Calculate the geometric means for all traces. Above we made sure that | |
| 247 # there is at least one trace and that the number of results is the same | |
| 248 # for each trace. | |
| 249 n_results = len(res.traces[0]["results"]) | |
| 250 total_results = [GeometricMean(t["results"][i] for t in res.traces) | |
| 251 for i in range(0, n_results)] | |
| 252 res.traces.append({ | |
| 253 "graphs": graph_names + ["Total"], | |
| 254 "units": res.traces[0]["units"], | |
| 255 "results": total_results, | |
| 256 "stddev": "", | |
| 257 }) | |
| 258 return res | |
| 259 | |
| 260 | |
| 261 def AccumulateGenericResults(graph_names, suite_units, iter_output): | |
| 262 """Iterates over the output of multiple benchmark reruns and accumulates | |
|
Michael Achenbach
2015/07/08 10:03:19
Basically copy from line 365.
| |
| 263 generic results. | |
| 264 | |
| 265 Args: | |
| 266 graph_names: List of names that configure the base path of the traces. E.g. | |
| 267 ['v8', 'Octane']. | |
| 268 suite_units: Measurement default units as defined by the benchmark suite. | |
| 269 iter_output: Iterator over the standard output of each test run. | |
| 270 Returns: A "Results" object. | |
| 271 """ | |
| 272 traces = OrderedDict() | |
| 273 for stdout in iter_output(): | |
| 274 for line in stdout.strip().splitlines(): | |
| 275 match = GENERIC_RESULTS_RE.match(line) | |
| 276 if match: | |
| 277 stddev = "" | |
| 278 graph = match.group(1) | |
| 279 trace = match.group(2) | |
| 280 body = match.group(3) | |
| 281 units = match.group(4) | |
| 282 match_stddev = RESULT_STDDEV_RE.match(body) | |
| 283 match_list = RESULT_LIST_RE.match(body) | |
| 284 errors = [] | |
| 285 if match_stddev: | |
| 286 result, stddev = map(str.strip, match_stddev.group(1).split(",")) | |
| 287 results = [result] | |
| 288 elif match_list: | |
| 289 results = map(str.strip, match_list.group(1).split(",")) | |
| 290 else: | |
| 291 results = [body.strip()] | |
| 292 | |
| 293 try: | |
| 294 results = map(lambda r: str(float(r)), results) | |
| 295 except ValueError: | |
| 296 results = [] | |
| 297 errors = ["Found non-numeric in %s" % | |
| 298 "/".join(graph_names + [graph, trace])] | |
| 299 | |
| 300 trace_result = traces.setdefault(trace, Results([{ | |
| 301 "graphs": graph_names + [graph, trace], | |
| 302 "units": (units or suite_units).strip(), | |
| 303 "results": [], | |
| 304 "stddev": "", | |
| 305 }], errors)) | |
| 306 trace_result.traces[0]["results"].extend(results) | |
| 307 trace_result.traces[0]["stddev"] = stddev | |
| 308 | |
| 309 return reduce(lambda r, t: r + t, traces.itervalues(), Results()) | |
| 310 | |
| 311 | |
| 172 class Node(object): | 312 class Node(object): |
| 173 """Represents a node in the suite tree structure.""" | 313 """Represents a node in the suite tree structure.""" |
| 174 def __init__(self, *args): | 314 def __init__(self, *args): |
| 175 self._children = [] | 315 self._children = [] |
| 176 | 316 |
| 177 def AppendChild(self, child): | 317 def AppendChild(self, child): |
| 178 self._children.append(child) | 318 self._children.append(child) |
| 179 | 319 |
| 180 | 320 |
| 181 class DefaultSentinel(Node): | 321 class DefaultSentinel(Node): |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 242 | 382 |
| 243 # A similar regular expression for the standard deviation (optional). | 383 # A similar regular expression for the standard deviation (optional). |
| 244 if parent.stddev_regexp: | 384 if parent.stddev_regexp: |
| 245 stddev_default = parent.stddev_regexp % re.escape(suite["name"]) | 385 stddev_default = parent.stddev_regexp % re.escape(suite["name"]) |
| 246 else: | 386 else: |
| 247 stddev_default = None | 387 stddev_default = None |
| 248 self.stddev_regexp = suite.get("stddev_regexp", stddev_default) | 388 self.stddev_regexp = suite.get("stddev_regexp", stddev_default) |
| 249 | 389 |
| 250 | 390 |
| 251 class Trace(Graph): | 391 class Trace(Graph): |
| 252 """Represents a leaf in the suite tree structure. | 392 """Represents a leaf in the suite tree structure.""" |
| 253 | |
| 254 Handles collection of measurements. | |
| 255 """ | |
| 256 def __init__(self, suite, parent, arch): | 393 def __init__(self, suite, parent, arch): |
| 257 super(Trace, self).__init__(suite, parent, arch) | 394 super(Trace, self).__init__(suite, parent, arch) |
| 258 assert self.results_regexp | 395 assert self.results_regexp |
| 259 self.results = [] | |
| 260 self.errors = [] | |
| 261 self.stddev = "" | |
| 262 | 396 |
| 263 def ConsumeOutput(self, stdout): | 397 def CreateMeasurement(self): |
| 264 try: | 398 return Measurement( |
| 265 result = re.search(self.results_regexp, stdout, re.M).group(1) | 399 self.graphs, |
| 266 self.results.append(str(float(result))) | 400 self.units, |
| 267 except ValueError: | 401 self.results_regexp, |
| 268 self.errors.append("Regexp \"%s\" returned a non-numeric for test %s." | 402 self.stddev_regexp, |
| 269 % (self.results_regexp, self.graphs[-1])) | 403 ) |
| 270 except: | |
| 271 self.errors.append("Regexp \"%s\" didn't match for test %s." | |
| 272 % (self.results_regexp, self.graphs[-1])) | |
| 273 | |
| 274 try: | |
| 275 if self.stddev_regexp and self.stddev: | |
| 276 self.errors.append("Test %s should only run once since a stddev " | |
| 277 "is provided by the test." % self.graphs[-1]) | |
| 278 if self.stddev_regexp: | |
| 279 self.stddev = re.search(self.stddev_regexp, stdout, re.M).group(1) | |
| 280 except: | |
| 281 self.errors.append("Regexp \"%s\" didn't match for test %s." | |
| 282 % (self.stddev_regexp, self.graphs[-1])) | |
| 283 | |
| 284 def GetResults(self): | |
| 285 return Results([{ | |
| 286 "graphs": self.graphs, | |
| 287 "units": self.units, | |
| 288 "results": self.results, | |
| 289 "stddev": self.stddev, | |
| 290 }], self.errors) | |
| 291 | 404 |
| 292 | 405 |
| 293 class Runnable(Graph): | 406 class Runnable(Graph): |
| 294 """Represents a runnable suite definition (i.e. has a main file). | 407 """Represents a runnable suite definition (i.e. has a main file). |
| 295 """ | 408 """ |
| 296 @property | 409 @property |
| 297 def main(self): | 410 def main(self): |
| 298 return self._suite.get("main", "") | 411 return self._suite.get("main", "") |
| 299 | 412 |
| 300 def ChangeCWD(self, suite_path): | 413 def ChangeCWD(self, suite_path): |
| 301 """Changes the cwd to to path defined in the current graph. | 414 """Changes the cwd to to path defined in the current graph. |
| 302 | 415 |
| 303 The tests are supposed to be relative to the suite configuration. | 416 The tests are supposed to be relative to the suite configuration. |
| 304 """ | 417 """ |
| 305 suite_dir = os.path.abspath(os.path.dirname(suite_path)) | 418 suite_dir = os.path.abspath(os.path.dirname(suite_path)) |
| 306 bench_dir = os.path.normpath(os.path.join(*self.path)) | 419 bench_dir = os.path.normpath(os.path.join(*self.path)) |
| 307 os.chdir(os.path.join(suite_dir, bench_dir)) | 420 os.chdir(os.path.join(suite_dir, bench_dir)) |
| 308 | 421 |
| 309 def GetCommandFlags(self, extra_flags=None): | 422 def GetCommandFlags(self, extra_flags=None): |
| 310 suffix = ["--"] + self.test_flags if self.test_flags else [] | 423 suffix = ["--"] + self.test_flags if self.test_flags else [] |
| 311 return self.flags + (extra_flags or []) + [self.main] + suffix | 424 return self.flags + (extra_flags or []) + [self.main] + suffix |
| 312 | 425 |
| 313 def GetCommand(self, shell_dir, extra_flags=None): | 426 def GetCommand(self, shell_dir, extra_flags=None): |
| 314 # TODO(machenbach): This requires +.exe if run on windows. | 427 # TODO(machenbach): This requires +.exe if run on windows. |
| 315 cmd = [os.path.join(shell_dir, self.binary)] | 428 cmd = [os.path.join(shell_dir, self.binary)] |
| 316 return cmd + self.GetCommandFlags(extra_flags=extra_flags) | 429 return cmd + self.GetCommandFlags(extra_flags=extra_flags) |
| 317 | 430 |
| 318 def Run(self, runner): | 431 def Run(self, runner): |
| 319 """Iterates over several runs and handles the output for all traces.""" | 432 """Iterates over several runs and handles the output for all traces.""" |
| 320 for stdout in runner(): | 433 return AccumulateResults(self.graphs, self._children, runner, self.total) |
| 321 for trace in self._children: | |
| 322 trace.ConsumeOutput(stdout) | |
| 323 res = reduce(lambda r, t: r + t.GetResults(), self._children, Results()) | |
| 324 | 434 |
| 325 if not res.traces or not self.total: | |
| 326 return res | |
| 327 | |
| 328 # Assume all traces have the same structure. | |
| 329 if len(set(map(lambda t: len(t["results"]), res.traces))) != 1: | |
| 330 res.errors.append("Not all traces have the same number of results.") | |
| 331 return res | |
| 332 | |
| 333 # Calculate the geometric means for all traces. Above we made sure that | |
| 334 # there is at least one trace and that the number of results is the same | |
| 335 # for each trace. | |
| 336 n_results = len(res.traces[0]["results"]) | |
| 337 total_results = [GeometricMean(t["results"][i] for t in res.traces) | |
| 338 for i in range(0, n_results)] | |
| 339 res.traces.append({ | |
| 340 "graphs": self.graphs + ["Total"], | |
| 341 "units": res.traces[0]["units"], | |
| 342 "results": total_results, | |
| 343 "stddev": "", | |
| 344 }) | |
| 345 return res | |
| 346 | 435 |
| 347 class RunnableTrace(Trace, Runnable): | 436 class RunnableTrace(Trace, Runnable): |
| 348 """Represents a runnable suite definition that is a leaf.""" | 437 """Represents a runnable suite definition that is a leaf.""" |
| 349 def __init__(self, suite, parent, arch): | 438 def __init__(self, suite, parent, arch): |
| 350 super(RunnableTrace, self).__init__(suite, parent, arch) | 439 super(RunnableTrace, self).__init__(suite, parent, arch) |
| 351 | 440 |
| 352 def Run(self, runner): | 441 def Run(self, runner): |
| 353 """Iterates over several runs and handles the output.""" | 442 """Iterates over several runs and handles the output.""" |
| 443 measurement = self.CreateMeasurement() | |
| 354 for stdout in runner(): | 444 for stdout in runner(): |
| 355 self.ConsumeOutput(stdout) | 445 measurement.ConsumeOutput(stdout) |
| 356 return self.GetResults() | 446 return measurement.GetResults() |
| 357 | 447 |
| 358 | 448 |
| 359 class RunnableGeneric(Runnable): | 449 class RunnableGeneric(Runnable): |
| 360 """Represents a runnable suite definition with generic traces.""" | 450 """Represents a runnable suite definition with generic traces.""" |
| 361 def __init__(self, suite, parent, arch): | 451 def __init__(self, suite, parent, arch): |
| 362 super(RunnableGeneric, self).__init__(suite, parent, arch) | 452 super(RunnableGeneric, self).__init__(suite, parent, arch) |
| 363 | 453 |
| 364 def Run(self, runner): | 454 def Run(self, runner): |
| 365 """Iterates over several runs and handles the output.""" | 455 return AccumulateGenericResults(self.graphs, self.units, runner) |
| 366 traces = OrderedDict() | |
| 367 for stdout in runner(): | |
| 368 for line in stdout.strip().splitlines(): | |
| 369 match = GENERIC_RESULTS_RE.match(line) | |
| 370 if match: | |
| 371 stddev = "" | |
| 372 graph = match.group(1) | |
| 373 trace = match.group(2) | |
| 374 body = match.group(3) | |
| 375 units = match.group(4) | |
| 376 match_stddev = RESULT_STDDEV_RE.match(body) | |
| 377 match_list = RESULT_LIST_RE.match(body) | |
| 378 errors = [] | |
| 379 if match_stddev: | |
| 380 result, stddev = map(str.strip, match_stddev.group(1).split(",")) | |
| 381 results = [result] | |
| 382 elif match_list: | |
| 383 results = map(str.strip, match_list.group(1).split(",")) | |
| 384 else: | |
| 385 results = [body.strip()] | |
| 386 | |
| 387 try: | |
| 388 results = map(lambda r: str(float(r)), results) | |
| 389 except ValueError: | |
| 390 results = [] | |
| 391 errors = ["Found non-numeric in %s" % | |
| 392 "/".join(self.graphs + [graph, trace])] | |
| 393 | |
| 394 trace_result = traces.setdefault(trace, Results([{ | |
| 395 "graphs": self.graphs + [graph, trace], | |
| 396 "units": (units or self.units).strip(), | |
| 397 "results": [], | |
| 398 "stddev": "", | |
| 399 }], errors)) | |
| 400 trace_result.traces[0]["results"].extend(results) | |
| 401 trace_result.traces[0]["stddev"] = stddev | |
| 402 | |
| 403 return reduce(lambda r, t: r + t, traces.itervalues(), Results()) | |
| 404 | 456 |
| 405 | 457 |
| 406 def MakeGraph(suite, arch, parent): | 458 def MakeGraph(suite, arch, parent): |
| 407 """Factory method for making graph objects.""" | 459 """Factory method for making graph objects.""" |
| 408 if isinstance(parent, Runnable): | 460 if isinstance(parent, Runnable): |
| 409 # Below a runnable can only be traces. | 461 # Below a runnable can only be traces. |
| 410 return Trace(suite, parent, arch) | 462 return Trace(suite, parent, arch) |
| 411 elif suite.get("main") is not None: | 463 elif suite.get("main") is not None: |
| 412 # A main file makes this graph runnable. Empty strings are accepted. | 464 # A main file makes this graph runnable. Empty strings are accepted. |
| 413 if suite.get("tests"): | 465 if suite.get("tests"): |
| (...skipping 313 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 727 | 779 |
| 728 if options.json_test_results: | 780 if options.json_test_results: |
| 729 results.WriteToFile(options.json_test_results) | 781 results.WriteToFile(options.json_test_results) |
| 730 else: # pragma: no cover | 782 else: # pragma: no cover |
| 731 print results | 783 print results |
| 732 | 784 |
| 733 return min(1, len(results.errors)) | 785 return min(1, len(results.errors)) |
| 734 | 786 |
| 735 if __name__ == "__main__": # pragma: no cover | 787 if __name__ == "__main__": # pragma: no cover |
| 736 sys.exit(Main(sys.argv[1:])) | 788 sys.exit(Main(sys.argv[1:])) |
| OLD | NEW |