c7de1e7dfba9a070fff450147c33eee8d7665567
[WebKit-https.git] / Websites / perf.webkit.org / public / js / helper-classes.js
1
2 // A point in a plot.
3 function PerfTestResult(runs, result, associatedBuild) {
4     this.metric = function () { return runs.metric(); }
5     this.values = function () { return result.values ? result.values.map(function (value) { return runs.scalingFactor() * value; }) : undefined; }
6     this.mean = function () { return runs.scalingFactor() * result.mean; }
7     this.unscaledMean = function () { return result.mean; }
8     this.confidenceIntervalDelta = function () {
9         return runs.scalingFactor() * this.unscaledConfidenceIntervalDelta();
10     }
11     this.unscaledConfidenceIntervalDelta = function (defaultValue) {
12         var delta = Statistics.confidenceIntervalDelta(0.95, result.iterationCount, result.sum, result.squareSum);
13         if (isNaN(delta) && defaultValue !== undefined)
14             return defaultValue;
15         return delta;
16     }
17     this.isInUnscaledInterval = function (min, max) {
18         var mean = this.unscaledMean();
19         var delta = this.unscaledConfidenceIntervalDelta(0);
20         return min <= mean - delta && mean + delta <= max;
21     }
22     this.isBetterThan = function(other) { return runs.smallerIsBetter() == (this.mean() < other.mean()); }
23     this.relativeDifference = function(other) { return (other.mean() - this.mean()) / this.mean(); }
24     this.formattedRelativeDifference = function (other) { return Math.abs(this.relativeDifference(other) * 100).toFixed(2) + '%'; }
25     this.formattedProgressionOrRegression = function (previousResult) {
26         return previousResult.formattedRelativeDifference(this) + ' ' + (this.isBetterThan(previousResult) ? 'better' : 'worse');
27     }
28     this.isStatisticallySignificant = function (other) {
29         var diff = Math.abs(other.mean() - this.mean());
30         return diff > this.confidenceIntervalDelta() && diff > other.confidenceIntervalDelta();
31     }
32     this.build = function () { return associatedBuild; }
33     this.label = function (previousResult) {
34         var mean = this.mean();
35         var label = mean.toPrecision(4) + ' ' + runs.unit();
36
37         var delta = this.confidenceIntervalDelta();
38         if (delta) {
39             var percentageStdev = delta * 100 / mean;
40             label += ' &plusmn; ' + percentageStdev.toFixed(2) + '%';
41         }
42
43         if (previousResult)
44             label += ' (' + this.formattedProgressionOrRegression(previousResult) + ')';
45
46         return label;
47     }
48 }
49
50 function TestBuild(repositories, builders, platform, rawRun) {
51     const revisions = rawRun.revisions;
52     var maxTime = 0;
53     var revisionCount = 0;
54     for (var repositoryId in revisions) {
55         maxTime = Math.max(maxTime, revisions[repositoryId][1]); // Revision is an pair (revision, time)
56         revisionCount++;
57     }
58     if (!maxTime)
59         maxTime = rawRun.buildTime;
60     maxTime = TestBuild.UTCtoPST(maxTime);
61     var maxTimeString;
62     var buildTime = TestBuild.UTCtoPST(rawRun.buildTime);
63     var buildTimeString;
64
65     this.time = function () { return maxTime; }
66     this.formattedTime = function () {
67         if (!maxTimeString)
68             maxTimeString = new Date(maxTime).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
69         return maxTimeString;
70     }
71     this.buildTime = function () { return buildTime; }
72     this.formattedBuildTime = function () {
73         if (!buildTimeString)
74             buildTimeString = new Date(buildTime).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
75         return buildTimeString;
76     }
77     this.builder = function () { return builders[rawRun.builder].name; }
78     this.buildNumber = function () { return rawRun.buildNumber; }
79     this.buildUrl = function () {
80         var builder = builders[rawRun.builder];
81         var template = builder.buildUrl;
82         if (!template)
83             return null;
84         return template.replace(/\$buildNumber/g, this.buildNumber()).replace(/\$builderName/g, builder.name);
85     }
86     this.platform = function () { return platform; }
87     this.revision = function(repositoryId) { return revisions[repositoryId][0]; }
88     this.formattedRevisions = function (previousBuild) {
89         var result = {};
90         for (var repositoryId in repositories) {
91             if (!revisions[repositoryId])
92                 continue;
93             var previousRevision = previousBuild ? previousBuild.revision(repositoryId) : undefined;
94             var currentRevision = this.revision(repositoryId);
95             if (previousRevision === currentRevision)
96                 previousRevision = undefined;
97
98             var revisionPrefix = '';
99             var revisionDelimiter = '-';
100             var isHash = false;
101             if (parseInt(currentRevision) == currentRevision) { // e.g. r12345.
102                 revisionPrefix = 'r';
103                 if (previousRevision)
104                     previousRevision = (parseInt(previousRevision) + 1);
105             } else if (currentRevision.indexOf(' ') >= 0) // e.g. 10.9 13C64.
106                 revisionDelimiter = ' - ';
107             else if (currentRevision.length == 40) // e.g. git hash
108                 isHash = true;
109
110             var labelForThisRepository;
111             if (isHash) {
112                 formattedCurrentHash = currentRevision.substring(0, 8);
113                 if (previousRevision)
114                     labelForThisRepository = previousRevision.substring(0, 8) + '..' + formattedCurrentHash;
115                 else
116                     labelForThisRepository = '@ ' + formattedCurrentHash;
117             } else {
118                 if (previousRevision)
119                     labelForThisRepository = revisionPrefix + previousRevision + revisionDelimiter + revisionPrefix + currentRevision;
120                 else
121                     labelForThisRepository = '@ ' + revisionPrefix + currentRevision;
122             }
123
124             var url;
125             var repository = repositories[repositoryId];
126             if (repository) {
127                 if (previousRevision)
128                     url = (repository['blameUrl'] || '').replace(/\$1/g, previousRevision).replace(/\$2/g, currentRevision);
129                 else
130                     url = (repository['url'] || '').replace(/\$1/g, currentRevision);
131             }
132
133             result[repository.name] = {
134                 'label': labelForThisRepository,
135                 'currentRevision': currentRevision,
136                 'previousRevision': previousRevision,
137                 'url': url,
138             };
139         }
140         return result;
141     }
142 }
143
144 TestBuild.UTCtoPST = function (date) {
145     // Pretend that PST is UTC since vanilla flot doesn't support multiple timezones.
146     const PSTOffsetInMilliseconds = 8 * 3600 * 1000;
147     return date - PSTOffsetInMilliseconds;
148 }
149 TestBuild.now = function () { return this.UTCtoPST(Date.now()); }
150
151 // A sequence of test results for a specific test on a specific platform
152 function PerfTestRuns(metric, platform) {
153     var results = [];
154     var cachedUnit = null;
155     var cachedScalingFactor = null;
156     var baselines = {};
157     var suffix = metric.name.match('([A-z][a-z]+|FrameRate)$')[0];
158     var unit = {'Combined': '', // Assume smaller is better for now.
159         'FrameRate': 'fps',
160         'Runs': '/s',
161         'Time': 'ms',
162         'Malloc': 'B',
163         'Heap': 'B',
164         'Allocations': 'B'}[suffix];
165
166     // We can't do this in PerfTestResult because all results for each metric need to share the same unit and the same scaling factor.
167     function computeScalingFactorIfNeeded() {
168         // FIXME: We shouldn't be adjusting units on every test result.
169         // We can only do this on the first test.
170         if (!results.length || cachedUnit)
171             return;
172
173         var mean = results[0].unscaledMean(); // FIXME: We should look at all values.
174         var kilo = unit == 'B' ? 1024 : 1000;
175         if (mean > 2 * kilo * kilo && unit != 'ms') {
176             cachedScalingFactor = 1 / kilo / kilo;
177             var unitFirstChar = unit.charAt(0);
178             cachedUnit = 'M' + (unitFirstChar.toUpperCase() == unitFirstChar ? '' : ' ') + unit;
179         } else if (mean > 2 * kilo) {
180             cachedScalingFactor = 1 / kilo;
181             cachedUnit = unit == 'ms' ? 's' : ('K ' + unit);
182         } else {
183             cachedScalingFactor = 1;
184             cachedUnit = unit;
185         }
186     }
187
188     this.metric = function () { return metric; }
189     this.platform = function () { return platform; }
190     this.setResults = function (newResults) {
191         results = newResults;
192         cachedUnit = null;
193         cachedScalingFactor = null;
194     }
195     this.lastResult = function () { return results[results.length - 1]; }
196     this.resultAt = function (i) { return results[i]; }
197
198     var resultsFilterCache;
199     var resultsFilterCacheMinTime;
200     function filteredResults(minTime) {
201         if (!minTime)
202             return results;
203         if (resultsFilterCacheMinTime != minTime) {
204             resultsFilterCache = results.filter(function (result) { return !minTime || result.build().time() >= minTime; });
205             resultsFilterCacheMinTime = minTime;
206         }
207         return resultsFilterCache;
208     }
209
210     function unscaledMeansForAllResults(minTime) {
211         return filteredResults(minTime).map(function (result) { return result.unscaledMean(); });
212     }
213
214     this.min = function (minTime) {
215         return this.scalingFactor() * filteredResults(minTime)
216             .reduce(function (minSoFar, result) { return Math.min(minSoFar, result.unscaledMean() - result.unscaledConfidenceIntervalDelta(0)); }, Number.MAX_VALUE);
217     }
218     this.max = function (minTime, baselineName) {
219         return this.scalingFactor() * filteredResults(minTime)
220             .reduce(function (maxSoFar, result) { return Math.max(maxSoFar, result.unscaledMean() + result.unscaledConfidenceIntervalDelta(0)); }, Number.MIN_VALUE);
221     }
222     this.countResults = function (minTime) {
223         return unscaledMeansForAllResults(minTime).length;
224     }
225     this.countResultsInInterval = function (minTime, min, max) {
226         var unscaledMin = min / this.scalingFactor();
227         var unscaledMax = max / this.scalingFactor();
228         return filteredResults(minTime).reduce(function (count, currentResult) {
229             return count + (currentResult.isInUnscaledInterval(unscaledMin, unscaledMax) ? 1 : 0); }, 0);
230     }
231     this.sampleStandardDeviation = function (minTime) {
232         var unscaledMeans = unscaledMeansForAllResults(minTime);
233         return this.scalingFactor() * Statistics.sampleStandardDeviation(unscaledMeans.length, Statistics.sum(unscaledMeans), Statistics.squareSum(unscaledMeans));
234     }
235     this.exponentialMovingArithmeticMean = function (minTime, alpha) {
236         var unscaledMeans = unscaledMeansForAllResults(minTime);
237         if (!unscaledMeans.length)
238             return NaN;
239         return this.scalingFactor() * unscaledMeans.reduce(function (movingAverage, currentMean) { return alpha * currentMean + (1 - alpha) * movingAverage; });
240     }
241     this.hasConfidenceInterval = function () { return !isNaN(this.lastResult().unscaledConfidenceIntervalDelta()); }
242     var meanPlotCache;
243     this.meanPlotData = function () {
244         if (!meanPlotCache)
245             meanPlotCache = results.map(function (result, index) { return [result.build().time(), result.mean()]; });
246         return meanPlotCache;
247     }
248     var upperConfidenceCache;
249     this.upperConfidencePlotData = function () {
250         if (!upperConfidenceCache) // FIXME: Use the actual confidence interval
251             upperConfidenceCache = results.map(function (result, index) { return [result.build().time(), result.mean() + result.confidenceIntervalDelta()]; });
252         return upperConfidenceCache;
253     }
254     var lowerConfidenceCache;
255     this.lowerConfidencePlotData = function () {
256         if (!lowerConfidenceCache) // FIXME: Use the actual confidence interval
257             lowerConfidenceCache = results.map(function (result, index) { return [result.build().time(), result.mean() - result.confidenceIntervalDelta()]; });
258         return lowerConfidenceCache;
259     }
260     this.scalingFactor = function() {
261         computeScalingFactorIfNeeded();
262         return cachedScalingFactor;
263     }
264     this.unit = function () {
265         computeScalingFactorIfNeeded();
266         return cachedUnit;
267     }
268     this.smallerIsBetter = function() { return unit == 'ms' || unit == 'B' || unit == ''; }
269 }
270
271 var URLState = new (function () {
272     var hash;
273     var parameters;
274     var updateTimer;
275
276     function parseIfNeeded() {
277         if (updateTimer || '#' + hash == location.hash)
278             return;
279         hash = location.hash.substr(1).replace(/\/+$/, '');
280         parameters = {};
281         hash.split(/[&;]/).forEach(function (token) {
282             var keyValue = token.split('=');
283             var key = decodeURIComponent(keyValue[0]);
284             if (key.length)
285                 parameters[key] = decodeURIComponent(keyValue.slice(1).join('='));
286         });
287     }
288
289     function updateHash() {
290         if (location.hash != hash)
291             location.hash = hash;
292     }
293
294     this.get = function (key, defaultValue) {
295         parseIfNeeded();
296         if (key in parameters)
297             return parameters[key];
298         else
299             return defaultValue;
300     }
301
302     function scheduleHashUpdate() {
303         var newHash = '';
304         for (key in parameters) {
305             if (newHash.length)
306                 newHash += '&';
307             newHash += encodeURIComponent(key) + '=' + encodeURIComponent(parameters[key]);
308         }
309         hash = newHash;
310         if (updateTimer)
311             clearTimeout(updateTimer);
312
313         updateTimer = setTimeout(function () {
314             updateTimer = undefined;
315             updateHash();
316         }, 500);
317     }
318
319     this.set = function (key, value) {
320         parseIfNeeded();
321         parameters[key] = value;
322         scheduleHashUpdate();
323     }
324
325     this.remove = function (key) {
326         parseIfNeeded();
327         delete parameters[key];
328         scheduleHashUpdate();
329     }
330
331     var watchCallbacks = {};
332     function onhashchange() {
333         if ('#' + hash == location.hash)
334             return;
335
336         // Race. If the hash had changed while we're waiting to update, ignore the change.
337         if (updateTimer) {
338             clearTimeout(updateTimer);
339             updateTimer = undefined;
340         }
341
342         // FIXME: Consider ignoring URLState.set/remove while notifying callbacks.
343         var oldParameters = parameters;
344         parseIfNeeded();
345         var callbacks = [];
346         var changedStates = [];
347         for (var key in watchCallbacks) {
348             if (parameters[key] == oldParameters[key])
349                 continue;
350             changedStates.push(key);
351             callbacks.push(watchCallbacks[key]);
352         }
353
354         for (var i = 0; i < callbacks.length; i++)
355             callbacks[i](changedStates);
356     }
357     $(window).bind('hashchange', onhashchange);
358
359     // FIXME: Support multiple callbacks on a single key.
360     this.watch = function (key, callback) {
361         parseIfNeeded();
362         watchCallbacks[key] = callback;
363     }
364 });
365
366 function Tooltip(containerParent, className) {
367     var container;
368     var self = this;
369     var hideTimer; // Use setTimeout(~, 0) to workaround the race condition that arises when moving mouse fast.
370     var previousContent;
371
372     function ensureContainer() {
373         if (container)
374             return;
375         container = document.createElement('div');
376         $(containerParent).append(container);
377         container.className = className;
378         container.style.position = 'absolute';
379         $(container).hide();
380     }
381
382     this.show = function (x, y, content) {
383         if (hideTimer) {
384             clearTimeout(hideTimer);
385             hideTimer = undefined;
386         }
387
388         if (previousContent === content) {
389             $(container).show();
390             return;
391         }
392         previousContent = content;
393
394         ensureContainer();
395         container.innerHTML = content;
396         $(container).show();
397         // FIXME: Style specific computation like this one shouldn't be in Tooltip class.
398         var top = y - $(container).outerHeight() - 15;
399         if (top < 0) {
400             $(container).addClass('inverted');
401             top = y + 15;
402         } else
403             $(container).removeClass('inverted');
404         $(container).offset({left: x - $(container).outerWidth() / 2, top: top});
405     }
406
407     this.hide = function () {
408         if (!container)
409             return;
410
411         if (hideTimer)
412             clearTimeout(hideTimer);
413         hideTimer = setTimeout(function () {
414             $(container).fadeOut(100);
415             previousResult = undefined;
416         }, 0);
417     }
418
419     this.remove = function (immediately) {
420         if (!container)
421             return;
422
423         if (hideTimer)
424             clearTimeout(hideTimer);
425
426         $(container).remove();
427         container = undefined;
428         previousResult = undefined;
429     }
430
431     this.toggle = function () {
432         $(container).toggle();
433         document.body.appendChild(container); // Toggled tooltip should show up at the top.
434     }
435
436     this.bindClick = function (callback) {
437         ensureContainer();
438         $(container).bind('click', callback);
439     }
440     this.bindMouseEnter = function (callback) {
441         ensureContainer();
442         $(container).bind('mouseenter', callback);
443     }
444 }