Title: [212853] trunk/Websites/perf.webkit.org
Revision
212853
Author
[email protected]
Date
2017-02-22 14:07:39 -0800 (Wed, 22 Feb 2017)

Log Message

Make sampling algorithm more stable and introduce an abstraction for sampled data
https://bugs.webkit.org/show_bug.cgi?id=168693

Reviewed by Chris Dumez.

Before this patch, TimeSeriesChart's resampling resulted in some points poping up and disappearing as
the width of a chart is changed. e.g. when resizing the browser window. The bug was by caused by
the sample for a given width not always including all points for a smaller width so as the width is
expanded, some point may be dropped.

Fixed this by using a much simpler algorithm of always picking a point when the time interval between
the preceding point and the succeeding point is larger than the minimum space we allow for a given width.

Also introduced a new abstraction around the sample data: TimeSeriesView. A TimeSeriesView provides
a similar API to TimeSeries for a subset of the time series filtered by a time range a custom function.
This paves a way to adding the ability to select baseline, etc... on the chart status view.

TimeSeriesView can be in two modes:
Mode 1. The view represents a contiguous subrange of TimeSeries - In this mode, this._data references
        the underlying TimeSeries's _data directly, and we use _startingIndex to adjust index given to
        find the relative index. Finding the next point or the previous point of a given point is done
        via looking up the point's seriesIndex and doing a simple arithmetic. In general, an index is
        converted to the absolute index in the underlying TimeSeries's _data array.

Mode 2. The view represents a filtered non-contiguous subset of TimeSeries -  In this mode, this._data is
        its own array. Finding the next point or the previous point of a given point requires finding
        a sibling point in the underlying TimeSeries which is in this view. Since this may result in O(n)
        traversal and a hash lookup, we lazily build a map of each point to its position in _data instead.

* public/v3/components/chart-status-view.js:
(ChartStatusView.prototype.updateStatusIfNeeded): Call selectedPoints instead of sampledDataBetween for
clarity. This function now returns a TimeSeriesView instead of a raw array.

* public/v3/components/interactive-time-series-chart.js:
(InteractiveTimeSeriesChart.prototype.currentPoint): Updated now that _sampledTimeSeriesData contains
an array of TimeSeriesView's. Note that diff is either 0, -1, or 1.
(InteractiveTimeSeriesChart.prototype.selectedPoints): Ditto. sampledDataBetween no longer exists since
we can simply call viewTimeRange on TimeSeriesView returned by sampledDataBetween.
(InteractiveTimeSeriesChart.prototype.firstSelectedPoint): Ditto.
(InteractiveTimeSeriesChart.prototype._sampleTimeSeries): Use add since excludedPoints is now a Set.

* public/v3/components/time-series-chart.js:
(TimeSeriesChart.prototype.sampledDataBetween): Deleted.
(TimeSeriesChart.prototype.firstSampledPointBetweenTime): Deleted.
(TimeSeriesChart.prototype._ensureSampledTimeSeries): Modernized the code. Use the the time interval of
the chart divided by the number of allowed points as the time interval used in the new sampling algorithm.
(TimeSeriesChart.prototype._sampleTimeSeries): Rewritten. We also create TimeSeriesView here.
(TimeSeriesChart.prototype._sampleTimeSeries.findMedian): Deleted.
(TimeSeriesChart.prototype._updateCanvasSizeIfClientSizeChanged): Fixed a bug that the canvas size wasn't
set to the correct value on Chrome when a high DPI screen is used.

* public/v3/models/time-series.js:
(TimeSeries.prototype.viewBetweenPoints): Renamed from dataBetweenPoints. Now returns a TimeSeriesView.
(TimeSeriesView): Added. This constructor is to be called by viewBetweenPoints, viewTimeRange, and filter.
(TimeSeriesView.prototype._buildPointIndexMap): Added. Used in mode (2).
(TimeSeriesView.prototype.length): Added.
(TimeSeriesView.prototype.firstPoint): Added.
(TimeSeriesView.prototype.lastPoint): Added.
(TimeSeriesView.prototype.nextPoint): Added. Note index is always a position in this._data. In mode (1),
this is the position of the point in the underlying TimeSeries' _data. In mode (2), this is the position
of the point in this._data which is dictinct from the underlying TimeSeries' _data.
(TimeSeriesView.prototype.previousPoint): Ditto.
(TimeSeriesView.prototype.findPointByIndex): Added. Finds the point using the positional index from the
beginning of this view. findPointByIndex(0) on one view may not be same as findPointByIndex(0) of another.
(TimeSeriesView.prototype.findById): Added. This is O(n).
(TimeSeriesView.prototype.values): Added. Returns the value of each point in this view.
(TimeSeriesView.prototype.filter): Added. Creates a new view with a subset of data points the predicate
function returned true.
(TimeSeriesView.prototype.viewTimeRange): Added. Creates a new view with a subset of data points for the
given time ragne. When the resultant view would include all points of this view, it simply returns itself
as an optimization.
(TimeSeriesView.prototype.firstPointInTimeRange): Added. Returns the first point in the view which lies
within the specified time range.
(TimeSeriesView.prototype.Symbol.iterator): Added. Iterates over each point in the view.

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskChartPane.prototype.selectedPoints): Use selectedPoints in lieu of getting selection and then
calling sampledDataBetween with that range.

* public/v3/pages/summary-page.js:
(SummaryPageConfigurationGroup.set _medianForTimeRange): Modernized.

* unit-tests/time-series-tests.js: Added tests for TimeSeries and TimeSeriesView. Already caught bugs!
(addPointsToSeries):

Modified Paths

Added Paths

Diff

Modified: trunk/Websites/perf.webkit.org/ChangeLog (212852 => 212853)


--- trunk/Websites/perf.webkit.org/ChangeLog	2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/ChangeLog	2017-02-22 22:07:39 UTC (rev 212853)
@@ -1,3 +1,90 @@
+2017-02-21  Ryosuke Niwa  <[email protected]>
+
+        Make sampling algorithm more stable and introduce an abstraction for sampled data
+        https://bugs.webkit.org/show_bug.cgi?id=168693
+
+        Reviewed by Chris Dumez.
+
+        Before this patch, TimeSeriesChart's resampling resulted in some points poping up and disappearing as
+        the width of a chart is changed. e.g. when resizing the browser window. The bug was by caused by
+        the sample for a given width not always including all points for a smaller width so as the width is
+        expanded, some point may be dropped.
+
+        Fixed this by using a much simpler algorithm of always picking a point when the time interval between
+        the preceding point and the succeeding point is larger than the minimum space we allow for a given width.
+
+        Also introduced a new abstraction around the sample data: TimeSeriesView. A TimeSeriesView provides
+        a similar API to TimeSeries for a subset of the time series filtered by a time range a custom function.
+        This paves a way to adding the ability to select baseline, etc... on the chart status view.
+
+        TimeSeriesView can be in two modes:
+        Mode 1. The view represents a contiguous subrange of TimeSeries - In this mode, this._data references
+                the underlying TimeSeries's _data directly, and we use _startingIndex to adjust index given to
+                find the relative index. Finding the next point or the previous point of a given point is done
+                via looking up the point's seriesIndex and doing a simple arithmetic. In general, an index is
+                converted to the absolute index in the underlying TimeSeries's _data array.
+
+        Mode 2. The view represents a filtered non-contiguous subset of TimeSeries -  In this mode, this._data is
+                its own array. Finding the next point or the previous point of a given point requires finding
+                a sibling point in the underlying TimeSeries which is in this view. Since this may result in O(n)
+                traversal and a hash lookup, we lazily build a map of each point to its position in _data instead.
+
+        * public/v3/components/chart-status-view.js:
+        (ChartStatusView.prototype.updateStatusIfNeeded): Call selectedPoints instead of sampledDataBetween for
+        clarity. This function now returns a TimeSeriesView instead of a raw array.
+
+        * public/v3/components/interactive-time-series-chart.js:
+        (InteractiveTimeSeriesChart.prototype.currentPoint): Updated now that _sampledTimeSeriesData contains
+        an array of TimeSeriesView's. Note that diff is either 0, -1, or 1.
+        (InteractiveTimeSeriesChart.prototype.selectedPoints): Ditto. sampledDataBetween no longer exists since
+        we can simply call viewTimeRange on TimeSeriesView returned by sampledDataBetween.
+        (InteractiveTimeSeriesChart.prototype.firstSelectedPoint): Ditto.
+        (InteractiveTimeSeriesChart.prototype._sampleTimeSeries): Use add since excludedPoints is now a Set.
+
+        * public/v3/components/time-series-chart.js:
+        (TimeSeriesChart.prototype.sampledDataBetween): Deleted.
+        (TimeSeriesChart.prototype.firstSampledPointBetweenTime): Deleted.
+        (TimeSeriesChart.prototype._ensureSampledTimeSeries): Modernized the code. Use the the time interval of
+        the chart divided by the number of allowed points as the time interval used in the new sampling algorithm.
+        (TimeSeriesChart.prototype._sampleTimeSeries): Rewritten. We also create TimeSeriesView here.
+        (TimeSeriesChart.prototype._sampleTimeSeries.findMedian): Deleted.
+        (TimeSeriesChart.prototype._updateCanvasSizeIfClientSizeChanged): Fixed a bug that the canvas size wasn't
+        set to the correct value on Chrome when a high DPI screen is used.
+
+        * public/v3/models/time-series.js:
+        (TimeSeries.prototype.viewBetweenPoints): Renamed from dataBetweenPoints. Now returns a TimeSeriesView.
+        (TimeSeriesView): Added. This constructor is to be called by viewBetweenPoints, viewTimeRange, and filter.
+        (TimeSeriesView.prototype._buildPointIndexMap): Added. Used in mode (2).
+        (TimeSeriesView.prototype.length): Added.
+        (TimeSeriesView.prototype.firstPoint): Added.
+        (TimeSeriesView.prototype.lastPoint): Added.
+        (TimeSeriesView.prototype.nextPoint): Added. Note index is always a position in this._data. In mode (1),
+        this is the position of the point in the underlying TimeSeries' _data. In mode (2), this is the position
+        of the point in this._data which is dictinct from the underlying TimeSeries' _data.
+        (TimeSeriesView.prototype.previousPoint): Ditto.
+        (TimeSeriesView.prototype.findPointByIndex): Added. Finds the point using the positional index from the
+        beginning of this view. findPointByIndex(0) on one view may not be same as findPointByIndex(0) of another.
+        (TimeSeriesView.prototype.findById): Added. This is O(n).
+        (TimeSeriesView.prototype.values): Added. Returns the value of each point in this view.
+        (TimeSeriesView.prototype.filter): Added. Creates a new view with a subset of data points the predicate
+        function returned true.
+        (TimeSeriesView.prototype.viewTimeRange): Added. Creates a new view with a subset of data points for the
+        given time ragne. When the resultant view would include all points of this view, it simply returns itself
+        as an optimization.
+        (TimeSeriesView.prototype.firstPointInTimeRange): Added. Returns the first point in the view which lies
+        within the specified time range.
+        (TimeSeriesView.prototype.Symbol.iterator): Added. Iterates over each point in the view.
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskChartPane.prototype.selectedPoints): Use selectedPoints in lieu of getting selection and then
+        calling sampledDataBetween with that range.
+
+        * public/v3/pages/summary-page.js:
+        (SummaryPageConfigurationGroup.set _medianForTimeRange): Modernized.
+
+        * unit-tests/time-series-tests.js: Added tests for TimeSeries and TimeSeriesView. Already caught bugs!
+        (addPointsToSeries):
+
 2017-02-17  Ryosuke Niwa  <[email protected]>
 
         Add tests for the time series chart and fix bugs I found along the way

Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js (212852 => 212853)


--- trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js	2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js	2017-02-22 22:07:39 UTC (rev 212853)
@@ -50,14 +50,15 @@
                 return false;
 
             if (selection) {
-                var data = "" selection[0], selection[1]);
-                if (!data)
+                const view = this._chart.selectedPoints('current');
+                if (!view)
                     return false;
 
-                if (data && data.length > 1) {
+                if (view && view.length() > 1) {
+                    console.log(view.length(), view.firstPoint(), view.lastPoint())
                     this._usedSelection = selection;
-                    currentPoint = data[data.length - 1];
-                    previousPoint = data[0];
+                    currentPoint = view.lastPoint();
+                    previousPoint = view.firstPoint();
                 }
             } else  {
                 currentPoint = this._chart.currentPoint();

Modified: trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js (212852 => 212853)


--- trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js	2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js	2017-02-22 22:07:39 UTC (rev 212853)
@@ -23,6 +23,7 @@
             return null;
 
         if (!this._sampledTimeSeriesData) {
+            // FIXME: Why are we not using diff in this code path?
             this._ensureFetchedTimeSeries();
             for (var series of this._fetchedTimeSeries) {
                 var point = series.findById(id);
@@ -32,15 +33,15 @@
             return null;
         }
 
-        for (var data of this._sampledTimeSeriesData) {
-            if (!data)
+        for (var view of this._sampledTimeSeriesData) {
+            if (!view)
                 continue;
-            var index = data.findIndex(function (point) { return point.id == id; });
-            if (index < 0)
+            let point = view.findById(id);
+            if (!point)
                 continue;
-            if (diff)
-                index += diff;
-            return data[Math.min(Math.max(0, index), data.length)];
+            if (!diff)
+                return point;
+            return (point && diff > 0 ? view.nextPoint(point) : view.previousPoint(point)) || point;
         }
         return null;
     }
@@ -49,14 +50,16 @@
 
     selectedPoints(type)
     {
-        var selection = this._selectionTimeRange;
-        return selection ? this.sampledDataBetween(type, selection[0], selection[1]) : null;
+        const selection = this._selectionTimeRange;
+        const data = ""
+        return selection && data ? data.viewTimeRange(selection[0], selection[1]) : null;
     }
 
     firstSelectedPoint(type)
     {
-        var selection = this._selectionTimeRange;
-        return selection ? this.firstSampledPointBetweenTime(type, selection[0], selection[1]) : null;
+        const selection = this._selectionTimeRange;
+        const data = ""
+        return selection && data ? data.firstPointInTimeRange(selection[0], selection[1]) : null;
     }
 
     lockedIndicator() { return this._indicatorIsLocked ? this.currentPoint() : null; }
@@ -383,10 +386,10 @@
         return metrics;
     }
 
-    _sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints)
+    _sampleTimeSeries(data, minimumTimeDiff, excludedPoints)
     {
         if (this._indicatorID)
-            excludedPoints.push(this._indicatorID);
+            excludedPoints.add(this._indicatorID);
         return super._sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints);
     }
 

Modified: trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js (212852 => 212853)


--- trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js	2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js	2017-02-22 22:07:39 UTC (rev 212853)
@@ -131,22 +131,6 @@
         return null;
     }
 
-    sampledDataBetween(type, startTime, endTime)
-    {
-        var data = ""
-        if (!data)
-            return null;
-        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;
@@ -497,29 +481,28 @@
 
         Instrumentation.startMeasuringTime('TimeSeriesChart', 'ensureSampledTimeSeries');
 
-        var self = this;
-        var startTime = this._startTime;
-        var endTime = this._endTime;
-        this._sampledTimeSeriesData = this._sourceList.map(function (source, sourceIndex) {
-            var timeSeries = self._fetchedTimeSeries[sourceIndex];
+        const startTime = this._startTime;
+        const endTime = this._endTime;
+        this._sampledTimeSeriesData = this._sourceList.map((source, sourceIndex) => {
+            const timeSeries = this._fetchedTimeSeries[sourceIndex];
             if (!timeSeries)
                 return null;
 
             // A chart with X px width shouldn't have more than 2X / <radius-of-points> data points.
-            var maximumNumberOfPoints = 2 * metrics.chartWidth / (source.pointRadius || 2);
+            const maximumNumberOfPoints = 2 * metrics.chartWidth / (source.pointRadius || 2);
 
-            var pointAfterStart = timeSeries.findPointAfterTime(startTime);
-            var pointBeforeStart = (pointAfterStart ? timeSeries.previousPoint(pointAfterStart) : null) || timeSeries.firstPoint();
-            var pointAfterEnd = timeSeries.findPointAfterTime(endTime) || timeSeries.lastPoint();
+            const pointAfterStart = timeSeries.findPointAfterTime(startTime);
+            const pointBeforeStart = (pointAfterStart ? timeSeries.previousPoint(pointAfterStart) : null) || timeSeries.firstPoint();
+            const pointAfterEnd = timeSeries.findPointAfterTime(endTime) || timeSeries.lastPoint();
             if (!pointBeforeStart || !pointAfterEnd)
                 return null;
 
             // FIXME: Move this to TimeSeries.prototype.
-            var filteredData = timeSeries.dataBetweenPoints(pointBeforeStart, pointAfterEnd);
+            const view = timeSeries.viewBetweenPoints(pointBeforeStart, pointAfterEnd);
             if (!source.sampleData)
-                return filteredData;
+                return view;
 
-            return self._sampleTimeSeries(filteredData, maximumNumberOfPoints, filteredData.slice(-1).map(function (point) { return point.id; }));
+            return this._sampleTimeSeries(view, (endTime - startTime) / maximumNumberOfPoints, new Set);
         });
 
         Instrumentation.endMeasuringTime('TimeSeriesChart', 'ensureSampledTimeSeries');
@@ -529,49 +512,24 @@
         return true;
     }
 
-    _sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints)
+    _sampleTimeSeries(view, minimumTimeDiff, excludedPoints)
     {
+        if (view.length() < 2)
+            return view;
+
         Instrumentation.startMeasuringTime('TimeSeriesChart', 'sampleTimeSeries');
 
-        // FIXME: Do this in O(n) using quickselect: https://en.wikipedia.org/wiki/Quickselect
-        function findMedian(list, startIndex, indexAfterEnd)
-        {
-            var sortedList = list.slice(startIndex, indexAfterEnd).sort(function (a, b) { return a.value - b.value; });
-            return sortedList[Math.floor(sortedList.length / 2)];
-        }
+        const sampledData = view.filter((point, i) => {
+            if (excludedPoints.has(point.id))
+                return true;
+            let previousPoint = view.previousPoint(point) || point;
+            let nextPoint = view.nextPoint(point) || point;
+            return nextPoint.time - previousPoint.time >= minimumTimeDiff;
+        });
 
-        var samplingSize = Math.ceil(data.length / maximumNumberOfPoints);
-
-        var totalTimeDiff = data[data.length - 1].time - data[0].time;
-        var timePerSample = totalTimeDiff / maximumNumberOfPoints;
-
-        var sampledData = [];
-        var lastIndex = data.length - 1;
-        var i = 0;
-        while (i <= lastIndex) {
-            var startPoint = data[i];
-            var j;
-            for (j = i; j <= lastIndex; j++) {
-                var endPoint = data[j];
-                if (excludedPoints.includes(endPoint.id)) {
-                    j--;
-                    break;
-                }
-                if (endPoint.time - startPoint.time >= timePerSample)
-                    break;
-            }
-            if (i < j - 1) {
-                sampledData.push(findMedian(data, i, j));
-                i = j;
-            } else {
-                sampledData.push(startPoint);
-                i++;
-            }
-        }
-
         Instrumentation.endMeasuringTime('TimeSeriesChart', 'sampleTimeSeries');
 
-        Instrumentation.reportMeasurement('TimeSeriesChart', 'samplingRatio', '%', sampledData.length / data.length * 100);
+        Instrumentation.reportMeasurement('TimeSeriesChart', 'samplingRatio', '%', sampledData.length() / view.length() * 100);
 
         return sampledData;
     }
@@ -624,6 +582,8 @@
         var scale = window.devicePixelRatio;
         canvas.width = newWidth * scale;
         canvas.height = newHeight * scale;
+        canvas.style.width = newWidth + 'px';
+        canvas.style.height = newHeight + 'px';
         this._contextScaleX = scale;
         this._contextScaleY = scale;
         this._width = newWidth;

Modified: trunk/Websites/perf.webkit.org/public/v3/models/time-series.js (212852 => 212853)


--- trunk/Websites/perf.webkit.org/public/v3/models/time-series.js	2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/models/time-series.js	2017-02-22 22:07:39 UTC (rev 212853)
@@ -74,14 +74,148 @@
 
     findPointAfterTime(time) { return this._data.find(function (point) { return point.time >= time; }); }
 
-    dataBetweenPoints(firstPoint, lastPoint)
+    viewBetweenPoints(firstPoint, lastPoint)
     {
         console.assert(firstPoint.series == this);
         console.assert(lastPoint.series == this);
-        return this._data.slice(firstPoint.seriesIndex, lastPoint.seriesIndex + 1);
+        return new TimeSeriesView(this, firstPoint.seriesIndex, lastPoint.seriesIndex + 1);
     }
-
 };
 
+class TimeSeriesView {
+    constructor(timeSeries, startingIndex, afterEndingIndex, filteredData = null)
+    {
+        console.assert(timeSeries instanceof TimeSeries);
+        console.assert(startingIndex <= afterEndingIndex);
+        console.assert(afterEndingIndex <= timeSeries._data.length);
+        this._timeSeries = timeSeries;
+        this._data = filteredData || timeSeries._data;
+        this._values = null;
+        this._length = afterEndingIndex - startingIndex;
+        this._startingIndex = startingIndex;
+        this._afterEndingIndex = afterEndingIndex;
+        this._pointIndexMap = null;
+
+        if (this._data != timeSeries._data) {
+            this._findIndexForPoint = (point) => {
+                if (this._pointIndexMap == null)
+                    this._buildPointIndexMap();
+                return this._pointIndexMap.get(point);
+            }
+        } else
+            this._findIndexForPoint = (point) => { return point.seriesIndex; }
+    }
+
+    _buildPointIndexMap()
+    {
+        this._pointIndexMap = new Map;
+        const data = ""
+        const length = data.length;
+        for (let i = 0; i < length; i++)
+            this._pointIndexMap.set(data[i], i);
+    }
+
+    length() { return this._length; }
+
+    firstPoint() { return this._length ? this._data[this._startingIndex] : null; }
+    lastPoint() { return this._length ? this._data[this._afterEndingIndex - 1] : null; }
+
+    nextPoint(point)
+    {
+        let index = this._findIndexForPoint(point);
+        index++;
+        if (index == this._afterEndingIndex)
+            return null;
+        return this._data[index];
+    }
+
+    previousPoint(point)
+    {
+        const index = this._findIndexForPoint(point);
+        if (index == this._startingIndex)
+            return null;
+        return this._data[index - 1];
+    }
+
+    findPointByIndex(index)
+    {
+        index += this._startingIndex;
+        if (index < 0 || index >= this._afterEndingIndex)
+            return null;
+        return this._data[index];
+    }
+
+    findById(id)
+    {
+        for (let point of this) {
+            if (point.id == id)
+                return point;
+        }
+        return null;
+    }
+
+    values()
+    {
+        if (this._values == null) {
+            this._values = new Array(this._length);
+            let i = 0;
+            for (let point of this)
+                this._values[i++] = point.value;
+        }
+        return this._values;
+    }
+
+    filter(callback)
+    {
+        const data = ""
+        const filteredData = [];
+        for (let i = this._startingIndex; i < this._afterEndingIndex; i++) {
+            if (callback(data[i], i))
+                filteredData.push(data[i]);
+        }
+        return new TimeSeriesView(this._timeSeries, 0, filteredData.length, filteredData);
+    }
+
+    viewTimeRange(startTime, endTime)
+    {
+        const data = ""
+        let startingIndex = null;
+        let endingIndex = null;
+        for (let i = this._startingIndex; i < this._afterEndingIndex; i++) {
+            if (startingIndex == null && data[i].time >= startTime)
+                startingIndex = i;
+            if (data[i].time <= endTime)
+                endingIndex = i;
+        }
+        if (startingIndex == null || endingIndex == null)
+            return new TimeSeriesView(this._timeSeries, 0, 0, data);
+        return new TimeSeriesView(this._timeSeries, startingIndex, endingIndex + 1, data);
+    }
+
+    firstPointInTimeRange(startTime, endTime)
+    {
+        console.assert(startTime <= endTime);
+        for (let point of this) {
+            if (point.time > endTime)
+                return null;
+            if (point.time >= startTime)
+                return point;
+        }
+        return null;
+    }
+
+    [Symbol.iterator]()
+    {
+        const data = ""
+        const end = this._afterEndingIndex;
+        let i = this._startingIndex;
+        return {
+            next() {
+                return {value: data[i], done: i++ == end};
+            }
+        };
+    }
+}
+
 if (typeof module != 'undefined')
     module.exports.TimeSeries = TimeSeries;

Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js (212852 => 212853)


--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js	2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js	2017-02-22 22:07:39 UTC (rev 212853)
@@ -24,11 +24,7 @@
 
     selectedPoints()
     {
-        var selection = this._mainChart ? this._mainChart.currentSelection() : null;
-        if (!selection)
-            return null;
-
-        return this._mainChart.sampledDataBetween('current', selection[0], selection[1]);
+        return this._mainChart ? this._mainChart.selectedPoints('current') : null;
     }
 }
 

Modified: trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js (212852 => 212853)


--- trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js	2017-02-22 21:55:03 UTC (rev 212852)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js	2017-02-22 22:07:39 UTC (rev 212853)
@@ -364,13 +364,12 @@
         if (!timeSeries.firstPoint())
             return NaN;
 
-        var startPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint();
-        var afterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint();
-        var endPoint = timeSeries.previousPoint(afterEndPoint);
+        const startPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint();
+        const afterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint();
+        let endPoint = timeSeries.previousPoint(afterEndPoint);
         if (!endPoint || startPoint == afterEndPoint)
             endPoint = afterEndPoint;
 
-        var points = timeSeries.dataBetweenPoints(startPoint, endPoint).map(function (point) { return point.value; });
-        return Statistics.median(points);
+        return Statistics.median(timeSeries.viewBetweenPoints(startPoint, endPoint).values());
     }
 }

Added: trunk/Websites/perf.webkit.org/unit-tests/time-series-tests.js (0 => 212853)


--- trunk/Websites/perf.webkit.org/unit-tests/time-series-tests.js	                        (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/time-series-tests.js	2017-02-22 22:07:39 UTC (rev 212853)
@@ -0,0 +1,435 @@
+'use strict';
+
+const assert = require('assert');
+if (!assert.almostEqual)
+    assert.almostEqual = require('./resources/almost-equal.js');
+
+const MockRemoteAPI = require('./resources/mock-remote-api.js').MockRemoteAPI;
+require('../tools/js/v3-models.js');
+
+let threePoints;
+let fivePoints;
+beforeEach(() => {
+    threePoints = [
+        {id: 910, time: 101, value: 110},
+        {id: 902, time: 220, value: 102},
+        {id: 930, time: 303, value: 130},
+    ];
+    fivePoints = [...threePoints,
+        {id: 904, time: 400, value: 114},
+        {id: 950, time: 505, value: 105},
+        {id: 960, time: 600, value: 116},
+    ];
+});
+
+function addPointsToSeries(timeSeries, list = threePoints)
+{
+    for (let point of list)
+        timeSeries.append(point);
+}
+
+describe('TimeSeries', () => {
+
+    describe('length', () => {
+        it('should return the length', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.length(), 3);
+        });
+
+        it('should return 0 when there are no points', () => {
+            const timeSeries = new TimeSeries();
+            assert.equal(timeSeries.length(), 0);
+        });
+    });
+
+    describe('firstPoint', () => {
+        it('should return the first point', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.firstPoint(), threePoints[0]);
+        });
+
+        it('should return null when there are no points', () => {
+            const timeSeries = new TimeSeries();
+            assert.equal(timeSeries.firstPoint(), null);
+        });
+    });
+
+    describe('lastPoint', () => {
+        it('should return the first point', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.lastPoint(), threePoints[2]);
+        });
+
+        it('should return null when there are no points', () => {
+            const timeSeries = new TimeSeries();
+            assert.equal(timeSeries.lastPoint(), null);
+        });
+    });
+
+    describe('nextPoint', () => {
+        it('should return the next point when called on the first point', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.nextPoint(threePoints[0]), threePoints[1]);
+        });
+
+        it('should return the next point when called on a mid-point', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.nextPoint(threePoints[1]), threePoints[2]);
+        });
+
+        it('should return null when called on the last point', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.nextPoint(threePoints[2]), null);
+        });
+    });
+
+    describe('previousPoint', () => {
+        it('should return null when called on the first point', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.previousPoint(threePoints[0]), null);
+        });
+
+        it('should return the previous point when called on a mid-point', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.previousPoint(threePoints[1]), threePoints[0]);
+        });
+
+        it('should return the previous point when called on the last point', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.previousPoint(threePoints[2]), threePoints[1]);
+        });
+    });
+
+    describe('findPointByIndex', () => {
+        it('should return null the index is less than 0', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointByIndex(-10), null);
+            assert.equal(timeSeries.findPointByIndex(-1), null);
+        });
+
+        it('should return null when the index is greater than or equal to the length', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointByIndex(10), null);
+            assert.equal(timeSeries.findPointByIndex(3), null);
+        });
+
+        it('should return null when the index is not a number', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointByIndex(undefined), null);
+            assert.equal(timeSeries.findPointByIndex(NaN), null);
+            assert.equal(timeSeries.findPointByIndex('a'), null);
+        });
+
+        it('should return the point at the specified index when it is in the valid range', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointByIndex(0), threePoints[0]);
+            assert.equal(timeSeries.findPointByIndex(1), threePoints[1]);
+            assert.equal(timeSeries.findPointByIndex(2), threePoints[2]);
+        });
+    });
+
+    describe('findById', () => {
+        it('should return the point with the specified ID', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findById(threePoints[0].id), threePoints[0]);
+            assert.equal(timeSeries.findById(threePoints[1].id), threePoints[1]);
+            assert.equal(timeSeries.findById(threePoints[2].id), threePoints[2]);
+        });
+
+        it('should return null for a non-existent ID', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findById(null), null);
+            assert.equal(timeSeries.findById(undefined), null);
+            assert.equal(timeSeries.findById(NaN), null);
+            assert.equal(timeSeries.findById('a'), null);
+            assert.equal(timeSeries.findById(4231563246), null);
+        });
+    });
+
+    describe('findPointAfterTime', () => {
+        it('should return the point at the specified time', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[0].time), threePoints[0]);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[1].time), threePoints[1]);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[2].time), threePoints[2]);
+        });
+
+        it('should return the point after the specified time', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[0].time - 0.1), threePoints[0]);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[1].time - 0.1), threePoints[1]);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[2].time - 0.1), threePoints[2]);
+        });
+
+        it('should return null when there are no points after the specified time', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries);
+            assert.equal(timeSeries.findPointAfterTime(threePoints[2].time + 0.1), null);
+        });
+
+        it('should return the first point when there are multiple points at the specified time', () => {
+            const timeSeries = new TimeSeries();
+            const points = [
+                {id: 909, time:  99, value: 105},
+                {id: 910, time: 100, value: 110},
+                {id: 902, time: 100, value: 102},
+                {id: 930, time: 101, value: 130},
+            ];
+            addPointsToSeries(timeSeries, points);
+            assert.equal(timeSeries.findPointAfterTime(points[1].time), points[1]);
+        });
+    });
+
+    describe('viewBetweenPoints', () => {
+
+        it('should return a view between two points', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const view = timeSeries.viewBetweenPoints(fivePoints[1], fivePoints[3]);
+            assert.equal(view.length(), 3);
+            assert.equal(view.firstPoint(), fivePoints[1]);
+            assert.equal(view.lastPoint(), fivePoints[3]);
+
+            assert.equal(view.nextPoint(fivePoints[1]), fivePoints[2]);
+            assert.equal(view.nextPoint(fivePoints[2]), fivePoints[3]);
+            assert.equal(view.nextPoint(fivePoints[3]), null);
+
+            assert.equal(view.previousPoint(fivePoints[1]), null);
+            assert.equal(view.previousPoint(fivePoints[2]), fivePoints[1]);
+            assert.equal(view.previousPoint(fivePoints[3]), fivePoints[2]);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[1]);
+            assert.equal(view.findPointByIndex(1), fivePoints[2]);
+            assert.equal(view.findPointByIndex(2), fivePoints[3]);
+            assert.equal(view.findPointByIndex(3), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), fivePoints[1]);
+            assert.equal(view.findById(fivePoints[2].id), fivePoints[2]);
+            assert.equal(view.findById(fivePoints[3].id), fivePoints[3]);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[1].value, fivePoints[2].value, fivePoints[3].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), fivePoints[2]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], fivePoints.slice(1, 4));
+        });
+
+        it('should return a view with exactly one point for when the starting point is identical to the ending point', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const view = timeSeries.viewBetweenPoints(fivePoints[2], fivePoints[2]);
+            assert.equal(view.length(), 1);
+            assert.equal(view.firstPoint(), fivePoints[2]);
+            assert.equal(view.lastPoint(), fivePoints[2]);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[2]);
+            assert.equal(view.findPointByIndex(1), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), null);
+            assert.equal(view.findById(fivePoints[2].id), fivePoints[2]);
+            assert.equal(view.findById(fivePoints[3].id), null);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[2].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[2]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[2]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), fivePoints[2]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], [fivePoints[2]]);
+        });
+
+    });
+
+});
+
+describe('TimeSeriesView', () => {
+
+    describe('filter', () => {
+        it('should create a filtered view', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const originalView = timeSeries.viewBetweenPoints(fivePoints[0], fivePoints[4]);
+            const view = originalView.filter((point) => { return point == fivePoints[1] || point == fivePoints[3]; });
+
+            assert.equal(view.length(), 2);
+            assert.equal(view.firstPoint(), fivePoints[1]);
+            assert.equal(view.lastPoint(), fivePoints[3]);
+
+            assert.equal(view.nextPoint(fivePoints[1]), fivePoints[3]);
+            assert.equal(view.nextPoint(fivePoints[3]), null);
+
+            assert.equal(view.previousPoint(fivePoints[1]), null);
+            assert.equal(view.previousPoint(fivePoints[3]), fivePoints[1]);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[1]);
+            assert.equal(view.findPointByIndex(1), fivePoints[3]);
+            assert.equal(view.findPointByIndex(2), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), fivePoints[1]);
+            assert.equal(view.findById(fivePoints[2].id), null);
+            assert.equal(view.findById(fivePoints[3].id), fivePoints[3]);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[1].value, fivePoints[3].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], [fivePoints[1], fivePoints[3]]);
+        });
+    });
+
+    describe('viewTimeRange', () => {
+        it('should create a view filtered by the specified time range', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const originalView = timeSeries.viewBetweenPoints(fivePoints[0], fivePoints[4]);
+            const view = originalView.viewTimeRange(fivePoints[1].time - 0.1, fivePoints[4].time - 0.1);
+
+            assert.equal(view.length(), 3);
+            assert.equal(view.firstPoint(), fivePoints[1]);
+            assert.equal(view.lastPoint(), fivePoints[3]);
+
+            assert.equal(view.nextPoint(fivePoints[1]), fivePoints[2]);
+            assert.equal(view.nextPoint(fivePoints[2]), fivePoints[3]);
+            assert.equal(view.nextPoint(fivePoints[3]), null);
+
+            assert.equal(view.previousPoint(fivePoints[1]), null);
+            assert.equal(view.previousPoint(fivePoints[2]), fivePoints[1]);
+            assert.equal(view.previousPoint(fivePoints[3]), fivePoints[2]);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[1]);
+            assert.equal(view.findPointByIndex(1), fivePoints[2]);
+            assert.equal(view.findPointByIndex(2), fivePoints[3]);
+            assert.equal(view.findPointByIndex(3), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), fivePoints[1]);
+            assert.equal(view.findById(fivePoints[2].id), fivePoints[2]);
+            assert.equal(view.findById(fivePoints[3].id), fivePoints[3]);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[1].value, fivePoints[2].value, fivePoints[3].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), fivePoints[1]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), fivePoints[2]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], fivePoints.slice(1, 4));
+        });
+
+        it('should create a view filtered by the specified time range on a view already filtered by a time range', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const originalView = timeSeries.viewBetweenPoints(fivePoints[0], fivePoints[4]);
+            const prefilteredView = originalView.viewTimeRange(fivePoints[1].time - 0.1, fivePoints[4].time - 0.1);
+            const view = prefilteredView.viewTimeRange(fivePoints[3].time - 0.1, fivePoints[3].time + 0.1);
+
+            assert.equal(view.length(), 1);
+            assert.equal(view.firstPoint(), fivePoints[3]);
+            assert.equal(view.lastPoint(), fivePoints[3]);
+
+            assert.equal(view.nextPoint(fivePoints[3]), null);
+            assert.equal(view.previousPoint(fivePoints[3]), null);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[3]);
+            assert.equal(view.findPointByIndex(1), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), null);
+            assert.equal(view.findById(fivePoints[2].id), null);
+            assert.equal(view.findById(fivePoints[3].id), fivePoints[3]);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[3].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], [fivePoints[3]]);
+        });
+
+        it('should create a view filtered by the specified time range on a view already filtered', () => {
+            const timeSeries = new TimeSeries();
+            addPointsToSeries(timeSeries, fivePoints);
+            const originalView = timeSeries.viewBetweenPoints(fivePoints[0], fivePoints[4]);
+            const prefilteredView = originalView.filter((point) => { return point == fivePoints[1] || point == fivePoints[3]; });
+            const view = prefilteredView.viewTimeRange(fivePoints[3].time - 0.1, fivePoints[3].time + 0.1);
+
+            assert.equal(view.length(), 1);
+            assert.equal(view.firstPoint(), fivePoints[3]);
+            assert.equal(view.lastPoint(), fivePoints[3]);
+
+            assert.equal(view.nextPoint(fivePoints[3]), null);
+            assert.equal(view.previousPoint(fivePoints[3]), null);
+
+            assert.equal(view.findPointByIndex(0), fivePoints[3]);
+            assert.equal(view.findPointByIndex(1), null);
+
+            assert.equal(view.findById(fivePoints[0].id), null);
+            assert.equal(view.findById(fivePoints[1].id), null);
+            assert.equal(view.findById(fivePoints[2].id), null);
+            assert.equal(view.findById(fivePoints[3].id), fivePoints[3]);
+            assert.equal(view.findById(fivePoints[4].id), null);
+
+            assert.deepEqual(view.values(), [fivePoints[3].value]);
+
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time, fivePoints[1].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[0].time, fivePoints[0].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[1].time + 0.1, fivePoints[2].time), null);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[3].time - 0.1, fivePoints[4].time), fivePoints[3]);
+            assert.deepEqual(view.firstPointInTimeRange(fivePoints[4].time, fivePoints[4].time), null);
+
+            assert.deepEqual([...view], [fivePoints[3]]);
+        });
+    });
+
+});
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to