|  | #!/usr/bin/env python | 
|  | # Copyright 2017 the V8 project authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  | ''' | 
|  | python %prog | 
|  |  | 
|  | Compare perf trybot JSON files and output the results into a pleasing HTML page. | 
|  | Examples: | 
|  | %prog -t "ia32 results" Result,../result.json Master,/path-to/master.json -o results.html | 
|  | %prog -t "x64 results" ../result.json master.json -o results.html | 
|  | ''' | 
|  |  | 
|  | # for py2/py3 compatibility | 
|  | from __future__ import print_function | 
|  |  | 
|  | from collections import OrderedDict | 
|  | import json | 
|  | import math | 
|  | from argparse import ArgumentParser | 
|  | import os | 
|  | import shutil | 
|  | import sys | 
|  | import tempfile | 
|  |  | 
|  | PERCENT_CONSIDERED_SIGNIFICANT = 0.5 | 
|  | PROBABILITY_CONSIDERED_SIGNIFICANT = 0.02 | 
|  | PROBABILITY_CONSIDERED_MEANINGLESS = 0.05 | 
|  |  | 
|  | class Statistics: | 
|  | @staticmethod | 
|  | def Mean(values): | 
|  | return float(sum(values)) / len(values) | 
|  |  | 
|  | @staticmethod | 
|  | def Variance(values, average): | 
|  | return map(lambda x: (x - average) ** 2, values) | 
|  |  | 
|  | @staticmethod | 
|  | def StandardDeviation(values, average): | 
|  | return math.sqrt(Statistics.Mean(Statistics.Variance(values, average))) | 
|  |  | 
|  | @staticmethod | 
|  | def ComputeZ(baseline_avg, baseline_sigma, mean, n): | 
|  | if baseline_sigma == 0: | 
|  | return 1000.0; | 
|  | return abs((mean - baseline_avg) / (baseline_sigma / math.sqrt(n))) | 
|  |  | 
|  | # Values from http://www.fourmilab.ch/rpkp/experiments/analysis/zCalc.html | 
|  | @staticmethod | 
|  | def ComputeProbability(z): | 
|  | if z > 2.575829: # p 0.005: two sided < 0.01 | 
|  | return 0 | 
|  | if z > 2.326348: # p 0.010 | 
|  | return 0.01 | 
|  | if z > 2.170091: # p 0.015 | 
|  | return 0.02 | 
|  | if z > 2.053749: # p 0.020 | 
|  | return 0.03 | 
|  | if z > 1.959964: # p 0.025: two sided < 0.05 | 
|  | return 0.04 | 
|  | if z > 1.880793: # p 0.030 | 
|  | return 0.05 | 
|  | if z > 1.811910: # p 0.035 | 
|  | return 0.06 | 
|  | if z > 1.750686: # p 0.040 | 
|  | return 0.07 | 
|  | if z > 1.695397: # p 0.045 | 
|  | return 0.08 | 
|  | if z > 1.644853: # p 0.050: two sided < 0.10 | 
|  | return 0.09 | 
|  | if z > 1.281551: # p 0.100: two sided < 0.20 | 
|  | return 0.10 | 
|  | return 0.20 # two sided p >= 0.20 | 
|  |  | 
|  |  | 
|  | class ResultsDiff: | 
|  | def __init__(self, significant, notable, percentage_string): | 
|  | self.significant_ = significant | 
|  | self.notable_ = notable | 
|  | self.percentage_string_ = percentage_string | 
|  |  | 
|  | def percentage_string(self): | 
|  | return self.percentage_string_; | 
|  |  | 
|  | def isSignificant(self): | 
|  | return self.significant_ | 
|  |  | 
|  | def isNotablyPositive(self): | 
|  | return self.notable_ > 0 | 
|  |  | 
|  | def isNotablyNegative(self): | 
|  | return self.notable_ < 0 | 
|  |  | 
|  |  | 
|  | class BenchmarkResult: | 
|  | def __init__(self, units, count, result, sigma): | 
|  | self.units_ = units | 
|  | self.count_ = float(count) | 
|  | self.result_ = float(result) | 
|  | self.sigma_ = float(sigma) | 
|  |  | 
|  | def Compare(self, other): | 
|  | if self.units_ != other.units_: | 
|  | print ("Incompatible units: %s and %s" % (self.units_, other.units_)) | 
|  | sys.exit(1) | 
|  |  | 
|  | significant = False | 
|  | notable = 0 | 
|  | percentage_string = "" | 
|  | # compute notability and significance. | 
|  | if self.units_ == "score": | 
|  | compare_num = 100*self.result_/other.result_ - 100 | 
|  | else: | 
|  | compare_num = 100*other.result_/self.result_ - 100 | 
|  | if abs(compare_num) > 0.1: | 
|  | percentage_string = "%3.1f" % (compare_num) | 
|  | z = Statistics.ComputeZ(other.result_, other.sigma_, | 
|  | self.result_, self.count_) | 
|  | p = Statistics.ComputeProbability(z) | 
|  | if p < PROBABILITY_CONSIDERED_SIGNIFICANT: | 
|  | significant = True | 
|  | if compare_num >= PERCENT_CONSIDERED_SIGNIFICANT: | 
|  | notable = 1 | 
|  | elif compare_num <= -PERCENT_CONSIDERED_SIGNIFICANT: | 
|  | notable = -1 | 
|  | return ResultsDiff(significant, notable, percentage_string) | 
|  |  | 
|  | def result(self): | 
|  | return self.result_ | 
|  |  | 
|  | def sigma(self): | 
|  | return self.sigma_ | 
|  |  | 
|  |  | 
|  | class Benchmark: | 
|  | def __init__(self, name): | 
|  | self.name_ = name | 
|  | self.runs_ = {} | 
|  |  | 
|  | def name(self): | 
|  | return self.name_ | 
|  |  | 
|  | def getResult(self, run_name): | 
|  | return self.runs_.get(run_name) | 
|  |  | 
|  | def appendResult(self, run_name, trace): | 
|  | values = map(float, trace['results']) | 
|  | count = len(values) | 
|  | mean = Statistics.Mean(values) | 
|  | stddev = float(trace.get('stddev') or | 
|  | Statistics.StandardDeviation(values, mean)) | 
|  | units = trace["units"] | 
|  | # print run_name, units, count, mean, stddev | 
|  | self.runs_[run_name] = BenchmarkResult(units, count, mean, stddev) | 
|  |  | 
|  |  | 
|  | class BenchmarkSuite: | 
|  | def __init__(self, name): | 
|  | self.name_ = name | 
|  | self.benchmarks_ = {} | 
|  |  | 
|  | def SortedTestKeys(self): | 
|  | keys = self.benchmarks_.keys() | 
|  | keys.sort() | 
|  | t = "Total" | 
|  | if t in keys: | 
|  | keys.remove(t) | 
|  | keys.append(t) | 
|  | return keys | 
|  |  | 
|  | def name(self): | 
|  | return self.name_ | 
|  |  | 
|  | def getBenchmark(self, benchmark_name): | 
|  | benchmark_object = self.benchmarks_.get(benchmark_name) | 
|  | if benchmark_object == None: | 
|  | benchmark_object = Benchmark(benchmark_name) | 
|  | self.benchmarks_[benchmark_name] = benchmark_object | 
|  | return benchmark_object | 
|  |  | 
|  |  | 
|  | class ResultTableRenderer: | 
|  | def __init__(self, output_file): | 
|  | self.benchmarks_ = [] | 
|  | self.print_output_ = [] | 
|  | self.output_file_ = output_file | 
|  |  | 
|  | def Print(self, str_data): | 
|  | self.print_output_.append(str_data) | 
|  |  | 
|  | def FlushOutput(self): | 
|  | string_data = "\n".join(self.print_output_) | 
|  | print_output = [] | 
|  | if self.output_file_: | 
|  | # create a file | 
|  | with open(self.output_file_, "w") as text_file: | 
|  | text_file.write(string_data) | 
|  | else: | 
|  | print(string_data) | 
|  |  | 
|  | def bold(self, data): | 
|  | return "<b>%s</b>" % data | 
|  |  | 
|  | def red(self, data): | 
|  | return "<font color=\"red\">%s</font>" % data | 
|  |  | 
|  |  | 
|  | def green(self, data): | 
|  | return "<font color=\"green\">%s</font>" % data | 
|  |  | 
|  | def PrintHeader(self): | 
|  | data = """<html> | 
|  | <head> | 
|  | <title>Output</title> | 
|  | <style type="text/css"> | 
|  | /* | 
|  | Style inspired by Andy Ferra's gist at https://gist.github.com/andyferra/2554919 | 
|  | */ | 
|  | body { | 
|  | font-family: Helvetica, arial, sans-serif; | 
|  | font-size: 14px; | 
|  | line-height: 1.6; | 
|  | padding-top: 10px; | 
|  | padding-bottom: 10px; | 
|  | background-color: white; | 
|  | padding: 30px; | 
|  | } | 
|  | h1, h2, h3, h4, h5, h6 { | 
|  | margin: 20px 0 10px; | 
|  | padding: 0; | 
|  | font-weight: bold; | 
|  | -webkit-font-smoothing: antialiased; | 
|  | cursor: text; | 
|  | position: relative; | 
|  | } | 
|  | h1 { | 
|  | font-size: 28px; | 
|  | color: black; | 
|  | } | 
|  |  | 
|  | h2 { | 
|  | font-size: 24px; | 
|  | border-bottom: 1px solid #cccccc; | 
|  | color: black; | 
|  | } | 
|  |  | 
|  | h3 { | 
|  | font-size: 18px; | 
|  | } | 
|  |  | 
|  | h4 { | 
|  | font-size: 16px; | 
|  | } | 
|  |  | 
|  | h5 { | 
|  | font-size: 14px; | 
|  | } | 
|  |  | 
|  | h6 { | 
|  | color: #777777; | 
|  | font-size: 14px; | 
|  | } | 
|  |  | 
|  | p, blockquote, ul, ol, dl, li, table, pre { | 
|  | margin: 15px 0; | 
|  | } | 
|  |  | 
|  | li p.first { | 
|  | display: inline-block; | 
|  | } | 
|  |  | 
|  | ul, ol { | 
|  | padding-left: 30px; | 
|  | } | 
|  |  | 
|  | ul :first-child, ol :first-child { | 
|  | margin-top: 0; | 
|  | } | 
|  |  | 
|  | ul :last-child, ol :last-child { | 
|  | margin-bottom: 0; | 
|  | } | 
|  |  | 
|  | table { | 
|  | padding: 0; | 
|  | } | 
|  |  | 
|  | table tr { | 
|  | border-top: 1px solid #cccccc; | 
|  | background-color: white; | 
|  | margin: 0; | 
|  | padding: 0; | 
|  | } | 
|  |  | 
|  | table tr:nth-child(2n) { | 
|  | background-color: #f8f8f8; | 
|  | } | 
|  |  | 
|  | table tr th { | 
|  | font-weight: bold; | 
|  | border: 1px solid #cccccc; | 
|  | text-align: left; | 
|  | margin: 0; | 
|  | padding: 6px 13px; | 
|  | } | 
|  | table tr td { | 
|  | border: 1px solid #cccccc; | 
|  | text-align: right; | 
|  | margin: 0; | 
|  | padding: 6px 13px; | 
|  | } | 
|  | table tr td.name-column { | 
|  | text-align: left; | 
|  | } | 
|  | table tr th :first-child, table tr td :first-child { | 
|  | margin-top: 0; | 
|  | } | 
|  | table tr th :last-child, table tr td :last-child { | 
|  | margin-bottom: 0; | 
|  | } | 
|  | </style> | 
|  | </head> | 
|  | <body> | 
|  | """ | 
|  | self.Print(data) | 
|  |  | 
|  | def StartSuite(self, suite_name, run_names): | 
|  | self.Print("<h2>") | 
|  | self.Print("<a name=\"%s\">%s</a> <a href=\"#top\">(top)</a>" % | 
|  | (suite_name, suite_name)) | 
|  | self.Print("</h2>"); | 
|  | self.Print("<table class=\"benchmark\">") | 
|  | self.Print("<thead>") | 
|  | self.Print("  <th>Test</th>") | 
|  | main_run = None | 
|  | for run_name in run_names: | 
|  | self.Print("  <th>%s</th>" % run_name) | 
|  | if main_run == None: | 
|  | main_run = run_name | 
|  | else: | 
|  | self.Print("  <th>%</th>") | 
|  | self.Print("</thead>") | 
|  | self.Print("<tbody>") | 
|  |  | 
|  |  | 
|  | def FinishSuite(self): | 
|  | self.Print("</tbody>") | 
|  | self.Print("</table>") | 
|  |  | 
|  |  | 
|  | def StartBenchmark(self, benchmark_name): | 
|  | self.Print("  <tr>") | 
|  | self.Print("    <td class=\"name-column\">%s</td>" % benchmark_name) | 
|  |  | 
|  | def FinishBenchmark(self): | 
|  | self.Print("  </tr>") | 
|  |  | 
|  |  | 
|  | def PrintResult(self, run): | 
|  | if run == None: | 
|  | self.PrintEmptyCell() | 
|  | return | 
|  | self.Print("    <td>%3.1f</td>" % run.result()) | 
|  |  | 
|  |  | 
|  | def PrintComparison(self, run, main_run): | 
|  | if run == None or main_run == None: | 
|  | self.PrintEmptyCell() | 
|  | return | 
|  | diff = run.Compare(main_run) | 
|  | res = diff.percentage_string() | 
|  | if diff.isSignificant(): | 
|  | res = self.bold(res) | 
|  | if diff.isNotablyPositive(): | 
|  | res = self.green(res) | 
|  | elif diff.isNotablyNegative(): | 
|  | res = self.red(res) | 
|  | self.Print("    <td>%s</td>" % res) | 
|  |  | 
|  |  | 
|  | def PrintEmptyCell(self): | 
|  | self.Print("    <td></td>") | 
|  |  | 
|  |  | 
|  | def StartTOC(self, title): | 
|  | self.Print("<h1>%s</h1>" % title) | 
|  | self.Print("<ul>") | 
|  |  | 
|  | def FinishTOC(self): | 
|  | self.Print("</ul>") | 
|  |  | 
|  | def PrintBenchmarkLink(self, benchmark): | 
|  | self.Print("<li><a href=\"#" + benchmark + "\">" + benchmark + "</a></li>") | 
|  |  | 
|  | def PrintFooter(self): | 
|  | data = """</body> | 
|  | </html> | 
|  | """ | 
|  | self.Print(data) | 
|  |  | 
|  |  | 
|  | def Render(args): | 
|  | benchmark_suites = {} | 
|  | run_names = OrderedDict() | 
|  |  | 
|  | for json_file_list in args.json_file_list: | 
|  | run_name = json_file_list[0] | 
|  | if run_name.endswith(".json"): | 
|  | # The first item in the list is also a file name | 
|  | run_name = os.path.splitext(run_name)[0] | 
|  | filenames = json_file_list | 
|  | else: | 
|  | filenames = json_file_list[1:] | 
|  |  | 
|  | for filename in filenames: | 
|  | print ("Processing result set \"%s\", file: %s" % (run_name, filename)) | 
|  | with open(filename) as json_data: | 
|  | data = json.load(json_data) | 
|  |  | 
|  | run_names[run_name] = 0 | 
|  |  | 
|  | for error in data["errors"]: | 
|  | print("Error:", error) | 
|  |  | 
|  | for trace in data["traces"]: | 
|  | suite_name = trace["graphs"][0] | 
|  | benchmark_name = "/".join(trace["graphs"][1:]) | 
|  |  | 
|  | benchmark_suite_object = benchmark_suites.get(suite_name) | 
|  | if benchmark_suite_object == None: | 
|  | benchmark_suite_object = BenchmarkSuite(suite_name) | 
|  | benchmark_suites[suite_name] = benchmark_suite_object | 
|  |  | 
|  | benchmark_object = benchmark_suite_object.getBenchmark(benchmark_name) | 
|  | benchmark_object.appendResult(run_name, trace); | 
|  |  | 
|  |  | 
|  | renderer = ResultTableRenderer(args.output) | 
|  | renderer.PrintHeader() | 
|  |  | 
|  | title = args.title or "Benchmark results" | 
|  | renderer.StartTOC(title) | 
|  | for suite_name, benchmark_suite_object in sorted(benchmark_suites.iteritems()): | 
|  | renderer.PrintBenchmarkLink(suite_name) | 
|  | renderer.FinishTOC() | 
|  |  | 
|  | for suite_name, benchmark_suite_object in sorted(benchmark_suites.iteritems()): | 
|  | renderer.StartSuite(suite_name, run_names) | 
|  | for benchmark_name in benchmark_suite_object.SortedTestKeys(): | 
|  | benchmark_object = benchmark_suite_object.getBenchmark(benchmark_name) | 
|  | # print suite_name, benchmark_object.name() | 
|  |  | 
|  | renderer.StartBenchmark(benchmark_name) | 
|  | main_run = None | 
|  | main_result = None | 
|  | for run_name in run_names: | 
|  | result = benchmark_object.getResult(run_name) | 
|  | renderer.PrintResult(result) | 
|  | if main_run == None: | 
|  | main_run = run_name | 
|  | main_result = result | 
|  | else: | 
|  | renderer.PrintComparison(result, main_result) | 
|  | renderer.FinishBenchmark() | 
|  | renderer.FinishSuite() | 
|  |  | 
|  | renderer.PrintFooter() | 
|  | renderer.FlushOutput() | 
|  |  | 
|  | def CommaSeparatedList(arg): | 
|  | return [x for x in arg.split(',')] | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | parser = ArgumentParser(description="Compare perf trybot JSON files and " + | 
|  | "output the results into a pleasing HTML page.") | 
|  | parser.add_argument("-t", "--title", dest="title", | 
|  | help="Optional title of the web page") | 
|  | parser.add_argument("-o", "--output", dest="output", | 
|  | help="Write html output to this file rather than stdout") | 
|  | parser.add_argument("json_file_list", nargs="+", type=CommaSeparatedList, | 
|  | help="[column name,]./path-to/result.json - a comma-separated" + | 
|  | " list of optional column name and paths to json files") | 
|  |  | 
|  | args = parser.parse_args() | 
|  | Render(args) |