Title: [201564] trunk/Websites/perf.webkit.org
Revision
201564
Author
[email protected]
Date
2016-06-01 12:55:38 -0700 (Wed, 01 Jun 2016)

Log Message

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.

Modified Paths

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;
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to