MeasurementSet should merge last four segments into two if values are identical.
[WebKit.git] / Websites / perf.webkit.org / public / v3 / models / measurement-set.js
1 'use strict';
2
3 if (!Array.prototype.includes)
4     Array.prototype.includes = function (value) { return this.indexOf(value) >= 0; }
5
6 class MeasurementSet {
7     constructor(platformId, metricId, lastModified)
8     {
9         this._platformId = platformId;
10         this._metricId = metricId;
11         this._lastModified = +lastModified;
12
13         this._sortedClusters = [];
14         this._primaryClusterEndTime = null;
15         this._clusterCount = null;
16         this._clusterStart = null;
17         this._clusterSize = null;
18         this._allFetches = {};
19         this._callbackMap = new Map;
20         this._primaryClusterPromise = null;
21         this._segmentationCache = new Map;
22     }
23
24     platformId() { return this._platformId; }
25     metricId() { return this._metricId; }
26
27     static findSet(platformId, metricId, lastModified)
28     {
29         if (!this._set)
30             this._set = {};
31         var key = platformId + '-' + metricId;
32         if (!this._set[key])
33             this._set[key] = new MeasurementSet(platformId, metricId, lastModified);
34         return this._set[key];
35     }
36
37     findClusters(startTime, endTime)
38     {
39         var clusterStart = this._clusterStart;
40         var clusterSize = this._clusterSize;
41
42         var clusters = [];
43         var clusterEnd = clusterStart + Math.floor(Math.max(0, startTime - clusterStart) / clusterSize) * clusterSize;
44
45         var lastClusterEndTime = this._primaryClusterEndTime;
46         var firstClusterEndTime = lastClusterEndTime - clusterSize * (this._clusterCount - 1);
47         do {
48             clusterEnd += clusterSize;
49             if (firstClusterEndTime <= clusterEnd && clusterEnd <= this._primaryClusterEndTime)
50                 clusters.push(clusterEnd);
51         } while (clusterEnd < endTime);
52
53         return clusters;
54     }
55
56     fetchBetween(startTime, endTime, callback, noCache)
57     {
58         if (noCache) {
59             this._primaryClusterPromise = null;
60             this._allFetches = {};
61         }
62         if (!this._primaryClusterPromise)
63             this._primaryClusterPromise = this._fetchPrimaryCluster(noCache);
64         var self = this;
65         this._primaryClusterPromise.catch(callback);
66         return this._primaryClusterPromise.then(function () {
67             self._allFetches[self._primaryClusterEndTime] = self._primaryClusterPromise;
68             return Promise.all(self.findClusters(startTime, endTime).map(function (clusterEndTime) {
69                 return self._ensureClusterPromise(clusterEndTime, callback);
70             }));
71         });
72     }
73
74     _ensureClusterPromise(clusterEndTime, callback)
75     {
76         if (!this._callbackMap.has(clusterEndTime))
77             this._callbackMap.set(clusterEndTime, new Set);
78         var callbackSet = this._callbackMap.get(clusterEndTime);
79         callbackSet.add(callback);
80
81         var promise = this._allFetches[clusterEndTime];
82         if (promise)
83             promise.then(callback, callback);
84         else {
85             promise = this._fetchSecondaryCluster(clusterEndTime);
86             for (var existingCallback of callbackSet)
87                 promise.then(existingCallback, existingCallback);
88             this._allFetches[clusterEndTime] = promise;
89         }
90
91         return promise;
92     }
93
94     _constructUrl(useCache, clusterEndTime)
95     {
96         if (!useCache) {
97             return `/api/measurement-set?platform=${this._platformId}&metric=${this._metricId}`;
98         }
99         var url;
100         url = `/data/measurement-set-${this._platformId}-${this._metricId}`;
101         if (clusterEndTime)
102             url += '-' + +clusterEndTime;
103         url += '.json';
104         return url;
105     }
106
107     _fetchPrimaryCluster(noCache)
108     {
109         var self = this;
110         if (noCache) {
111             return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null)).then(function (data) {
112                 self._didFetchJSON(true, data);
113             });
114         }
115
116         return RemoteAPI.getJSONWithStatus(self._constructUrl(true, null)).then(function (data) {
117             if (+data['lastModified'] < self._lastModified)
118                 return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null));
119             return data;
120         }).catch(function (error) {
121             if(error == 404)
122                 return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null));
123             return Promise.reject(error);
124         }).then(function (data) {
125             self._didFetchJSON(true, data);
126         });
127     }
128
129     _fetchSecondaryCluster(endTime)
130     {
131         var self = this;
132         return RemoteAPI.getJSONWithStatus(self._constructUrl(true, endTime)).then(function (data) {
133             self._didFetchJSON(false, data);
134         });
135     }
136
137     _didFetchJSON(isPrimaryCluster, response, clusterEndTime)
138     {
139         if (isPrimaryCluster) {
140             this._primaryClusterEndTime = response['endTime'];
141             this._clusterCount = response['clusterCount'];
142             this._clusterStart = response['clusterStart'];
143             this._clusterSize = response['clusterSize'];
144         } else
145             console.assert(this._primaryClusterEndTime);
146
147         this._addFetchedCluster(new MeasurementCluster(response));
148     }
149
150     _addFetchedCluster(cluster)
151     {
152         for (var clusterIndex = 0; clusterIndex < this._sortedClusters.length; clusterIndex++) {
153             var startTime = this._sortedClusters[clusterIndex].startTime();
154             if (cluster.startTime() <= startTime) {
155                 this._sortedClusters.splice(clusterIndex, startTime == cluster.startTime() ? 1 : 0, cluster);
156                 return;
157             }
158         }
159         this._sortedClusters.push(cluster);
160     }
161
162     hasFetchedRange(startTime, endTime)
163     {
164         console.assert(startTime < endTime);
165         let foundStart = false;
166         let previousEndTime = null;
167         endTime = Math.min(endTime, this._primaryClusterEndTime);
168         for (var cluster of this._sortedClusters) {
169             const containsStart = cluster.startTime() <= startTime && startTime <= cluster.endTime();
170             const containsEnd = cluster.startTime() <= endTime && endTime <= cluster.endTime();
171             const preceedingClusterIsMissing = previousEndTime !== null && previousEndTime != cluster.startTime();
172             if (containsStart && containsEnd)
173                 return true;
174             if (containsStart)
175                 foundStart = true;
176             if (foundStart && preceedingClusterIsMissing)
177                 return false;
178             if (containsEnd)
179                 return foundStart; // Return true iff there were not missing clusters from the one that contains startTime
180             previousEndTime = cluster.endTime();
181         }
182         return false; // Didn't find a cluster that contains startTime or endTime
183     }
184
185     fetchedTimeSeries(configType, includeOutliers, extendToFuture)
186     {
187         Instrumentation.startMeasuringTime('MeasurementSet', 'fetchedTimeSeries');
188
189         // FIXME: Properly construct TimeSeries.
190         var series = new TimeSeries();
191         var idMap = {};
192         for (var cluster of this._sortedClusters)
193             cluster.addToSeries(series, configType, includeOutliers, idMap);
194
195         if (extendToFuture)
196             series.extendToFuture();
197
198         Instrumentation.endMeasuringTime('MeasurementSet', 'fetchedTimeSeries');
199
200         return series;
201     }
202
203     fetchSegmentation(segmentationName, parameters, configType, includeOutliers, extendToFuture)
204     {
205         var cacheMap = this._segmentationCache.get(configType);
206         if (!cacheMap) {
207             cacheMap = new WeakMap;
208             this._segmentationCache.set(configType, cacheMap);
209         }
210
211         var timeSeries = new TimeSeries;
212         var idMap = {};
213         var promises = [];
214         for (var cluster of this._sortedClusters) {
215             var clusterStart = timeSeries.length();
216             cluster.addToSeries(timeSeries, configType, includeOutliers, idMap);
217             var clusterEnd = timeSeries.length();
218             promises.push(this._cachedClusterSegmentation(segmentationName, parameters, cacheMap,
219                 cluster, timeSeries, clusterStart, clusterEnd, idMap));
220         }
221         if (!timeSeries.length())
222             return Promise.resolve(null);
223
224         var self = this;
225         return Promise.all(promises).then(function (clusterSegmentations) {
226             var segmentationSeries = [];
227             var addSegmentMergingIdenticalSegments = function (startingPoint, endingPoint) {
228                 var value = Statistics.mean(timeSeries.valuesBetweenRange(startingPoint.seriesIndex, endingPoint.seriesIndex));
229                 if (!segmentationSeries.length || value !== segmentationSeries[segmentationSeries.length - 1].value) {
230                     segmentationSeries.push({value: value, time: startingPoint.time, seriesIndex: startingPoint.seriesIndex, interval: function () { return null; }});
231                     segmentationSeries.push({value: value, time: endingPoint.time, seriesIndex: endingPoint.seriesIndex, interval: function () { return null; }});
232                 } else
233                     segmentationSeries[segmentationSeries.length - 1].seriesIndex = endingPoint.seriesIndex;
234             };
235
236             let startingIndex = 0;
237             for (const segmentation of clusterSegmentations) {
238                 for (const endingIndex of segmentation) {
239                     addSegmentMergingIdenticalSegments(timeSeries.findPointByIndex(startingIndex), timeSeries.findPointByIndex(endingIndex));
240                     startingIndex = endingIndex;
241                 }
242             }
243             if (extendToFuture)
244                 timeSeries.extendToFuture();
245             addSegmentMergingIdenticalSegments(timeSeries.findPointByIndex(startingIndex), timeSeries.lastPoint());
246             return segmentationSeries;
247         });
248     }
249
250     _cachedClusterSegmentation(segmentationName, parameters, cacheMap, cluster, timeSeries, clusterStart, clusterEnd, idMap)
251     {
252         var cache = cacheMap.get(cluster);
253         if (cache && this._validateSegmentationCache(cache, segmentationName, parameters)) {
254             var segmentationByIndex = new Array(cache.segmentation.length);
255             for (var i = 0; i < cache.segmentation.length; i++) {
256                 var id = cache.segmentation[i];
257                 if (!(id in idMap))
258                     return null;
259                 segmentationByIndex[i] = idMap[id];
260             }
261             return Promise.resolve(segmentationByIndex);
262         }
263
264         var clusterValues = timeSeries.valuesBetweenRange(clusterStart, clusterEnd);
265         return this._invokeSegmentationAlgorithm(segmentationName, parameters, clusterValues).then(function (segmentationInClusterIndex) {
266             // Remove cluster start/end as segmentation points. Otherwise each cluster will be placed into its own segment. 
267             var segmentation = segmentationInClusterIndex.slice(1, -1).map(function (index) { return clusterStart + index; });
268             var cache = segmentation.map(function (index) { return timeSeries.findPointByIndex(index).id; });
269             cacheMap.set(cluster, {segmentationName: segmentationName, segmentationParameters: parameters.slice(), segmentation: cache});
270             return segmentation;
271         });
272     }
273
274     _validateSegmentationCache(cache, segmentationName, parameters)
275     {
276         if (cache.segmentationName != segmentationName)
277             return false;
278         if (!!cache.segmentationParameters != !!parameters)
279             return false;
280         if (parameters) {
281             if (parameters.length != cache.segmentationParameters.length)
282                 return false;
283             for (var i = 0; i < parameters.length; i++) {
284                 if (parameters[i] != cache.segmentationParameters[i])
285                     return false;
286             }
287         }
288         return true;
289     }
290
291     _invokeSegmentationAlgorithm(segmentationName, parameters, timeSeriesValues)
292     {
293         var args = [timeSeriesValues].concat(parameters || []);
294
295         var timeSeriesIsShortEnoughForSyncComputation = timeSeriesValues.length < 100;
296         if (timeSeriesIsShortEnoughForSyncComputation || !AsyncTask.isAvailable()) {
297             Instrumentation.startMeasuringTime('_invokeSegmentationAlgorithm', 'syncSegmentation');
298             var segmentation = Statistics[segmentationName].apply(timeSeriesValues, args);
299             Instrumentation.endMeasuringTime('_invokeSegmentationAlgorithm', 'syncSegmentation');
300             return Promise.resolve(segmentation);
301         }
302
303         var task = new AsyncTask(segmentationName, args);
304         return task.execute().then(function (response) {
305             Instrumentation.reportMeasurement('_invokeSegmentationAlgorithm', 'workerStartLatency', 'ms', response.startLatency);
306             Instrumentation.reportMeasurement('_invokeSegmentationAlgorithm', 'workerTime', 'ms', response.workerTime);
307             Instrumentation.reportMeasurement('_invokeSegmentationAlgorithm', 'totalTime', 'ms', response.totalTime);
308             return response.result;
309         });
310     }
311
312 }
313
314 if (typeof module != 'undefined')
315     module.exports.MeasurementSet = MeasurementSet;