Strip out more characters when creating permalinks
[WebKit-https.git] / PerformanceTests / MotionMark / resources / debug-runner / motionmark.js
1 /*
2  * Copyright (C) 2015-2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25 ProgressBar = Utilities.createClass(
26     function(element, ranges)
27     {
28         this._element = element;
29         this._ranges = ranges;
30         this._currentRange = 0;
31         this._updateElement();
32     }, {
33
34     _updateElement: function()
35     {
36         this._element.style.width = (this._currentRange * (100 / this._ranges)) + "%";
37     },
38
39     incrementRange: function()
40     {
41         ++this._currentRange;
42         this._updateElement();
43     }
44 });
45
46 DeveloperResultsTable = Utilities.createSubclass(ResultsTable,
47     function(element, headers)
48     {
49         ResultsTable.call(this, element, headers);
50     }, {
51
52     _addGraphButton: function(td, testName, testResult, testData)
53     {
54         var button = Utilities.createElement("button", { class: "small-button" }, td);
55         button.textContent = Strings.text.graph + "…";
56         button.testName = testName;
57         button.testResult = testResult;
58         button.testData = testData;
59
60         button.addEventListener("click", function(e) {
61             benchmarkController.showTestGraph(e.target.testName, e.target.testResult, e.target.testData);
62         });
63     },
64
65     _isNoisyMeasurement: function(jsonExperiment, data, measurement, options)
66     {
67         const percentThreshold = 10;
68         const averageThreshold = 2;
69
70         if (measurement == Strings.json.measurements.percent)
71             return data[Strings.json.measurements.percent] >= percentThreshold;
72
73         if (jsonExperiment == Strings.json.frameLength && measurement == Strings.json.measurements.average)
74             return Math.abs(data[Strings.json.measurements.average] - options["frame-rate"]) >= averageThreshold;
75
76         return false;
77     },
78
79     _addTest: function(testName, testResult, options, testData)
80     {
81         var row = Utilities.createElement("tr", {}, this.element);
82
83         var isNoisy = false;
84         [Strings.json.complexity, Strings.json.frameLength].forEach(function (experiment) {
85             var data = testResult[experiment];
86             for (var measurement in data) {
87                 if (this._isNoisyMeasurement(experiment, data, measurement, options))
88                     isNoisy = true;
89             }
90         }, this);
91
92         this._flattenedHeaders.forEach(function (header) {
93             var className = "";
94             if (header.className) {
95                 if (typeof header.className == "function")
96                     className = header.className(testResult, options);
97                 else
98                     className = header.className;
99             }
100
101             if (header.text == Strings.text.testName) {
102                 if (isNoisy)
103                     className += " noisy-results";
104                 var td = Utilities.createElement("td", { class: className }, row);
105                 td.textContent = testName;
106                 return;
107             }
108
109             var td = Utilities.createElement("td", { class: className }, row);
110             if (header.title == Strings.text.graph) {
111                 this._addGraphButton(td, testName, testResult, testData);
112             } else if (!("text" in header)) {
113                 td.textContent = testResult[header.title];
114             } else if (typeof header.text == "string") {
115                 var data = testResult[header.text];
116                 if (typeof data == "number")
117                     data = data.toFixed(2);
118                 td.textContent = data;
119             } else
120                 td.textContent = header.text(testResult);
121         }, this);
122     }
123 });
124
125 Utilities.extendObject(window.benchmarkRunnerClient, {
126     testsCount: null,
127     progressBar: null,
128
129     initialize: function(suites, options)
130     {
131         this.testsCount = this.iterationCount * suites.reduce(function (count, suite) { return count + suite.tests.length; }, 0);
132         this.options = options;
133     },
134
135     willStartFirstIteration: function()
136     {
137         this.results = new ResultsDashboard(this.options);
138         this.progressBar = new ProgressBar(document.getElementById("progress-completed"), this.testsCount);
139     },
140
141     didRunTest: function(testData)
142     {
143         this.progressBar.incrementRange();
144         this.results.calculateScore(testData);
145     }
146 });
147
148 Utilities.extendObject(window.sectionsManager, {
149     setSectionHeader: function(sectionIdentifier, title)
150     {
151         document.querySelector("#" + sectionIdentifier + " h1").textContent = title;
152     },
153
154     populateTable: function(tableIdentifier, headers, dashboard)
155     {
156         var table = new DeveloperResultsTable(document.getElementById(tableIdentifier), headers);
157         table.showIterations(dashboard);
158     }
159 });
160
161 window.optionsManager =
162 {
163     valueForOption: function(name)
164     {
165         var formElement = document.forms["benchmark-options"].elements[name];
166         if (formElement.type == "checkbox")
167             return formElement.checked;
168         else if (formElement.constructor === HTMLCollection) {
169             for (var i = 0; i < formElement.length; ++i) {
170                 var radio = formElement[i];
171                 if (radio.checked)
172                     return formElement.value;
173             }
174             return null;
175         }
176         return formElement.value;
177     },
178
179     updateUIFromLocalStorage: function()
180     {
181         var formElements = document.forms["benchmark-options"].elements;
182
183         for (var i = 0; i < formElements.length; ++i) {
184             var formElement = formElements[i];
185             var name = formElement.id || formElement.name;
186             var type = formElement.type;
187
188             var value = localStorage.getItem(name);
189             if (value === null)
190                 continue;
191
192             if (type == "number")
193                 formElements[name].value = +value;
194             else if (type == "checkbox")
195                 formElements[name].checked = value == "true";
196             else if (type == "radio")
197                 formElements[name].value = value;
198         }
199     },
200
201     updateLocalStorageFromUI: function()
202     {
203         var formElements = document.forms["benchmark-options"].elements;
204         var options = {};
205
206         for (var i = 0; i < formElements.length; ++i) {
207             var formElement = formElements[i];
208             var name = formElement.id || formElement.name;
209             var type = formElement.type;
210
211             if (type == "number")
212                 options[name] = +formElement.value;
213             else if (type == "checkbox")
214                 options[name] = formElement.checked;
215             else if (type == "radio") {
216                 var radios = formElements[name];
217                 if (radios.constructor === HTMLCollection) {
218                     for (var j = 0; j < radios.length; ++j) {
219                         var radio = radios[j];
220                         if (radio.checked) {
221                             options[name] = radio.value;
222                             break;
223                         }
224                     }
225                 } else
226                     options[name] = formElements[name].value;
227             }
228
229             try {
230                 localStorage.setItem(name, options[name]);
231             } catch (e) {}
232         }
233
234         return options;
235     },
236
237     updateDisplay: function()
238     {
239         document.body.classList.remove("display-minimal");
240         document.body.classList.remove("display-progress-bar");
241
242         document.body.classList.add("display-" + optionsManager.valueForOption("display"));
243     },
244     
245     updateTiles: function()
246     {
247         document.body.classList.remove("tiles-big");
248         document.body.classList.remove("tiles-classic");
249
250         document.body.classList.add("tiles-" + optionsManager.valueForOption("tiles"));
251     }
252 };
253
254 window.suitesManager =
255 {
256     _treeElement: function()
257     {
258         return document.querySelector("#suites > .tree");
259     },
260
261     _suitesElements: function()
262     {
263         return document.querySelectorAll("#suites > ul > li");
264     },
265
266     _checkboxElement: function(element)
267     {
268         return element.querySelector("input[type='checkbox']:not(.expand-button)");
269     },
270
271     _editElement: function(element)
272     {
273         return element.querySelector("input[type='number']");
274     },
275
276     _editsElements: function()
277     {
278         return document.querySelectorAll("#suites input[type='number']");
279     },
280
281     _localStorageNameForTest: function(suiteName, testName)
282     {
283         return suiteName + "/" + testName;
284     },
285
286     _updateSuiteCheckboxState: function(suiteCheckbox)
287     {
288         var numberEnabledTests = 0;
289         suiteCheckbox.testsElements.forEach(function(testElement) {
290             var testCheckbox = this._checkboxElement(testElement);
291             if (testCheckbox.checked)
292                 ++numberEnabledTests;
293         }, this);
294         suiteCheckbox.checked = numberEnabledTests > 0;
295         suiteCheckbox.indeterminate = numberEnabledTests > 0 && numberEnabledTests < suiteCheckbox.testsElements.length;
296     },
297
298     isAtLeastOneTestSelected: function()
299     {
300         var suitesElements = this._suitesElements();
301
302         for (var i = 0; i < suitesElements.length; ++i) {
303             var suiteElement = suitesElements[i];
304             var suiteCheckbox = this._checkboxElement(suiteElement);
305
306             if (suiteCheckbox.checked)
307                 return true;
308         }
309
310         return false;
311     },
312
313     _onChangeSuiteCheckbox: function(event)
314     {
315         var selected = event.target.checked;
316         event.target.testsElements.forEach(function(testElement) {
317             var testCheckbox = this._checkboxElement(testElement);
318             testCheckbox.checked = selected;
319         }, this);
320         benchmarkController.updateStartButtonState();
321     },
322
323     _onChangeTestCheckbox: function(suiteCheckbox)
324     {
325         this._updateSuiteCheckboxState(suiteCheckbox);
326         benchmarkController.updateStartButtonState();
327     },
328
329     _createSuiteElement: function(treeElement, suite, id)
330     {
331         var suiteElement = Utilities.createElement("li", {}, treeElement);
332         var expand = Utilities.createElement("input", { type: "checkbox",  class: "expand-button", id: id }, suiteElement);
333         var label = Utilities.createElement("label", { class: "tree-label", for: id }, suiteElement);
334
335         var suiteCheckbox = Utilities.createElement("input", { type: "checkbox" }, label);
336         suiteCheckbox.suite = suite;
337         suiteCheckbox.onchange = this._onChangeSuiteCheckbox.bind(this);
338         suiteCheckbox.testsElements = [];
339
340         label.appendChild(document.createTextNode(" " + suite.name));
341         return suiteElement;
342     },
343
344     _createTestElement: function(listElement, test, suiteCheckbox)
345     {
346         var testElement = Utilities.createElement("li", {}, listElement);
347         var span = Utilities.createElement("label", { class: "tree-label" }, testElement);
348
349         var testCheckbox = Utilities.createElement("input", { type: "checkbox" }, span);
350         testCheckbox.test = test;
351         testCheckbox.onchange = function(event) {
352             this._onChangeTestCheckbox(event.target.suiteCheckbox);
353         }.bind(this);
354         testCheckbox.suiteCheckbox = suiteCheckbox;
355
356         suiteCheckbox.testsElements.push(testElement);
357         span.appendChild(document.createTextNode(" " + test.name + " "));
358
359         testElement.appendChild(document.createTextNode(" "));
360         var link = Utilities.createElement("span", {}, testElement);
361         link.classList.add("link");
362         link.textContent = "link";
363         link.suiteName = Utilities.stripUnwantedCharactersForURL(suiteCheckbox.suite.name);
364         link.testName = test.name;
365         link.onclick = function(event) {
366             var element = event.target;
367             var title = "Link to run “" + element.testName + "” with current options:";
368             var url = location.href.split(/[?#]/)[0];
369             var options = optionsManager.updateLocalStorageFromUI();
370             Utilities.extendObject(options, {
371                 "suite-name": element.suiteName,
372                 "test-name": Utilities.stripUnwantedCharactersForURL(element.testName)
373             });
374             var complexity = suitesManager._editElement(element.parentNode).value;
375             if (complexity)
376                 options.complexity = complexity;
377             prompt(title, url + Utilities.convertObjectToQueryString(options));
378         };
379
380         var complexity = Utilities.createElement("input", { type: "number" }, testElement);
381         complexity.relatedCheckbox = testCheckbox;
382         complexity.oninput = function(event) {
383             var relatedCheckbox = event.target.relatedCheckbox;
384             relatedCheckbox.checked = true;
385             this._onChangeTestCheckbox(relatedCheckbox.suiteCheckbox);
386         }.bind(this);
387         return testElement;
388     },
389
390     createElements: function()
391     {
392         var treeElement = this._treeElement();
393
394         Suites.forEach(function(suite, index) {
395             var suiteElement = this._createSuiteElement(treeElement, suite, "suite-" + index);
396             var listElement = Utilities.createElement("ul", {}, suiteElement);
397             var suiteCheckbox = this._checkboxElement(suiteElement);
398
399             suite.tests.forEach(function(test) {
400                 this._createTestElement(listElement, test, suiteCheckbox);
401             }, this);
402         }, this);
403     },
404
405     updateEditsElementsState: function()
406     {
407         var editsElements = this._editsElements();
408         var showComplexityInputs = optionsManager.valueForOption("controller") == "fixed";
409
410         for (var i = 0; i < editsElements.length; ++i) {
411             var editElement = editsElements[i];
412             if (showComplexityInputs)
413                 editElement.classList.add("selected");
414             else
415                 editElement.classList.remove("selected");
416         }
417     },
418
419     updateUIFromLocalStorage: function()
420     {
421         var suitesElements = this._suitesElements();
422
423         for (var i = 0; i < suitesElements.length; ++i) {
424             var suiteElement = suitesElements[i];
425             var suiteCheckbox = this._checkboxElement(suiteElement);
426             var suite = suiteCheckbox.suite;
427
428             suiteCheckbox.testsElements.forEach(function(testElement) {
429                 var testCheckbox = this._checkboxElement(testElement);
430                 var testEdit = this._editElement(testElement);
431                 var test = testCheckbox.test;
432
433                 var str = localStorage.getItem(this._localStorageNameForTest(suite.name, test.name));
434                 if (str === null)
435                     return;
436
437                 var value = JSON.parse(str);
438                 testCheckbox.checked = value.checked;
439                 testEdit.value = value.complexity;
440             }, this);
441
442             this._updateSuiteCheckboxState(suiteCheckbox);
443         }
444
445         benchmarkController.updateStartButtonState();
446     },
447
448     updateLocalStorageFromUI: function()
449     {
450         var suitesElements = this._suitesElements();
451         var suites = [];
452
453         for (var i = 0; i < suitesElements.length; ++i) {
454             var suiteElement = suitesElements[i];
455             var suiteCheckbox = this._checkboxElement(suiteElement);
456             var suite = suiteCheckbox.suite;
457
458             var tests = [];
459             suiteCheckbox.testsElements.forEach(function(testElement) {
460                 var testCheckbox = this._checkboxElement(testElement);
461                 var testEdit = this._editElement(testElement);
462                 var test = testCheckbox.test;
463
464                 if (testCheckbox.checked) {
465                     test.complexity = testEdit.value;
466                     tests.push(test);
467                 }
468
469                 var value = { checked: testCheckbox.checked, complexity: testEdit.value };
470                 try {
471                     localStorage.setItem(this._localStorageNameForTest(suite.name, test.name), JSON.stringify(value));
472                 } catch (e) {}
473             }, this);
474
475             if (tests.length)
476                 suites.push(new Suite(suiteCheckbox.suite.name, tests));
477         }
478
479         return suites;
480     },
481
482     suitesFromQueryString: function(suiteName, testName)
483     {
484         suiteName = decodeURIComponent(suiteName);
485         testName = decodeURIComponent(testName);
486
487         var suites = [];
488         var suiteRegExp = new RegExp(suiteName, "i");
489         var testRegExp = new RegExp(testName, "i");
490
491         for (var i = 0; i < Suites.length; ++i) {
492             var suite = Suites[i];
493             if (!Utilities.stripUnwantedCharactersForURL(suite.name).match(suiteRegExp))
494                 continue;
495
496             var test;
497             for (var j = 0; j < suite.tests.length; ++j) {
498                 suiteTest = suite.tests[j];
499                 if (Utilities.stripUnwantedCharactersForURL(suiteTest.name).match(testRegExp)) {
500                     test = suiteTest;
501                     break;
502                 }
503             }
504
505             if (!test)
506                 continue;
507
508             suites.push(new Suite(suiteName, [test]));
509         };
510
511         return suites;
512     },
513
514     updateLocalStorageFromJSON: function(results)
515     {
516         for (var suiteName in results[Strings.json.results.tests]) {
517             var suiteResults = results[Strings.json.results.tests][suiteName];
518             for (var testName in suiteResults) {
519                 var testResults = suiteResults[testName];
520                 var data = testResults[Strings.json.controller];
521                 var complexity = Math.round(data[Strings.json.measurements.average]);
522
523                 var value = { checked: true, complexity: complexity };
524                 try {
525                     localStorage.setItem(this._localStorageNameForTest(suiteName, testName), JSON.stringify(value));
526                 } catch (e) {}
527             }
528         }
529     }
530 }
531
532 Utilities.extendObject(window.benchmarkController, {
533     initialize: function()
534     {
535         document.forms["benchmark-options"].addEventListener("change", benchmarkController.onBenchmarkOptionsChanged, true);
536         document.forms["graph-type"].addEventListener("change", benchmarkController.onGraphTypeChanged, true);
537         document.forms["time-graph-options"].addEventListener("change", benchmarkController.onTimeGraphOptionsChanged, true);
538         document.forms["complexity-graph-options"].addEventListener("change", benchmarkController.onComplexityGraphOptionsChanged, true);
539         optionsManager.updateUIFromLocalStorage();
540         optionsManager.updateDisplay();
541         optionsManager.updateTiles();
542
543         if (benchmarkController.startBenchmarkImmediatelyIfEncoded())
544             return;
545
546         benchmarkController.addOrientationListenerIfNecessary();
547         suitesManager.createElements();
548         suitesManager.updateUIFromLocalStorage();
549         suitesManager.updateEditsElementsState();
550
551         var dropTarget = document.getElementById("drop-target");
552         function stopEvent(e) {
553             e.stopPropagation();
554             e.preventDefault();
555         }
556         dropTarget.addEventListener("dragenter", stopEvent, false);
557         dropTarget.addEventListener("dragover", stopEvent, false);
558         dropTarget.addEventListener("dragleave", stopEvent, false);
559         dropTarget.addEventListener("drop", function (e) {
560             e.stopPropagation();
561             e.preventDefault();
562
563             if (!e.dataTransfer.files.length)
564                 return;
565
566             var file = e.dataTransfer.files[0];
567
568             var reader = new FileReader();
569             reader.filename = file.name;
570             reader.onload = function(e) {
571                 var run = JSON.parse(e.target.result);
572                 if (run.debugOutput instanceof Array)
573                     run = run.debugOutput[0];
574                 benchmarkRunnerClient.results = new ResultsDashboard(run.options, run.data);
575                 benchmarkController.showResults();
576             };
577
578             reader.readAsText(file);
579             document.title = "File: " + reader.filename;
580         }, false);
581     },
582
583     updateStartButtonState: function()
584     {
585         var startButton = document.getElementById("run-benchmark");
586         if ("isInLandscapeOrientation" in this && !this.isInLandscapeOrientation) {
587             startButton.disabled = true;
588             return;
589         }
590         startButton.disabled = !suitesManager.isAtLeastOneTestSelected();
591     },
592
593     onBenchmarkOptionsChanged: function(event)
594     {
595         switch (event.target.name) {
596         case "controller":
597             suitesManager.updateEditsElementsState();
598             break;
599         case "display":
600             optionsManager.updateDisplay();
601             break;
602         case "tiles":
603             optionsManager.updateTiles();
604             break;
605         }
606     },
607
608     startBenchmark: function()
609     {
610         benchmarkController.determineCanvasSize();
611         benchmarkController.options = optionsManager.updateLocalStorageFromUI();
612         benchmarkController.suites = suitesManager.updateLocalStorageFromUI();
613         this._startBenchmark(benchmarkController.suites, benchmarkController.options, "running-test");
614     },
615
616     startBenchmarkImmediatelyIfEncoded: function()
617     {
618         benchmarkController.options = Utilities.convertQueryStringToObject(location.search);
619         if (!benchmarkController.options)
620             return false;
621
622         benchmarkController.suites = suitesManager.suitesFromQueryString(benchmarkController.options["suite-name"], benchmarkController.options["test-name"]);
623         if (!benchmarkController.suites.length)
624             return false;
625
626         setTimeout(function() {
627             this._startBenchmark(benchmarkController.suites, benchmarkController.options, "running-test");
628         }.bind(this), 0);
629         return true;
630     },
631
632     restartBenchmark: function()
633     {
634         this._startBenchmark(benchmarkController.suites, benchmarkController.options, "running-test");
635     },
636
637     showResults: function()
638     {
639         if (!this.addedKeyEvent) {
640             document.addEventListener("keypress", this.handleKeyPress, false);
641             this.addedKeyEvent = true;
642         }
643
644         var dashboard = benchmarkRunnerClient.results;
645         if (dashboard.options["controller"] == "ramp")
646             Headers.details[3].disabled = true;
647         else {
648             Headers.details[1].disabled = true;
649             Headers.details[4].disabled = true;
650         }
651
652         if (dashboard.options[Strings.json.configuration]) {
653             document.body.classList.remove("small", "medium", "large");
654             document.body.classList.add(dashboard.options[Strings.json.configuration]);
655         }
656
657         var score = dashboard.score;
658         var confidence = ((dashboard.scoreLowerBound / score - 1) * 100).toFixed(2) +
659             "% / +" + ((dashboard.scoreUpperBound / score - 1) * 100).toFixed(2) + "%";
660         sectionsManager.setSectionScore("results", score.toFixed(2), confidence);
661         sectionsManager.populateTable("results-header", Headers.testName, dashboard);
662         sectionsManager.populateTable("results-score", Headers.score, dashboard);
663         sectionsManager.populateTable("results-data", Headers.details, dashboard);
664         sectionsManager.showSection("results", true);
665
666         suitesManager.updateLocalStorageFromJSON(dashboard.results[0]);
667     },
668
669     showTestGraph: function(testName, testResult, testData)
670     {
671         sectionsManager.setSectionHeader("test-graph", testName);
672         sectionsManager.showSection("test-graph", true);
673         this.updateGraphData(testResult, testData, benchmarkRunnerClient.results.options);
674     }
675 });