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 () {
12 return Statistics.confidenceIntervalDelta(0.95, result.iterationCount, result.sum, result.squareSum);
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');
20 this.isStatisticallySignificant = function (other) {
21 var diff = Math.abs(other.mean() - this.mean());
22 return diff > this.confidenceIntervalDelta() && diff > other.confidenceIntervalDelta();
24 this.build = function () { return associatedBuild; }
25 this.label = function (previousResult) {
26 var mean = this.mean();
27 var label = mean.toPrecision(4) + ' ' + runs.unit();
29 var delta = this.confidenceIntervalDelta();
31 var percentageStdev = delta * 100 / mean;
32 label += ' ± ' + percentageStdev.toFixed(2) + '%';
36 label += ' (' + this.formattedProgressionOrRegression(previousResult) + ')';
42 function TestBuild(repositories, builders, platform, rawRun) {
43 const revisions = rawRun.revisions;
45 var revisionCount = 0;
46 for (var repositoryName in revisions) {
47 maxTime = Math.max(maxTime, revisions[repositoryName][1]); // Revision is an pair (revision, time)
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$/, '');
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;
67 this.platform = function () { return platform; }
68 this.revision = function(repositoryName) { return revisions[repositoryName][0]; }
69 this.formattedRevisions = function (previousBuild) {
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;
77 var revisionPrefix = '';
78 if (currentRevision.length < 10) { // SVN-like revision.
81 previousRevision = (parseInt(previousRevision) + 1);
84 var labelForThisRepository = revisionCount ? repositoryName : '';
85 if (previousRevision) {
86 if (labelForThisRepository)
87 labelForThisRepository += ' ';
88 labelForThisRepository += revisionPrefix + previousRevision + '-' + revisionPrefix + currentRevision;
90 labelForThisRepository += ' @ ' + revisionPrefix + currentRevision;
93 var repository = repositories[repositoryName];
96 url = (repository['blameUrl'] || '').replace(/\$1/g, previousRevision).replace(/\$2/g, currentRevision);
98 url = (repository['url'] || '').replace(/\$1/g, currentRevision);
101 result[repositoryName] = {
102 'label': labelForThisRepository,
103 'currentRevision': currentRevision,
104 'previousRevision': previousRevision,
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;
117 TestBuild.now = function () { return this.UTCtoPST(Date.now()); }
119 // A sequence of test results for a specific test on a specific platform
120 function PerfTestRuns(metric, platform) {
122 var cachedUnit = null;
123 var cachedScalingFactor = null;
125 var unit = {'Combined': '', // Assume smaller is better for now.
131 'Allocations': 'bytes',
132 'EndAllocations': 'bytes',
133 'MaxAllocations': 'bytes',
134 'MeanAllocations': 'bytes'}[metric.name];
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)
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);
152 cachedScalingFactor = 1;
157 this.metric = function () { return metric; }
158 this.platform = function () { return platform; }
159 this.addResult = function (newResult) {
160 if (results.indexOf(newResult) >= 0)
162 results.push(newResult);
164 cachedScalingFactor = null;
166 this.lastResult = function () { return results[results.length - 1]; }
167 this.resultAt = function (i) { return results[i]; }
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;
180 this.min = function (minTime) {
181 return this.scalingFactor() * unscaledMeansForAllResults(minTime)
182 .reduce(function (minSoFar, currentMean) { return Math.min(minSoFar, currentMean); }, Number.MAX_VALUE);
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);
188 this.sampleStandardDeviation = function (minTime) {
189 var unscaledMeans = unscaledMeansForAllResults(minTime);
190 return this.scalingFactor() * Statistics.sampleStandardDeviation(unscaledMeans.length, Statistics.sum(unscaledMeans), Statistics.squareSum(unscaledMeans));
192 this.exponentialMovingArithmeticMean = function (minTime, alpha) {
193 var unscaledMeans = unscaledMeansForAllResults(minTime);
194 if (!unscaledMeans.length)
196 return this.scalingFactor() * unscaledMeans.reduce(function (movingAverage, currentMean) { return alpha * movingAverage + (1 - alpha) * movingAverage; });
198 this.hasConfidenceInterval = function () { return !isNaN(this.lastResult().unscaledConfidenceIntervalDelta()); }
200 this.meanPlotData = function () {
202 meanPlotCache = results.map(function (result, index) { return [result.build().time(), result.mean()]; });
203 return meanPlotCache;
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;
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;
217 this.scalingFactor = function() {
218 computeScalingFactorIfNeeded();
219 return cachedScalingFactor;
221 this.unit = function () {
222 computeScalingFactorIfNeeded();
225 this.smallerIsBetter = function() { return unit == 'ms' || unit == 'bytes' || unit == ''; }
228 var URLState = new (function () {
233 function parseIfNeeded() {
234 if (updateTimer || '#' + hash == location.hash)
236 hash = location.hash.substr(1).replace(/\/+$/, '');
238 hash.split(/[&;]/).forEach(function (token) {
239 var keyValue = token.split('=');
240 var key = decodeURIComponent(keyValue[0]);
242 parameters[key] = decodeURIComponent(keyValue.slice(1).join('='));
246 function updateHash() {
247 if (location.hash != hash)
248 location.hash = hash;
251 this.get = function (key, defaultValue) {
253 if (key in parameters)
254 return parameters[key];
259 function scheduleHashUpdate() {
261 for (key in parameters) {
264 newHash += encodeURIComponent(key) + '=' + encodeURIComponent(parameters[key]);
268 clearTimeout(updateTimer);
270 updateTimer = setTimeout(function () {
271 updateTimer = undefined;
276 this.set = function (key, value) {
278 parameters[key] = value;
279 scheduleHashUpdate();
282 this.remove = function (key) {
284 delete parameters[key];
285 scheduleHashUpdate();
288 var watchCallbacks = {};
289 function onhashchange() {
290 if ('#' + hash == location.hash)
293 // Race. If the hash had changed while we're waiting to update, ignore the change.
295 clearTimeout(updateTimer);
296 updateTimer = undefined;
299 // FIXME: Consider ignoring URLState.set/remove while notifying callbacks.
300 var oldParameters = parameters;
303 var changedStates = [];
304 for (var key in watchCallbacks) {
305 if (parameters[key] == oldParameters[key])
307 changedStates.push(key);
308 callbacks.push(watchCallbacks[key]);
311 for (var i = 0; i < callbacks.length; i++)
312 callbacks[i](changedStates);
314 $(window).bind('hashchange', onhashchange);
316 // FIXME: Support multiple callbacks on a single key.
317 this.watch = function (key, callback) {
319 watchCallbacks[key] = callback;
323 function Tooltip(containerParent, className) {
326 var hideTimer; // Use setTimeout(~, 0) to workaround the race condition that arises when moving mouse fast.
329 function ensureContainer() {
332 container = document.createElement('div');
333 $(containerParent).append(container);
334 container.className = className;
335 container.style.position = 'absolute';
339 this.show = function (x, y, content) {
341 clearTimeout(hideTimer);
342 hideTimer = undefined;
345 if (previousContent === content) {
349 previousContent = content;
352 container.innerHTML = content;
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});
358 this.hide = function () {
363 clearTimeout(hideTimer);
364 hideTimer = setTimeout(function () {
365 $(container).fadeOut(100);
366 previousResult = undefined;
370 this.remove = function (immediately) {
375 clearTimeout(hideTimer);
377 $(container).remove();
378 container = undefined;
379 previousResult = undefined;
382 this.toggle = function () {
383 $(container).toggle();
384 document.body.appendChild(container); // Toggled tooltip should show up at the top.
387 this.bindClick = function (callback) {
389 $(container).bind('click', callback);
391 this.bindMouseEnter = function (callback) {
393 $(container).bind('mouseenter', callback);