Add Websites/browserbench.org
[WebKit-https.git] / Websites / browserbench.org / MotionMark / resources / runner / animometer.js
1 ResultsDashboard = Utilities.createClass(
2     function(options, testData)
3     {
4         this._iterationsSamplers = [];
5         this._options = options;
6         this._results = null;
7         if (testData) {
8             this._iterationsSamplers = testData;
9             this._processData();
10         }
11     }, {
12
13     push: function(suitesSamplers)
14     {
15         this._iterationsSamplers.push(suitesSamplers);
16     },
17
18     _processData: function()
19     {
20         this._results = {};
21         this._results[Strings.json.results.iterations] = [];
22
23         var iterationsScores = [];
24         this._iterationsSamplers.forEach(function(iteration, index) {
25             var testsScores = [];
26             var testsLowerBoundScores = [];
27             var testsUpperBoundScores = [];
28
29             var result = {};
30             this._results[Strings.json.results.iterations][index] = result;
31
32             var suitesResult = {};
33             result[Strings.json.results.tests] = suitesResult;
34
35             for (var suiteName in iteration) {
36                 var suiteData = iteration[suiteName];
37
38                 var suiteResult = {};
39                 suitesResult[suiteName] = suiteResult;
40
41                 for (var testName in suiteData) {
42                     if (!suiteData[testName][Strings.json.result])
43                         this.calculateScore(suiteData[testName]);
44
45                     suiteResult[testName] = suiteData[testName][Strings.json.result];
46                     delete suiteData[testName][Strings.json.result];
47
48                     testsScores.push(suiteResult[testName][Strings.json.score]);
49                     testsLowerBoundScores.push(suiteResult[testName][Strings.json.scoreLowerBound]);
50                     testsUpperBoundScores.push(suiteResult[testName][Strings.json.scoreUpperBound]);
51                 }
52             }
53
54             result[Strings.json.score] = Statistics.geometricMean(testsScores);
55             result[Strings.json.scoreLowerBound] = Statistics.geometricMean(testsLowerBoundScores);
56             result[Strings.json.scoreUpperBound] = Statistics.geometricMean(testsUpperBoundScores);
57             iterationsScores.push(result[Strings.json.score]);
58         }, this);
59
60         this._results[Strings.json.score] = Statistics.sampleMean(iterationsScores.length, iterationsScores.reduce(function(a, b) { return a + b; }));
61         this._results[Strings.json.scoreLowerBound] = this._results[Strings.json.results.iterations][0][Strings.json.scoreLowerBound];
62         this._results[Strings.json.scoreUpperBound] = this._results[Strings.json.results.iterations][0][Strings.json.scoreUpperBound];
63     },
64
65     calculateScore: function(data)
66     {
67         var result = {};
68         data[Strings.json.result] = result;
69         var samples = data[Strings.json.samples];
70
71         var desiredFrameLength = 1000/60;
72         if (this._options["controller"] == "ramp30")
73             desiredFrameLength = 1000/30;
74
75         function findRegression(series, profile) {
76             var minIndex = Math.round(.025 * series.length);
77             var maxIndex = Math.round(.975 * (series.length - 1));
78             var minComplexity = series.getFieldInDatum(minIndex, Strings.json.complexity);
79             var maxComplexity = series.getFieldInDatum(maxIndex, Strings.json.complexity);
80
81             if (Math.abs(maxComplexity - minComplexity) < 20 && maxIndex - minIndex < 20) {
82                 minIndex = 0;
83                 maxIndex = series.length - 1;
84                 minComplexity = series.getFieldInDatum(minIndex, Strings.json.complexity);
85                 maxComplexity = series.getFieldInDatum(maxIndex, Strings.json.complexity);
86             }
87
88             var complexityIndex = series.fieldMap[Strings.json.complexity];
89             var frameLengthIndex = series.fieldMap[Strings.json.frameLength];
90             var regressionOptions = { desiredFrameLength: desiredFrameLength };
91             if (profile)
92                 regressionOptions.preferredProfile = profile;
93             return {
94                 minComplexity: minComplexity,
95                 maxComplexity: maxComplexity,
96                 samples: series.slice(minIndex, maxIndex + 1),
97                 regression: new Regression(
98                     series.data,
99                     function (data, i) { return data[i][complexityIndex]; },
100                     function (data, i) { return data[i][frameLengthIndex]; },
101                     minIndex, maxIndex, regressionOptions)
102             };
103         }
104
105         var complexitySamples;
106         // Convert these samples into SampleData objects if needed
107         [Strings.json.complexity, Strings.json.complexityAverage, Strings.json.controller].forEach(function(seriesName) {
108             var series = samples[seriesName];
109             if (series && !(series instanceof SampleData))
110                 samples[seriesName] = new SampleData(series.fieldMap, series.data);
111         });
112
113         var isRampController = ["ramp", "ramp30"].indexOf(this._options["controller"]) != -1;
114         var predominantProfile = "";
115         if (isRampController) {
116             var profiles = {};
117             data[Strings.json.controller].forEach(function(regression) {
118                 if (regression[Strings.json.regressions.profile]) {
119                     var profile = regression[Strings.json.regressions.profile];
120                     profiles[profile] = (profiles[profile] || 0) + 1;
121                 }
122             });
123
124             var maxProfileCount = 0;
125             for (var profile in profiles) {
126                 if (profiles[profile] > maxProfileCount) {
127                     predominantProfile = profile;
128                     maxProfileCount = profiles[profile];
129                 }
130             }
131         }
132
133         [Strings.json.complexity, Strings.json.complexityAverage].forEach(function(seriesName) {
134             if (!(seriesName in samples))
135                 return;
136
137             var regression = {};
138             result[seriesName] = regression;
139             var regressionResult = findRegression(samples[seriesName], predominantProfile);
140             if (seriesName == Strings.json.complexity)
141                 complexitySamples = regressionResult.samples;
142             var calculation = regressionResult.regression;
143             regression[Strings.json.regressions.segment1] = [
144                 [regressionResult.minComplexity, calculation.s1 + calculation.t1 * regressionResult.minComplexity],
145                 [calculation.complexity, calculation.s1 + calculation.t1 * calculation.complexity]
146             ];
147             regression[Strings.json.regressions.segment2] = [
148                 [calculation.complexity, calculation.s2 + calculation.t2 * calculation.complexity],
149                 [regressionResult.maxComplexity, calculation.s2 + calculation.t2 * regressionResult.maxComplexity]
150             ];
151             regression[Strings.json.complexity] = calculation.complexity;
152             regression[Strings.json.measurements.stdev] = Math.sqrt(calculation.error / samples[seriesName].length);
153         });
154
155         if (isRampController) {
156             var timeComplexity = new Experiment;
157             data[Strings.json.controller].forEach(function(regression) {
158                 timeComplexity.sample(regression[Strings.json.complexity]);
159             });
160
161             var experimentResult = {};
162             result[Strings.json.controller] = experimentResult;
163             experimentResult[Strings.json.score] = timeComplexity.mean();
164             experimentResult[Strings.json.measurements.average] = timeComplexity.mean();
165             experimentResult[Strings.json.measurements.stdev] = timeComplexity.standardDeviation();
166             experimentResult[Strings.json.measurements.percent] = timeComplexity.percentage();
167
168             const bootstrapIterations = 2500;
169             var bootstrapResult = Regression.bootstrap(complexitySamples.data, bootstrapIterations, function(resampleData) {
170                 var complexityIndex = complexitySamples.fieldMap[Strings.json.complexity];
171                 resampleData.sort(function(a, b) {
172                     return a[complexityIndex] - b[complexityIndex];
173                 });
174
175                 var resample = new SampleData(complexitySamples.fieldMap, resampleData);
176                 var regressionResult = findRegression(resample, predominantProfile);
177                 return regressionResult.regression.complexity;
178             }, .8);
179
180             result[Strings.json.complexity][Strings.json.bootstrap] = bootstrapResult;
181             result[Strings.json.score] = bootstrapResult.median;
182             result[Strings.json.scoreLowerBound] = bootstrapResult.confidenceLow;
183             result[Strings.json.scoreUpperBound] = bootstrapResult.confidenceHigh;
184         } else {
185             var marks = data[Strings.json.marks];
186             var samplingStartIndex = 0, samplingEndIndex = -1;
187             if (Strings.json.samplingStartTimeOffset in marks)
188                 samplingStartIndex = marks[Strings.json.samplingStartTimeOffset].index;
189             if (Strings.json.samplingEndTimeOffset in marks)
190                 samplingEndIndex = marks[Strings.json.samplingEndTimeOffset].index;
191
192             var averageComplexity = new Experiment;
193             var averageFrameLength = new Experiment;
194             var controllerSamples = samples[Strings.json.controller];
195             controllerSamples.forEach(function (sample, i) {
196                 if (i >= samplingStartIndex && (samplingEndIndex == -1 || i < samplingEndIndex)) {
197                     averageComplexity.sample(controllerSamples.getFieldInDatum(sample, Strings.json.complexity));
198                     var smoothedFrameLength = controllerSamples.getFieldInDatum(sample, Strings.json.smoothedFrameLength);
199                     if (smoothedFrameLength && smoothedFrameLength != -1)
200                         averageFrameLength.sample(smoothedFrameLength);
201                 }
202             });
203
204             var experimentResult = {};
205             result[Strings.json.controller] = experimentResult;
206             experimentResult[Strings.json.measurements.average] = averageComplexity.mean();
207             experimentResult[Strings.json.measurements.concern] = averageComplexity.concern(Experiment.defaults.CONCERN);
208             experimentResult[Strings.json.measurements.stdev] = averageComplexity.standardDeviation();
209             experimentResult[Strings.json.measurements.percent] = averageComplexity.percentage();
210
211             experimentResult = {};
212             result[Strings.json.frameLength] = experimentResult;
213             experimentResult[Strings.json.measurements.average] = 1000 / averageFrameLength.mean();
214             experimentResult[Strings.json.measurements.concern] = averageFrameLength.concern(Experiment.defaults.CONCERN);
215             experimentResult[Strings.json.measurements.stdev] = averageFrameLength.standardDeviation();
216             experimentResult[Strings.json.measurements.percent] = averageFrameLength.percentage();
217
218             result[Strings.json.score] = averageComplexity.score(Experiment.defaults.CONCERN);
219             result[Strings.json.scoreLowerBound] = result[Strings.json.score] - averageFrameLength.standardDeviation();
220             result[Strings.json.scoreUpperBound] = result[Strings.json.score] + averageFrameLength.standardDeviation();
221         }
222     },
223
224     get data()
225     {
226         return this._iterationsSamplers;
227     },
228
229     get results()
230     {
231         if (this._results)
232             return this._results[Strings.json.results.iterations];
233         this._processData();
234         return this._results[Strings.json.results.iterations];
235     },
236
237     get options()
238     {
239         return this._options;
240     },
241
242     _getResultsProperty: function(property)
243     {
244         if (this._results)
245             return this._results[property];
246         this._processData();
247         return this._results[property];
248     },
249
250     get score()
251     {
252         return this._getResultsProperty(Strings.json.score);
253     },
254
255     get scoreLowerBound()
256     {
257         return this._getResultsProperty(Strings.json.scoreLowerBound);
258     },
259
260     get scoreUpperBound()
261     {
262         return this._getResultsProperty(Strings.json.scoreUpperBound);
263     }
264 });
265
266 ResultsTable = Utilities.createClass(
267     function(element, headers)
268     {
269         this.element = element;
270         this._headers = headers;
271
272         this._flattenedHeaders = [];
273         this._headers.forEach(function(header) {
274             if (header.disabled)
275                 return;
276
277             if (header.children)
278                 this._flattenedHeaders = this._flattenedHeaders.concat(header.children);
279             else
280                 this._flattenedHeaders.push(header);
281         }, this);
282
283         this._flattenedHeaders = this._flattenedHeaders.filter(function (header) {
284             return !header.disabled;
285         });
286
287         this.clear();
288     }, {
289
290     clear: function()
291     {
292         this.element.textContent = "";
293     },
294
295     _addHeader: function()
296     {
297         var thead = Utilities.createElement("thead", {}, this.element);
298         var row = Utilities.createElement("tr", {}, thead);
299
300         this._headers.forEach(function (header) {
301             if (header.disabled)
302                 return;
303
304             var th = Utilities.createElement("th", {}, row);
305             if (header.title != Strings.text.graph)
306                 th.innerHTML = header.title;
307             if (header.children)
308                 th.colSpan = header.children.length;
309         });
310     },
311
312     _addBody: function()
313     {
314         this.tbody = Utilities.createElement("tbody", {}, this.element);
315     },
316
317     _addEmptyRow: function()
318     {
319         var row = Utilities.createElement("tr", {}, this.tbody);
320         this._flattenedHeaders.forEach(function (header) {
321             return Utilities.createElement("td", { class: "suites-separator" }, row);
322         });
323     },
324
325     _addTest: function(testName, testResult, options)
326     {
327         var row = Utilities.createElement("tr", {}, this.tbody);
328
329         this._flattenedHeaders.forEach(function (header) {
330             var td = Utilities.createElement("td", {}, row);
331             if (header.text == Strings.text.testName) {
332                 td.textContent = testName;
333             } else if (typeof header.text == "string") {
334                 var data = testResult[header.text];
335                 if (typeof data == "number")
336                     data = data.toFixed(2);
337                 td.innerHTML = data;
338             } else
339                 td.innerHTML = header.text(testResult);
340         }, this);
341     },
342
343     _addIteration: function(iterationResult, iterationData, options)
344     {
345         var testsResults = iterationResult[Strings.json.results.tests];
346         for (var suiteName in testsResults) {
347             this._addEmptyRow();
348             var suiteResult = testsResults[suiteName];
349             var suiteData = iterationData[suiteName];
350             for (var testName in suiteResult)
351                 this._addTest(testName, suiteResult[testName], options, suiteData[testName]);
352         }
353     },
354
355     showIterations: function(dashboard)
356     {
357         this.clear();
358         this._addHeader();
359         this._addBody();
360
361         var iterationsResults = dashboard.results;
362         iterationsResults.forEach(function(iterationResult, index) {
363             this._addIteration(iterationResult, dashboard.data[index], dashboard.options);
364         }, this);
365     }
366 });
367
368 window.benchmarkRunnerClient = {
369     iterationCount: 1,
370     options: null,
371     results: null,
372
373     initialize: function(suites, options)
374     {
375         this.options = options;
376     },
377
378     willStartFirstIteration: function()
379     {
380         this.results = new ResultsDashboard(this.options);
381     },
382
383     didRunSuites: function(suitesSamplers)
384     {
385         this.results.push(suitesSamplers);
386     },
387
388     didRunTest: function(testData)
389     {
390         this.results.calculateScore(testData);
391     },
392
393     didFinishLastIteration: function()
394     {
395         benchmarkController.showResults();
396     }
397 };
398
399 window.sectionsManager =
400 {
401     showSection: function(sectionIdentifier, pushState)
402     {
403         var sections = document.querySelectorAll("main > section");
404         for (var i = 0; i < sections.length; ++i) {
405             document.body.classList.remove("showing-" + sections[i].id);
406         }
407         document.body.classList.add("showing-" + sectionIdentifier);
408
409         var currentSectionElement = document.querySelector("section.selected");
410         console.assert(currentSectionElement);
411
412         var newSectionElement = document.getElementById(sectionIdentifier);
413         console.assert(newSectionElement);
414
415         currentSectionElement.classList.remove("selected");
416         newSectionElement.classList.add("selected");
417
418         if (pushState)
419             history.pushState({section: sectionIdentifier}, document.title);
420     },
421
422     setSectionScore: function(sectionIdentifier, score, confidence)
423     {
424         document.querySelector("#" + sectionIdentifier + " .score").textContent = score;
425         if (confidence)
426             document.querySelector("#" + sectionIdentifier + " .confidence").textContent = confidence;
427     },
428
429     populateTable: function(tableIdentifier, headers, dashboard)
430     {
431         var table = new ResultsTable(document.getElementById(tableIdentifier), headers);
432         table.showIterations(dashboard);
433     }
434 };
435
436 window.benchmarkController = {
437     initialize: function()
438     {
439         benchmarkController.addOrientationListenerIfNecessary();
440     },
441
442     determineCanvasSize: function() {
443         var match = window.matchMedia("(max-device-width: 760px)");
444         if (match.matches) {
445             document.body.classList.add("small");
446             return;
447         }
448
449         match = window.matchMedia("(max-device-width: 1600px)");
450         if (match.matches) {
451             document.body.classList.add("medium");
452             return;
453         }
454
455         match = window.matchMedia("(max-width: 1600px)");
456         if (match.matches) {
457             document.body.classList.add("medium");
458             return;
459         }
460
461         document.body.classList.add("large");
462     },
463
464     addOrientationListenerIfNecessary: function() {
465         if (!("orientation" in window))
466             return;
467
468         this.orientationQuery = window.matchMedia("(orientation: landscape)");
469         this._orientationChanged(this.orientationQuery);
470         this.orientationQuery.addListener(this._orientationChanged);
471     },
472
473     _orientationChanged: function(match)
474     {
475         benchmarkController.isInLandscapeOrientation = match.matches;
476         if (match.matches)
477             document.querySelector(".start-benchmark p").classList.add("hidden");
478         else
479             document.querySelector(".start-benchmark p").classList.remove("hidden");
480         benchmarkController.updateStartButtonState();
481     },
482
483     updateStartButtonState: function()
484     {
485         document.getElementById("run-benchmark").disabled = !this.isInLandscapeOrientation;
486     },
487
488     _startBenchmark: function(suites, options, frameContainerID)
489     {
490         benchmarkController.determineCanvasSize();
491
492         var configuration = document.body.className.match(/small|medium|large/);
493         if (configuration)
494             options[Strings.json.configuration] = configuration[0];
495
496         benchmarkRunnerClient.initialize(suites, options);
497         var frameContainer = document.getElementById(frameContainerID);
498         var runner = new BenchmarkRunner(suites, frameContainer, benchmarkRunnerClient);
499         runner.runMultipleIterations();
500
501         sectionsManager.showSection("test-container");
502     },
503
504     startBenchmark: function()
505     {
506         var options = {
507             "test-interval": 30,
508             "display": "minimal",
509             "tiles": "big",
510             "controller": "ramp",
511             "kalman-process-error": 1,
512             "kalman-measurement-error": 4,
513             "time-measurement": "performance"
514         };
515         this._startBenchmark(Suites, options, "test-container");
516     },
517
518     showResults: function()
519     {
520         if (!this.addedKeyEvent) {
521             document.addEventListener("keypress", this.handleKeyPress, false);
522             this.addedKeyEvent = true;
523         }
524
525         var dashboard = benchmarkRunnerClient.results;
526         var score = dashboard.score;
527         var confidence = "±" + (Statistics.largestDeviationPercentage(dashboard.scoreLowerBound, score, dashboard.scoreUpperBound) * 100).toFixed(2) + "%";
528         sectionsManager.setSectionScore("results", score.toFixed(2), confidence);
529         sectionsManager.populateTable("results-header", Headers.testName, dashboard);
530         sectionsManager.populateTable("results-score", Headers.score, dashboard);
531         sectionsManager.populateTable("results-data", Headers.details, dashboard);
532         sectionsManager.showSection("results", true);
533     },
534
535     handleKeyPress: function(event)
536     {
537         switch (event.charCode)
538         {
539         case 27:  // esc
540             benchmarkController.hideDebugInfo();
541             break;
542         case 106: // j
543             benchmarkController.showDebugInfo();
544             break;
545         case 115: // s
546             benchmarkController.selectResults(event.target);
547             break;
548         }
549     },
550
551     hideDebugInfo: function()
552     {
553         var overlay = document.getElementById("overlay");
554         if (!overlay)
555             return;
556         document.body.removeChild(overlay);
557     },
558
559     showDebugInfo: function()
560     {
561         if (document.getElementById("overlay"))
562             return;
563
564         var overlay = Utilities.createElement("div", {
565             id: "overlay"
566         }, document.body);
567         var container = Utilities.createElement("div", {}, overlay);
568
569         var header = Utilities.createElement("h3", {}, container);
570         header.textContent = "Debug Output";
571
572         var data = Utilities.createElement("div", {}, container);
573         data.textContent = "Please wait...";
574         setTimeout(function() {
575             var output = {
576                 options: benchmarkRunnerClient.results.options,
577                 data: benchmarkRunnerClient.results.data
578             };
579             data.textContent = JSON.stringify(output, function(key, value) {
580                 if (typeof value === 'number')
581                     return Utilities.toFixedNumber(value, 3);
582                 return value;
583             }, 1);
584         }, 0);
585         data.onclick = function() {
586             var selection = window.getSelection();
587             selection.removeAllRanges();
588             var range = document.createRange();
589             range.selectNode(data);
590             selection.addRange(range);
591         };
592
593         var button = Utilities.createElement("button", {}, container);
594         button.textContent = "Done";
595         button.onclick = function() {
596             benchmarkController.hideDebugInfo();
597         };
598     },
599
600     selectResults: function(target)
601     {
602         target.selectRange = ((target.selectRange || 0) + 1) % 3;
603
604         var selection = window.getSelection();
605         selection.removeAllRanges();
606         var range = document.createRange();
607         switch (target.selectRange) {
608             case 0: {
609                 range.selectNode(document.getElementById("results-score"));
610                 break;
611             }
612             case 1: {
613                 range.setStart(document.querySelector("#results .score"), 0);
614                 range.setEndAfter(document.querySelector("#results-score"), 0);
615                 break;
616             }
617             case 2: {
618                 range.selectNodeContents(document.querySelector("#results .score"));
619                 break;
620             }
621         }
622         selection.addRange(range);
623     }
624 };
625
626 window.addEventListener("load", function() { benchmarkController.initialize(); });