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