Diff
Modified: trunk/Websites/perf.webkit.org/ChangeLog (201563 => 201564)
--- trunk/Websites/perf.webkit.org/ChangeLog 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/ChangeLog 2016-06-01 19:55:38 UTC (rev 201564)
@@ -1,3 +1,119 @@
+2016-05-31 Ryosuke Niwa <[email protected]>
+
+ v3 UI should support marking and unmarking outliers as well as hiding them
+ https://bugs.webkit.org/show_bug.cgi?id=158248
+
+ Rubber-stamped by Chris Dumez.
+
+ Added the support for marking and unmarking a sequence of points as outliers. Unlike v2, we now support marking
+ multiple points as outliers in a single click. Also fixed a bug that outliers are never explicitly hidden in v3 UI.
+
+ This patch splits ChartStyles.createChartSourceList into two functions: resolveConfiguration and createSourceList
+ to separate the work of resolving platform and metric IDs to their respective model objects, and creating a source
+ list used by TimeSeriesChart to fetch measurement sets. createSourceList is called again when filtering options are
+ changed.
+
+ It also adds noCache option to TimeSeriesChart's fetchMeasurementSets, MeasurementSet's fetchBetween and
+ _fetchPrimaryCluster to update the measurement sets after marking or unmarking points as outliers. In addition, it
+ fixes a bug that the annotation bars for analysis tasks are not updated in charts page after creating an analysis
+ task by adding noCache option to ChartPaneBase's fetchAnalysisTasks, AnalysisTask's fetchByPlatformAndMetric and
+ _fetchSubset.
+
+ Finally, this patch splits ChartPane._makeAnchorToOpenPane into _makePopoverActionItem, _makePopoverOpenOnHover and
+ _setPopoverVisibility for clarity.
+
+ * public/v3/components/chart-pane-base.js:
+ (ChartPaneBase): Added _disableSampling and _showOutliers as instance variables.
+ (ChartPaneBase.prototype.configure):
+ (ChartPaneBase.prototype.isSamplingEnabled): Added.
+ (ChartPaneBase.prototype.setSamplingEnabled): Added. When a filtering option is updated, recreate the source list
+ so that TimeSeriesChart.setSourceList can re-fetch the measurement set JSONs.
+ (ChartPaneBase.prototype.isShowingOutliers): Added.
+ (ChartPaneBase.prototype.setShowOutliers): Added. Ditto for calling _updateSourceList.
+ (ChartPaneBase.prototype._updateSourceList): Added.
+ (ChartPaneBase.prototype.fetchAnalysisTasks): Renamed from _fetchAnalysisTasks. Now takes noCache as an argument
+ instead of platform and metric IDs since they're on instance variables.
+
+ * public/v3/components/chart-styles.js:
+ (ChartStyles.resolveConfiguration): Renamed from createChartSourceList. Just resolves platform and metric IDs.
+ (ChartStyles.createSourceList): Extracted from createChartSourceList since it needs to be called when a filtering
+ option is changed as well as when ChartPaneBase.prototype.configure is called.
+ (ChartStyles.baselineStyle): Now takes filtering options.
+ (ChartStyles.targetStyle): Ditto.
+ (ChartStyles.currentStyle): Ditto.
+
+ * public/v3/components/interactive-time-series-chart.js:
+ (InteractiveTimeSeriesChart.prototype.currentPoint): Find the point in _fetchedTimeSeries when
+ _sampledTimeSeriesData hasn't been computed yet as a fallback (e.g. when the chart hasn't been rendered yet).
+ (InteractiveTimeSeriesChart.prototype.selectedPoints): Added.
+ (InteractiveTimeSeriesChart.prototype.firstSelectedPoint): Added.
+ (InteractiveTimeSeriesChart.prototype.lockedIndicator): Added. Returns the current point if it's locked.
+
+ * public/v3/components/time-series-chart.js:
+ (TimeSeriesChart.prototype.setDomain):
+ (TimeSeriesChart.prototype.setSourceList): Added. Re-create _fetchedTimeSeries when filtering options have changed.
+ Don't re-fetch measurement set JSONs here since showing outliers can be done entirely in the front end.
+ (TimeSeriesChart.prototype.fetchMeasurementSets): Extracted out of setDomain. Now takes noCache as an argument.
+ ChartPane._markAsOutlier
+ (TimeSeriesChart.prototype.firstSampledPointBetweenTime): Added.
+
+ * public/v3/models/analysis-task.js:
+ (AnalysisTask.fetchByPlatformAndMetric): Added noCache as an argument.
+ (AnalysisTask._fetchSubset): Ditto.
+
+ * public/v3/models/measurement-adaptor.js:
+ (MeasurementAdaptor.prototype.isOutlier): Added.
+ (MeasurementAdaptor.prototype.applyToAnalysisResults): Add markedOutlier as a property on each point.
+
+ * public/v3/models/measurement-cluster.js:
+ (MeasurementCluster.prototype.addToSeries): Fixed the bug that filtering outliers was broken as _markedOutlierIndex
+ is undefined here. Use MeasurementAdaptor's isOutlier instead.
+
+ * public/v3/models/measurement-set.js:
+ (MeasurementSet.prototype.fetchBetween): Added noCache as an argument. Reset _primaryClusterPromise and _allFetches
+ when noCache is true since we need to re-fetch the primary cluster as well as all secondary clusters now.
+ (MeasurementSet.prototype._fetchPrimaryCluster): Added noCache as an argument. Directly invoke the JSON API at
+ /api/measurement-set to re-generate all clusters' JSON files instead of first fetching the cached version.
+ (MeasurementSet.prototype._fetchSecondaryCluster):
+ (MeasurementSet.prototype._didFetchJSON): Removed a bogus assertion since this function is called on secondary
+ clusters as well as primary clusters.
+ (MeasurementSet.prototype._addFetchedCluster): Reimplemented this function using an insertion sort. Also remove the
+ existing entry if the fetch cluster should replace it.
+
+ * public/v3/models/time-series.js:
+ (TimeSeries.prototype.dataBetweenPoints): Removed the dead code to filter out outliers. This is done in addToSeries
+ of MeasurementCluster instead.
+
+ * public/v3/pages/chart-pane.js:
+ (ChartPane): Renamed pane to popover since it was confusing to have a pane inside a pane class. As such, renamed
+ _paneOpenedByClick to _lockedPopover.
+ (ChartPane.prototype.serializeState): Added the code to serialize filtering options in the serialized state URL.
+ (ChartPane.prototype.updateFromSerializedState): Ditto for parsing.
+ (ChartPane.prototype._analyzeRange): Extracted out of render(). Also fixed a bug that the charts page don't show
+ the newly created analysis task by invoking fetchAnalysisTasks with noCache set to true.
+ (ChartPane.prototype._markAsOutlier): Added.
+ (ChartPane.prototype._renderActionToolbar): A bunch of changes due to pane -> popover rename. Also added a popover
+ for filtering options.
+ (ChartPane.prototype._makePopoverActionItem): Extracted from _makeAnchorToOpenPane.
+ (ChartPane.prototype._makePopoverOpenOnHover): Ditto.
+ (ChartPane.prototype._setPopoverVisibility): Ditto.
+ (ChartPane.prototype._renderFilteringPopover): Added.
+ (ChartPane.htmlTemplate): Added a popover for specifying filtering options. Also added .popover on each popover.
+ (ChartPane.cssTemplate): Updated the style to make use of .popover.
+
+ * public/v3/pages/charts-page.js:
+ (ChartsPage.prototype.graphOptionsDidChange): Added. Updates the URL state when a filtering option is modified.
+
+ * public/v3/pages/dashboard-page.js:
+ (DashboardPage.prototype._createChartForCell):
+
+ * public/v3/pages/page-router.js:
+ (PageRouter.prototype._serializeHashQueryValue): Serialize a set of strings as | separated tokens.
+ (PageRouter.prototype._deserializeHashQueryValue): Rewrote the function as the serialized URL can no longer be
+ parsed as a JSON as | separated tokens can't be converted into a valid JSON construct with a simple regex.
+
+ * unit-tests/measurement-set-tests.js: Added a test case for fetchBetween with noCache=true.
+
2016-05-24 Ryosuke Niwa <[email protected]>
Another build fix after r201307.
Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -10,6 +10,8 @@
this._metricId = null;
this._platform = null;
this._metric = null;
+ this._disableSampling = false;
+ this._showOutliers = false;
this._overviewChart = null;
this._mainChart = null;
@@ -20,7 +22,7 @@
configure(platformId, metricId)
{
- var result = ChartStyles.createChartSourceList(platformId, metricId);
+ var result = ChartStyles.resolveConfiguration(platformId, metricId);
this._errorMessage = result.error;
this._platformId = platformId;
this._metricId = metricId;
@@ -39,10 +41,11 @@
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(result.sourceList, overviewOptions);
+ this._overviewChart = new InteractiveTimeSeriesChart(sourceList, overviewOptions);
this.renderReplace(this.content().querySelector('.chart-pane-overview'), this._overviewChart);
var mainOptions = ChartStyles.mainChartOptions(formatter);
@@ -51,7 +54,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(result.sourceList, mainOptions);
+ this._mainChart = new InteractiveTimeSeriesChart(sourceList, mainOptions);
this.renderReplace(this.content().querySelector('.chart-pane-main'), this._mainChart);
this._mainChartStatus = new ChartPaneStatusView(result.metric, this._mainChart, this._requestOpeningCommitViewer.bind(this));
@@ -59,14 +62,35 @@
this.content().querySelector('.chart-pane').addEventListener('keyup', this._keyup.bind(this));
- this._fetchAnalysisTasks(platformId, metricId);
+ this.fetchAnalysisTasks(false);
}
- _fetchAnalysisTasks(platformId, metricId)
+ isSamplingEnabled() { return !this._disableSampling; }
+ setSamplingEnabled(enabled)
{
+ this._disableSampling = !enabled;
+ this._updateSourceList();
+ }
+
+ isShowingOutliers() { return this._showOutliers; }
+ setShowOutliers(show)
+ {
+ this._showOutliers = !!show;
+ this._updateSourceList();
+ }
+
+ _updateSourceList()
+ {
+ var sourceList = ChartStyles.createSourceList(this._platform, this._metric, this._disableSampling, this._showOutliers);
+ this._mainChart.setSourceList(sourceList);
+ this._overviewChart.setSourceList(sourceList);
+ }
+
+ fetchAnalysisTasks(noCache)
+ {
// FIXME: we need to update the annotation bars when the change type of tasks change.
var self = this;
- AnalysisTask.fetchByPlatformAndMetric(platformId, metricId).then(function (tasks) {
+ AnalysisTask.fetchByPlatformAndMetric(this._platformId, this._metricId, noCache).then(function (tasks) {
self._tasksForAnnotations = tasks;
self.render();
});
Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-styles.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -1,6 +1,6 @@
class ChartStyles {
- static createChartSourceList(platformId, metricId)
+ static resolveConfiguration(platformId, metricId)
{
var platform = Platform.findById(platformId);
var metric = Metric.findById(metricId);
@@ -11,26 +11,35 @@
if (!lastModified)
return {platform: platform, metric: metric, error: `No results on ${platform.name()}`};
- var measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
- var sourceList = [
- this.baselineStyle(measurementSet, 'baseline'),
- this.targetStyle(measurementSet, 'target'),
- this.currentStyle(measurementSet, 'current'),
- ];
-
return {
platform: platform,
metric: metric,
- sourceList: sourceList,
};
}
- static baselineStyle(measurementSet)
+ static createSourceList(platform, metric, disableSampling, includeOutlier)
{
+ console.assert(platform instanceof Platform);
+ console.assert(metric instanceof Metric);
+
+ var lastModified = platform.lastModified(metric);
+ console.assert(lastModified);
+
+ 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),
+ ];
+ }
+
+ static baselineStyle(measurementSet, disableSampling, includeOutlier)
+ {
return {
measurementSet: measurementSet,
extendToFuture: true,
- sampleData: true,
+ sampleData: !disableSampling,
+ includeOutliers: includeOutlier,
type: 'baseline',
pointStyle: '#f33',
pointRadius: 2,
@@ -41,12 +50,13 @@
};
}
- static targetStyle(measurementSet)
+ static targetStyle(measurementSet, disableSampling, includeOutlier)
{
return {
measurementSet: measurementSet,
extendToFuture: true,
- sampleData: true,
+ sampleData: !disableSampling,
+ includeOutliers: includeOutlier,
type: 'target',
pointStyle: '#33f',
pointRadius: 2,
@@ -57,11 +67,12 @@
};
}
- static currentStyle(measurementSet)
+ static currentStyle(measurementSet, disableSampling, includeOutlier)
{
return {
measurementSet: measurementSet,
- sampleData: true,
+ sampleData: !disableSampling,
+ includeOutliers: includeOutlier,
type: 'current',
pointStyle: '#333',
pointRadius: 2,
Modified: trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -18,13 +18,20 @@
currentPoint(diff)
{
- if (!this._sampledTimeSeriesData)
- return null;
-
var id = this._indicatorID;
if (!id)
return null;
+ if (!this._sampledTimeSeriesData) {
+ this._ensureFetchedTimeSeries();
+ for (var series of this._fetchedTimeSeries) {
+ var point = series.findById(id);
+ if (point)
+ return point;
+ }
+ return null;
+ }
+
for (var data of this._sampledTimeSeriesData) {
if (!data)
continue;
@@ -40,6 +47,21 @@
currentSelection() { return this._selectionTimeRange; }
+ selectedPoints(type)
+ {
+ var selection = this._selectionTimeRange;
+ return selection ? this.sampledDataBetween(type, selection[0], selection[1]) : null;
+ }
+
+ firstSelectedPoint(type)
+ {
+ var selection = this._selectionTimeRange;
+ return selection ? this.firstSampledPointBetweenTime(type, selection[0], selection[1]) : null;
+ }
+
+ lockedIndicator() { return this._indicatorIsLocked ? this.currentPoint() : null; }
+
+
setIndicator(id, shouldLock)
{
var selectionDidChange = !!this._sampledTimeSeriesData;
Modified: trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -66,9 +66,20 @@
console.assert(startTime < endTime, 'startTime must be before endTime');
this._startTime = startTime;
this._endTime = endTime;
+ this.fetchMeasurementSets(false);
+ }
+
+ setSourceList(sourceList)
+ {
+ this._sourceList = sourceList;
+ this.fetchMeasurementSets(false);
+ }
+
+ fetchMeasurementSets(noCache)
+ {
for (var source of this._sourceList) {
if (source.measurementSet)
- source.measurementSet.fetchBetween(startTime, endTime, this._didFetchMeasurementSet.bind(this, source.measurementSet));
+ source.measurementSet.fetchBetween(this._startTime, this._endTime, this._didFetchMeasurementSet.bind(this, source.measurementSet), noCache);
}
this._sampledTimeSeriesData = null;
this._valueRangeCache = null;
@@ -103,6 +114,14 @@
return data.filter(function (point) { return startTime <= point.time && point.time <= endTime; });
}
+ firstSampledPointBetweenTime(type, startTime, endTime)
+ {
+ var data = ""
+ if (!data)
+ return null;
+ return data.find(function (point) { return startTime <= point.time && point.time <= endTime; });
+ }
+
setAnnotations(annotations)
{
this._annotations = annotations;
Modified: trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -166,9 +166,9 @@
return this._fetchSubset({buildRequest: id}).then(function (tasks) { return tasks[0]; });
}
- static fetchByPlatformAndMetric(platformId, metricId)
+ static fetchByPlatformAndMetric(platformId, metricId, noCache)
{
- return this._fetchSubset({platform: platformId, metric: metricId}).then(function (data) {
+ return this._fetchSubset({platform: platformId, metric: metricId}, noCache).then(function (data) {
return AnalysisTask.findByPlatformAndMetric(platformId, metricId);
});
}
@@ -198,11 +198,11 @@
});
}
- static _fetchSubset(params)
+ static _fetchSubset(params, noCache)
{
if (this._fetchAllPromise)
return this._fetchAllPromise;
- return this.cachedFetch('../api/analysis-tasks', params).then(this._constructAnalysisTasksFromRawData.bind(this));
+ return this.cachedFetch('../api/analysis-tasks', params, noCache).then(this._constructAnalysisTasksFromRawData.bind(this));
}
static fetchAll()
Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-adaptor.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-adaptor.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-adaptor.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -28,6 +28,11 @@
return row[this._idIndex];
}
+ isOutlier(row)
+ {
+ return row[this._markedOutlierIndex];
+ }
+
applyToAnalysisResults(row)
{
var adaptedRow = this.applyTo(row);
@@ -50,6 +55,7 @@
var self = this;
return {
id: id,
+ markedOutlier: row[this._markedOutlierIndex],
buildId: buildId,
metricId: null,
configType: null,
Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -21,7 +21,7 @@
var id = self._adaptor.extractId(row);
if (id in idMap)
return;
- if (row[self._markedOutlierIndex] && !includeOutliers)
+ if (self._adaptor.isOutlier(row) && !includeOutliers)
return;
idMap[id] = true;
Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -55,10 +55,14 @@
return clusters;
}
- fetchBetween(startTime, endTime, callback)
+ fetchBetween(startTime, endTime, callback, noCache)
{
- if (!this._primaryClusterPromise)
- this._primaryClusterPromise = this._fetchPrimaryCluster();
+ if (noCache) {
+ this._primaryClusterPromise = null;
+ this._allFetches = {};
+ }
+ if (!this._primaryClusterPromise || noCache)
+ this._primaryClusterPromise = this._fetchPrimaryCluster(noCache);
var self = this;
this._primaryClusterPromise.catch(callback);
return this._primaryClusterPromise.then(function () {
@@ -86,8 +90,16 @@
return url;
}
- _fetchPrimaryCluster() {
+ _fetchPrimaryCluster(noCache)
+ {
var self = this;
+ if (noCache) {
+ return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null)).then(function (data) {
+ self._didFetchJSON(true, data);
+ self._allFetches[self._primaryClusterEndTime] = self._primaryClusterPromise;
+ });
+ }
+
return RemoteAPI.getJSONWithStatus(self._constructUrl(true, null)).then(function (data) {
if (+data['lastModified'] < self._lastModified)
return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null));
@@ -102,7 +114,8 @@
});
}
- _fetchSecondaryCluster(endTime) {
+ _fetchSecondaryCluster(endTime)
+ {
var self = this;
return RemoteAPI.getJSONWithStatus(self._constructUrl(true, endTime)).then(function (data) {
self._didFetchJSON(false, data);
@@ -111,8 +124,6 @@
_didFetchJSON(isPrimaryCluster, response, clusterEndTime)
{
- console.assert(isPrimaryCluster);
-
if (isPrimaryCluster) {
this._primaryClusterEndTime = response['endTime'];
this._clusterCount = response['clusterCount'];
@@ -126,10 +137,14 @@
_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);
- this._sortedClusters = this._sortedClusters.sort(function (c1, c2) {
- return c1.startTime() - c2.startTime();
- });
}
hasFetchedRange(startTime, endTime)
Modified: trunk/Websites/perf.webkit.org/public/v3/models/time-series.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/models/time-series.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/models/time-series.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -61,13 +61,9 @@
dataBetweenPoints(firstPoint, lastPoint)
{
- var data = ""
- var filteredData = [];
- for (var i = firstPoint.seriesIndex; i <= lastPoint.seriesIndex; i++) {
- if (!data[i].markedOutlier)
- filteredData.push(data[i]);
- }
- return filteredData;
+ console.assert(firstPoint.series == this);
+ console.assert(lastPoint.series == this);
+ return this._data.slice(firstPoint.seriesIndex, lastPoint.seriesIndex + 1);
}
};
Modified: trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/chart-pane.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -6,7 +6,7 @@
this._mainChartIndicatorWasLocked = false;
this._chartsPage = chartsPage;
- this._paneOpenedByClick = null;
+ this._lockedPopover = null;
this.content().querySelector('close-button').component().setCallback(chartsPage.closePane.bind(chartsPage, this));
@@ -24,6 +24,16 @@
else if (this._mainChartIndicatorWasLocked && currentPoint)
state[2] = currentPoint.id;
}
+
+ var graphOptions = new Set;
+ if (!this.isSamplingEnabled())
+ graphOptions.add('noSampling');
+ if (this.isShowingOutliers())
+ graphOptions.add('showOutliers');
+
+ if (graphOptions.size)
+ state[3] = graphOptions;
+
return state;
}
@@ -40,6 +50,18 @@
this._mainChartIndicatorWasLocked = true;
} else
this._mainChart.setIndicator(null, false);
+
+ // FIXME: This forces sourceList to be set twice. First in configure inside the constructor then here.
+ var graphOptions = state[3];
+ if (graphOptions instanceof Set) {
+ this.setSamplingEnabled(!graphOptions.has('nosampling'));
+ 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
+ // FIXME: state[5] specifies envelope in v2 UI
+ // FIXME: state[6] specifies change detection algorithm in v2 UI
}
setOverviewSelection(selection)
@@ -90,6 +112,35 @@
super._indicatorDidChange(indicatorID, isLocked);
}
+ _analyzeRange(pointsRangeForAnalysis)
+ {
+ var router = this._chartsPage.router();
+ var newWindow = window.open(router.url('analysis/task/create'), '_blank');
+
+ var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
+ var name = analyzePopover.querySelector('input').value;
+ var self = this;
+ AnalysisTask.create(name, pointsRangeForAnalysis.startPointId, pointsRangeForAnalysis.endPointId).then(function (data) {
+ newWindow.location.href = "" + data['taskId']);
+ self.fetchAnalysisTasks(true);
+ // FIXME: Refetch the list of analysis tasks.
+ }, function (error) {
+ newWindow.location.href = "" {error: error});
+ });
+ }
+
+ _markAsOutlier(markAsOutlier, points)
+ {
+ var self = this;
+ return Promise.all(points.map(function (point) {
+ return PrivilegedAPI.sendRequest('update-run-status', {'run': point.id, 'markedOutlier': markAsOutlier});
+ })).then(function () {
+ self._mainChart.fetchMeasurementSets(true /* noCache */);
+ }, function (error) {
+ alert('Failed to update the outlier status: ' + error);
+ }).catch();
+ }
+
render()
{
if (this._platform && this._metric) {
@@ -122,115 +173,140 @@
})));
}
- var platformPane = this.content().querySelector('.chart-pane-alternative-platforms');
+ var platformPopover = this.content().querySelector('.chart-pane-alternative-platforms');
var alternativePlatforms = this._chartsPage.alternatePlatforms(platform, metric);
if (alternativePlatforms.length) {
- this.renderReplace(platformPane, Platform.sortByName(alternativePlatforms).map(function (platform) {
+ this.renderReplace(platformPopover, Platform.sortByName(alternativePlatforms).map(function (platform) {
return element('li', link(platform.label(), function () {
self._chartsPage.insertPaneAfter(platform, metric, self);
}));
}));
- actions.push(element('li', {class: this._paneOpenedByClick == platformPane ? 'selected' : ''},
- this._makeAnchorToOpenPane(platformPane, 'Other Platforms', true)));
- } else {
- platformPane.style.display = 'none';
- }
+ actions.push(this._makePopoverActionItem(platformPopover, 'Other Platforms', true));
+ } else
+ platformPopover.style.display = 'none';
- var analyzePane = this.content().querySelector('.chart-pane-analyze-pane');
+ var analyzePopover = this.content().querySelector('.chart-pane-analyze-popover');
var pointsRangeForAnalysis = this._mainChartStatus.pointsRangeForAnalysis();
if (pointsRangeForAnalysis) {
- actions.push(element('li', {class: this._paneOpenedByClick == analyzePane ? 'selected' : ''},
- this._makeAnchorToOpenPane(analyzePane, 'Analyze', false)));
-
- var router = this._chartsPage.router();
- analyzePane._onsubmit_ = function (event) {
+ actions.push(this._makePopoverActionItem(analyzePopover, 'Analyze', false));
+ analyzePopover._onsubmit_ = function (event) {
event.preventDefault();
- var newWindow = window.open(router.url('analysis/task/create'), '_blank');
-
- var name = analyzePane.querySelector('input').value;
- AnalysisTask.create(name, pointsRangeForAnalysis.startPointId, pointsRangeForAnalysis.endPointId).then(function (data) {
- newWindow.location.href = "" + data['taskId']);
- // FIXME: Refetch the list of analysis tasks.
- }, function (error) {
- newWindow.location.href = "" {error: error});
- });
+ self._analyzeRange(pointsRangeForAnalysis);
}
} else {
- analyzePane.style.display = 'none';
- analyzePane._onsubmit_ = function (event) { event.preventDefault(); }
+ analyzePopover.style.display = 'none';
+ analyzePopover._onsubmit_ = function (event) { event.preventDefault(); }
}
- this._paneOpenedByClick = null;
+ var filteringOptions = this.content().querySelector('.chart-pane-filtering-options');
+ actions.push(this._makePopoverActionItem(filteringOptions, 'Filtering', true));
+
+ this._renderFilteringPopover();
+
+ this._lockedPopover = null;
this.renderReplace(this.content().querySelector('.chart-pane-action-buttons'), actions);
}
- _makeAnchorToOpenPane(pane, label, shouldRespondToHover)
+ _makePopoverActionItem(popover, label, shouldRespondToHover)
{
- var anchor = null;
- var ignoreMouseLeave = false;
var self = this;
- var setPaneVisibility = function (pane, shouldShow) {
- var anchor = pane.anchor;
- if (shouldShow) {
- var width = anchor.offsetParent.offsetWidth;
- pane.style.top = anchor.offsetTop + anchor.offsetHeight + 'px';
- pane.style.right = (width - anchor.offsetLeft - anchor.offsetWidth) + 'px';
- }
- pane.style.display = shouldShow ? null : 'none';
- anchor.parentNode.className = shouldShow ? 'selected' : '';
- if (self._paneOpenedByClick == pane && !shouldShow)
- self._paneOpenedByClick = null;
+ popover.anchor = ComponentBase.createLink(label, function () {
+ var makeVisible = self._lockedPopover != popover;
+ self._setPopoverVisibility(popover, makeVisible);
+ if (makeVisible)
+ self._lockedPopover = popover;
+ });
+ if (shouldRespondToHover)
+ this._makePopoverOpenOnHover(popover);
+
+ return ComponentBase.createElement('li', {class: this._lockedPopover == popover ? 'selected' : ''}, popover.anchor);
+ }
+
+ _makePopoverOpenOnHover(popover)
+ {
+ var mouseIsInAnchor = false;
+ var mouseIsInPopover = false;
+
+ var self = this;
+ var closeIfNeeded = function () {
+ setTimeout(function () {
+ if (self._lockedPopover != popover && !mouseIsInAnchor && !mouseIsInPopover)
+ self._setPopoverVisibility(popover, false);
+ }, 0);
}
- var attributes = {
- href: '#',
- onclick: function (event) {
- event.preventDefault();
- var shouldShowPane = pane.style.display == 'none';
- if (shouldShowPane) {
- if (self._paneOpenedByClick)
- setPaneVisibility(self._paneOpenedByClick, false);
- self._paneOpenedByClick = pane;
- }
- setPaneVisibility(pane, shouldShowPane);
- },
- };
- if (shouldRespondToHover) {
- var mouseIsInAnchor = false;
- var mouseIsInPane = false;
+ popover.anchor._onmouseenter_ = function () {
+ if (self._lockedPopover)
+ return;
+ mouseIsInAnchor = true;
+ self._setPopoverVisibility(popover, true);
+ }
+ popover.anchor._onmouseleave_ = function () {
+ mouseIsInAnchor = false;
+ closeIfNeeded();
+ }
- attributes._onmouseenter_ = function () {
- if (self._paneOpenedByClick)
- return;
- mouseIsInAnchor = true;
- setPaneVisibility(pane, true);
- }
- attributes._onmouseleave_ = function () {
- setTimeout(function () {
- if (!mouseIsInPane)
- setPaneVisibility(pane, false);
- }, 0);
- mouseIsInAnchor = false;
- }
+ popover._onmouseenter_ = function () {
+ mouseIsInPopover = true;
+ }
+ popover._onmouseleave_ = function () {
+ mouseIsInPopover = false;
+ closeIfNeeded();
+ }
+ }
- pane._onmouseleave_ = function () {
- setTimeout(function () {
- if (!mouseIsInAnchor)
- setPaneVisibility(pane, false);
- }, 0);
- mouseIsInPane = false;
- }
- pane._onmouseenter_ = function () {
- mouseIsInPane = true;
- }
+ _setPopoverVisibility(popover, visible)
+ {
+ var anchor = popover.anchor;
+ if (visible) {
+ var width = anchor.offsetParent.offsetWidth;
+ popover.style.top = anchor.offsetTop + anchor.offsetHeight + 'px';
+ popover.style.right = (width - anchor.offsetLeft - anchor.offsetWidth) + 'px';
}
+ popover.style.display = visible ? null : 'none';
+ anchor.parentNode.className = visible ? 'selected' : '';
- var anchor = ComponentBase.createElement('a', attributes, label);
- pane.anchor = anchor;
- return anchor;
+ if (this._lockedPopover && this._lockedPopover != popover && visible)
+ this._setPopoverVisibility(this._lockedPopover, false);
+
+ if (this._lockedPopover == popover && !visible)
+ this._lockedPopover = null;
}
+ _renderFilteringPopover()
+ {
+ var enableSampling = this.content().querySelector('.enable-sampling');
+ enableSampling.checked = this.isSamplingEnabled();
+ enableSampling._onchange_ = function () {
+ self.setSamplingEnabled(enableSampling.checked);
+ self._chartsPage.graphOptionsDidChange();
+ }
+
+ var showOutliers = this.content().querySelector('.show-outliers');
+ showOutliers.checked = this.isShowingOutliers();
+ showOutliers._onchange_ = function () {
+ self.setShowOutliers(showOutliers.checked);
+ self._chartsPage.graphOptionsDidChange();
+ }
+
+ var markAsOutlierButton = this.content().querySelector('.mark-as-outlier');
+ var firstSelectedPoint = this._mainChart.lockedIndicator();
+ if (!firstSelectedPoint)
+ firstSelectedPoint = this._mainChart.firstSelectedPoint('current');
+ var alreayMarkedAsOutlier = firstSelectedPoint && firstSelectedPoint.markedOutlier;
+
+ var self = this;
+ markAsOutlierButton.textContent = (alreayMarkedAsOutlier ? 'Unmark' : 'Mark') + ' selected points as outlier';
+ markAsOutlierButton._onclick_ = function () {
+ var selectedPoints = [firstSelectedPoint];
+ if (self._mainChart.currentSelection('current'))
+ selectedPoints = self._mainChart.selectedPoints('current');
+ self._markAsOutlier(!alreayMarkedAsOutlier, selectedPoints);
+ }
+ markAsOutlierButton.disabled = !firstSelectedPoint;
+ }
+
static paneHeaderTemplate()
{
return `
@@ -241,11 +317,16 @@
<li class="close"><close-button></close-button></li>
</ul>
<ul class="chart-pane-action-buttons buttoned-toolbar"></ul>
- <ul class="chart-pane-alternative-platforms" style="display:none"></ul>
- <form class="chart-pane-analyze-pane" style="display:none">
+ <ul class="chart-pane-alternative-platforms popover" style="display:none"></ul>
+ <form class="chart-pane-analyze-popover popover" style="display:none">
<input type="text" required>
<button>Create</button>
</form>
+ <ul class="chart-pane-filtering-options popover" style="display:none">
+ <li><label><input type="checkbox" class="enable-sampling">Sampling</label></li>
+ <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>
</nav>
</header>
`;
@@ -309,8 +390,7 @@
line-height: 0.9rem;
}
- .chart-pane-actions .chart-pane-alternative-platforms,
- .chart-pane-analyze-pane {
+ .chart-pane-actions .popover {
position: absolute;
top: 0;
right: 0;
@@ -325,10 +405,10 @@
margin-right: -0.2rem;
}
- .chart-pane-alternative-platforms li {
+ .chart-pane-actions .popover li {
}
- .chart-pane-alternative-platforms li a {
+ .chart-pane-actions .popover li a {
display: block;
text-decoration: none;
color: inherit;
@@ -336,16 +416,20 @@
padding: 0.2rem 0.5rem;
}
- .chart-pane-alternative-platforms a:hover,
- .chart-pane-analyze-pane input:focus {
+ .chart-pane-actions .popover a:hover,
+ .chart-pane-actions .popover input:focus {
background: rgba(204, 153, 51, 0.1);
}
- .chart-pane-analyze-pane {
+ .chart-pane-actions .chart-pane-analyze-popover {
padding: 0.5rem;
}
- .chart-pane-analyze-pane input {
+ .chart-pane-actions .popover label {
+ font-size: 0.9rem;
+ }
+
+ .chart-pane-actions .popover input[type=text] {
font-size: 1rem;
width: 15rem;
outline: none;
Modified: trunk/Websites/perf.webkit.org/public/v3/pages/charts-page.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/pages/charts-page.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/charts-page.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -185,6 +185,11 @@
this.scheduleUrlStateUpdate();
}
+ graphOptionsDidChange(pane)
+ {
+ this.scheduleUrlStateUpdate();
+ }
+
setOpenRepository(repository)
{
this._currentRepositoryId = repository ? repository.id() : null;
Modified: trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/dashboard-page.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -125,13 +125,13 @@
if (!platformId || !metricId)
return '';
- var result = ChartStyles.createChartSourceList(platformId, metricId);
+ var result = ChartStyles.resolveConfiguration(platformId, metricId);
if (result.error)
return result.error;
var options = ChartStyles.dashboardOptions(result.metric.makeFormatter(3));
options._ondata_ = this._fetchedData.bind(this);
- var chart = new TimeSeriesChart(result.sourceList, options);
+ var chart = new TimeSeriesChart(ChartStyles.createSourceList(result.platform, result.metric, false, false), options);
this._charts.push(chart);
var statusView = new ChartStatusView(result.metric, chart);
Modified: trunk/Websites/perf.webkit.org/public/v3/pages/page-router.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/public/v3/pages/page-router.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/page-router.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -127,34 +127,50 @@
_serializeHashQueryValue(value)
{
- if (!(value instanceof Array)) {
- console.assert(value === null || typeof(value) === 'number' || /[A-Za-z0-9]*/.test(value));
- return value === null ? 'null' : value;
+ if (value instanceof Array) {
+ var serializedItems = [];
+ for (var item of value)
+ serializedItems.push(this._serializeHashQueryValue(item));
+ return '(' + serializedItems.join('-') + ')';
}
-
- var serializedItems = [];
- for (var item of value)
- serializedItems.push(this._serializeHashQueryValue(item));
- return '(' + serializedItems.join('-') + ')';
+ if (value instanceof Set)
+ return Array.from(value).sort().join('|');
+ console.assert(value === null || value === undefined || typeof(value) === 'number' || /[0-9]*/.test(value));
+ return value === null || value === undefined ? 'null' : value;
}
_deserializeHashQueryValue(value)
{
- var json = value.replace(/\(/g, '[').replace(/\)/g, ']').replace(/-/g, ',');
- try {
- return JSON.parse(json);
- } catch (error) {
-
- // Some applications don't linkify two consecutive closing parentheses: )).
- // Try fixing adding one extra parenthesis to see if that works.
- var missingClosingBrackets = this._countOccurrences(json, /\[/g) - this._countOccurrences(json, /\]/g);
- var fix = new Array(missingClosingBrackets).fill(']').join('');
- try {
- return JSON.parse(json + fix);
- } catch (newError) { }
-
- return value;
+ if (value.charAt(0) == '(') {
+ var nestingLevel = 0;
+ var end = 0;
+ var start = 1;
+ var result = [];
+ for (var character of value) {
+ if (character == '(')
+ nestingLevel++;
+ else if (character == ')') {
+ nestingLevel--;
+ if (!nestingLevel)
+ break;
+ } else if (nestingLevel == 1 && character == '-') {
+ result.push(this._deserializeHashQueryValue(value.substring(start, end)));
+ start = end + 1;
+ }
+ end++;
+ }
+ result.push(this._deserializeHashQueryValue(value.substring(start, end)));
+ return result;
}
+ if (value == 'true')
+ return true;
+ if (value == 'false')
+ return true;
+ if (value.match(/^[0-9\.]+$/))
+ return parseFloat(value);
+ if (value.match(/^[A-Za-z][A-Za-z0-9|]*$/))
+ return new Set(value.toLowerCase().split('|'));
+ return null;
}
_countOccurrences(string, regex)
Modified: trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js (201563 => 201564)
--- trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js 2016-06-01 19:47:19 UTC (rev 201563)
+++ trunk/Websites/perf.webkit.org/unit-tests/measurement-set-tests.js 2016-06-01 19:55:38 UTC (rev 201564)
@@ -290,6 +290,100 @@
});
});
+ it('should request the uncached primary cluster when noCache is true', function (done) {
+ var set = MeasurementSet.findSet(1, 1, 3000);
+ var callCount = 0;
+ set.fetchBetween(1000, 3000, function () {
+ callCount++;
+ });
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].url, '../data/measurement-set-1-1.json');
+
+ requests[0].resolve({
+ 'clusterStart': 1000,
+ 'clusterSize': 1000,
+ 'formatMap': [],
+ 'configurations': {current: []},
+ 'startTime': 2000,
+ 'endTime': 3000,
+ 'lastModified': 3000,
+ 'clusterCount': 2,
+ 'status': 'OK'});
+
+ var noCacheFetchCount = 0;
+ waitForMeasurementSet().then(function () {
+ assert.equal(callCount, 1);
+ assert.equal(noCacheFetchCount, 0);
+ assert.equal(set._sortedClusters.length, 1);
+ assert.equal(requests.length, 2);
+ assert.equal(requests[1].url, '../data/measurement-set-1-1-2000.json');
+
+ requests[1].resolve({
+ 'clusterStart': 1000,
+ 'clusterSize': 1000,
+ 'formatMap': [],
+ 'configurations': {current: []},
+ 'startTime': 1000,
+ 'endTime': 2000,
+ 'lastModified': 3000,
+ 'clusterCount': 2,
+ 'status': 'OK'});
+
+ set.fetchBetween(1000, 3000, function () {
+ noCacheFetchCount++;
+ }, true /* noCache */);
+
+ return waitForMeasurementSet();
+ }).then(function () {
+ assert.equal(callCount, 2);
+ assert.equal(noCacheFetchCount, 0);
+ assert.equal(set._sortedClusters.length, 2);
+ assert.equal(requests.length, 3);
+ assert.equal(requests[2].url, '../api/measurement-set?platform=1&metric=1');
+
+ requests[2].resolve({
+ 'clusterStart': 1000,
+ 'clusterSize': 1000,
+ 'formatMap': [],
+ 'configurations': {current: []},
+ 'startTime': 2000,
+ 'endTime': 3000,
+ 'lastModified': 3000,
+ 'clusterCount': 2,
+ 'status': 'OK'});
+
+ return waitForMeasurementSet();
+ }).then(function () {
+ assert.equal(callCount, 2);
+ assert.equal(noCacheFetchCount, 1);
+ assert.equal(set._sortedClusters.length, 2);
+ assert.equal(requests.length, 4);
+ assert.equal(requests[3].url, '../data/measurement-set-1-1-2000.json');
+
+ requests[3].resolve({
+ 'clusterStart': 1000,
+ 'clusterSize': 1000,
+ 'formatMap': [],
+ 'configurations': {current: []},
+ 'startTime': 1000,
+ 'endTime': 2000,
+ 'lastModified': 3000,
+ 'clusterCount': 2,
+ 'status': 'OK'});
+
+ return waitForMeasurementSet();
+ }).then(function () {
+ assert.equal(callCount, 2);
+ assert.equal(noCacheFetchCount, 2);
+ assert.equal(set._sortedClusters.length, 2);
+ assert.equal(requests.length, 4);
+
+ done();
+ }).catch(function (error) {
+ done(error);
+ });
+ });
+
it('should not request the primary cluster twice when multiple clients request it but should invoke all callbacks', function (done) {
var set = MeasurementSet.findSet(1, 1, 3000);
var callCount = 0;