Diff
Modified: trunk/Websites/perf.webkit.org/ChangeLog (204295 => 204296)
--- trunk/Websites/perf.webkit.org/ChangeLog 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/ChangeLog 2016-08-09 21:22:54 UTC (rev 204296)
@@ -1,3 +1,143 @@
+2016-08-08 Ryosuke Niwa <[email protected]>
+
+ Always show segmentation on v3 charts page
+ https://bugs.webkit.org/show_bug.cgi?id=160576
+
+ Rubber-stamped by Chris Dumez.
+
+ Added "Trend Lines" popover to select and customize a moving average or a segmentation to show on charts page
+ and made Schwarz criterion segmentation the default trend line for all charts.
+
+ Because computing the segmentation is expensive, we use WebWorker to parallelize the computation via AsyncTask.
+ We also compute and cache the segmentation for each cluster separately to avoid processing the entire measurement
+ set as that could take 10-20s total, which was a huge problem in v2 UI. v3 UI's approach is more incremental and
+ even opens up an opportunity to cache the results in the server side.
+
+ Also brought back "shading" for the confidence interval drawing as done in v1 and v2 UI.
+
+ * public/shared/statistics.js:
+ (Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion): Added segmentCountWeight and gridSize as arguments
+ to customize the algorithm.
+ (Statistics.splitIntoSegmentsUntilGoodEnough): Takes segmentCountWeight as BirgeAndMassartC.
+
+ * public/v3/async-task.js: Added.
+ (AsyncTask): Added. This class represents a task such as computing segmentation to be executed in a worker.
+ (AsyncTask.prototype.execute): Added. Returns a promise that gets resolved when the specified task completes.
+ (AsyncTaskWorker.waitForAvailableWorker): Added. Calls the given callback with the first available worker. When
+ all workers are processing some tasks, it waits until one becomes available by putting the callback into a queue.
+ _didRecieveMessage pops an item out of this queue when a worker completes a task. We don't use a promise here
+ because calling this function multiple times synchronously could result in all the returned promises getting
+ resolved with the same worker as none of the callers get to lock away the first available worker until the end
+ of the current micro-task.
+ (AsyncTaskWorker._makeWorkerEventuallyAvailable): Added. A helper function for waitForAvailableWorker. Start
+ a new worker if the number of workers we've started is less than the number of extra cores (e.g. 7 if there are
+ 8 cores on the machine). Avoid starting a new worker if we've started a new worker within the last 50 ms since
+ starting a new worker takes some time.
+ (AsyncTaskWorker._findAvailableWorker): Added. Finds a worker that's available right now if there is any.
+ (AsyncTaskWorker): Added. An instance of AsyncTaskWorker represents a Web worker.
+ (AsyncTaskWorker.prototype.id): Added.
+ (AsyncTaskWorker.prototype.sendTask): Added. Sends a task represented by AsyncTask to the worker.
+ (AsyncTaskWorker.prototype._didRecieveMessage): Added. This function gets called when the current task completes
+ in the worker. Pop the next callback if some caller of waitForAvailableWorker is still waiting. Otherwise stop
+ the worker after one second of waiting to avoid worker churning.
+ (AsyncTaskWorker.workerDidRecieveMessage): Added. Called by onmessage on the worker. Executes the specified task
+ and sends back a message upon completion with the appropriate timing data.
+
+ * public/v3/components/chart-pane-base.js:
+ (ChartPaneBase.prototype.configure): Uses _createSourceList.
+ (ChartPaneBase.prototype._createSourceList): Added. Extracted from configure to customize the source list for
+ the main chart and the overview chart.
+ (ChartPaneBase.prototype._updateSourceList): Uses _createSourceList.
+
+ * public/v3/components/chart-styles.js:
+ (ChartStyles.createSourceList): Added a boolean showPoint as an extra argument. This specifies whether circles
+ are drawn for each data point.
+ (ChartStyles.baselineStyle): Added styles for foreground lines and background lines. They're used for trend lines
+ and underlying raw data respectively when trend lines are shown.
+ (ChartStyles.targetStyle): Ditto.
+ (ChartStyles.currentStyle): Ditto.
+
+ * public/v3/components/time-series-chart.js:
+ (TimeSeriesChart): Added _trendLines, _renderedTrendLines, and _fetchedTimeSeries as instance variables.
+ (TimeSeriesChart.prototype.setSourceList): Clear _fetchedTimeSeries before calling setSourceList for consistency.
+ (TimeSeriesChart.prototype.sourceList): Added.
+ (TimeSeriesChart.prototype.clearTrendLines): Added.
+ (TimeSeriesChart.prototype.setTrendLine): Added. Preserves the existing trend lines for other sources. This is
+ necessary because segmentation for "current" and "baseline" lines may become available at different times, and we
+ don't want to clear one or the other when setting one.
+ (TimeSeriesChart.prototype._layout): Added a call to _ensureTrendLines.
+ (TimeSeriesChart.prototype._renderChartContent): Call _renderTimeSeries for trend lines. Trend lines are always
+ foreground lines and "regular" raw data points are drawn as background if there are trend lines.
+ (TimeSeriesChart.prototype._renderTimeSeries): Added layerName as an argument. It could be an empty string,
+ "foreground", or "background". Draw a "shade" just like v1 and v2 UI instead of vertical lines for the confidence
+ intervals. Pick "foreground", "background", or "regular" chart style based on layerName. Also avoid drawing data
+ points when *PointRadius is set to zero to reduce the runtime of this function.
+ (TimeSeriesChart.prototype._sourceOptionWithFallback): Added.
+ (TimeSeriesChart.prototype._ensureSampledTimeSeries): When *PointRadius is 0, show as many points as there are x
+ coordinates as a fallback instead of showing every point.
+ (TimeSeriesChart.prototype._ensureTrendLines): Added. Returns true if the chart contents haven't been re-rendered
+ since the last update to trend lines. This flag is unset by setTrendLine.
+
+ * public/v3/index.html:
+
+ * public/v3/models/measurement-cluster.js:
+ (MeasurementCluster.prototype.addToSeries): Store the data points' index to idMap to help aid MeasurementSet's
+ _cachedClusterSegmentation efficiently re-create the segmentation from the cache.
+
+ * public/v3/models/measurement-set.js:
+ (MeasurementSet): Added _segmentationCache as an instance variable.
+ (MeasurementSet.prototype.fetchSegmentation): Added. Calls _cachedClusterSegmentation on each cluster, and
+ constructs the time series representation of the segmentation from the results.
+ (MeasurementSet.prototype._cachedClusterSegmentation): Computes and caches the segmentation for each cluster.
+ The cache of segmentation stores ID of each measurement set at which segment changes instead of its index since
+ the latter could change in any moment when a new test result is reported, or an existing test result is removed
+ from the time series; e.g. when it's marked as an outlier.
+ (MeasurementSet.prototype._validateSegmentationCache): Added. Checks whether the cached segmentation's name and
+ its parameters match that of the requested one.
+ (MeasurementSet.prototype._invokeSegmentationAlgorithm): Added. Invokes the segmentation algorithm either in the
+ main thread or in a Web worker via AsyncTask API based on the size of the time series. While parallelizing the
+ work is beneficial when the data set is large, the overhead can add up if we keep processing a very small data
+ set in a worker.
+
+ * public/v3/models/time-series.js: Made the file compatible with Node.
+ (TimeSeries.prototype.length): Added.
+ (TimeSeries.prototype.valuesBetweenRange): Added.
+
+ * public/v3/pages/chart-pane.js:
+ (createTrendLineExecutableFromAveragingFunction): Added.
+ (ChartTrendLineTypes): Added. Similar to StatisticsStrategies (statistics-strategies.js) in v2 UI.
+ (ChartPane): Added _trendLineType, _trendLineParameters, _trendLineVersion, and _renderedTrendLineOptions as
+ instance variables.
+ (ChartPane.prototype.serializeState): Serialize the trend line option. This format is compatible with v2 UI.
+ (ChartPane.prototype.updateFromSerializedState): Ditto. Parsing is compatible with v2 UI except that we now have
+ the default trend line set when the specified ID doesn't match an existing type ID.
+ (ChartPane.prototype._renderActionToolbar): Added a call to _renderTrendLinePopover. This is the popover that
+ specifies the type of a trend line to show as well as its parameters.
+ (ChartPane.prototype._renderTrendLinePopover): Added. A popover for specifying and customizing a trend line.
+ (ChartPane.prototype._trendLineTypeDidChange): Added. Called when a new trend line is selected.
+ (ChartPane.prototype._defaultParametersForTrendLine): Added.
+ (ChartPane.prototype._trendLineParameterDidChange): Added. Called when the trend lines' parameters are changed.
+ (ChartPane.prototype._didFetchData): Added. Overrides the one in ChartPaneBase to trigger a trend line update.
+ (ChartPane.prototype._updateTrendLine): Added. Update the trend line. Since segmentation can take an arbitrary
+ long time, avoid updating trend lines if this function had been called again (possibly for a different trend line
+ type or with different parameters) before the results become available; hence the versioning.
+ (ChartPane.paneHeaderTemplate): Added the trend line popover.
+ (ChartPane.cssTemplate): Added styles for the trend line popover. Also use a more opaque background color behind
+ popovers when the -webkit-backdrop-filter property is not supported.
+
+ * public/v3/pages/dashboard-page.js:
+ (DashboardPage.prototype._createChartForCell): Call createSourceList with showPoints set to true to preserve the
+ existing behavior.
+
+ * tools/js/v3-models.js: Include TimeSeries object.
+
+ * unit-tests/measurement-set-tests.js: Added two test cases for MeasurementSet's fetchSegmentation.
+
+ * unit-tests/resources/almost-equal.js: Added.
+ (almostEqual): Extracted out of statistics-tests.js.
+
+ * unit-tests/statistics-tests.js:
+
2016-08-05 Ryosuke Niwa <[email protected]>
segmentTimeSeriesByMaximizingSchwarzCriterion returns a bogus result on empty charts
Modified: trunk/Websites/perf.webkit.org/public/shared/statistics.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/public/shared/statistics.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/shared/statistics.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -173,13 +173,13 @@
}
this.debuggingSegmentation = false;
- this.segmentTimeSeriesByMaximizingSchwarzCriterion = function (values) {
+ this.segmentTimeSeriesByMaximizingSchwarzCriterion = function (values, segmentCountWeight, gridSize) {
// Split the time series into grids since splitIntoSegmentsUntilGoodEnough is O(n^2).
- var gridLength = 500;
+ var gridLength = gridSize || 500;
var totalSegmentation = [0];
for (var gridCount = 0; gridCount < Math.ceil(values.length / gridLength); gridCount++) {
var gridValues = values.slice(gridCount * gridLength, (gridCount + 1) * gridLength);
- var segmentation = splitIntoSegmentsUntilGoodEnough(gridValues);
+ var segmentation = splitIntoSegmentsUntilGoodEnough(gridValues, segmentCountWeight);
if (Statistics.debuggingSegmentation)
console.log('grid=' + gridCount, segmentation);
@@ -271,7 +271,7 @@
function oneSidedToTwoSidedProbability(probability) { return 2 * probability - 1; }
function twoSidedToOneSidedProbability(probability) { return (1 - (1 - probability) / 2); }
- function splitIntoSegmentsUntilGoodEnough(values) {
+ function splitIntoSegmentsUntilGoodEnough(values, BirgeAndMassartC) {
if (values.length < 2)
return [0, values.length];
@@ -279,7 +279,7 @@
var SchwarzCriterionBeta = Math.log1p(values.length - 1) / values.length;
- var BirgeAndMassartC = 2.5; // Suggested by the authors.
+ BirgeAndMassartC = BirgeAndMassartC || 2.5; // Suggested by the authors.
var BirgeAndMassartPenalization = function (segmentCount) {
return segmentCount * (1 + BirgeAndMassartC * Math.log1p(values.length / segmentCount - 1));
}
Added: trunk/Websites/perf.webkit.org/public/v3/async-task.js (0 => 204296)
--- trunk/Websites/perf.webkit.org/public/v3/async-task.js (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/async-task.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -0,0 +1,151 @@
+
+class AsyncTask {
+
+ constructor(method, args)
+ {
+ this._method = method;
+ this._args = args;
+ }
+
+ execute()
+ {
+ if (!(this._method in Statistics))
+ throw `${this._method} is not a valid method of Statistics`;
+
+ AsyncTask._asyncMessageId++;
+
+ var startTime = Date.now();
+ var method = this._method;
+ var args = this._args;
+ return new Promise(function (resolve, reject) {
+ AsyncTaskWorker.waitForAvailableWorker(function (worker) {
+ worker.sendTask({id: AsyncTask._asyncMessageId, method: method, args: args}).then(function (data) {
+ var startLatency = data.workerStartTime - startTime;
+ var totalTime = Date.now() - startTime;
+ var callback = data.status == 'resolve' ? resolve : reject;
+ callback({result: data.result, workerId: worker.id(), startLatency: startLatency, totalTime: totalTime, workerTime: data.workerTime});
+ });
+ });
+ });
+ }
+
+}
+
+AsyncTask._asyncMessageId = 0;
+
+class AsyncTaskWorker {
+
+ // Takes a callback instead of returning a promise because a worker can become unavailable before the end of the current microtask.
+ static waitForAvailableWorker(callback)
+ {
+ var worker = this._makeWorkerEventuallyAvailable();
+ if (worker)
+ callback(worker);
+ this._queue.push(callback);
+ }
+
+ static _makeWorkerEventuallyAvailable()
+ {
+ var worker = this._findAvailableWorker();
+ if (worker)
+ return worker;
+
+ var canStartMoreWorker = this._workerSet.size < this._maxWorkerCount;
+ if (!canStartMoreWorker)
+ return null;
+
+ if (this._latestStartTime > Date.now() - 50) {
+ setTimeout(function () {
+ var worker = AsyncTaskWorker._findAvailableWorker();
+ if (worker)
+ AsyncTaskWorker._queue.pop()(worker);
+ }, 50);
+ return null;
+ }
+ return new AsyncTaskWorker;
+ }
+
+ static _findAvailableWorker()
+ {
+ for (var worker of this._workerSet) {
+ if (!worker._currentTaskId)
+ return worker;
+ }
+ return null;
+ }
+
+ constructor()
+ {
+ this._webWorker = new Worker('async-task.js');
+ this._webWorker._onmessage_ = this._didRecieveMessage.bind(this);
+ this._id = AsyncTaskWorker._workerId;
+ this._startTime = Date.now();
+ this._currentTaskId = null;
+ this._callback = null;
+
+ AsyncTaskWorker._latestStartTime = this._startTime;
+ AsyncTaskWorker._workerId++;
+ AsyncTaskWorker._workerSet.add(this);
+ }
+
+ id() { return this._id; }
+
+ sendTask(task)
+ {
+ console.assert(!this._currentTaskId);
+ console.assert(task.id);
+ var self = this;
+ this._currentTaskId = task.id;
+ return new Promise(function (resolve) {
+ self._webWorker.postMessage(task);
+ self._callback = resolve;
+ });
+ }
+
+ _didRecieveMessage(event)
+ {
+ var callback = this._callback;
+
+ console.assert(this._currentTaskId);
+ this._currentTaskId = null;
+ this._callback = null;
+
+ if (AsyncTaskWorker._queue.length)
+ AsyncTaskWorker._queue.pop()(this);
+ else {
+ var self = this;
+ setTimeout(function () {
+ if (self._currentTaskId == null)
+ AsyncTaskWorker._workerSet.delete(self);
+ }, 1000);
+ }
+
+ callback(event.data);
+ }
+
+ static workerDidRecieveMessage(event)
+ {
+ var data = ""
+ var id = data.id;
+ var method = Statistics[data.method];
+ var startTime = Date.now();
+ try {
+ var returnValue = method.apply(Statistics, data.args);
+ postMessage({'id': id, 'status': 'resolve', 'result': returnValue, 'workerStartTime': startTime, 'workerTime': Date.now() - startTime});
+ } catch (error) {
+ postMessage({'id': id, 'status': 'reject', 'result': error.toString(), 'workerStartTime': startTime, 'workerTime': Date.now() - startTime});
+ throw error;
+ }
+ }
+}
+
+AsyncTaskWorker._maxWorkerCount = typeof navigator != 'undefined' && 'hardwareConcurrency' in navigator ? Math.max(1, navigator.hardwareConcurrency - 1) : 1;
+AsyncTaskWorker._workerSet = new Set;
+AsyncTaskWorker._queue = [];
+AsyncTaskWorker._workerId = 1;
+AsyncTaskWorker._latestStartTime = 0;
+
+if (typeof module == 'undefined' && typeof window == 'undefined' && typeof importScripts != 'undefined') { // Inside a worker
+ importScripts('/shared/statistics.js');
+ _onmessage_ = AsyncTaskWorker.workerDidRecieveMessage.bind(AsyncTaskWorker);
+}
Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -42,11 +42,9 @@
var formatter = result.metric.makeFormatter(4);
var self = this;
- var sourceList = ChartStyles.createSourceList(this._platform, this._metric, this._disableSampling, this._showOutliers);
-
var overviewOptions = ChartStyles.overviewChartOptions(formatter);
overviewOptions.selection._onchange_ = this._overviewSelectionDidChange.bind(this);
- this._overviewChart = new InteractiveTimeSeriesChart(sourceList, overviewOptions);
+ this._overviewChart = new InteractiveTimeSeriesChart(this._createSourceList(false), overviewOptions);
this.renderReplace(this.content().querySelector('.chart-pane-overview'), this._overviewChart);
var mainOptions = ChartStyles.mainChartOptions(formatter);
@@ -55,7 +53,7 @@
mainOptions.selection._onzoom_ = this._mainSelectionDidZoom.bind(this);
mainOptions.annotations._onclick_ = this._openAnalysisTask.bind(this);
mainOptions._ondata_ = this._didFetchData.bind(this);
- this._mainChart = new InteractiveTimeSeriesChart(sourceList, mainOptions);
+ this._mainChart = new InteractiveTimeSeriesChart(this._createSourceList(true), mainOptions);
this.renderReplace(this.content().querySelector('.chart-pane-main'), this._mainChart);
this._mainChartStatus = new ChartPaneStatusView(result.metric, this._mainChart, this._requestOpeningCommitViewer.bind(this));
@@ -80,11 +78,15 @@
this._updateSourceList();
}
+ _createSourceList(isMainChart)
+ {
+ return ChartStyles.createSourceList(this._platform, this._metric, this._disableSampling, this._showOutliers, isMainChart);
+ }
+
_updateSourceList()
{
- var sourceList = ChartStyles.createSourceList(this._platform, this._metric, this._disableSampling, this._showOutliers);
- this._mainChart.setSourceList(sourceList);
- this._overviewChart.setSourceList(sourceList);
+ this._mainChart.setSourceList(this._createSourceList(true));
+ this._overviewChart.setSourceList(this._createSourceList(false));
}
fetchAnalysisTasks(noCache)
Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -17,7 +17,7 @@
};
}
- static createSourceList(platform, metric, disableSampling, includeOutlier)
+ static createSourceList(platform, metric, disableSampling, includeOutlier, showPoint)
{
console.assert(platform instanceof Platform);
console.assert(metric instanceof Metric);
@@ -27,13 +27,13 @@
var measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
return [
- this.baselineStyle(measurementSet, disableSampling, includeOutlier),
- this.targetStyle(measurementSet, disableSampling, includeOutlier),
- this.currentStyle(measurementSet, disableSampling, includeOutlier),
+ this.baselineStyle(measurementSet, disableSampling, includeOutlier, showPoint),
+ this.targetStyle(measurementSet, disableSampling, includeOutlier, showPoint),
+ this.currentStyle(measurementSet, disableSampling, includeOutlier, showPoint),
];
}
- static baselineStyle(measurementSet, disableSampling, includeOutlier)
+ static baselineStyle(measurementSet, disableSampling, includeOutlier, showPoint)
{
return {
measurementSet: measurementSet,
@@ -42,15 +42,20 @@
includeOutliers: includeOutlier,
type: 'baseline',
pointStyle: '#f33',
- pointRadius: 2,
- lineStyle: '#f99',
+ pointRadius: showPoint ? 2 : 0,
+ lineStyle: showPoint ? '#f99' : '#f66',
lineWidth: 1.5,
- intervalStyle: '#fdd',
- intervalWidth: 2,
+ intervalStyle: 'rgba(255, 153, 153, 0.25)',
+ intervalWidth: 3,
+ foregroundLineStyle: '#f33',
+ foregroundPointRadius: 0,
+ backgroundIntervalStyle: 'rgba(255, 153, 153, 0.1)',
+ backgroundPointStyle: '#f99',
+ backgroundLineStyle: '#fcc',
};
}
- static targetStyle(measurementSet, disableSampling, includeOutlier)
+ static targetStyle(measurementSet, disableSampling, includeOutlier, showPoint)
{
return {
measurementSet: measurementSet,
@@ -59,15 +64,20 @@
includeOutliers: includeOutlier,
type: 'target',
pointStyle: '#33f',
- pointRadius: 2,
- lineStyle: '#99f',
+ pointRadius: showPoint ? 2 : 0,
+ lineStyle: showPoint ? '#99f' : '#66f',
lineWidth: 1.5,
- intervalStyle: '#ddf',
- intervalWidth: 2,
+ intervalStyle: 'rgba(153, 153, 255, 0.25)',
+ intervalWidth: 3,
+ foregroundLineStyle: '#33f',
+ foregroundPointRadius: 0,
+ backgroundIntervalStyle: 'rgba(153, 153, 255, 0.1)',
+ backgroundPointStyle: '#99f',
+ backgroundLineStyle: '#ccf',
};
}
- static currentStyle(measurementSet, disableSampling, includeOutlier)
+ static currentStyle(measurementSet, disableSampling, includeOutlier, showPoint)
{
return {
measurementSet: measurementSet,
@@ -75,11 +85,16 @@
includeOutliers: includeOutlier,
type: 'current',
pointStyle: '#333',
- pointRadius: 2,
- lineStyle: '#999',
+ pointRadius: showPoint ? 2 : 0,
+ lineStyle: showPoint ? '#999' : '#666',
lineWidth: 1.5,
- intervalStyle: '#ddd',
- intervalWidth: 2,
+ intervalStyle: 'rgba(153, 153, 153, 0.25)',
+ intervalWidth: 3,
+ foregroundLineStyle: '#333',
+ foregroundPointRadius: 0,
+ backgroundIntervalStyle: 'rgba(153, 153, 153, 0.1)',
+ backgroundPointStyle: '#999',
+ backgroundLineStyle: '#ccc',
interactive: true,
};
}
Modified: trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -7,9 +7,11 @@
this.element().style.position = 'relative';
this._canvas = null;
this._sourceList = sourceList;
+ this._trendLines = null;
this._options = options;
this._fetchedTimeSeries = null;
this._sampledTimeSeriesData = null;
+ this._renderedTrendLines = false;
this._valueRangeCache = null;
this._annotations = null;
this._annotationRows = null;
@@ -76,6 +78,7 @@
console.assert(startTime < endTime, 'startTime must be before endTime');
this._startTime = startTime;
this._endTime = endTime;
+ this._fetchedTimeSeries = null;
this.fetchMeasurementSets(false);
}
@@ -82,10 +85,30 @@
setSourceList(sourceList)
{
this._sourceList = sourceList;
+ this._fetchedTimeSeries = null;
this.fetchMeasurementSets(false);
- this._fetchedTimeSeries = null;
}
+ sourceList() { return this._sourceList; }
+
+ clearTrendLines()
+ {
+ this._trendLines = null;
+ this._renderedTrendLines = false;
+ this.enqueueToRender();
+ }
+
+ setTrendLine(sourceIndex, trendLine)
+ {
+ if (this._trendLines)
+ this._trendLines = this._trendLines.slice(0);
+ else
+ this._trendLines = [];
+ this._trendLines[sourceIndex] = trendLine;
+ this._renderedTrendLines = false;
+ this.enqueueToRender();
+ }
+
fetchMeasurementSets(noCache)
{
var fetching = false;
@@ -198,6 +221,7 @@
var doneWork = this._updateCanvasSizeIfClientSizeChanged();
var metrics = this._computeHorizontalRenderingMetrics();
doneWork |= this._ensureSampledTimeSeries(metrics);
+ doneWork |= this._ensureTrendLines();
doneWork |= this._ensureValueRangeCache();
this._computeVerticalRenderingMetrics(metrics);
doneWork |= this._layoutAnnotationBars(metrics);
@@ -386,9 +410,16 @@
var source = this._sourceList[i];
var series = this._sampledTimeSeriesData[i];
if (series)
- this._renderTimeSeries(context, metrics, source, series);
+ this._renderTimeSeries(context, metrics, source, series, this._trendLines && this._trendLines[i] ? 'background' : '');
}
+ for (var i = 0; i < this._sourceList.length; i++) {
+ var source = this._sourceList[i];
+ var trendLine = this._trendLines ? this._trendLines[i] : null;
+ if (series && trendLine)
+ this._renderTimeSeries(context, metrics, source, trendLine, 'foreground');
+ }
+
if (!this._annotationRows)
return;
@@ -402,7 +433,7 @@
}
}
- _renderTimeSeries(context, metrics, source, series)
+ _renderTimeSeries(context, metrics, source, series, layerName)
{
for (var point of series) {
point.x = metrics.timeToX(point.time);
@@ -412,29 +443,45 @@
context.strokeStyle = source.intervalStyle;
context.fillStyle = source.intervalStyle;
context.lineWidth = source.intervalWidth;
+
+ context.beginPath();
+ var width = 1;
for (var i = 0; i < series.length; i++) {
var point = series[i];
- if (!point.interval)
- continue;
- context.beginPath();
- context.moveTo(point.x, metrics.valueToY(point.interval[0]))
- context.lineTo(point.x, metrics.valueToY(point.interval[1]));
- context.stroke();
+ var interval = point.interval();
+ var value = interval ? interval[0] : point.value;
+ context.lineTo(point.x - width, metrics.valueToY(value));
+ context.lineTo(point.x + width, metrics.valueToY(value));
}
+ for (var i = series.length - 1; i >= 0; i--) {
+ var point = series[i];
+ var interval = point.interval();
+ var value = interval ? interval[1] : point.value;
+ context.lineTo(point.x + width, metrics.valueToY(value));
+ context.lineTo(point.x - width, metrics.valueToY(value));
+ }
+ context.fill();
- context.strokeStyle = source.lineStyle;
- context.lineWidth = source.lineWidth;
+ context.strokeStyle = this._sourceOptionWithFallback(source, layerName + 'LineStyle', 'lineStyle');
+ context.lineWidth = this._sourceOptionWithFallback(source, layerName + 'LineWidth', 'lineWidth');
context.beginPath();
for (var point of series)
context.lineTo(point.x, point.y);
context.stroke();
- context.fillStyle = source.pointStyle;
- var radius = source.pointRadius;
- for (var point of series)
- this._fillCircle(context, point.x, point.y, radius);
+ context.fillStyle = this._sourceOptionWithFallback(source, layerName + 'PointStyle', 'pointStyle');
+ var radius = this._sourceOptionWithFallback(source, layerName + 'PointRadius', 'pointRadius');
+ if (radius) {
+ for (var point of series)
+ this._fillCircle(context, point.x, point.y, radius);
+ }
}
+ _sourceOptionWithFallback(option, preferred, fallback)
+ {
+ return preferred in option ? option[preferred] : option[fallback];
+ }
+
_fillCircle(context, cx, cy, radius)
{
context.beginPath();
@@ -476,7 +523,7 @@
return null;
// A chart with X px width shouldn't have more than 2X / <radius-of-points> data points.
- var maximumNumberOfPoints = 2 * metrics.chartWidth / source.pointRadius;
+ var maximumNumberOfPoints = 2 * metrics.chartWidth / (source.pointRadius || 2);
var pointAfterStart = timeSeries.findPointAfterTime(startTime);
var pointBeforeStart = (pointAfterStart ? timeSeries.previousPoint(pointAfterStart) : null) || timeSeries.firstPoint();
@@ -547,6 +594,14 @@
return sampledData;
}
+ _ensureTrendLines()
+ {
+ if (this._renderedTrendLines)
+ return false;
+ this._renderedTrendLines = true;
+ return true;
+ }
+
_ensureValueRangeCache()
{
if (this._valueRangeCache)
Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (204295 => 204296)
--- trunk/Websites/perf.webkit.org/public/v3/index.html 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html 2016-08-09 21:22:54 UTC (rev 204296)
@@ -43,6 +43,7 @@
<script src=""
<script src=""
<script src=""
+ <script src=""
<script src=""
<script src=""
Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -21,9 +21,9 @@
var point = this._adaptor.applyTo(row);
if (point.id in idMap || (!includeOutliers && point.isOutlier))
continue;
- idMap[point.id] = true;
+ series.append(point);
+ idMap[point.id] = point.seriesIndex;
point.cluster = this;
- series.append(point);
}
}
}
Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -18,6 +18,7 @@
this._allFetches = {};
this._callbackMap = new Map;
this._primaryClusterPromise = null;
+ this._segmentationCache = new Map;
}
platformId() { return this._platformId; }
@@ -196,6 +197,113 @@
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 addSegment = function (startingPoint, endingPoint) {
+ var value = Statistics.mean(timeSeries.valuesBetweenRange(startingPoint.seriesIndex, endingPoint.seriesIndex));
+ segmentationSeries.push({value: value, time: startingPoint.time, interval: function () { return null; }});
+ segmentationSeries.push({value: value, time: endingPoint.time, interval: function () { return null; }});
+ };
+
+ var startingIndex = 0;
+ for (var segmentation of clusterSegmentations) {
+ for (var endingIndex of segmentation) {
+ addSegment(timeSeries.findPointByIndex(startingIndex), timeSeries.findPointByIndex(endingIndex));
+ startingIndex = endingIndex;
+ }
+ }
+ if (extendToFuture)
+ timeSeries.extendToFuture();
+ addSegment(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) {
+ 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')
Modified: trunk/Websites/perf.webkit.org/public/v3/models/time-series.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/public/v3/models/time-series.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/models/time-series.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -1,12 +1,15 @@
+'use strict';
// v3 UI still relies on RunsData for associating metrics with units.
// Use declartive syntax once that dependency has been removed.
-TimeSeries = class {
+var TimeSeries = class {
constructor()
{
this._data = [];
}
+ length() { return this._data.length; }
+
append(item)
{
console.assert(item.series === undefined);
@@ -29,6 +32,17 @@
});
}
+ valuesBetweenRange(startingIndex, endingIndex)
+ {
+ startingIndex = Math.max(startingIndex, 0);
+ endingIndex = Math.min(endingIndex, this._data.length);
+ var length = endingIndex - startingIndex;
+ var values = new Array(length);
+ for (var i = 0; i < length; i++)
+ values[i] = this._data[startingIndex + i].value;
+ return values;
+ }
+
firstPoint() { return this._data.length ? this._data[0] : null; }
lastPoint() { return this._data.length ? this._data[this._data.length - 1] : null; }
@@ -67,3 +81,6 @@
}
};
+
+if (typeof module != 'undefined')
+ module.exports.TimeSeries = TimeSeries;
Modified: trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -1,4 +1,69 @@
+function createTrendLineExecutableFromAveragingFunction(callback) {
+ return function (source, parameters) {
+ var timeSeries = source.measurementSet.fetchedTimeSeries(source.type, source.includeOutliers, source.extendToFuture);
+ var values = timeSeries.values();
+ if (!values.length)
+ return Promise.resolve(null);
+
+ var averageValues = callback.call(null, values, parameters[0], parameters[1]);
+ if (!averageValues)
+ return Promise.resolve(null);
+
+ var interval = function () { return null; }
+ var result = new Array(averageValues.length);
+ for (var i = 0; i < averageValues.length; i++)
+ result[i] = {time: timeSeries.findPointByIndex(i).time, value: averageValues[i], interval: interval};
+
+ return Promise.resolve(result);
+ }
+}
+
+var ChartTrendLineTypes = [
+ {
+ id: 0,
+ label: 'None',
+ },
+ {
+ id: 5,
+ label: 'Segmentation',
+ execute: function (source, parameters) {
+ return source.measurementSet.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', parameters,
+ source.type, source.includeOutliers, source.extendToFuture).then(function (segmentation) {
+ return segmentation;
+ });
+ },
+ parameterList: [
+ {label: "Segment count weight", value: 2.5, min: 0.01, max: 10, step: 0.01},
+ {label: "Grid size", value: 500, min: 100, max: 10000, step: 10}
+ ]
+ },
+ {
+ id: 1,
+ label: 'Simple Moving Average',
+ parameterList: [
+ {label: "Backward window size", value: 8, min: 2, step: 1},
+ {label: "Forward window size", value: 4, min: 0, step: 1}
+ ],
+ execute: createTrendLineExecutableFromAveragingFunction(Statistics.movingAverage.bind(Statistics))
+ },
+ {
+ id: 2,
+ label: 'Cumulative Moving Average',
+ execute: createTrendLineExecutableFromAveragingFunction(Statistics.cumulativeMovingAverage.bind(Statistics))
+ },
+ {
+ id: 3,
+ label: 'Exponential Moving Average',
+ parameterList: [
+ {label: "Smoothing factor", value: 0.01, min: 0.001, max: 0.9, step: 0.001},
+ ],
+ execute: createTrendLineExecutableFromAveragingFunction(Statistics.exponentialMovingAverage.bind(Statistics))
+ },
+];
+ChartTrendLineTypes.DefaultType = ChartTrendLineTypes[1];
+
+
class ChartPane extends ChartPaneBase {
constructor(chartsPage, platformId, metricId)
{
@@ -7,6 +72,10 @@
this._mainChartIndicatorWasLocked = false;
this._chartsPage = chartsPage;
this._lockedPopover = null;
+ this._trendLineType = null;
+ this._trendLineParameters = [];
+ this._trendLineVersion = 0;
+ this._renderedTrandLineOptions = false;
this.content().querySelector('close-button').component().setCallback(chartsPage.closePane.bind(chartsPage, this));
@@ -34,6 +103,9 @@
if (graphOptions.size)
state[3] = graphOptions;
+ if (this._trendLineType)
+ state[4] = [this._trendLineType.id].concat(this._trendLineParameters);
+
return state;
}
@@ -52,6 +124,7 @@
this._mainChart.setIndicator(null, false);
// FIXME: This forces sourceList to be set twice. First in configure inside the constructor then here.
+ // FIXME: Show full y-axis when graphOptions is true to be compatible with v2 UI.
var graphOptions = state[3];
if (graphOptions instanceof Set) {
this.setSamplingEnabled(!graphOptions.has('nosampling'));
@@ -58,8 +131,21 @@
this.setShowOutliers(graphOptions.has('showoutliers'));
}
- // FIXME: Show full y-axis when graphOptions is true to be compatible with v2 UI.
- // FIXME: state[4] specifies moving average in v2 UI
+ var trendLineOptions = state[4];
+ if (!(trendLineOptions instanceof Array))
+ trendLineOptions = [];
+
+ var trendLineId = trendLineOptions[0];
+ var trendLineType = ChartTrendLineTypes.find(function (type) { return type.id == trendLineId; }) || ChartTrendLineTypes.DefaultType;
+
+ this._trendLineType = trendLineType;
+ this._trendLineParameters = (trendLineType.parameterList || []).map(function (parameter, index) {
+ var specifiedValue = parseFloat(trendLineOptions[index + 1]);
+ return !isNaN(specifiedValue) ? specifiedValue : parameter.value;
+ });
+ this._updateTrendLine();
+ this._renderedTrandLineOptions = false;
+
// FIXME: state[5] specifies envelope in v2 UI
// FIXME: state[6] specifies change detection algorithm in v2 UI
}
@@ -202,7 +288,11 @@
var filteringOptions = this.content().querySelector('.chart-pane-filtering-options');
actions.push(this._makePopoverActionItem(filteringOptions, 'Filtering', true));
+ var trendLineOptions = this.content().querySelector('.chart-pane-trend-line-options');
+ actions.push(this._makePopoverActionItem(trendLineOptions, 'Trend lines', true));
+
this._renderFilteringPopover();
+ this._renderTrendLinePopover();
this._lockedPopover = null;
this.renderReplace(this.content().querySelector('.chart-pane-action-buttons'), actions);
@@ -307,6 +397,117 @@
markAsOutlierButton.disabled = !firstSelectedPoint;
}
+ _renderTrendLinePopover()
+ {
+ var element = ComponentBase.createElement;
+ var link = ComponentBase.createLink;
+ var self = this;
+
+ if (this._trendLineType == null) {
+ this.renderReplace(this.content().querySelector('.trend-line-types'), [
+ element('select', {onchange: this._trendLineTypeDidChange.bind(this)},
+ ChartTrendLineTypes.map(function (type) {
+ return element('option', type == self._trendLineType ? {value: type.id, selected: true} : {value: type.id}, type.label);
+ }))
+ ]);
+ } else
+ this.content().querySelector('.trend-line-types select').value = this._trendLineType.id;
+
+ if (this._renderedTrandLineOptions)
+ return;
+ this._renderedTrandLineOptions = true;
+
+ if (this._trendLineParameters.length) {
+ var configuredParameters = this._trendLineParameters;
+ this.renderReplace(this.content().querySelector('.trend-line-parameter-list'), [
+ element('h3', 'Parameters'),
+ element('ul', this._trendLineType.parameterList.map(function (parameter, index) {
+ var attributes = {type: 'number'};
+ for (var name in parameter)
+ attributes[name] = parameter[name];
+ attributes.value = configuredParameters[index];
+ var input = element('input', attributes);
+ input.parameterIndex = index;
+ input._oninput_ = self._trendLineParameterDidChange.bind(self);
+ input._onchange_ = self._trendLineParameterDidChange.bind(self);
+ return element('li', element('label', [parameter.label + ': ', input]));
+ }))
+ ]);
+ } else
+ this.renderReplace(this.content().querySelector('.trend-line-parameter-list'), []);
+ }
+
+ _trendLineTypeDidChange(event)
+ {
+ var newType = ChartTrendLineTypes.find(function (type) { return type.id == event.target.value });
+ if (newType == this._trendLineType)
+ return;
+
+ this._trendLineType = newType;
+ this._trendLineParameters = this._defaultParametersForTrendLine(newType);
+ this._renderedTrandLineOptions = false;
+
+ this._updateTrendLine();
+ this._chartsPage.graphOptionsDidChange();
+ this.render();
+ }
+
+ _defaultParametersForTrendLine(type)
+ {
+ return type && type.parameterList ? type.parameterList.map(function (parameter) { return parameter.value; }) : [];
+ }
+
+ _trendLineParameterDidChange(event)
+ {
+ var input = event.target;
+ var index = input.parameterIndex;
+ var newValue = parseFloat(input.value);
+ if (this._trendLineParameters[index] == newValue)
+ return;
+ this._trendLineParameters[index] = newValue;
+ var self = this;
+ setTimeout(function () { // Some trend lines, e.g. sementations, are expensive.
+ if (self._trendLineParameters[index] != newValue)
+ return;
+ self._updateTrendLine();
+ self._chartsPage.graphOptionsDidChange();
+ }, 500);
+ }
+
+ _didFetchData()
+ {
+ super._didFetchData();
+ this._updateTrendLine();
+ }
+
+ _updateTrendLine()
+ {
+ if (!this._mainChart.sourceList())
+ return;
+
+ this._trendLineVersion++;
+ var currentTrendLineType = this._trendLineType || ChartTrendLineTypes.DefaultType;
+ var currentTrendLineParameters = this._trendLineParameters || this._defaultParametersForTrendLine(currentTrendLineType);
+ var currentTrendLineVersion = this._trendLineVersion;
+ var self = this;
+ var sourceList = this._mainChart.sourceList();
+
+ if (!currentTrendLineType.execute) {
+ this._mainChart.clearTrendLines();
+ this.render();
+ } else {
+ // Wait for all trendlines to be ready. Otherwise we might see FOC when the domain is expanded.
+ Promise.all(sourceList.map(function (source, sourceIndex) {
+ return currentTrendLineType.execute.call(null, source, currentTrendLineParameters).then(function (trendlineSeries) {
+ if (self._trendLineVersion == currentTrendLineVersion)
+ self._mainChart.setTrendLine(sourceIndex, trendlineSeries);
+ });
+ })).then(function () {
+ self.render();
+ });
+ }
+ }
+
static paneHeaderTemplate()
{
return `
@@ -327,6 +528,10 @@
<li><label><input type="checkbox" class="show-outliers">Show outliers</label></li>
<li><button class="mark-as-outlier">Mark selected points as outlier</button></li>
</ul>
+ <ul class="chart-pane-trend-line-options popover" style="display:none">
+ <div class="trend-line-types"></div>
+ <div class="trend-line-parameter-list"></div>
+ </ul>
</nav>
</header>
`;
@@ -397,14 +602,20 @@
border: solid 1px #ccc;
border-radius: 0.2rem;
z-index: 10;
- background: rgba(255, 255, 255, 0.8);
- -webkit-backdrop-filter: blur(0.5rem);
padding: 0.2rem 0;
margin: 0;
margin-top: -0.2rem;
margin-right: -0.2rem;
+ background: rgba(255, 255, 255, 0.95);
}
+ @supports ( -webkit-backdrop-filter: blur(0.5rem) ) {
+ .chart-pane-actions .popover {
+ background: rgba(255, 255, 255, 0.6);
+ -webkit-backdrop-filter: blur(0.5rem);
+ }
+ }
+
.chart-pane-actions .popover li {
}
@@ -429,6 +640,32 @@
font-size: 0.9rem;
}
+ .chart-pane-actions .popover.chart-pane-filtering-options {
+ padding: 0.2rem;
+ }
+
+ .chart-pane-actions .popover.chart-pane-trend-line-options h3 {
+ font-size: 0.9rem;
+ line-height: 0.9rem;
+ font-weight: inherit;
+ margin: 0;
+ padding: 0.2rem;
+ border-bottom: solid 1px #ccc;
+ }
+
+ .chart-pane-actions .popover.chart-pane-trend-line-options select,
+ .chart-pane-actions .popover.chart-pane-trend-line-options label {
+ margin: 0.2rem;
+ }
+
+ .chart-pane-actions .popover.chart-pane-trend-line-options label {
+ font-size: 0.8rem;
+ }
+
+ .chart-pane-actions .popover.chart-pane-trend-line-options input {
+ width: 2.5rem;
+ }
+
.chart-pane-actions .popover input[type=text] {
font-size: 1rem;
width: 15rem;
Modified: trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -134,7 +134,7 @@
var options = ChartStyles.dashboardOptions(result.metric.makeFormatter(3));
options._ondata_ = this._fetchedData.bind(this);
- var chart = new TimeSeriesChart(ChartStyles.createSourceList(result.platform, result.metric, false, false), options);
+ var chart = new TimeSeriesChart(ChartStyles.createSourceList(result.platform, result.metric, false, false, true), options);
this._charts.push(chart);
var statusView = new ChartStatusView(result.metric, chart);
Modified: trunk/Websites/perf.webkit.org/tools/js/v3-models.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/tools/js/v3-models.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/tools/js/v3-models.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -26,6 +26,7 @@
importFromV3('models/root-set.js', 'RootSet');
importFromV3('models/test.js', 'Test');
importFromV3('models/test-group.js', 'TestGroup');
+importFromV3('models/time-series.js', 'TimeSeries');
importFromV3('privileged-api.js', 'PrivilegedAPI');
importFromV3('instrumentation.js', 'Instrumentation');
Modified: trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -1,6 +1,8 @@
'use strict';
var assert = require('assert');
+if (!assert.almostEqual)
+ assert.almostEqual = require('./resources/almost-equal.js');
let MockRemoteAPI = require('./resources/mock-remote-api.js').MockRemoteAPI;
require('../tools/js/v3-models.js');
@@ -741,4 +743,143 @@
});
+ describe('fetchSegmentation', function () {
+
+ var simpleSegmentableValues = [
+ 1546.5603, 1548.1536, 1563.5452, 1539.7823, 1546.4184, 1548.9299, 1532.5444, 1546.2800, 1547.1760, 1551.3507,
+ 1548.3277, 1544.7673, 1542.7157, 1538.1700, 1538.0948, 1543.0364, 1537.9737, 1542.2611, 1543.9685, 1546.4901,
+ 1544.4080, 1540.8671, 1537.3353, 1549.4331, 1541.4436, 1544.1299, 1550.1770, 1553.1872, 1549.3417, 1542.3788,
+ 1543.5094, 1541.7905, 1537.6625, 1547.3840, 1538.5185, 1549.6764, 1556.6138, 1552.0476, 1541.7629, 1544.7006,
+ /* segments changes here */
+ 1587.1390, 1594.5451, 1586.2430, 1596.7310, 1548.1423
+ ];
+
+ function makeSampleRuns(values, startRunId, startTime, timeIncrement)
+ {
+ var runId = startRunId;
+ var buildId = 3400;
+ var buildNumber = 1;
+ var makeRun = function (value, commitTime) {
+ return [runId++, value, 1, value, value, false, [], commitTime, commitTime + 10, buildId++, buildNumber++, MockModels.builder.id()];
+ }
+
+ timeIncrement = Math.floor(timeIncrement);
+ var runs = values.map(function (value, index) { return makeRun(value, startTime + index * timeIncrement); })
+
+ return runs;
+ }
+
+ it('should be able to segment a single cluster', function (done) {
+ var set = MeasurementSet.findSet(1, 1, 5000);
+ var promise = set.fetchBetween(4000, 5000);
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].url, '../data/measurement-set-1-1.json');
+
+ requests[0].resolve({
+ 'clusterStart': 1000,
+ 'clusterSize': 1000,
+ 'formatMap': ['id', 'mean', 'iterationCount', 'sum', 'squareSum', 'markedOutlier', 'revisions', 'commitTime', 'build', 'buildTime', 'buildNumber', 'builder'],
+ 'configurations': {current: makeSampleRuns(simpleSegmentableValues, 6400, 4000, 1000 / 50)},
+ 'startTime': 4000,
+ 'endTime': 5000,
+ 'lastModified': 5000,
+ 'clusterCount': 4,
+ 'status': 'OK'});
+
+ var timeSeries;
+ assert.equal(set.fetchedTimeSeries('current', false, false).length(), 0);
+ waitForMeasurementSet().then(function () {
+ timeSeries = set.fetchedTimeSeries('current', false, false);
+ assert.equal(timeSeries.length(), 45);
+ assert.equal(timeSeries.firstPoint().time, 4000);
+ assert.equal(timeSeries.lastPoint().time, 4880);
+ return set.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', [], 'current', false);
+ }).then(function (segmentation) {
+ assert.equal(segmentation.length, 4);
+
+ assert.equal(segmentation[0].time, 4000);
+ assert.almostEqual(segmentation[0].value, 1545.082);
+ assert.equal(segmentation[0].value, segmentation[1].value);
+ assert.equal(segmentation[1].time, timeSeries.findPointByIndex(39).time);
+
+ assert.equal(segmentation[2].time, timeSeries.findPointByIndex(39).time);
+ assert.almostEqual(segmentation[2].value, 1581.872);
+ assert.equal(segmentation[2].value, segmentation[3].value);
+ assert.equal(segmentation[3].time, 4880);
+ done();
+ }).catch(done);
+ });
+
+ it('should be able to segment two clusters', function (done) {
+ var set = MeasurementSet.findSet(1, 1, 5000);
+ var promise = set.fetchBetween(3000, 5000);
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].url, '../data/measurement-set-1-1.json');
+
+ requests[0].resolve({
+ 'clusterStart': 1000,
+ 'clusterSize': 1000,
+ 'formatMap': ['id', 'mean', 'iterationCount', 'sum', 'squareSum', 'markedOutlier', 'revisions', 'commitTime', 'build', 'buildTime', 'buildNumber', 'builder'],
+ 'configurations': {current: makeSampleRuns(simpleSegmentableValues.slice(30), 6400, 4000, 1000 / 30)},
+ 'startTime': 4000,
+ 'endTime': 5000,
+ 'lastModified': 5000,
+ 'clusterCount': 4,
+ 'status': 'OK'});
+
+ waitForMeasurementSet().then(function () {
+ assert.equal(requests.length, 2);
+ assert.equal(requests[1].url, '../data/measurement-set-1-1-4000.json');
+ return set.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', [], 'current', false);
+ }).then(function (segmentation) {
+ var timeSeries = set.fetchedTimeSeries('current', false, false);
+ assert.equal(timeSeries.length(), 15);
+ assert.equal(timeSeries.firstPoint().time, 4000);
+ assert.equal(timeSeries.lastPoint().time, 4462);
+
+ assert.equal(segmentation.length, 4);
+ assert.equal(segmentation[0].time, timeSeries.firstPoint().time);
+ assert.almostEqual(segmentation[0].value, 1545.441);
+ assert.equal(segmentation[0].value, segmentation[1].value);
+ assert.equal(segmentation[1].time, timeSeries.findPointByIndex(9).time);
+
+ assert.equal(segmentation[2].time, timeSeries.findPointByIndex(9).time);
+ assert.almostEqual(segmentation[2].value, 1581.872);
+ assert.equal(segmentation[2].value, segmentation[3].value);
+ assert.equal(segmentation[3].time, timeSeries.lastPoint().time);
+
+ requests[1].resolve({
+ 'clusterStart': 1000,
+ 'clusterSize': 1000,
+ 'formatMap': ['id', 'mean', 'iterationCount', 'sum', 'squareSum', 'markedOutlier', 'revisions', 'commitTime', 'build', 'buildTime', 'buildNumber', 'builder'],
+ 'configurations': {current: makeSampleRuns(simpleSegmentableValues.slice(0, 30), 6500, 3000, 1000 / 30)},
+ 'startTime': 3000,
+ 'endTime': 4000,
+ 'lastModified': 5000,
+ 'clusterCount': 4,
+ 'status': 'OK'});
+ return waitForMeasurementSet();
+ }).then(function () {
+ return set.fetchSegmentation('segmentTimeSeriesByMaximizingSchwarzCriterion', [], 'current', false);
+ }).then(function (segmentation) {
+ var timeSeries = set.fetchedTimeSeries('current', false, false);
+ assert.equal(timeSeries.length(), 45);
+ assert.equal(timeSeries.firstPoint().time, 3000);
+ assert.equal(timeSeries.lastPoint().time, 4462);
+ assert.equal(segmentation.length, 4);
+
+ assert.equal(segmentation[0].time, timeSeries.firstPoint().time);
+ assert.almostEqual(segmentation[0].value, 1545.082);
+ assert.equal(segmentation[0].value, segmentation[1].value);
+ assert.equal(segmentation[1].time, timeSeries.findPointByIndex(39).time);
+
+ assert.equal(segmentation[2].time, timeSeries.findPointByIndex(39).time);
+ assert.almostEqual(segmentation[2].value, 1581.872);
+ assert.equal(segmentation[2].value, segmentation[3].value);
+ assert.equal(segmentation[3].time, timeSeries.lastPoint().time);
+ done();
+ }).catch(done);
+ });
+
+ });
});
Added: trunk/Websites/perf.webkit.org/unit-tests/resources/almost-equal.js (0 => 204296)
--- trunk/Websites/perf.webkit.org/unit-tests/resources/almost-equal.js (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/resources/almost-equal.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -0,0 +1,26 @@
+var assert = require('assert');
+
+function almostEqual(actual, expected, precision, message)
+{
+ var suffiedMessage = (message ? message + ' ' : '');
+ if (isNaN(expected)) {
+ assert(isNaN(actual), `${suffiedMessage}expected NaN but got ${actual}`);
+ return;
+ }
+
+ if (expected == 0) {
+ assert.equal(actual, expected, message);
+ return;
+ }
+
+ if (!precision)
+ precision = 6;
+ var tolerance = 1 / Math.pow(10, precision);
+ var relativeDifference = Math.abs((actual - expected) / expected);
+ var percentDifference = (relativeDifference * 100).toFixed(2);
+ assert(relativeDifference < tolerance,
+ `${suffiedMessage}expected ${expected} but got ${actual} (${percentDifference}% difference)`);
+}
+
+if (typeof module != 'undefined')
+ module.exports = almostEqual;
Modified: trunk/Websites/perf.webkit.org/unit-tests/statistics-tests.js (204295 => 204296)
--- trunk/Websites/perf.webkit.org/unit-tests/statistics-tests.js 2016-08-09 21:22:26 UTC (rev 204295)
+++ trunk/Websites/perf.webkit.org/unit-tests/statistics-tests.js 2016-08-09 21:22:54 UTC (rev 204296)
@@ -2,30 +2,10 @@
var assert = require('assert');
var Statistics = require('../public/shared/statistics.js');
+if (!assert.almostEqual)
+ assert.almostEqual = require('./resources/almost-equal.js');
-if (!assert.almostEqual) {
- assert.almostEqual = function (actual, expected, precision, message) {
- var suffiedMessage = (message ? message + ' ' : '');
- if (isNaN(expected)) {
- assert(isNaN(actual), `${suffiedMessage}expected NaN but got ${actual}`);
- return;
- }
- if (expected == 0) {
- assert.equal(actual, expected, message);
- return;
- }
-
- if (!precision)
- precision = 6;
- var tolerance = 1 / Math.pow(10, precision);
- var relativeDifference = Math.abs((actual - expected) / expected);
- var percentDifference = (relativeDifference * 100).toFixed(2);
- assert(relativeDifference < tolerance,
- `${suffiedMessage}expected ${expected} but got ${actual} (${percentDifference}% difference)`);
- }
-}
-
describe('assert.almostEqual', function () {
it('should not throw when values are identical', function () {
assert.doesNotThrow(function () { assert.almostEqual(1, 1); });