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