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 |