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
- trunk/Websites/perf.webkit.org/ChangeLog
- trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js
- trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js
- trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js
- trunk/Websites/perf.webkit.org/public/v3/models/time-series.js
- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js
- trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js
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
