1 #!/usr/bin/env python 2 # Copyright 2015 the V8 project authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 ''' 6 python %prog 7 8 Convert a perf trybot JSON file into a pleasing HTML page. It can read 9 from standard input or via the --filename option. Examples: 10 11 cat results.json | %prog --title "ia32 results" 12 %prog -f results.json -t "ia32 results" -o results.html 13 ''' 14 15 import commands 16 import json 17 import math 18 from optparse import OptionParser 19 import os 20 import shutil 21 import sys 22 import tempfile 23 24 PERCENT_CONSIDERED_SIGNIFICANT = 0.5 25 PROBABILITY_CONSIDERED_SIGNIFICANT = 0.02 26 PROBABILITY_CONSIDERED_MEANINGLESS = 0.05 27 28 29 def ComputeZ(baseline_avg, baseline_sigma, mean, n): 30 if baseline_sigma == 0: 31 return 1000.0; 32 return abs((mean - baseline_avg) / (baseline_sigma / math.sqrt(n))) 33 34 35 # Values from http://www.fourmilab.ch/rpkp/experiments/analysis/zCalc.html 36 def ComputeProbability(z): 37 if z > 2.575829: # p 0.005: two sided < 0.01 38 return 0 39 if z > 2.326348: # p 0.010 40 return 0.01 41 if z > 2.170091: # p 0.015 42 return 0.02 43 if z > 2.053749: # p 0.020 44 return 0.03 45 if z > 1.959964: # p 0.025: two sided < 0.05 46 return 0.04 47 if z > 1.880793: # p 0.030 48 return 0.05 49 if z > 1.811910: # p 0.035 50 return 0.06 51 if z > 1.750686: # p 0.040 52 return 0.07 53 if z > 1.695397: # p 0.045 54 return 0.08 55 if z > 1.644853: # p 0.050: two sided < 0.10 56 return 0.09 57 if z > 1.281551: # p 0.100: two sided < 0.20 58 return 0.10 59 return 0.20 # two sided p >= 0.20 60 61 62 class Result: 63 def __init__(self, test_name, count, hasScoreUnits, result, sigma, 64 master_result, master_sigma): 65 self.result_ = float(result) 66 self.sigma_ = float(sigma) 67 self.master_result_ = float(master_result) 68 self.master_sigma_ = float(master_sigma) 69 self.significant_ = False 70 self.notable_ = 0 71 self.percentage_string_ = "" 72 # compute notability and significance. 73 if hasScoreUnits: 74 compare_num = 100*self.result_/self.master_result_ - 100 75 else: 76 compare_num = 100*self.master_result_/self.result_ - 100 77 if abs(compare_num) > 0.1: 78 self.percentage_string_ = "%3.1f" % (compare_num) 79 z = ComputeZ(self.master_result_, self.master_sigma_, self.result_, count) 80 p = ComputeProbability(z) 81 if p < PROBABILITY_CONSIDERED_SIGNIFICANT: 82 self.significant_ = True 83 if compare_num >= PERCENT_CONSIDERED_SIGNIFICANT: 84 self.notable_ = 1 85 elif compare_num <= -PERCENT_CONSIDERED_SIGNIFICANT: 86 self.notable_ = -1 87 88 def result(self): 89 return self.result_ 90 91 def sigma(self): 92 return self.sigma_ 93 94 def master_result(self): 95 return self.master_result_ 96 97 def master_sigma(self): 98 return self.master_sigma_ 99 100 def percentage_string(self): 101 return self.percentage_string_; 102 103 def isSignificant(self): 104 return self.significant_ 105 106 def isNotablyPositive(self): 107 return self.notable_ > 0 108 109 def isNotablyNegative(self): 110 return self.notable_ < 0 111 112 113 class Benchmark: 114 def __init__(self, name, data): 115 self.name_ = name 116 self.tests_ = {} 117 for test in data: 118 # strip off "<name>/" prefix, allowing for subsequent "/"s 119 test_name = test.split("/", 1)[1] 120 self.appendResult(test_name, data[test]) 121 122 # tests is a dictionary of Results 123 def tests(self): 124 return self.tests_ 125 126 def SortedTestKeys(self): 127 keys = self.tests_.keys() 128 keys.sort() 129 t = "Total" 130 if t in keys: 131 keys.remove(t) 132 keys.append(t) 133 return keys 134 135 def name(self): 136 return self.name_ 137 138 def appendResult(self, test_name, test_data): 139 with_string = test_data["result with patch "] 140 data = with_string.split() 141 master_string = test_data["result without patch"] 142 master_data = master_string.split() 143 runs = int(test_data["runs"]) 144 units = test_data["units"] 145 hasScoreUnits = units == "score" 146 self.tests_[test_name] = Result(test_name, 147 runs, 148 hasScoreUnits, 149 data[0], data[2], 150 master_data[0], master_data[2]) 151 152 153 class BenchmarkRenderer: 154 def __init__(self, output_file): 155 self.print_output_ = [] 156 self.output_file_ = output_file 157 158 def Print(self, str_data): 159 self.print_output_.append(str_data) 160 161 def FlushOutput(self): 162 string_data = "\n".join(self.print_output_) 163 print_output = [] 164 if self.output_file_: 165 # create a file 166 with open(self.output_file_, "w") as text_file: 167 text_file.write(string_data) 168 else: 169 print(string_data) 170 171 def RenderOneBenchmark(self, benchmark): 172 self.Print("<h2>") 173 self.Print("<a name=\"" + benchmark.name() + "\">") 174 self.Print(benchmark.name() + "</a> <a href=\"#top\">(top)</a>") 175 self.Print("</h2>"); 176 self.Print("<table class=\"benchmark\">") 177 self.Print("<thead>") 178 self.Print(" <th>Test</th>") 179 self.Print(" <th>Result</th>") 180 self.Print(" <th>Master</th>") 181 self.Print(" <th>%</th>") 182 self.Print("</thead>") 183 self.Print("<tbody>") 184 tests = benchmark.tests() 185 for test in benchmark.SortedTestKeys(): 186 t = tests[test] 187 self.Print(" <tr>") 188 self.Print(" <td>" + test + "</td>") 189 self.Print(" <td>" + str(t.result()) + "</td>") 190 self.Print(" <td>" + str(t.master_result()) + "</td>") 191 t = tests[test] 192 res = t.percentage_string() 193 if t.isSignificant(): 194 res = self.bold(res) 195 if t.isNotablyPositive(): 196 res = self.green(res) 197 elif t.isNotablyNegative(): 198 res = self.red(res) 199 self.Print(" <td>" + res + "</td>") 200 self.Print(" </tr>") 201 self.Print("</tbody>") 202 self.Print("</table>") 203 204 def ProcessJSONData(self, data, title): 205 self.Print("<h1>" + title + "</h1>") 206 self.Print("<ul>") 207 for benchmark in data: 208 if benchmark != "errors": 209 self.Print("<li><a href=\"#" + benchmark + "\">" + benchmark + "</a></li>") 210 self.Print("</ul>") 211 for benchmark in data: 212 if benchmark != "errors": 213 benchmark_object = Benchmark(benchmark, data[benchmark]) 214 self.RenderOneBenchmark(benchmark_object) 215 216 def bold(self, data): 217 return "<b>" + data + "</b>" 218 219 def red(self, data): 220 return "<font color=\"red\">" + data + "</font>" 221 222 223 def green(self, data): 224 return "<font color=\"green\">" + data + "</font>" 225 226 def PrintHeader(self): 227 data = """<html> 228 <head> 229 <title>Output</title> 230 <style type="text/css"> 231 /* 232 Style inspired by Andy Ferra's gist at https://gist.github.com/andyferra/2554919 233 */ 234 body { 235 font-family: Helvetica, arial, sans-serif; 236 font-size: 14px; 237 line-height: 1.6; 238 padding-top: 10px; 239 padding-bottom: 10px; 240 background-color: white; 241 padding: 30px; 242 } 243 h1, h2, h3, h4, h5, h6 { 244 margin: 20px 0 10px; 245 padding: 0; 246 font-weight: bold; 247 -webkit-font-smoothing: antialiased; 248 cursor: text; 249 position: relative; 250 } 251 h1 { 252 font-size: 28px; 253 color: black; 254 } 255 256 h2 { 257 font-size: 24px; 258 border-bottom: 1px solid #cccccc; 259 color: black; 260 } 261 262 h3 { 263 font-size: 18px; 264 } 265 266 h4 { 267 font-size: 16px; 268 } 269 270 h5 { 271 font-size: 14px; 272 } 273 274 h6 { 275 color: #777777; 276 font-size: 14px; 277 } 278 279 p, blockquote, ul, ol, dl, li, table, pre { 280 margin: 15px 0; 281 } 282 283 li p.first { 284 display: inline-block; 285 } 286 287 ul, ol { 288 padding-left: 30px; 289 } 290 291 ul :first-child, ol :first-child { 292 margin-top: 0; 293 } 294 295 ul :last-child, ol :last-child { 296 margin-bottom: 0; 297 } 298 299 table { 300 padding: 0; 301 } 302 303 table tr { 304 border-top: 1px solid #cccccc; 305 background-color: white; 306 margin: 0; 307 padding: 0; 308 } 309 310 table tr:nth-child(2n) { 311 background-color: #f8f8f8; 312 } 313 314 table tr th { 315 font-weight: bold; 316 border: 1px solid #cccccc; 317 text-align: left; 318 margin: 0; 319 padding: 6px 13px; 320 } 321 table tr td { 322 border: 1px solid #cccccc; 323 text-align: left; 324 margin: 0; 325 padding: 6px 13px; 326 } 327 table tr th :first-child, table tr td :first-child { 328 margin-top: 0; 329 } 330 table tr th :last-child, table tr td :last-child { 331 margin-bottom: 0; 332 } 333 </style> 334 </head> 335 <body> 336 """ 337 self.Print(data) 338 339 def PrintFooter(self): 340 data = """</body> 341 </html> 342 """ 343 self.Print(data) 344 345 346 def Render(opts, args): 347 if opts.filename: 348 with open(opts.filename) as json_data: 349 data = json.load(json_data) 350 else: 351 # load data from stdin 352 data = json.load(sys.stdin) 353 354 if opts.title: 355 title = opts.title 356 elif opts.filename: 357 title = opts.filename 358 else: 359 title = "Benchmark results" 360 renderer = BenchmarkRenderer(opts.output) 361 renderer.PrintHeader() 362 renderer.ProcessJSONData(data, title) 363 renderer.PrintFooter() 364 renderer.FlushOutput() 365 366 367 if __name__ == '__main__': 368 parser = OptionParser(usage=__doc__) 369 parser.add_option("-f", "--filename", dest="filename", 370 help="Specifies the filename for the JSON results " 371 "rather than reading from stdin.") 372 parser.add_option("-t", "--title", dest="title", 373 help="Optional title of the web page.") 374 parser.add_option("-o", "--output", dest="output", 375 help="Write html output to this file rather than stdout.") 376 377 (opts, args) = parser.parse_args() 378 Render(opts, args) 379