Changeset 212946 in webkit


Ignore:
Timestamp:
Feb 23, 2017 10:57:04 PM (7 years ago)
Author:
rniwa@webkit.org
Message:

New sampling algorithm shows very few points when zoomed out
https://bugs.webkit.org/show_bug.cgi?id=168813

Reviewed by Saam Barati.

When a chart is zoomed out to a large time interval, the new sampling algorithm introduced in r212853 can
hide most of the data points because the difference between the preceding point's time and the succeeding
point's time of most points will be below the threshold we computed.

Instead, rank each data point based on the aforementioned time interval difference, and pick the first M data
points when M data points are to be shown.

This makes the new algorithm behave like our old algorithm while keeping it stable still. Note that this
algorithm still biases data points without a close neighboring point but this seems to work out in practice
because such a point tends to be an important sample anyway, and we don't have a lot of space between
data points since we aim to show about one point per pixel.

  • browser-tests/index.html:

(CanvasTest.canvasContainsColor): Extracted from one of the test cases and generalized. Returns true when
the specified region of the canvas contains a specified color (alpha is optional).

  • browser-tests/time-series-chart-tests.js: Added a test case for sampling. It checks that sampling happens

and that we always show some data point even when zoomed out to a large time interval.
(createChartWithSampleCluster):

  • public/v3/components/interactive-time-series-chart.js:

(InteractiveTimeSeriesChart.prototype._sampleTimeSeries):

  • public/v3/components/time-series-chart.js:

(TimeSeriesChart.prototype._ensureSampledTimeSeries): M, the number of data points we pick must be computed
based on the width of data points we're about to draw constrained by the canvas size. e.g. when the canvas
is only half filled, we shouldn't be showing two points per pixel in the filled region.
(TimeSeriesChart.prototype._sampleTimeSeries): Refined the algorithm. First, compute the time difference or
the rank for each N data points. Sort those ranks in descending order (in the order we prefer), and include
all data points above the M-th rank in the sample.
(TimeSeriesChart.prototype.computeTimeGrid): Revert the inadvertent change in r212935.

  • public/v3/models/time-series.js:

(TimeSeriesView.prototype.filter): Fixed a bug that the indices passed onto the callback were shifted by the
starting index.

  • unit-tests/time-series-tests.js: Added a test case to ensure callbacks are called with correct data points

and indices.

Location:
trunk/Websites/perf.webkit.org
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • trunk/Websites/perf.webkit.org/ChangeLog

    r212935 r212946  
     12017-02-23  Ryosuke Niwa  <rniwa@webkit.org>
     2
     3        New sampling algorithm shows very few points when zoomed out
     4        https://bugs.webkit.org/show_bug.cgi?id=168813
     5
     6        Reviewed by Saam Barati.
     7
     8        When a chart is zoomed out to a large time interval, the new sampling algorithm introduced in r212853 can
     9        hide most of the data points because the difference between the preceding point's time and the succeeding
     10        point's time of most points will be below the threshold we computed.
     11
     12        Instead, rank each data point based on the aforementioned time interval difference, and pick the first M data
     13        points when M data points are to be shown.
     14
     15        This makes the new algorithm behave like our old algorithm while keeping it stable still. Note that this
     16        algorithm still biases data points without a close neighboring point but this seems to work out in practice
     17        because such a point tends to be an important sample anyway, and we don't have a lot of space between
     18        data points since we aim to show about one point per pixel.
     19
     20        * browser-tests/index.html:
     21        (CanvasTest.canvasContainsColor): Extracted from one of the test cases and generalized. Returns true when
     22        the specified region of the canvas contains a specified color (alpha is optional).
     23        * browser-tests/time-series-chart-tests.js: Added a test case for sampling. It checks that sampling happens
     24        and that we always show some data point even when zoomed out to a large time interval.
     25        (createChartWithSampleCluster):
     26
     27        * public/v3/components/interactive-time-series-chart.js:
     28        (InteractiveTimeSeriesChart.prototype._sampleTimeSeries):
     29        * public/v3/components/time-series-chart.js:
     30        (TimeSeriesChart.prototype._ensureSampledTimeSeries): M, the number of data points we pick must be computed
     31        based on the width of data points we're about to draw constrained by the canvas size. e.g. when the canvas
     32        is only half filled, we shouldn't be showing two points per pixel in the filled region.
     33        (TimeSeriesChart.prototype._sampleTimeSeries): Refined the algorithm. First, compute the time difference or
     34        the rank for each N data points. Sort those ranks in descending order (in the order we prefer), and include
     35        all data points above the M-th rank in the sample.
     36        (TimeSeriesChart.prototype.computeTimeGrid): Revert the inadvertent change in r212935.
     37
     38        * public/v3/models/time-series.js:
     39        (TimeSeriesView.prototype.filter): Fixed a bug that the indices passed onto the callback were shifted by the
     40        starting index.
     41        * unit-tests/time-series-tests.js: Added a test case to ensure callbacks are called with correct data points
     42        and indices.
     43
    1442017-02-23  Ryosuke Niwa  <rniwa@webkit.org>
    245
  • trunk/Websites/perf.webkit.org/browser-tests/index.html

    r212923 r212946  
    164164
    165165    canvasImageData(canvas) { return canvasImageData(canvas); },
     166
     167    canvasContainsColor(canvas, color, rect = {})
     168    {
     169        const content = canvas.getContext('2d').getImageData(rect.x || 0, rect.y || 0, rect.width || canvas.width, rect.height || canvas.height);
     170        let found = false;
     171        const data = content.data;
     172        for (let startOfPixel = 0; startOfPixel < data.length; startOfPixel += 4) {
     173            let r = data[startOfPixel];
     174            let g = data[startOfPixel + 1];
     175            let b = data[startOfPixel + 2];
     176            let a = data[startOfPixel + 3];
     177            if (r == color.r && g == color.g && b == color.b && (color.a == undefined || a == color.a))
     178                return true;
     179        }
     180        return false;
     181    },
     182
    166183    expectCanvasesMatch(canvas1, canvas2) { return canvasRefTest(canvas1, canvas2, true); },
    167184    expectCanvasesMismatch(canvas1, canvas2) { return canvasRefTest(canvas1, canvas2, false); },
  • trunk/Websites/perf.webkit.org/browser-tests/time-series-chart-tests.js

    r212935 r212946  
    7878        {
    7979            type: 'current',
     80            lineStyle: options.lineStyle || '#666',
    8081            measurementSet: MeasurementSet.findSet(1, 1, 0),
    8182            interactive: options.interactive || false,
    82             includeOutliers: options.includeOutliers || false
     83            includeOutliers: options.includeOutliers || false,
     84            sampleData: options.sampleData || false,
    8385        }], chartOptions);
    8486    const element = chart.element();
     
    542544                expect(chart.content().querySelector('canvas')).to.be(null);
    543545                return waitForComponentsToRender(context).then(() => {
    544                     console.log('done')
    545546                    expect(chart.content().querySelector('canvas')).to.not.be(null);
    546547                });
     
    829830                    CanvasTest.expectCanvasesMismatch(canvasWithYAxis1, canvasWithYAxis2);
    830831
    831                     let content1 = CanvasTest.canvasImageData(canvasWithYAxis1);
    832                     let foundGridLine = false;
    833                     for (let y = 0; y < content1.height; y++) {
    834                         let endOfY = content1.width * 4 * y;
    835                         let r = content1.data[endOfY - 4];
    836                         let g = content1.data[endOfY - 3];
    837                         let b = content1.data[endOfY - 2];
    838                         if (r == 204 && g == 204 && b == 204) {
    839                             foundGridLine = true;
    840                             break;
    841                         }
    842                     }
    843                     expect(foundGridLine).to.be(true);
     832                    expect(CanvasTest.canvasContainsColor(canvasWithYAxis1, {r: 204, g: 204, b: 204},
     833                        {x: canvasWithYAxis1.width - 1, width: 1, y: 0, height: canvasWithYAxis1.height})).to.be(true);
     834                });
     835            });
     836        });
     837
     838        it('should render the sampled time series', () => {
     839            const context = new BrowsingContext();
     840            return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
     841                const chartWithoutSampling = createChartWithSampleCluster(context, {}, {lineStyle: 'rgb(0, 128, 255)', width: '100px', height: '100px', sampleData: false});
     842                const chartWithSampling = createChartWithSampleCluster(context, {}, {lineStyle: 'rgb(0, 128, 255)', width: '100px', height: '100px', sampleData: true});
     843
     844                chartWithoutSampling.setDomain(sampleCluster.startTime, sampleCluster.endTime);
     845                chartWithoutSampling.fetchMeasurementSets();
     846                respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
     847
     848                chartWithSampling.setDomain(sampleCluster.startTime, sampleCluster.endTime);
     849                chartWithSampling.fetchMeasurementSets();
     850
     851                let canvasWithSampling;
     852                let canvasWithoutSampling;
     853                return waitForComponentsToRender(context).then(() => {
     854                    canvasWithoutSampling = chartWithoutSampling.content().querySelector('canvas');
     855                    canvasWithSampling = chartWithSampling.content().querySelector('canvas');
     856
     857                    CanvasTest.expectCanvasesMatch(canvasWithSampling, canvasWithoutSampling);
     858                    expect(CanvasTest.canvasContainsColor(canvasWithoutSampling, {r: 0, g: 128, b: 255})).to.be(true);
     859                    expect(CanvasTest.canvasContainsColor(canvasWithSampling, {r: 0, g: 128, b: 255})).to.be(true);
     860
     861                    const diff = sampleCluster.endTime - sampleCluster.startTime;
     862                    chartWithoutSampling.setDomain(sampleCluster.startTime - 2 * diff, sampleCluster.endTime);
     863                    chartWithSampling.setDomain(sampleCluster.startTime - 2 * diff, sampleCluster.endTime);
     864
     865                    CanvasTest.fillCanvasBeforeRedrawCheck(canvasWithoutSampling);
     866                    CanvasTest.fillCanvasBeforeRedrawCheck(canvasWithSampling);
     867                    return waitForComponentsToRender(context);
     868                }).then(() => {
     869                    expect(CanvasTest.hasCanvasBeenRedrawn(canvasWithoutSampling)).to.be(true);
     870                    expect(CanvasTest.hasCanvasBeenRedrawn(canvasWithSampling)).to.be(true);
     871
     872                    expect(CanvasTest.canvasContainsColor(canvasWithoutSampling, {r: 0, g: 128, b: 255})).to.be(true);
     873                    expect(CanvasTest.canvasContainsColor(canvasWithSampling, {r: 0, g: 128, b: 255})).to.be(true);
     874
     875                    CanvasTest.expectCanvasesMismatch(canvasWithSampling, canvasWithoutSampling);
    844876                });
    845877            });
  • trunk/Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js

    r212923 r212946  
    383383    }
    384384
    385     _sampleTimeSeries(data, minimumTimeDiff, excludedPoints)
     385    _sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints)
    386386    {
    387387        if (this._indicatorID)
    388388            excludedPoints.add(this._indicatorID);
    389         return super._sampleTimeSeries(data, minimumTimeDiff, excludedPoints);
     389        return super._sampleTimeSeries(data, maximumNumberOfPoints, excludedPoints);
    390390    }
    391391
  • trunk/Websites/perf.webkit.org/public/v3/components/time-series-chart.js

    r212935 r212946  
    491491                return null;
    492492
    493             // A chart with X px width shouldn't have more than 2X / <radius-of-points> data points.
    494             const maximumNumberOfPoints = 2 * metrics.chartWidth / (source.pointRadius || 2);
    495 
    496493            const pointAfterStart = timeSeries.findPointAfterTime(startTime);
    497494            const pointBeforeStart = (pointAfterStart ? timeSeries.previousPoint(pointAfterStart) : null) || timeSeries.firstPoint();
     
    505502                return view;
    506503
    507             return this._sampleTimeSeries(view, (endTime - startTime) / maximumNumberOfPoints, new Set);
     504            // A chart with X px width shouldn't have more than 2X / <radius-of-points> data points.
     505            const viewWidth = Math.min(metrics.chartWidth, metrics.timeToX(pointAfterEnd.time) - metrics.timeToX(pointBeforeStart.time));
     506            const maximumNumberOfPoints = 2 * viewWidth / (source.pointRadius || 2);
     507
     508            return this._sampleTimeSeries(view, maximumNumberOfPoints, new Set);
    508509        });
    509510
     
    515516    }
    516517
    517     _sampleTimeSeries(view, minimumTimeDiff, excludedPoints)
    518     {
    519         if (view.length() < 2)
     518    _sampleTimeSeries(view, maximumNumberOfPoints, excludedPoints)
     519    {
     520
     521        if (view.length() < 2 || maximumNumberOfPoints >= view.length() || maximumNumberOfPoints < 1)
    520522            return view;
    521523
    522524        Instrumentation.startMeasuringTime('TimeSeriesChart', 'sampleTimeSeries');
    523525
    524         const sampledData = view.filter((point, i) => {
    525             if (excludedPoints.has(point.id))
    526                 return true;
     526        let ranks = new Array(view.length());
     527        let i = 0;
     528        for (let point of view) {
    527529            let previousPoint = view.previousPoint(point) || point;
    528530            let nextPoint = view.nextPoint(point) || point;
    529             return nextPoint.time - previousPoint.time >= minimumTimeDiff;
     531            ranks[i] = nextPoint.time - previousPoint.time;
     532            i++;
     533        }
     534
     535        const sortedRanks = ranks.slice(0).sort((a, b) => b - a);
     536        const minimumRank = sortedRanks[Math.floor(maximumNumberOfPoints)];
     537        const sampledData = view.filter((point, i) => {
     538            return excludedPoints.has(point.id) || ranks[i] >= minimumRank;
    530539        });
    531540
     
    620629        let previousDate = null;
    621630        let previousMonth = null;
    622         while (currentTime <= max) {
     631        while (currentTime <= max && result.length < maxLabels) {
    623632            const time = new Date(currentTime);
    624633            const month = time.getUTCMonth() + 1;
  • trunk/Websites/perf.webkit.org/public/v3/models/time-series.js

    r212853 r212946  
    168168    filter(callback)
    169169    {
    170         const data = this._data;
    171170        const filteredData = [];
    172         for (let i = this._startingIndex; i < this._afterEndingIndex; i++) {
    173             if (callback(data[i], i))
    174                 filteredData.push(data[i]);
     171        let i = 0;
     172        for (let point of this) {
     173            if (callback(point, i))
     174                filteredData.push(point);
     175            i++;
    175176        }
    176177        return new TimeSeriesView(this._timeSeries, 0, filteredData.length, filteredData);
  • trunk/Websites/perf.webkit.org/unit-tests/time-series-tests.js

    r212853 r212946  
    277277
    278278    describe('filter', () => {
     279        it('should call callback with an element in the view and its index', () => {
     280            const timeSeries = new TimeSeries();
     281            addPointsToSeries(timeSeries, fivePoints);
     282            const originalView = timeSeries.viewBetweenPoints(fivePoints[1], fivePoints[3]);
     283            const points = [];
     284            const indices = [];
     285            const view = originalView.filter((point, index) => {
     286                points.push(point);
     287                indices.push(index);
     288            });
     289            assert.deepEqual(points, [fivePoints[1], fivePoints[2], fivePoints[3]]);
     290            assert.deepEqual(indices, [0, 1, 2]);
     291        });
     292
    279293        it('should create a filtered view', () => {
    280294            const timeSeries = new TimeSeries();
Note: See TracChangeset for help on using the changeset viewer.