962ee163d6cdff330a47aa654f3bb87318178329
[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',
165         'Score': 'pt'}[suffix];
166
167     // We can't do this in PerfTestResult because all results for each metric need to share the same unit and the same scaling factor.
168     function computeScalingFactorIfNeeded() {
169         // FIXME: We shouldn't be adjusting units on every test result.
170         // We can only do this on the first test.
171         if (!results.length || cachedUnit)
172             return;
173
174         var mean = results[0].unscaledMean(); // FIXME: We should look at all values.
175         var kilo = unit == 'B' ? 1024 : 1000;
176         if (mean > 2 * kilo * kilo && unit != 'ms') {
177             cachedScalingFactor = 1 / kilo / kilo;
178             var unitFirstChar = unit.charAt(0);
179             cachedUnit = 'M' + (unitFirstChar.toUpperCase() == unitFirstChar ? '' : ' ') + unit;
180         } else if (mean > 2 * kilo) {
181             cachedScalingFactor = 1 / kilo;
182             cachedUnit = unit == 'ms' ? 's' : ('K ' + unit);
183         } else {
184             cachedScalingFactor = 1;
185             cachedUnit = unit;
186         }
187     }
188
189     this.metric = function () { return metric; }
190     this.platform = function () { return platform; }
191     this.setResults = function (newResults) {
192         results = newResults;
193         cachedUnit = null;
194         cachedScalingFactor = null;
195     }
196     this.lastResult = function () { return results[results.length - 1]; }
197     this.resultAt = function (i) { return results[i]; }
198
199     var resultsFilterCache;
200     var resultsFilterCacheMinTime;
201     function filteredResults(minTime) {
202         if (!minTime)
203             return results;
204         if (resultsFilterCacheMinTime != minTime) {
205             resultsFilterCache = results.filter(function (result) { return !minTime || result.build().time() >= minTime; });
206             resultsFilterCacheMinTime = minTime;
207         }
208         return resultsFilterCache;
209     }
210
211     function unscaledMeansForAllResults(minTime) {
212         return filteredResults(minTime).map(function (result) { return result.unscaledMean(); });
213     }
214
215     this.min = function (minTime) {
216         return this.scalingFactor() * filteredResults(minTime)
217             .reduce(function (minSoFar, result) { return Math.min(minSoFar, result.unscaledMean() - result.unscaledConfidenceIntervalDelta(0)); }, Number.MAX_VALUE);
218     }
219     this.max = function (minTime, baselineName) {
220         return this.scalingFactor() * filteredResults(minTime)
221             .reduce(function (maxSoFar, result) { return Math.max(maxSoFar, result.unscaledMean() + result.unscaledConfidenceIntervalDelta(0)); }, Number.MIN_VALUE);
222     }
223     this.countResults = function (minTime) {
224         return unscaledMeansForAllResults(minTime).length;
225     }
226     this.countResultsInInterval = function (minTime, min, max) {
227         var unscaledMin = min / this.scalingFactor();
228         var unscaledMax = max / this.scalingFactor();
229         return filteredResults(minTime).reduce(function (count, currentResult) {
230             return count + (currentResult.isInUnscaledInterval(unscaledMin, unscaledMax) ? 1 : 0); }, 0);
231     }
232     this.sampleStandardDeviation = function (minTime) {
233         var unscaledMeans = unscaledMeansForAllResults(minTime);
234         return this.scalingFactor() * Statistics.sampleStandardDeviation(unscaledMeans.length, Statistics.sum(unscaledMeans), Statistics.squareSum(unscaledMeans));
235     }
236     this.exponentialMovingArithmeticMean = function (minTime, alpha) {
237         var unscaledMeans = unscaledMeansForAllResults(minTime);
238         if (!unscaledMeans.length)
239             return NaN;
240         return this.scalingFactor() * unscaledMeans.reduce(function (movingAverage, currentMean) { return alpha * currentMean + (1 - alpha) * movingAverage; });
241     }
242     this.hasConfidenceInterval = function () { return !isNaN(this.lastResult().unscaledConfidenceIntervalDelta()); }
243     var meanPlotCache;
244     this.meanPlotData = function () {
245         if (!meanPlotCache)
246             meanPlotCache = results.map(function (result, index) { return [result.build().time(), result.mean()]; });
247         return meanPlotCache;
248     }
249     var upperConfidenceCache;
250     this.upperConfidencePlotData = function () {
251         if (!upperConfidenceCache) // FIXME: Use the actual confidence interval
252             upperConfidenceCache = results.map(function (result, index) { return [result.build().time(), result.mean() + result.confidenceIntervalDelta()]; });
253         return upperConfidenceCache;
254     }
255     var lowerConfidenceCache;
256     this.lowerConfidencePlotData = function () {
257         if (!lowerConfidenceCache) // FIXME: Use the actual confidence interval
258             lowerConfidenceCache = results.map(function (result, index) { return [result.build().time(), result.mean() - result.confidenceIntervalDelta()]; });
259         return lowerConfidenceCache;
260     }
261     this.scalingFactor = function() {
262         computeScalingFactorIfNeeded();
263         return cachedScalingFactor;
264     }
265     this.unit = function () {
266         computeScalingFactorIfNeeded();
267         return cachedUnit;
268     }
269     this.smallerIsBetter = function() { return unit == 'ms' || unit == 'B' || unit == ''; }
270 }
271
272 var URLState = new (function () {
273     var hash;
274     var parameters;
275     var updateTimer;
276
277     function parseIfNeeded() {
278         if (updateTimer || '#' + hash == location.hash)
279             return;
280         hash = location.hash.substr(1).replace(/\/+$/, '');
281         parameters = {};
282         hash.split(/[&;]/).forEach(function (token) {
283             var keyValue = token.split('=');
284             var key = decodeURIComponent(keyValue[0]);
285             if (key.length)
286                 parameters[key] = decodeURIComponent(keyValue.slice(1).join('='));
287         });
288     }
289
290     function updateHash() {
291         if (location.hash != hash)
292             location.hash = hash;
293     }
294
295     this.get = function (key, defaultValue) {
296         parseIfNeeded();
297         if (key in parameters)
298             return parameters[key];
299         else
300             return defaultValue;
301     }
302
303     function scheduleHashUpdate() {
304         var newHash = '';
305         for (key in parameters) {
306             if (newHash.length)
307                 newHash += '&';
308             newHash += encodeURIComponent(key) + '=' + encodeURIComponent(parameters[key]);
309         }
310         hash = newHash;
311         if (updateTimer)
312             clearTimeout(updateTimer);
313
314         updateTimer = setTimeout(function () {
315             updateTimer = undefined;
316             updateHash();
317         }, 500);
318     }
319
320     this.set = function (key, value) {
321         parseIfNeeded();
322         parameters[key] = value;
323         scheduleHashUpdate();
324     }
325
326     this.remove = function (key) {
327         parseIfNeeded();
328         delete parameters[key];
329         scheduleHashUpdate();
330     }
331
332     var watchCallbacks = {};
333     function onhashchange() {
334         if ('#' + hash == location.hash)
335             return;
336
337         // Race. If the hash had changed while we're waiting to update, ignore the change.
338         if (updateTimer) {
339             clearTimeout(updateTimer);
340             updateTimer = undefined;
341         }
342
343         // FIXME: Consider ignoring URLState.set/remove while notifying callbacks.
344         var oldParameters = parameters;
345         parseIfNeeded();
346         var callbacks = [];
347         var changedStates = [];
348         for (var key in watchCallbacks) {
349             if (parameters[key] == oldParameters[key])
350                 continue;
351             changedStates.push(key);
352             callbacks.push(watchCallbacks[key]);
353         }
354
355         for (var i = 0; i < callbacks.length; i++)
356             callbacks[i](changedStates);
357     }
358     $(window).bind('hashchange', onhashchange);
359
360     // FIXME: Support multiple callbacks on a single key.
361     this.watch = function (key, callback) {
362         parseIfNeeded();
363         watchCallbacks[key] = callback;
364     }
365 });
366
367 function Tooltip(containerParent, className) {
368     var container;
369     var self = this;
370     var hideTimer; // Use setTimeout(~, 0) to workaround the race condition that arises when moving mouse fast.
371     var previousContent;
372
373     function ensureContainer() {
374         if (container)
375             return;
376         container = document.createElement('div');
377         $(containerParent).append(container);
378         container.className = className;
379         container.style.position = 'absolute';
380         $(container).hide();
381     }
382
383     this.show = function (x, y, content) {
384         if (hideTimer) {
385             clearTimeout(hideTimer);
386             hideTimer = undefined;
387         }
388
389         if (previousContent === content) {
390             $(container).show();
391             return;
392         }
393         previousContent = content;
394
395         ensureContainer();
396         container.innerHTML = content;
397         $(container).show();
398         // FIXME: Style specific computation like this one shouldn't be in Tooltip class.
399         var top = y - $(container).outerHeight() - 15;
400         if (top < 0) {
401             $(container).addClass('inverted');
402             top = y + 15;
403         } else
404             $(container).removeClass('inverted');
405         $(container).offset({left: x - $(container).outerWidth() / 2, top: top});
406     }
407
408     this.hide = function () {
409         if (!container)
410             return;
411
412         if (hideTimer)
413             clearTimeout(hideTimer);
414         hideTimer = setTimeout(function () {
415             $(container).fadeOut(100);
416             previousResult = undefined;
417         }, 0);
418     }
419
420     this.remove = function (immediately) {
421         if (!container)
422             return;
423
424         if (hideTimer)
425             clearTimeout(hideTimer);
426
427         $(container).remove();
428         container = undefined;
429         previousResult = undefined;
430     }
431
432     this.toggle = function () {
433         $(container).toggle();
434         document.body.appendChild(container); // Toggled tooltip should show up at the top.
435     }
436
437     this.bindClick = function (callback) {
438         ensureContainer();
439         $(container).bind('click', callback);
440     }
441     this.bindMouseEnter = function (callback) {
442         ensureContainer();
443         $(container).bind('mouseenter', callback);
444     }
445 }