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();
11 this.unscaledConfidenceIntervalDelta = function (defaultValue) {
12 var delta = Statistics.confidenceIntervalDelta(0.95, result.iterationCount, result.sum, result.squareSum);
13 if (isNaN(delta) && defaultValue !== undefined)
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;
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');
28 this.isStatisticallySignificant = function (other) {
29 var diff = Math.abs(other.mean() - this.mean());
30 return diff > this.confidenceIntervalDelta() && diff > other.confidenceIntervalDelta();
32 this.build = function () { return associatedBuild; }
33 this.label = function (previousResult) {
34 var mean = this.mean();
35 var label = mean.toPrecision(4) + ' ' + runs.unit();
37 var delta = this.confidenceIntervalDelta();
39 var percentageStdev = delta * 100 / mean;
40 label += ' ± ' + percentageStdev.toFixed(2) + '%';
44 label += ' (' + this.formattedProgressionOrRegression(previousResult) + ')';
50 function TestBuild(repositories, builders, platform, rawRun) {
51 const revisions = rawRun.revisions;
53 var revisionCount = 0;
54 for (var repositoryId in revisions) {
55 maxTime = Math.max(maxTime, revisions[repositoryId][1]); // Revision is an pair (revision, time)
59 maxTime = rawRun.buildTime;
60 maxTime = TestBuild.UTCtoPST(maxTime);
62 var buildTime = TestBuild.UTCtoPST(rawRun.buildTime);
65 this.time = function () { return maxTime; }
66 this.formattedTime = function () {
68 maxTimeString = new Date(maxTime).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
71 this.buildTime = function () { return buildTime; }
72 this.formattedBuildTime = function () {
74 buildTimeString = new Date(buildTime).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
75 return buildTimeString;
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;
84 return template.replace(/\$buildNumber/g, this.buildNumber()).replace(/\$builderName/g, builder.name);
86 this.platform = function () { return platform; }
87 this.revision = function(repositoryId) { return revisions[repositoryId][0]; }
88 this.formattedRevisions = function (previousBuild) {
90 for (var repositoryId in repositories) {
91 if (!revisions[repositoryId])
93 var previousRevision = previousBuild ? previousBuild.revision(repositoryId) : undefined;
94 var currentRevision = this.revision(repositoryId);
95 if (previousRevision === currentRevision)
96 previousRevision = undefined;
98 var revisionPrefix = '';
99 var revisionDelimiter = '-';
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
110 var labelForThisRepository;
112 formattedCurrentHash = currentRevision.substring(0, 8);
113 if (previousRevision)
114 labelForThisRepository = previousRevision.substring(0, 8) + '..' + formattedCurrentHash;
116 labelForThisRepository = '@ ' + formattedCurrentHash;
118 if (previousRevision)
119 labelForThisRepository = revisionPrefix + previousRevision + revisionDelimiter + revisionPrefix + currentRevision;
121 labelForThisRepository = '@ ' + revisionPrefix + currentRevision;
125 var repository = repositories[repositoryId];
127 if (previousRevision)
128 url = (repository['blameUrl'] || '').replace(/\$1/g, previousRevision).replace(/\$2/g, currentRevision);
130 url = (repository['url'] || '').replace(/\$1/g, currentRevision);
133 result[repository.name] = {
134 'label': labelForThisRepository,
135 'currentRevision': currentRevision,
136 'previousRevision': previousRevision,
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;
149 TestBuild.now = function () { return this.UTCtoPST(Date.now()); }
151 // A sequence of test results for a specific test on a specific platform
152 function PerfTestRuns(metric, platform) {
154 var cachedUnit = null;
155 var cachedScalingFactor = null;
157 var suffix = metric.name.match('([A-z][a-z]+|FrameRate)$')[0];
158 var unit = {'Combined': '', // Assume smaller is better for now.
165 'Score': 'pt'}[suffix];
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)
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);
184 cachedScalingFactor = 1;
189 this.metric = function () { return metric; }
190 this.platform = function () { return platform; }
191 this.setResults = function (newResults) {
192 results = newResults;
194 cachedScalingFactor = null;
196 this.lastResult = function () { return results[results.length - 1]; }
197 this.resultAt = function (i) { return results[i]; }
199 var resultsFilterCache;
200 var resultsFilterCacheMinTime;
201 function filteredResults(minTime) {
204 if (resultsFilterCacheMinTime != minTime) {
205 resultsFilterCache = results.filter(function (result) { return !minTime || result.build().time() >= minTime; });
206 resultsFilterCacheMinTime = minTime;
208 return resultsFilterCache;
211 function unscaledMeansForAllResults(minTime) {
212 return filteredResults(minTime).map(function (result) { return result.unscaledMean(); });
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);
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);
223 this.countResults = function (minTime) {
224 return unscaledMeansForAllResults(minTime).length;
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);
232 this.sampleStandardDeviation = function (minTime) {
233 var unscaledMeans = unscaledMeansForAllResults(minTime);
234 return this.scalingFactor() * Statistics.sampleStandardDeviation(unscaledMeans.length, Statistics.sum(unscaledMeans), Statistics.squareSum(unscaledMeans));
236 this.exponentialMovingArithmeticMean = function (minTime, alpha) {
237 var unscaledMeans = unscaledMeansForAllResults(minTime);
238 if (!unscaledMeans.length)
240 return this.scalingFactor() * unscaledMeans.reduce(function (movingAverage, currentMean) { return alpha * currentMean + (1 - alpha) * movingAverage; });
242 this.hasConfidenceInterval = function () { return !isNaN(this.lastResult().unscaledConfidenceIntervalDelta()); }
244 this.meanPlotData = function () {
246 meanPlotCache = results.map(function (result, index) { return [result.build().time(), result.mean()]; });
247 return meanPlotCache;
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;
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;
261 this.scalingFactor = function() {
262 computeScalingFactorIfNeeded();
263 return cachedScalingFactor;
265 this.unit = function () {
266 computeScalingFactorIfNeeded();
269 this.smallerIsBetter = function() { return unit == 'ms' || unit == 'B' || unit == ''; }
272 var URLState = new (function () {
277 function parseIfNeeded() {
278 if (updateTimer || '#' + hash == location.hash)
280 hash = location.hash.substr(1).replace(/\/+$/, '');
282 hash.split(/[&;]/).forEach(function (token) {
283 var keyValue = token.split('=');
284 var key = decodeURIComponent(keyValue[0]);
286 parameters[key] = decodeURIComponent(keyValue.slice(1).join('='));
290 function updateHash() {
291 if (location.hash != hash)
292 location.hash = hash;
295 this.get = function (key, defaultValue) {
297 if (key in parameters)
298 return parameters[key];
303 function scheduleHashUpdate() {
305 for (key in parameters) {
308 newHash += encodeURIComponent(key) + '=' + encodeURIComponent(parameters[key]);
312 clearTimeout(updateTimer);
314 updateTimer = setTimeout(function () {
315 updateTimer = undefined;
320 this.set = function (key, value) {
322 parameters[key] = value;
323 scheduleHashUpdate();
326 this.remove = function (key) {
328 delete parameters[key];
329 scheduleHashUpdate();
332 var watchCallbacks = {};
333 function onhashchange() {
334 if ('#' + hash == location.hash)
337 // Race. If the hash had changed while we're waiting to update, ignore the change.
339 clearTimeout(updateTimer);
340 updateTimer = undefined;
343 // FIXME: Consider ignoring URLState.set/remove while notifying callbacks.
344 var oldParameters = parameters;
347 var changedStates = [];
348 for (var key in watchCallbacks) {
349 if (parameters[key] == oldParameters[key])
351 changedStates.push(key);
352 callbacks.push(watchCallbacks[key]);
355 for (var i = 0; i < callbacks.length; i++)
356 callbacks[i](changedStates);
358 $(window).bind('hashchange', onhashchange);
360 // FIXME: Support multiple callbacks on a single key.
361 this.watch = function (key, callback) {
363 watchCallbacks[key] = callback;
367 function Tooltip(containerParent, className) {
370 var hideTimer; // Use setTimeout(~, 0) to workaround the race condition that arises when moving mouse fast.
373 function ensureContainer() {
376 container = document.createElement('div');
377 $(containerParent).append(container);
378 container.className = className;
379 container.style.position = 'absolute';
383 this.show = function (x, y, content) {
385 clearTimeout(hideTimer);
386 hideTimer = undefined;
389 if (previousContent === content) {
393 previousContent = content;
396 container.innerHTML = content;
398 // FIXME: Style specific computation like this one shouldn't be in Tooltip class.
399 var top = y - $(container).outerHeight() - 15;
401 $(container).addClass('inverted');
404 $(container).removeClass('inverted');
405 $(container).offset({left: x - $(container).outerWidth() / 2, top: top});
408 this.hide = function () {
413 clearTimeout(hideTimer);
414 hideTimer = setTimeout(function () {
415 $(container).fadeOut(100);
416 previousResult = undefined;
420 this.remove = function (immediately) {
425 clearTimeout(hideTimer);
427 $(container).remove();
428 container = undefined;
429 previousResult = undefined;
432 this.toggle = function () {
433 $(container).toggle();
434 document.body.appendChild(container); // Toggled tooltip should show up at the top.
437 this.bindClick = function (callback) {
439 $(container).bind('click', callback);
441 this.bindMouseEnter = function (callback) {
443 $(container).bind('mouseenter', callback);