#!/usr/bin/env python # Copyright 2015 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 Convert a perf trybot JSON file into a pleasing HTML page. It can read from standard input or via the --filename option. Examples: cat results.json | %prog --title "ia32 results" %prog -f results.json -t "ia32 results" -o results.html ''' import json import math from optparse import OptionParser import os import shutil import sys import tempfile PERCENT_CONSIDERED_SIGNIFICANT = 0.5 PROBABILITY_CONSIDERED_SIGNIFICANT = 0.02 PROBABILITY_CONSIDERED_MEANINGLESS = 0.05 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 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 Result: def __init__(self, test_name, count, hasScoreUnits, result, sigma, master_result, master_sigma): self.result_ = float(result) self.sigma_ = float(sigma) self.master_result_ = float(master_result) self.master_sigma_ = float(master_sigma) self.significant_ = False self.notable_ = 0 self.percentage_string_ = "" # compute notability and significance. try: if hasScoreUnits: compare_num = 100*self.result_/self.master_result_ - 100 else: compare_num = 100*self.master_result_/self.result_ - 100 if abs(compare_num) > 0.1: self.percentage_string_ = "%3.1f" % (compare_num) z = ComputeZ(self.master_result_, self.master_sigma_, self.result_, count) p = ComputeProbability(z) if p < PROBABILITY_CONSIDERED_SIGNIFICANT: self.significant_ = True if compare_num >= PERCENT_CONSIDERED_SIGNIFICANT: self.notable_ = 1 elif compare_num <= -PERCENT_CONSIDERED_SIGNIFICANT: self.notable_ = -1 except ZeroDivisionError: self.percentage_string_ = "NaN" self.significant_ = True def result(self): return self.result_ def sigma(self): return self.sigma_ def master_result(self): return self.master_result_ def master_sigma(self): return self.master_sigma_ 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 Benchmark: def __init__(self, name, data): self.name_ = name self.tests_ = {} for test in data: # strip off "<name>/" prefix, allowing for subsequent "/"s test_name = test.split("/", 1)[1] self.appendResult(test_name, data[test]) # tests is a dictionary of Results def tests(self): return self.tests_ def SortedTestKeys(self): keys = self.tests_.keys() keys.sort() t = "Total" if t in keys: keys.remove(t) keys.append(t) return keys def name(self): return self.name_ def appendResult(self, test_name, test_data): with_string = test_data["result with patch "] data = with_string.split() master_string = test_data["result without patch"] master_data = master_string.split() runs = int(test_data["runs"]) units = test_data["units"] hasScoreUnits = units == "score" self.tests_[test_name] = Result(test_name, runs, hasScoreUnits, data[0], data[2], master_data[0], master_data[2]) class BenchmarkRenderer: def __init__(self, output_file): 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 RenderOneBenchmark(self, benchmark): self.Print("<h2>") self.Print("<a name=\"" + benchmark.name() + "\">") self.Print(benchmark.name() + "</a> <a href=\"#top\">(top)</a>") self.Print("</h2>"); self.Print("<table class=\"benchmark\">") self.Print("<thead>") self.Print(" <th>Test</th>") self.Print(" <th>Result</th>") self.Print(" <th>Master</th>") self.Print(" <th>%</th>") self.Print("</thead>") self.Print("<tbody>") tests = benchmark.tests() for test in benchmark.SortedTestKeys(): t = tests[test] self.Print(" <tr>") self.Print(" <td>" + test + "</td>") self.Print(" <td>" + str(t.result()) + "</td>") self.Print(" <td>" + str(t.master_result()) + "</td>") t = tests[test] res = t.percentage_string() if t.isSignificant(): res = self.bold(res) if t.isNotablyPositive(): res = self.green(res) elif t.isNotablyNegative(): res = self.red(res) self.Print(" <td>" + res + "</td>") self.Print(" </tr>") self.Print("</tbody>") self.Print("</table>") def ProcessJSONData(self, data, title): self.Print("<h1>" + title + "</h1>") self.Print("<ul>") for benchmark in data: if benchmark != "errors": self.Print("<li><a href=\"#" + benchmark + "\">" + benchmark + "</a></li>") self.Print("</ul>") for benchmark in data: if benchmark != "errors": benchmark_object = Benchmark(benchmark, data[benchmark]) self.RenderOneBenchmark(benchmark_object) def bold(self, data): return "<b>" + data + "</b>" def red(self, data): return "<font color=\"red\">" + data + "</font>" def green(self, data): return "<font color=\"green\">" + data + "</font>" 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: left; margin: 0; padding: 6px 13px; } 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 PrintFooter(self): data = """</body> </html> """ self.Print(data) def Render(opts, args): if opts.filename: with open(opts.filename) as json_data: data = json.load(json_data) else: # load data from stdin data = json.load(sys.stdin) if opts.title: title = opts.title elif opts.filename: title = opts.filename else: title = "Benchmark results" renderer = BenchmarkRenderer(opts.output) renderer.PrintHeader() renderer.ProcessJSONData(data, title) renderer.PrintFooter() renderer.FlushOutput() if __name__ == '__main__': parser = OptionParser(usage=__doc__) parser.add_option("-f", "--filename", dest="filename", help="Specifies the filename for the JSON results " "rather than reading from stdin.") parser.add_option("-t", "--title", dest="title", help="Optional title of the web page.") parser.add_option("-o", "--output", dest="output", help="Write html output to this file rather than stdout.") (opts, args) = parser.parse_args() Render(opts, args)