blob: bdc9ab862d62d279281f1db28e37fb4fe37cf6e7 [file] [log] [blame]
// We don't use DS.Model for these object types because we can't afford to process millions of them.
if (!Array.prototype.find) {
Array.prototype.find = function (callback) {
for (var item of this) {
if (callback(item))
return item;
}
return undefined;
}
}
var PrivilegedAPI = {
_token: null,
_expiration: null,
_maxNetworkLatency: 3 * 60 * 1000 /* 3 minutes */,
};
PrivilegedAPI.sendRequest = function (url, parameters)
{
return this._generateTokenInServerIfNeeded().then(function (token) {
return PrivilegedAPI._post(url, $.extend({token: token}, parameters));
});
}
PrivilegedAPI._generateTokenInServerIfNeeded = function ()
{
var self = this;
return new Ember.RSVP.Promise(function (resolve, reject) {
if (self._token && self._expiration > Date.now() + self._maxNetworkLatency)
resolve(self._token);
PrivilegedAPI._post('generate-csrf-token')
.then(function (result, reject) {
self._token = result['token'];
self._expiration = new Date(result['expiration']);
resolve(self._token);
}).catch(reject);
});
}
PrivilegedAPI._post = function (url, parameters)
{
return new Ember.RSVP.Promise(function (resolve, reject) {
$.ajax({
url: '../privileged-api/' + url,
type: 'POST',
contentType: 'application/json',
data: parameters ? JSON.stringify(parameters) : '{}',
dataType: 'json',
}).done(function (data) {
if (data.status != 'OK') {
console.log('PrivilegedAPI failed', data);
reject(data.status);
} else
resolve(data);
}).fail(function (xhr, status, error) {
reject(xhr.status + (error ? ', ' + error : '') + '\n\nWith response:\n' + xhr.responseText);
});
});
}
var CommitLogs = {
_cachedCommitsByRepository: {}
};
CommitLogs.fetchCommits = function (repository, from, to, keyword)
{
var params = [];
if (from && to) {
params.push(['from', from]);
params.push(['to', to]);
}
if (keyword)
params.push(['keyword', keyword]);
// FIXME: We should be able to use the cache if all commits in the range have been cached.
var useCache = from && to && !keyword;
var url = '../api/commits/' + repository + '/?' + params.map(function (keyValue) {
return encodeURIComponent(keyValue[0]) + '=' + encodeURIComponent(keyValue[1]);
}).join('&');
if (useCache) {
var cachedCommitsForRange = CommitLogs._cachedCommitsBetween(repository, from, to);
if (cachedCommitsForRange)
return new Ember.RSVP.Promise(function (resolve) { resolve(cachedCommitsForRange); });
}
return new Ember.RSVP.Promise(function (resolve, reject) {
$.getJSON(url, function (data) {
if (data.status != 'OK') {
reject(data.status);
return;
}
var fetchedCommits = data.commits;
fetchedCommits.forEach(function (commit) { commit.time = new Date(commit.time); });
if (useCache)
CommitLogs._cacheConsecutiveCommits(repository, from, to, fetchedCommits);
resolve(fetchedCommits);
}).fail(function (xhr, status, error) {
reject(xhr.status + (error ? ', ' + error : ''));
})
});
}
CommitLogs._cachedCommitsBetween = function (repository, from, to)
{
var cachedCommits = this._cachedCommitsByRepository[repository];
if (!cachedCommits)
return null;
var startCommit = cachedCommits.commitsByRevision[from];
var endCommit = cachedCommits.commitsByRevision[to];
if (!startCommit || !endCommit)
return null;
return cachedCommits.commitsByTime.slice(startCommit.cacheIndex, endCommit.cacheIndex + 1);
}
CommitLogs._cacheConsecutiveCommits = function (repository, from, to, consecutiveCommits)
{
var cachedCommits = this._cachedCommitsByRepository[repository];
if (!cachedCommits) {
cachedCommits = {commitsByRevision: {}, commitsByTime: []};
this._cachedCommitsByRepository[repository] = cachedCommits;
}
consecutiveCommits.forEach(function (commit) {
if (cachedCommits.commitsByRevision[commit.revision])
return;
cachedCommits.commitsByRevision[commit.revision] = commit;
cachedCommits.commitsByTime.push(commit);
});
cachedCommits.commitsByTime.sort(function (a, b) { return a.time && b.time ? (a.time - b.time) : (a.order - b.order); });
cachedCommits.commitsByTime.forEach(function (commit, index) { commit.cacheIndex = index; });
}
function Measurement(rawData)
{
this._raw = rawData;
var latestTime = -1;
var revisions = this._raw['revisions'];
// FIXME: Fix this in the server side.
if (Array.isArray(revisions))
revisions = {};
this._raw['revisions'] = revisions;
for (var repositoryId in revisions) {
var commitTimeOrUndefined = revisions[repositoryId][1]; // e.g. ["162190", 1389945046000]
if (commitTimeOrUndefined && latestTime < commitTimeOrUndefined)
latestTime = commitTimeOrUndefined;
}
this._latestCommitTime = latestTime !== -1 ? new Date(latestTime) : null;
this._buildTime = new Date(this._raw['buildTime']);
this._confidenceInterval = undefined;
this._formattedRevisions = undefined;
}
Measurement.prototype.revisionForRepository = function (repositoryId)
{
var revisions = this._raw['revisions'];
var rawData = revisions[repositoryId];
return rawData ? rawData[0] : null;
}
Measurement.prototype.commitTimeForRepository = function (repositoryId)
{
var revisions = this._raw['revisions'];
var rawData = revisions[repositoryId];
return rawData ? new Date(rawData[1]) : null;
}
Measurement.prototype.formattedRevisions = function (previousMeasurement)
{
var revisions = this._raw['revisions'];
var previousRevisions = previousMeasurement ? previousMeasurement._raw['revisions'] : null;
var formattedRevisions = {};
for (var repositoryId in revisions) {
var currentRevision = revisions[repositoryId][0];
var previousRevision = previousRevisions && previousRevisions[repositoryId] ? previousRevisions[repositoryId][0] : null;
var formatttedRevision = Measurement.formatRevisionRange(currentRevision, previousRevision);
formattedRevisions[repositoryId] = formatttedRevision;
}
return formattedRevisions;
}
Measurement.formatRevisionRange = function (currentRevision, previousRevision)
{
var revisionChanged = false;
if (previousRevision == currentRevision)
previousRevision = null;
else
revisionChanged = true;
var revisionPrefix = '';
var revisionDelimiter = '-';
var label = '';
if (parseInt(currentRevision) == currentRevision) { // e.g. r12345.
currentRevision = parseInt(currentRevision);
revisionPrefix = 'r';
if (previousRevision)
previousRevision = (parseInt(previousRevision) + 1);
} else if (currentRevision.indexOf(' ') >= 0) // e.g. 10.9 13C64.
revisionDelimiter = ' - ';
else if (currentRevision.length == 40) { // e.g. git hash
var formattedCurrentHash = currentRevision.substring(0, 8);
if (previousRevision)
label = previousRevision.substring(0, 8) + '..' + formattedCurrentHash;
else
label = formattedCurrentHash;
}
if (!label) {
if (previousRevision)
label = revisionPrefix + previousRevision + revisionDelimiter + revisionPrefix + currentRevision;
else
label = revisionPrefix + currentRevision;
}
return {
label: label,
previousRevision: previousRevision,
currentRevision: currentRevision,
revisionChanged: revisionChanged
};
}
Measurement.prototype.id = function ()
{
return this._raw['id'];
}
Measurement.prototype.mean = function()
{
return this._raw['mean'];
}
Measurement.prototype.confidenceInterval = function()
{
if (this._confidenceInterval === undefined) {
var delta = Statistics.confidenceIntervalDelta(0.95, this._raw["iterationCount"], this._raw["sum"], this._raw["squareSum"]);
var mean = this.mean();
this._confidenceInterval = isNaN(delta) ? null : [mean - delta, mean + delta];
}
return this._confidenceInterval;
}
Measurement.prototype.latestCommitTime = function()
{
return this._latestCommitTime || this._buildTime;
}
Measurement.prototype.buildId = function()
{
return this._raw['build'];
}
Measurement.prototype.buildNumber = function ()
{
return this._raw['buildNumber'];
}
Measurement.prototype.builderId = function ()
{
return this._raw['builder'];
}
Measurement.prototype.buildTime = function()
{
return this._buildTime;
}
Measurement.prototype.formattedBuildTime = function ()
{
return Measurement._formatDate(this.buildTime());
}
Measurement._formatDate = function (date)
{
return date.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
}
Measurement.prototype.bugs = function ()
{
return this._raw['bugs'];
}
Measurement.prototype.hasBugs = function ()
{
var bugs = this.bugs();
return bugs && Object.keys(bugs).length;
}
Measurement.prototype.markedOutlier = function ()
{
return this._raw['markedOutlier'];
}
Measurement.prototype.setMarkedOutlier = function (markedOutlier)
{
var params = {'run': this.id(), 'markedOutlier': markedOutlier};
return PrivilegedAPI.sendRequest('update-run-status', params).then(function (data) {
}, function (error) {
alert('Failed to update the outlier status: ' + error);
});
}
function RunsData(rawData)
{
this._measurements = rawData.map(function (run) { return new Measurement(run); });
}
RunsData.prototype.count = function ()
{
return this._measurements.length;
}
RunsData.prototype.timeSeriesByCommitTime = function (includeOutliers, extendToFuture)
{
return this._timeSeriesByTimeInternal(true, includeOutliers, extendToFuture);
}
RunsData.prototype.timeSeriesByBuildTime = function (includeOutliers, extendToFuture)
{
return this._timeSeriesByTimeInternal(false, includeOutliers, extendToFuture);
}
RunsData.prototype._timeSeriesByTimeInternal = function (useCommitType, includeOutliers, extendToFuture)
{
var series = new Array();
var seriesIndex = 0;
for (var measurement of this._measurements) {
if (measurement.markedOutlier() && !includeOutliers)
continue;
series.push({
measurement: measurement,
time: useCommitType ? measurement.latestCommitTime() : measurement.buildTime(),
secondaryTime: measurement.buildTime(),
value: measurement.mean(),
interval: measurement.confidenceInterval(),
markedOutlier: measurement.markedOutlier(),
});
}
return new TimeSeries(series, extendToFuture);
}
// FIXME: We need to devise a way to fetch runs in multiple chunks so that
// we don't have to fetch the entire time series to just show the last 3 days.
RunsData.fetchRuns = function (platformId, metricId, testGroupId, useCache)
{
var url = this.pathForFetchingRuns(platformId, metricId, testGroupId, useCache);
return new Ember.RSVP.Promise(function (resolve, reject) {
$.getJSON(url, function (response) {
if (response.status != 'OK') {
reject(response.status);
return;
}
resolve(RunsData.createRunsDataInResponse(response));
}).fail(function (xhr, status, error) {
if (xhr.status == 404 && useCache)
resolve(null);
else
reject(xhr.status + (error ? ', ' + error : ''));
})
});
}
RunsData.pathForFetchingRuns = function (platformId, metricId, testGroupId, useCache)
{
var path = useCache ? '/data/' : '/api/runs/';
path += platformId + '-' + metricId + '.json';
if (testGroupId)
path += '?testGroup=' + testGroupId;
return path;
}
RunsData.createRunsDataInResponse = function (response)
{
delete response.status;
var data = response.configurations;
for (var config in data)
data[config] = new RunsData(data[config]);
if (response.lastModified)
response.lastModified = new Date(response.lastModified);
return response;
}
// FIXME: It was a mistake to put this in the client side. We should put this back in the JSON.
RunsData.unitFromMetricName = function (metricName)
{
var suffix = metricName.match('([A-z][a-z]+|FrameRate)$')[0];
var unit = {
'FrameRate': 'fps',
'Runs': '/s',
'Time': 'ms',
'Duration': 'ms',
'Malloc': 'B',
'Heap': 'B',
'Allocations': 'B',
'Size': 'B',
'Score': 'pt',
'Power': 'W'
}[suffix];
return unit;
}
RunsData.isSmallerBetter = function (unit)
{
return unit != 'fps' && unit != '/s' && unit != 'pt';
}
// FIXME: Extending the baseline/target to the future should be a server-side configuration.
function TimeSeries(series, extendToFuture)
{
this._series = series.sort(function (a, b) {
var diff = a.time - b.time;
return diff ? diff : a.secondaryTime - b.secondaryTime;
});
if (extendToFuture && this._series.length) {
var lastPoint = this._series[this._series.length - 1];
this._series.push({
measurement: lastPoint.measurement,
time: Date.now() + 24 * 3600 * 1000,
secondaryTime: Date.now() + 24 * 3600 * 1000,
value: lastPoint.value,
interval: lastPoint.interval,
markedOutlier: lastPoint.markedOutlier,
});
}
var self = this;
var min = undefined;
var max = undefined;
this._series.forEach(function (point, index) {
point.series = self;
point.seriesIndex = index;
if (min === undefined || min > point.value)
min = point.value;
if (max === undefined || max < point.value)
max = point.value;
});
this._min = min;
this._max = max;
}
TimeSeries.prototype.findPointByIndex = function (index)
{
if (!this._series || index < 0 || index >= this._series.length)
return null;
return this._series[index];
}
TimeSeries.prototype.findPointByBuild = function (buildId)
{
return this._series.find(function (point) { return point.measurement.buildId() == buildId; })
}
TimeSeries.prototype.findPointByRevisions = function (revisions)
{
return this._series.find(function (point, index) {
for (var repositoryId in revisions) {
if (point.measurement.revisionForRepository(repositoryId) != revisions[repositoryId])
return false;
}
return true;
});
}
TimeSeries.prototype.findPointByMeasurementId = function (measurementId)
{
return this._series.find(function (point) { return point.measurement.id() == measurementId; });
}
TimeSeries.prototype.findPointAfterTime = function (time)
{
return this._series.find(function (point) { return point.time >= time; });
}
TimeSeries.prototype.seriesBetweenPoints = function (startPoint, endPoint)
{
if (typeof(startPoint.seriesIndex) != "number" || typeof(endPoint.seriesIndex) != "number")
return null;
return this._series.slice(startPoint.seriesIndex, endPoint.seriesIndex + 1);
}
TimeSeries.prototype.minMaxForTimeRange = function (startTime, endTime, ignoreOutlier)
{
var data = this._series;
var i = 0;
if (startTime !== undefined) {
for (i = 0; i < data.length; i++) {
var point = data[i];
if (point.time >= startTime)
break;
}
if (i)
i--;
}
var min = Number.MAX_VALUE;
var max = Number.MIN_VALUE;
for (; i < data.length; i++) {
var point = data[i];
if (point.isOutlier && ignoreOutlier)
continue;
var currentMin = point.interval ? point.interval[0] : point.value;
var currentMax = point.interval ? point.interval[1] : point.value;
if (currentMin < min)
min = currentMin;
if (currentMax > max)
max = currentMax;
if (point.time >= endTime)
break;
}
return [min, max];
}
TimeSeries.prototype.series = function () { return this._series; }
TimeSeries.prototype.rawValues = function ()
{
return this._series.map(function (point) { return point.value });
}
TimeSeries.prototype.lastPoint = function ()
{
if (!this._series || !this._series.length)
return null;
return this._series[this._series.length - 1];
}
TimeSeries.prototype.previousPoint = function (point)
{
if (!point.seriesIndex)
return null;
return this._series[point.seriesIndex - 1];
}
TimeSeries.prototype.nextPoint = function (point)
{
if (!point.seriesIndex)
return null;
return this._series[point.seriesIndex + 1];
}
if (typeof module != 'undefined') {
Statistics = require('../shared/statistics.js');
module.exports.Measurement = Measurement;
module.exports.RunsData = RunsData;
module.exports.TimeSeries = TimeSeries;
}