Changeset 212853 in webkit
- Timestamp:
- Feb 22, 2017 2:07:39 PM (7 years ago)
- Location:
- trunk/Websites/perf.webkit.org
- Files:
-
- 1 added
- 7 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/Websites/perf.webkit.org/ChangeLog
r212542 r212853 1 2017-02-21 Ryosuke Niwa <rniwa@webkit.org> 2 3 Make sampling algorithm more stable and introduce an abstraction for sampled data 4 https://bugs.webkit.org/show_bug.cgi?id=168693 5 6 Reviewed by Chris Dumez. 7 8 Before this patch, TimeSeriesChart's resampling resulted in some points poping up and disappearing as 9 the width of a chart is changed. e.g. when resizing the browser window. The bug was by caused by 10 the sample for a given width not always including all points for a smaller width so as the width is 11 expanded, some point may be dropped. 12 13 Fixed this by using a much simpler algorithm of always picking a point when the time interval between 14 the preceding point and the succeeding point is larger than the minimum space we allow for a given width. 15 16 Also introduced a new abstraction around the sample data: TimeSeriesView. A TimeSeriesView provides 17 a similar API to TimeSeries for a subset of the time series filtered by a time range a custom function. 18 This paves a way to adding the ability to select baseline, etc... on the chart status view. 19 20 TimeSeriesView can be in two modes: 21 Mode 1. The view represents a contiguous subrange of TimeSeries - In this mode, this._data references 22 the underlying TimeSeries's _data directly, and we use _startingIndex to adjust index given to 23 find the relative index. Finding the next point or the previous point of a given point is done 24 via looking up the point's seriesIndex and doing a simple arithmetic. In general, an index is 25 converted to the absolute index in the underlying TimeSeries's _data array. 26 27 Mode 2. The view represents a filtered non-contiguous subset of TimeSeries - In this mode, this._data is 28 its own array. Finding the next point or the previous point of a given point requires finding 29 a sibling point in the underlying TimeSeries which is in this view. Since this may result in O(n) 30 traversal and a hash lookup, we lazily build a map of each point to its position in _data instead. 31 32 * public/v3/components/chart-status-view.js: 33 (ChartStatusView.prototype.updateStatusIfNeeded): Call selectedPoints instead of sampledDataBetween for 34 clarity. This function now returns a TimeSeriesView instead of a raw array. 35 36 * public/v3/components/interactive-time-series-chart.js: 37 (InteractiveTimeSeriesChart.prototype.currentPoint): Updated now that _sampledTimeSeriesData contains 38 an array of TimeSeriesView's. Note that diff is either 0, -1, or 1. 39 (InteractiveTimeSeriesChart.prototype.selectedPoints): Ditto. sampledDataBetween no longer exists since 40 we can simply call viewTimeRange on TimeSeriesView returned by sampledDataBetween. 41 (InteractiveTimeSeriesChart.prototype.firstSelectedPoint): Ditto. 42 (InteractiveTimeSeriesChart.prototype._sampleTimeSeries): Use add since excludedPoints is now a Set. 43 44 * public/v3/components/time-series-chart.js: 45 (TimeSeriesChart.prototype.sampledDataBetween): Deleted. 46 (TimeSeriesChart.prototype.firstSampledPointBetweenTime): Deleted. 47 (TimeSeriesChart.prototype._ensureSampledTimeSeries): Modernized the code. Use the the time interval of 48 the chart divided by the number of allowed points as the time interval used in the new sampling algorithm. 49 (TimeSeriesChart.prototype._sampleTimeSeries): Rewritten. We also create TimeSeriesView here. 50 (TimeSeriesChart.prototype._sampleTimeSeries.findMedian): Deleted. 51 (TimeSeriesChart.prototype._updateCanvasSizeIfClientSizeChanged): Fixed a bug that the canvas size wasn't 52 set to the correct value on Chrome when a high DPI screen is used. 53 54 * public/v3/models/time-series.js: 55 (TimeSeries.prototype.viewBetweenPoints): Renamed from dataBetweenPoints. Now returns a TimeSeriesView. 56 (TimeSeriesView): Added. This constructor is to be called by viewBetweenPoints, viewTimeRange, and filter. 57 (TimeSeriesView.prototype._buildPointIndexMap): Added. Used in mode (2). 58 (TimeSeriesView.prototype.length): Added. 59 (TimeSeriesView.prototype.firstPoint): Added. 60 (TimeSeriesView.prototype.lastPoint): Added. 61 (TimeSeriesView.prototype.nextPoint): Added. Note index is always a position in this._data. In mode (1), 62 this is the position of the point in the underlying TimeSeries' _data. In mode (2), this is the position 63 of the point in this._data which is dictinct from the underlying TimeSeries' _data. 64 (TimeSeriesView.prototype.previousPoint): Ditto. 65 (TimeSeriesView.prototype.findPointByIndex): Added. Finds the point using the positional index from the 66 beginning of this view. findPointByIndex(0) on one view may not be same as findPointByIndex(0) of another. 67 (TimeSeriesView.prototype.findById): Added. This is O(n). 68 (TimeSeriesView.prototype.values): Added. Returns the value of each point in this view. 69 (TimeSeriesView.prototype.filter): Added. Creates a new view with a subset of data points the predicate 70 function returned true. 71 (TimeSeriesView.prototype.viewTimeRange): Added. Creates a new view with a subset of data points for the 72 given time ragne. When the resultant view would include all points of this view, it simply returns itself 73 as an optimization. 74 (TimeSeriesView.prototype.firstPointInTimeRange): Added. Returns the first point in the view which lies 75 within the specified time range. 76 (TimeSeriesView.prototype.Symbol.iterator): Added. Iterates over each point in the view. 77 78 * public/v3/pages/analysis-task-page.js: 79 (AnalysisTaskChartPane.prototype.selectedPoints): Use selectedPoints in lieu of getting selection and then 80 calling sampledDataBetween with that range. 81 82 * public/v3/pages/summary-page.js: 83 (SummaryPageConfigurationGroup.set _medianForTimeRange): Modernized. 84 85 * unit-tests/time-series-tests.js: Added tests for TimeSeries and TimeSeriesView. Already caught bugs! 86 (addPointsToSeries): 87 1 88 2017-02-17 Ryosuke Niwa <rniwa@webkit.org> 2 89 -
trunk/Websites/perf.webkit.org/public/v3/components/chart-status-view.js
r200090 r212853 51 51 52 52 if (selection) { 53 var data = this._chart.sampledDataBetween('current', selection[0], selection[1]);54 if (! data)53 const view = this._chart.selectedPoints('current'); 54 if (!view) 55 55 return false; 56 56 57 if (data && data.length > 1) { 57 if (view && view.length() > 1) { 58 console.log(view.length(), view.firstPoint(), view.lastPoint()) 58 59 this._usedSelection = selection; 59 currentPoint = data[data.length - 1];60 previousPoint = data[0];60 currentPoint = view.lastPoint(); 61 previousPoint = view.firstPoint(); 61 62 } 62 63 } else { -
trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js
r210880 r212853 24 24 25 25 if (!this._sampledTimeSeriesData) { 26 // FIXME: Why are we not using diff in this code path? 26 27 this._ensureFetchedTimeSeries(); 27 28 for (var series of this._fetchedTimeSeries) { … … 33 34 } 34 35 35 for (var dataof this._sampledTimeSeriesData) {36 if (! data)36 for (var view of this._sampledTimeSeriesData) { 37 if (!view) 37 38 continue; 38 var index = data.findIndex(function (point) { return point.id == id; });39 if ( index < 0)39 let point = view.findById(id); 40 if (!point) 40 41 continue; 41 if ( diff)42 index += diff;43 return data[Math.min(Math.max(0, index), data.length)];42 if (!diff) 43 return point; 44 return (point && diff > 0 ? view.nextPoint(point) : view.previousPoint(point)) || point; 44 45 } 45 46 return null; … … 50 51 selectedPoints(type) 51 52 { 52 var selection = this._selectionTimeRange; 53 return selection ? this.sampledDataBetween(type, selection[0], selection[1]) : null; 53 const selection = this._selectionTimeRange; 54 const data = this.sampledTimeSeriesData(type); 55 return selection && data ? data.viewTimeRange(selection[0], selection[1]) : null; 54 56 } 55 57 56 58 firstSelectedPoint(type) 57 59 { 58 var selection = this._selectionTimeRange; 59 return selection ? this.firstSampledPointBetweenTime(type, selection[0], selection[1]) : null; 60 const selection = this._selectionTimeRange; 61 const data = this.sampledTimeSeriesData(type); 62 return selection && data ? data.firstPointInTimeRange(selection[0], selection[1]) : null; 60 63 } 61 64 … … 384 387 } 385 388 386 _sampleTimeSeries(data, m aximumNumberOfPoints, excludedPoints)389 _sampleTimeSeries(data, minimumTimeDiff, excludedPoints) 387 390 { 388 391 if (this._indicatorID) 389 excludedPoints. push(this._indicatorID);392 excludedPoints.add(this._indicatorID); 390 393 return super._sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints); 391 394 } -
trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js
r212542 r212853 132 132 } 133 133 134 sampledDataBetween(type, startTime, endTime)135 {136 var data = this.sampledTimeSeriesData(type);137 if (!data)138 return null;139 return data.filter(function (point) { return startTime <= point.time && point.time <= endTime; });140 }141 142 firstSampledPointBetweenTime(type, startTime, endTime)143 {144 var data = this.sampledTimeSeriesData(type);145 if (!data)146 return null;147 return data.find(function (point) { return startTime <= point.time && point.time <= endTime; });148 }149 150 134 setAnnotations(annotations) 151 135 { … … 498 482 Instrumentation.startMeasuringTime('TimeSeriesChart', 'ensureSampledTimeSeries'); 499 483 500 var self = this; 501 var startTime = this._startTime; 502 var endTime = this._endTime; 503 this._sampledTimeSeriesData = this._sourceList.map(function (source, sourceIndex) { 504 var timeSeries = self._fetchedTimeSeries[sourceIndex]; 484 const startTime = this._startTime; 485 const endTime = this._endTime; 486 this._sampledTimeSeriesData = this._sourceList.map((source, sourceIndex) => { 487 const timeSeries = this._fetchedTimeSeries[sourceIndex]; 505 488 if (!timeSeries) 506 489 return null; 507 490 508 491 // A chart with X px width shouldn't have more than 2X / <radius-of-points> data points. 509 varmaximumNumberOfPoints = 2 * metrics.chartWidth / (source.pointRadius || 2);510 511 varpointAfterStart = timeSeries.findPointAfterTime(startTime);512 varpointBeforeStart = (pointAfterStart ? timeSeries.previousPoint(pointAfterStart) : null) || timeSeries.firstPoint();513 varpointAfterEnd = timeSeries.findPointAfterTime(endTime) || timeSeries.lastPoint();492 const maximumNumberOfPoints = 2 * metrics.chartWidth / (source.pointRadius || 2); 493 494 const pointAfterStart = timeSeries.findPointAfterTime(startTime); 495 const pointBeforeStart = (pointAfterStart ? timeSeries.previousPoint(pointAfterStart) : null) || timeSeries.firstPoint(); 496 const pointAfterEnd = timeSeries.findPointAfterTime(endTime) || timeSeries.lastPoint(); 514 497 if (!pointBeforeStart || !pointAfterEnd) 515 498 return null; 516 499 517 500 // FIXME: Move this to TimeSeries.prototype. 518 var filteredData = timeSeries.dataBetweenPoints(pointBeforeStart, pointAfterEnd);501 const view = timeSeries.viewBetweenPoints(pointBeforeStart, pointAfterEnd); 519 502 if (!source.sampleData) 520 return filteredData;521 522 return self._sampleTimeSeries(filteredData, maximumNumberOfPoints, filteredData.slice(-1).map(function (point) { return point.id; }));503 return view; 504 505 return this._sampleTimeSeries(view, (endTime - startTime) / maximumNumberOfPoints, new Set); 523 506 }); 524 507 … … 530 513 } 531 514 532 _sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints) 533 { 515 _sampleTimeSeries(view, minimumTimeDiff, excludedPoints) 516 { 517 if (view.length() < 2) 518 return view; 519 534 520 Instrumentation.startMeasuringTime('TimeSeriesChart', 'sampleTimeSeries'); 535 521 536 // FIXME: Do this in O(n) using quickselect: https://en.wikipedia.org/wiki/Quickselect 537 function findMedian(list, startIndex, indexAfterEnd) 538 { 539 var sortedList = list.slice(startIndex, indexAfterEnd).sort(function (a, b) { return a.value - b.value; }); 540 return sortedList[Math.floor(sortedList.length / 2)]; 541 } 542 543 var samplingSize = Math.ceil(data.length / maximumNumberOfPoints); 544 545 var totalTimeDiff = data[data.length - 1].time - data[0].time; 546 var timePerSample = totalTimeDiff / maximumNumberOfPoints; 547 548 var sampledData = []; 549 var lastIndex = data.length - 1; 550 var i = 0; 551 while (i <= lastIndex) { 552 var startPoint = data[i]; 553 var j; 554 for (j = i; j <= lastIndex; j++) { 555 var endPoint = data[j]; 556 if (excludedPoints.includes(endPoint.id)) { 557 j--; 558 break; 559 } 560 if (endPoint.time - startPoint.time >= timePerSample) 561 break; 562 } 563 if (i < j - 1) { 564 sampledData.push(findMedian(data, i, j)); 565 i = j; 566 } else { 567 sampledData.push(startPoint); 568 i++; 569 } 570 } 522 const sampledData = view.filter((point, i) => { 523 if (excludedPoints.has(point.id)) 524 return true; 525 let previousPoint = view.previousPoint(point) || point; 526 let nextPoint = view.nextPoint(point) || point; 527 return nextPoint.time - previousPoint.time >= minimumTimeDiff; 528 }); 571 529 572 530 Instrumentation.endMeasuringTime('TimeSeriesChart', 'sampleTimeSeries'); 573 531 574 Instrumentation.reportMeasurement('TimeSeriesChart', 'samplingRatio', '%', sampledData.length / data.length* 100);532 Instrumentation.reportMeasurement('TimeSeriesChart', 'samplingRatio', '%', sampledData.length() / view.length() * 100); 575 533 576 534 return sampledData; … … 625 583 canvas.width = newWidth * scale; 626 584 canvas.height = newHeight * scale; 585 canvas.style.width = newWidth + 'px'; 586 canvas.style.height = newHeight + 'px'; 627 587 this._contextScaleX = scale; 628 588 this._contextScaleY = scale; -
trunk/Websites/perf.webkit.org/public/v3/models/time-series.js
r210880 r212853 75 75 findPointAfterTime(time) { return this._data.find(function (point) { return point.time >= time; }); } 76 76 77 dataBetweenPoints(firstPoint, lastPoint)77 viewBetweenPoints(firstPoint, lastPoint) 78 78 { 79 79 console.assert(firstPoint.series == this); 80 80 console.assert(lastPoint.series == this); 81 return this._data.slice(firstPoint.seriesIndex, lastPoint.seriesIndex + 1); 82 } 83 81 return new TimeSeriesView(this, firstPoint.seriesIndex, lastPoint.seriesIndex + 1); 82 } 84 83 }; 84 85 class TimeSeriesView { 86 constructor(timeSeries, startingIndex, afterEndingIndex, filteredData = null) 87 { 88 console.assert(timeSeries instanceof TimeSeries); 89 console.assert(startingIndex <= afterEndingIndex); 90 console.assert(afterEndingIndex <= timeSeries._data.length); 91 this._timeSeries = timeSeries; 92 this._data = filteredData || timeSeries._data; 93 this._values = null; 94 this._length = afterEndingIndex - startingIndex; 95 this._startingIndex = startingIndex; 96 this._afterEndingIndex = afterEndingIndex; 97 this._pointIndexMap = null; 98 99 if (this._data != timeSeries._data) { 100 this._findIndexForPoint = (point) => { 101 if (this._pointIndexMap == null) 102 this._buildPointIndexMap(); 103 return this._pointIndexMap.get(point); 104 } 105 } else 106 this._findIndexForPoint = (point) => { return point.seriesIndex; } 107 } 108 109 _buildPointIndexMap() 110 { 111 this._pointIndexMap = new Map; 112 const data = this._data; 113 const length = data.length; 114 for (let i = 0; i < length; i++) 115 this._pointIndexMap.set(data[i], i); 116 } 117 118 length() { return this._length; } 119 120 firstPoint() { return this._length ? this._data[this._startingIndex] : null; } 121 lastPoint() { return this._length ? this._data[this._afterEndingIndex - 1] : null; } 122 123 nextPoint(point) 124 { 125 let index = this._findIndexForPoint(point); 126 index++; 127 if (index == this._afterEndingIndex) 128 return null; 129 return this._data[index]; 130 } 131 132 previousPoint(point) 133 { 134 const index = this._findIndexForPoint(point); 135 if (index == this._startingIndex) 136 return null; 137 return this._data[index - 1]; 138 } 139 140 findPointByIndex(index) 141 { 142 index += this._startingIndex; 143 if (index < 0 || index >= this._afterEndingIndex) 144 return null; 145 return this._data[index]; 146 } 147 148 findById(id) 149 { 150 for (let point of this) { 151 if (point.id == id) 152 return point; 153 } 154 return null; 155 } 156 157 values() 158 { 159 if (this._values == null) { 160 this._values = new Array(this._length); 161 let i = 0; 162 for (let point of this) 163 this._values[i++] = point.value; 164 } 165 return this._values; 166 } 167 168 filter(callback) 169 { 170 const data = this._data; 171 const filteredData = []; 172 for (let i = this._startingIndex; i < this._afterEndingIndex; i++) { 173 if (callback(data[i], i)) 174 filteredData.push(data[i]); 175 } 176 return new TimeSeriesView(this._timeSeries, 0, filteredData.length, filteredData); 177 } 178 179 viewTimeRange(startTime, endTime) 180 { 181 const data = this._data; 182 let startingIndex = null; 183 let endingIndex = null; 184 for (let i = this._startingIndex; i < this._afterEndingIndex; i++) { 185 if (startingIndex == null && data[i].time >= startTime) 186 startingIndex = i; 187 if (data[i].time <= endTime) 188 endingIndex = i; 189 } 190 if (startingIndex == null || endingIndex == null) 191 return new TimeSeriesView(this._timeSeries, 0, 0, data); 192 return new TimeSeriesView(this._timeSeries, startingIndex, endingIndex + 1, data); 193 } 194 195 firstPointInTimeRange(startTime, endTime) 196 { 197 console.assert(startTime <= endTime); 198 for (let point of this) { 199 if (point.time > endTime) 200 return null; 201 if (point.time >= startTime) 202 return point; 203 } 204 return null; 205 } 206 207 [Symbol.iterator]() 208 { 209 const data = this._data; 210 const end = this._afterEndingIndex; 211 let i = this._startingIndex; 212 return { 213 next() { 214 return {value: data[i], done: i++ == end}; 215 } 216 }; 217 } 218 } 85 219 86 220 if (typeof module != 'undefined') -
trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js
r211196 r212853 25 25 selectedPoints() 26 26 { 27 var selection = this._mainChart ? this._mainChart.currentSelection() : null; 28 if (!selection) 29 return null; 30 31 return this._mainChart.sampledDataBetween('current', selection[0], selection[1]); 27 return this._mainChart ? this._mainChart.selectedPoints('current') : null; 32 28 } 33 29 } -
trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js
r210880 r212853 365 365 return NaN; 366 366 367 varstartPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint();368 varafterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint();369 varendPoint = timeSeries.previousPoint(afterEndPoint);367 const startPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint(); 368 const afterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint(); 369 let endPoint = timeSeries.previousPoint(afterEndPoint); 370 370 if (!endPoint || startPoint == afterEndPoint) 371 371 endPoint = afterEndPoint; 372 372 373 var points = timeSeries.dataBetweenPoints(startPoint, endPoint).map(function (point) { return point.value; }); 374 return Statistics.median(points); 373 return Statistics.median(timeSeries.viewBetweenPoints(startPoint, endPoint).values()); 375 374 } 376 375 }
Note: See TracChangeset
for help on using the changeset viewer.