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