#!/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 ''' 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)