blob: c2cd016f3692ddf4a2e5f2b9691c7c913de5c8aa [file] [log] [blame]
'use strict';
if (!Array.prototype.includes)
Array.prototype.includes = function (value) { return this.indexOf(value) >= 0; }
class MeasurementSet {
constructor(platformId, metricId, lastModified)
{
this._platformId = platformId;
this._metricId = metricId;
this._lastModified = +lastModified;
this._sortedClusters = [];
this._primaryClusterEndTime = null;
this._clusterCount = null;
this._clusterStart = null;
this._clusterSize = null;
this._allFetches = {};
this._callbackMap = new Map;
this._primaryClusterPromise = null;
this._segmentationCache = new Map;
}
platformId() { return this._platformId; }
metricId() { return this._metricId; }
static findSet(platformId, metricId, lastModified)
{
if (!this._set)
this._set = {};
var key = platformId + '-' + metricId;
if (!this._set[key])
this._set[key] = new MeasurementSet(platformId, metricId, lastModified);
return this._set[key];
}
findClusters(startTime, endTime)
{
var clusterStart = this._clusterStart;
var clusterSize = this._clusterSize;
var clusters = [];
var clusterEnd = clusterStart + Math.floor(Math.max(0, startTime - clusterStart) / clusterSize) * clusterSize;
var lastClusterEndTime = this._primaryClusterEndTime;
var firstClusterEndTime = lastClusterEndTime - clusterSize * (this._clusterCount - 1);
do {
clusterEnd += clusterSize;
if (firstClusterEndTime <= clusterEnd && clusterEnd <= this._primaryClusterEndTime)
clusters.push(clusterEnd);
} while (clusterEnd < endTime);
return clusters;
}
fetchBetween(startTime, endTime, callback, noCache)
{
if (noCache) {
this._primaryClusterPromise = null;
this._allFetches = {};
}
if (!this._primaryClusterPromise)
this._primaryClusterPromise = this._fetchPrimaryCluster(noCache);
var self = this;
if (callback)
this._primaryClusterPromise.catch(callback);
return this._primaryClusterPromise.then(function () {
self._allFetches[self._primaryClusterEndTime] = self._primaryClusterPromise;
return Promise.all(self.findClusters(startTime, endTime).map(function (clusterEndTime) {
return self._ensureClusterPromise(clusterEndTime, callback);
}));
});
}
_ensureClusterPromise(clusterEndTime, callback)
{
if (!this._callbackMap.has(clusterEndTime))
this._callbackMap.set(clusterEndTime, new Set);
var callbackSet = this._callbackMap.get(clusterEndTime);
if (callback)
callbackSet.add(callback);
var promise = this._allFetches[clusterEndTime];
if (promise) {
if (callback)
promise.then(callback, callback);
} else {
promise = this._fetchSecondaryCluster(clusterEndTime);
for (var existingCallback of callbackSet)
promise.then(existingCallback, existingCallback);
this._allFetches[clusterEndTime] = promise;
}
return promise;
}
_constructUrl(useCache, clusterEndTime)
{
if (!useCache) {
return `/api/measurement-set?platform=${this._platformId}&metric=${this._metricId}`;
}
var url;
url = `/data/measurement-set-${this._platformId}-${this._metricId}`;
if (clusterEndTime)
url += '-' + +clusterEndTime;
url += '.json';
return url;
}
_fetchPrimaryCluster(noCache)
{
var self = this;
if (noCache) {
return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null)).then(function (data) {
self._didFetchJSON(true, data);
});
}
return RemoteAPI.getJSONWithStatus(self._constructUrl(true, null)).then(function (data) {
if (+data['lastModified'] < self._lastModified)
return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null));
return data;
}).catch(function (error) {
if(error == 404)
return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null));
return Promise.reject(error);
}).then(function (data) {
self._didFetchJSON(true, data);
});
}
_fetchSecondaryCluster(endTime)
{
var self = this;
return RemoteAPI.getJSONWithStatus(self._constructUrl(true, endTime)).then(function (data) {
self._didFetchJSON(false, data);
});
}
_didFetchJSON(isPrimaryCluster, response, clusterEndTime)
{
if (isPrimaryCluster) {
this._primaryClusterEndTime = response['endTime'];
this._clusterCount = response['clusterCount'];
this._clusterStart = response['clusterStart'];
this._clusterSize = response['clusterSize'];
} else
console.assert(this._primaryClusterEndTime);
this._addFetchedCluster(new MeasurementCluster(response));
}
_addFetchedCluster(cluster)
{
for (var clusterIndex = 0; clusterIndex < this._sortedClusters.length; clusterIndex++) {
var startTime = this._sortedClusters[clusterIndex].startTime();
if (cluster.startTime() <= startTime) {
this._sortedClusters.splice(clusterIndex, startTime == cluster.startTime() ? 1 : 0, cluster);
return;
}
}
this._sortedClusters.push(cluster);
}
hasFetchedRange(startTime, endTime)
{
console.assert(startTime < endTime);
let foundStart = false;
let previousEndTime = null;
endTime = Math.min(endTime, this._primaryClusterEndTime);
for (var cluster of this._sortedClusters) {
const containsStart = cluster.startTime() <= startTime && startTime <= cluster.endTime();
const containsEnd = cluster.startTime() <= endTime && endTime <= cluster.endTime();
const preceedingClusterIsMissing = previousEndTime !== null && previousEndTime != cluster.startTime();
if (containsStart && containsEnd)
return true;
if (containsStart)
foundStart = true;
if (foundStart && preceedingClusterIsMissing)
return false;
if (containsEnd)
return foundStart; // Return true iff there were not missing clusters from the one that contains startTime
previousEndTime = cluster.endTime();
}
return false; // Didn't find a cluster that contains startTime or endTime
}
fetchedTimeSeries(configType, includeOutliers, extendToFuture)
{
Instrumentation.startMeasuringTime('MeasurementSet', 'fetchedTimeSeries');
// FIXME: Properly construct TimeSeries.
var series = new TimeSeries();
var idMap = {};
for (var cluster of this._sortedClusters)
cluster.addToSeries(series, configType, includeOutliers, idMap);
if (extendToFuture)
series.extendToFuture();
Instrumentation.endMeasuringTime('MeasurementSet', 'fetchedTimeSeries');
return series;
}
fetchSegmentation(segmentationName, parameters, configType, includeOutliers, extendToFuture)
{
var cacheMap = this._segmentationCache.get(configType);
if (!cacheMap) {
cacheMap = new WeakMap;
this._segmentationCache.set(configType, cacheMap);
}
var timeSeries = new TimeSeries;
var idMap = {};
var promises = [];
for (var cluster of this._sortedClusters) {
var clusterStart = timeSeries.length();
cluster.addToSeries(timeSeries, configType, includeOutliers, idMap);
var clusterEnd = timeSeries.length();
promises.push(this._cachedClusterSegmentation(segmentationName, parameters, cacheMap,
cluster, timeSeries, clusterStart, clusterEnd, idMap));
}
if (!timeSeries.length())
return Promise.resolve(null);
var self = this;
return Promise.all(promises).then(function (clusterSegmentations) {
var segmentationSeries = [];
var addSegmentMergingIdenticalSegments = function (startingPoint, endingPoint) {
var value = Statistics.mean(timeSeries.valuesBetweenRange(startingPoint.seriesIndex, endingPoint.seriesIndex));
if (!segmentationSeries.length || value !== segmentationSeries[segmentationSeries.length - 1].value) {
segmentationSeries.push({value: value, time: startingPoint.time, seriesIndex: startingPoint.seriesIndex, interval: function () { return null; }});
segmentationSeries.push({value: value, time: endingPoint.time, seriesIndex: endingPoint.seriesIndex, interval: function () { return null; }});
} else
segmentationSeries[segmentationSeries.length - 1].seriesIndex = endingPoint.seriesIndex;
};
let startingIndex = 0;
for (const segmentation of clusterSegmentations) {
for (const endingIndex of segmentation) {
addSegmentMergingIdenticalSegments(timeSeries.findPointByIndex(startingIndex), timeSeries.findPointByIndex(endingIndex));
startingIndex = endingIndex;
}
}
if (extendToFuture)
timeSeries.extendToFuture();
addSegmentMergingIdenticalSegments(timeSeries.findPointByIndex(startingIndex), timeSeries.lastPoint());
return segmentationSeries;
});
}
_cachedClusterSegmentation(segmentationName, parameters, cacheMap, cluster, timeSeries, clusterStart, clusterEnd, idMap)
{
var cache = cacheMap.get(cluster);
if (cache && this._validateSegmentationCache(cache, segmentationName, parameters)) {
var segmentationByIndex = new Array(cache.segmentation.length);
for (var i = 0; i < cache.segmentation.length; i++) {
var id = cache.segmentation[i];
if (!(id in idMap))
return null;
segmentationByIndex[i] = idMap[id];
}
return Promise.resolve(segmentationByIndex);
}
var clusterValues = timeSeries.valuesBetweenRange(clusterStart, clusterEnd);
return this._invokeSegmentationAlgorithm(segmentationName, parameters, clusterValues).then(function (segmentationInClusterIndex) {
// Remove cluster start/end as segmentation points. Otherwise each cluster will be placed into its own segment.
var segmentation = segmentationInClusterIndex.slice(1, -1).map(function (index) { return clusterStart + index; });
var cache = segmentation.map(function (index) { return timeSeries.findPointByIndex(index).id; });
cacheMap.set(cluster, {segmentationName: segmentationName, segmentationParameters: parameters.slice(), segmentation: cache});
return segmentation;
});
}
_validateSegmentationCache(cache, segmentationName, parameters)
{
if (cache.segmentationName != segmentationName)
return false;
if (!!cache.segmentationParameters != !!parameters)
return false;
if (parameters) {
if (parameters.length != cache.segmentationParameters.length)
return false;
for (var i = 0; i < parameters.length; i++) {
if (parameters[i] != cache.segmentationParameters[i])
return false;
}
}
return true;
}
_invokeSegmentationAlgorithm(segmentationName, parameters, timeSeriesValues)
{
var args = [timeSeriesValues].concat(parameters || []);
var timeSeriesIsShortEnoughForSyncComputation = timeSeriesValues.length < 100;
if (timeSeriesIsShortEnoughForSyncComputation || !AsyncTask.isAvailable()) {
Instrumentation.startMeasuringTime('_invokeSegmentationAlgorithm', 'syncSegmentation');
var segmentation = Statistics[segmentationName].apply(timeSeriesValues, args);
Instrumentation.endMeasuringTime('_invokeSegmentationAlgorithm', 'syncSegmentation');
return Promise.resolve(segmentation);
}
var task = new AsyncTask(segmentationName, args);
return task.execute().then(function (response) {
Instrumentation.reportMeasurement('_invokeSegmentationAlgorithm', 'workerStartLatency', 'ms', response.startLatency);
Instrumentation.reportMeasurement('_invokeSegmentationAlgorithm', 'workerTime', 'ms', response.workerTime);
Instrumentation.reportMeasurement('_invokeSegmentationAlgorithm', 'totalTime', 'ms', response.totalTime);
return response.result;
});
}
}
if (typeof module != 'undefined')
module.exports.MeasurementSet = MeasurementSet;