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