You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by su...@apache.org on 2021/05/27 06:26:45 UTC

[echarts] branch feature/auto-chunk-size created (now 36dab20)

This is an automated email from the ASF dual-hosted git repository.

sushuang pushed a change to branch feature/auto-chunk-size
in repository https://gitbox.apache.org/repos/asf/echarts.git.


      at 36dab20  feat: experiment of auto step for progressive rendering.

This branch includes the following new commits:

     new 36dab20  feat: experiment of auto step for progressive rendering.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org


[echarts] 01/01: feat: experiment of auto step for progressive rendering.

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

sushuang pushed a commit to branch feature/auto-chunk-size
in repository https://gitbox.apache.org/repos/asf/echarts.git

commit 36dab2060cd6bea346e22480bb5c61348714502f
Author: 100pah <su...@gmail.com>
AuthorDate: Thu May 27 14:25:57 2021 +0800

    feat: experiment of auto step for progressive rendering.
---
 src/core/Scheduler.ts                  | 119 ++++++-
 src/core/echarts.ts                    |  91 +++---
 src/core/task.ts                       |   4 +
 src/core/ticker.ts                     | 226 +++++++++++++
 src/model/Series.ts                    |   2 +-
 src/util/types.ts                      |   2 +-
 test/candlestick-large2.html           |   6 +-
 test/lib/caseFrame.js                  |   3 +-
 test/lib/config.js                     |   2 +-
 test/lib/frameInsight2.js              | 567 +++++++++++++++++++++++++++++++++
 test/lib/testHelper.js                 |   6 +-
 test/scatter-random-stream-layers.html | 268 ++++++++++++++++
 test/scatter-random-stream2.html       | 268 ++++++++++++++++
 13 files changed, 1515 insertions(+), 49 deletions(-)

diff --git a/src/core/Scheduler.ts b/src/core/Scheduler.ts
index 2081e4a..e3f9fb0 100644
--- a/src/core/Scheduler.ts
+++ b/src/core/Scheduler.ts
@@ -17,7 +17,7 @@
 * under the License.
 */
 
-import {each, map, isFunction, createHashMap, noop, HashMap, assert} from 'zrender/src/core/util';
+import {each, map, isFunction, createHashMap, noop, HashMap, assert, clone, bind} from 'zrender/src/core/util';
 import {
     createTask, Task, TaskContext,
     TaskProgressCallback, TaskProgressParams, TaskPlanCallbackReturn, PerformArgs
@@ -34,6 +34,8 @@ import { EChartsType } from './echarts';
 import SeriesModel from '../model/Series';
 import ChartView from '../view/Chart';
 import List from '../data/List';
+import { ZRenderType } from 'zrender/src/zrender';
+import { isMessageChannelTickerAvailable, OnTick, RuntimeStatistic, StatisticDataOnFrame, Ticker } from './ticker';
 
 export type GeneralTask = Task<TaskContext>;
 export type SeriesTask = Task<SeriesTaskContext>;
@@ -43,6 +45,13 @@ export type OverallTask = Task<OverallTaskContext> & {
 export type StubTask = Task<StubTaskContext> & {
     agent?: OverallTask;
 };
+export interface ScheduleOption {
+    // Experimental features, will not be exposed to users in this way in the final product.
+    ticker?: 'frame' | 'messageChannel';
+    timeQuota: number;
+};
+export interface ScheduleOptionInternal extends ScheduleOption {
+}
 
 export type Pipeline = {
     id: string
@@ -52,6 +61,7 @@ export type Pipeline = {
     progressiveEnabled: boolean,
     blockIndex: number,
     step: number,
+    useAutoStep: boolean;
     count: number,
     currentTask?: GeneralTask,
     context?: PipelineContext
@@ -99,6 +109,10 @@ interface StubTaskContext extends TaskContext {
     overallProgress: boolean;
 };
 
+const DEFAULT_MESSAGE_CHANNEL_PROGRESSIVE_CHUNK_SIZE = 20;
+const AUTO_STEP_FRAME_UPPER_BOUND = 20;
+const AUTO_STEP_FRAME_LOWER_BOUND = 15;
+
 class Scheduler {
 
     readonly ecInstance: EChartsType;
@@ -117,15 +131,23 @@ class Scheduler {
     // key: pipelineId
     private _pipelineMap: HashMap<Pipeline>;
 
+    private _ticker: Ticker;
+
+    private _scheduleOpt: ScheduleOptionInternal;
+
 
     constructor(
         ecInstance: EChartsType,
         api: ExtensionAPI,
+        zr: ZRenderType,
+        scheduleOpt: ScheduleOption,
+        onTick: OnTick,
         dataProcessorHandlers: StageHandlerInternal[],
         visualHandlers: StageHandlerInternal[]
     ) {
         this.ecInstance = ecInstance;
         this.api = api;
+        const internalScheduleOpt = this._scheduleOpt = normalizeScheduleOption(scheduleOpt);
 
         // Fix current processors in case that in some rear cases that
         // processors might be registered after echarts instance created.
@@ -134,6 +156,12 @@ class Scheduler {
         dataProcessorHandlers = this._dataProcessorHandlers = dataProcessorHandlers.slice();
         visualHandlers = this._visualHandlers = visualHandlers.slice();
         this._allHandlers = dataProcessorHandlers.concat(visualHandlers);
+
+        this._ticker = new Ticker(zr, internalScheduleOpt, onTick, bind(this._collectStatisticOnFrame, this));
+    }
+
+    start() {
+        this._ticker.start();
     }
 
     restoreData(ecModel: GlobalModel, payload: Payload): void {
@@ -235,8 +263,8 @@ class Scheduler {
         const scheduler = this;
         const pipelineMap = scheduler._pipelineMap = createHashMap();
 
-        ecModel.eachSeries(function (seriesModel) {
-            const progressive = seriesModel.getProgressive();
+        ecModel.eachSeries(seriesModel => {
+            const { step, useAutoStep } = this._getSeriesProgressiveChunkSize(seriesModel);
             const pipelineId = seriesModel.uid;
 
             pipelineMap.set(pipelineId, {
@@ -244,10 +272,11 @@ class Scheduler {
                 head: null,
                 tail: null,
                 threshold: seriesModel.getProgressiveThreshold(),
-                progressiveEnabled: progressive
+                progressiveEnabled: step
                     && !(seriesModel.preventIncremental && seriesModel.preventIncremental()),
                 blockIndex: -1,
-                step: Math.round(progressive || 700),
+                step: step,
+                useAutoStep: useAutoStep,
                 count: 0
             });
 
@@ -255,6 +284,45 @@ class Scheduler {
         });
     }
 
+    private _getSeriesProgressiveChunkSize(seriesModel: SeriesModel): {
+        step: number,
+        useAutoStep: boolean
+    } {
+        const progressive = seriesModel.getProgressive();
+        let step;
+        let useAutoStep;
+
+        if (progressive === 'auto') {
+            useAutoStep = true;
+            step = 5000;
+        }
+        else if (progressive) {
+            step = Math.round(progressive || 700);
+        }
+
+        if (progressive && this._scheduleOpt.ticker === 'messageChannel') {
+            // PENDING: measure real time cost for chunk size.
+            step = DEFAULT_MESSAGE_CHANNEL_PROGRESSIVE_CHUNK_SIZE;
+        }
+
+        return { step: step, useAutoStep: useAutoStep };
+    }
+
+    private _collectStatisticOnFrame(): StatisticDataOnFrame {
+        const pipelineMap = this._pipelineMap;
+        let samplePipeline: Pipeline;
+
+        pipelineMap.each(pipeline => {
+            if (!samplePipeline) {
+                samplePipeline = pipeline;
+            }
+        });
+        return {
+            sampleProcessedDataCount: samplePipeline.tail.getDueIndex(),
+            samplePipelineStep: samplePipeline.step
+        };
+    }
+
     prepareStageTasks(): void {
         const stageTaskMap = this._stageTaskMap;
         const ecModel = this.api.getModel();
@@ -288,6 +356,24 @@ class Scheduler {
         this._pipe(model, renderTask);
     }
 
+    updateStep(): void {
+        const scheduler = this;
+        const pipelineMap = scheduler._pipelineMap;
+
+        pipelineMap.each(pipeline => {
+            const recentFrameTime = this._ticker.getRecentFrameCost() - this._ticker.getRecentIdleCost();
+            if (pipeline.useAutoStep && recentFrameTime) {
+                const lastStep = pipeline.step;
+                if (recentFrameTime > AUTO_STEP_FRAME_UPPER_BOUND) {
+                    pipeline.step = 50 + Math.round(lastStep * AUTO_STEP_FRAME_UPPER_BOUND / recentFrameTime);
+                }
+                else if (recentFrameTime < AUTO_STEP_FRAME_LOWER_BOUND) {
+                    pipeline.step = 50 + Math.round(lastStep * AUTO_STEP_FRAME_LOWER_BOUND / recentFrameTime);
+                }
+            }
+        });
+    }
+
     performDataProcessorTasks(ecModel: GlobalModel, payload?: Payload): void {
         // If we do not use `block` here, it should be considered when to update modes.
         this._performStageTasks(this._dataProcessorHandlers, ecModel, payload, {block: true});
@@ -301,6 +387,10 @@ class Scheduler {
         this._performStageTasks(this._visualHandlers, ecModel, payload, opt);
     }
 
+    getRuntimeStatistic(): RuntimeStatistic {
+        return this._ticker.getRuntimeStatistic();
+    }
+
     private _performStageTasks(
         stageHandlers: StageHandlerInternal[],
         ecModel: GlobalModel,
@@ -686,4 +776,23 @@ function mockMethods(target: any, Clz: any): void {
     /* eslint-enable */
 }
 
+function normalizeScheduleOption(scheduleOpt: ScheduleOption): ScheduleOptionInternal {
+    const internalOpt = clone(scheduleOpt || {} as ScheduleOption);
+
+    let tickerType = internalOpt.ticker;
+    tickerType = internalOpt.ticker = (tickerType === 'messageChannel' && isMessageChannelTickerAvailable())
+        ? tickerType : 'frame';
+
+    if (tickerType === 'messageChannel') {
+        // By defualt use the empirical value used by react fiber for long time: 5ms
+        internalOpt.timeQuota = internalOpt.timeQuota || 5;
+    }
+    else {
+        // In the previous version we use 1ms for long time.
+        internalOpt.timeQuota = internalOpt.timeQuota || 1;
+    }
+
+    return internalOpt;
+}
+
 export default Scheduler;
diff --git a/src/core/echarts.ts b/src/core/echarts.ts
index c5a2396..81ed185 100644
--- a/src/core/echarts.ts
+++ b/src/core/echarts.ts
@@ -67,7 +67,7 @@ import * as modelUtil from '../util/model';
 import {throttle} from '../util/throttle';
 import {seriesStyleTask, dataStyleTask, dataColorPaletteTask} from '../visual/style';
 import loadingDefault from '../loading/default';
-import Scheduler from './Scheduler';
+import Scheduler, { ScheduleOption } from './Scheduler';
 import lightTheme from '../theme/light';
 import darkTheme from '../theme/dark';
 import {CoordinateSystemMaster, CoordinateSystemCreator, CoordinateSystemHostModel} from '../coord/CoordinateSystem';
@@ -110,6 +110,7 @@ import type {MorphDividingMethod} from 'zrender/src/tool/morphPath';
 import CanvasPainter from 'zrender/src/canvas/Painter';
 import SVGPainter from 'zrender/src/svg/Painter';
 import geoSourceManager from '../coord/geo/geoSourceManager';
+import { RequireMoreTick, RuntimeStatistic, ShouldYield } from './ticker';
 
 declare let global: any;
 
@@ -129,8 +130,6 @@ export const dependencies = {
     zrender: '5.1.0'
 };
 
-const TEST_FRAME_REMAIN_TIME = 1;
-
 const PRIORITY_PROCESSOR_SERIES_FILTER = 800;
 // Some data processors depends on the stack result dimension (to calculate data extent).
 // So data stack stage should be in front of data processing stage.
@@ -297,7 +296,9 @@ let renderSeries: (
     ecModel: GlobalModel,
     api: ExtensionAPI,
     payload: Payload | 'remain',
-    dirtyMap?: {[uid: string]: any}
+    dirtyMap?: {[uid: string]: any},
+    // A tmp prop, will be removed soon.
+    tmpProgressiveMode?: boolean
 ) => void;
 let performPostUpdateFuncs: (ecModel: GlobalModel, api: ExtensionAPI) => void;
 let createExtensionAPI: (ecIns: ECharts) => ExtensionAPI;
@@ -384,12 +385,13 @@ class ECharts extends Eventful<ECEventDefinition> {
         // Theme name or themeOption.
         theme?: string | ThemeOption,
         opts?: {
-            locale?: string | LocaleOption,
-            renderer?: RendererType,
-            devicePixelRatio?: number,
-            useDirtyRect?: boolean,
-            width?: number,
-            height?: number
+            locale?: string | LocaleOption;
+            renderer?: RendererType;
+            devicePixelRatio?: number;
+            useDirtyRect?: boolean;
+            width?: number;
+            height?: number;
+            schedule?: ScheduleOption;
         }
     ) {
         super(new ECEventProcessor());
@@ -448,7 +450,15 @@ class ECharts extends Eventful<ECEventDefinition> {
         timsort(visualFuncs, prioritySortFunc);
         timsort(dataProcessorFuncs, prioritySortFunc);
 
-        this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);
+        this._scheduler = new Scheduler(
+            this,
+            api,
+            zr,
+            opts.schedule,
+            zrUtil.bind(this._onframe, this),
+            dataProcessorFuncs,
+            visualFuncs
+        );
 
         this._messageCenter = new MessageCenter();
 
@@ -460,17 +470,17 @@ class ECharts extends Eventful<ECEventDefinition> {
         // In case some people write `window.onresize = chart.resize`
         this.resize = zrUtil.bind(this.resize, this);
 
-        zr.animation.on('frame', this._onframe, this);
-
         bindRenderedEvent(zr, this);
 
         bindMouseEvent(zr, this);
 
         // ECharts instance can be used as value.
         zrUtil.setAsPrimitive(this);
+
+        this._scheduler.start();
     }
 
-    private _onframe(): void {
+    private _onframe(shouldYield: ShouldYield, requireMoreTick: RequireMoreTick): void {
         if (this._disposed) {
             return;
         }
@@ -507,13 +517,13 @@ class ECharts extends Eventful<ECEventDefinition> {
         // Avoid do both lazy update and progress in one frame.
         else if (scheduler.unfinished) {
             // Stream progress.
-            let remainTime = TEST_FRAME_REMAIN_TIME;
             const ecModel = this._model;
             const api = this._api;
             scheduler.unfinished = false;
-            do {
-                const startTime = +new Date();
 
+            scheduler.updateStep();
+
+            do {
                 scheduler.performSeriesTasks(ecModel);
 
                 // Currently dataProcessorFuncs do not check threshold.
@@ -530,18 +540,18 @@ class ECharts extends Eventful<ECEventDefinition> {
                 // console.log('--- ec frame visual ---', remainTime);
                 scheduler.performVisualTasks(ecModel);
 
-                renderSeries(this, this._model, api, 'remain');
-
-                remainTime -= (+new Date() - startTime);
+                renderSeries(this, this._model, api, 'remain', null, true);
             }
-            while (remainTime > 0 && scheduler.unfinished);
-
+            while (!shouldYield() && scheduler.unfinished);
             // Call flush explicitly for trigger finished event.
             if (!scheduler.unfinished) {
                 this._zr.flush();
             }
             // Else, zr flushing be ensue within the same frame,
             // because zr flushing is after onframe event.
+            else {
+                requireMoreTick();
+            }
         }
     }
 
@@ -1339,6 +1349,9 @@ class ECharts extends Eventful<ECEventDefinition> {
         this.getZr().wakeUp();
     }
 
+    getRuntimeStatistic(): RuntimeStatistic {
+        return this._scheduler.getRuntimeStatistic();
+    }
 
     // A work around for no `internal` modifier in ts yet but
     // need to strictly hide private methods to JS users.
@@ -2015,13 +2028,14 @@ class ECharts extends Eventful<ECEventDefinition> {
             ecModel: GlobalModel,
             api: ExtensionAPI,
             payload: Payload | 'remain',
-            dirtyMap?: {[uid: string]: any}
+            dirtyMap?: {[uid: string]: any},
+            tmpProgressiveMode?: boolean
         ): void {
             // Render all charts
             const scheduler = ecIns._scheduler;
             const labelManager = ecIns._labelManager;
 
-            labelManager.clearLabels();
+            !tmpProgressiveMode && labelManager.clearLabels();
 
             let unfinished: boolean = false;
             ecModel.eachSeries(function (seriesModel) {
@@ -2032,7 +2046,7 @@ class ECharts extends Eventful<ECEventDefinition> {
                 scheduler.updatePayload(renderTask, payload);
 
                 // TODO states on marker.
-                clearStates(seriesModel, chartView);
+                !tmpProgressiveMode && clearStates(seriesModel, chartView);
 
                 if (dirtyMap && dirtyMap.get(seriesModel.uid)) {
                     renderTask.dirty();
@@ -2048,19 +2062,19 @@ class ECharts extends Eventful<ECEventDefinition> {
                 // increamental render (alway render from the __startIndex each frame)
                 // chartView.group.markRedraw();
 
-                updateBlend(seriesModel, chartView);
+                !tmpProgressiveMode && updateBlend(seriesModel, chartView);
 
-                updateSeriesElementSelection(seriesModel);
+                !tmpProgressiveMode && updateSeriesElementSelection(seriesModel);
 
                 // Add labels.
-                labelManager.addLabelsOfSeries(chartView);
+                !tmpProgressiveMode && labelManager.addLabelsOfSeries(chartView);
             });
 
             scheduler.unfinished = unfinished || scheduler.unfinished;
 
-            labelManager.updateLayoutConfig(api);
-            labelManager.layout(api);
-            labelManager.processLabelsOverall();
+            !tmpProgressiveMode && labelManager.updateLayoutConfig(api);
+            !tmpProgressiveMode && labelManager.layout(api);
+            !tmpProgressiveMode && labelManager.processLabelsOverall();
 
             ecModel.eachSeries(function (seriesModel) {
                 const chartView = ecIns._chartsMap[seriesModel.__viewId];
@@ -2069,12 +2083,12 @@ class ECharts extends Eventful<ECEventDefinition> {
 
                 // NOTE: Update states after label is updated.
                 // label should be in normal status when layouting.
-                updateStates(seriesModel, chartView);
+                !tmpProgressiveMode && updateStates(seriesModel, chartView);
             });
 
 
             // If use hover layer
-            updateHoverLayerStatus(ecIns, ecModel);
+            !tmpProgressiveMode && updateHoverLayerStatus(ecIns, ecModel);
         };
 
         performPostUpdateFuncs = function (ecModel: GlobalModel, api: ExtensionAPI): void {
@@ -2564,11 +2578,12 @@ export function init(
     dom: HTMLElement,
     theme?: string | object,
     opts?: {
-        renderer?: RendererType,
-        devicePixelRatio?: number,
-        width?: number,
-        height?: number,
-        locale?: string | LocaleOption
+        renderer?: RendererType;
+        devicePixelRatio?: number;
+        width?: number;
+        height?: number;
+        locale?: string | LocaleOption;
+        schedule?: ScheduleOption;
     }
 ): EChartsType {
     if (__DEV__) {
diff --git a/src/core/task.ts b/src/core/task.ts
index 97bcaeb..641da78 100644
--- a/src/core/task.ts
+++ b/src/core/task.ts
@@ -339,6 +339,10 @@ export class Task<Ctx extends TaskContext> {
         this._outputDueEnd = this._settedOutputEnd = end;
     }
 
+    getDueIndex(): number {
+        return this._dueIndex;
+    }
+
 }
 
 const iterator: TaskDataIterator = (function () {
diff --git a/src/core/ticker.ts b/src/core/ticker.ts
new file mode 100644
index 0000000..61001f0
--- /dev/null
+++ b/src/core/ticker.ts
@@ -0,0 +1,226 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*   http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied.  See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+/* global performance, requestIdleCallback */
+
+import { isFunction, noop } from 'zrender/src/core/util';
+import { ZRenderType } from 'zrender/src/zrender';
+import { ScheduleOptionInternal } from './Scheduler';
+
+
+export type ShouldYield = () => boolean;
+export type RequireMoreTick = () => void;
+export type OnTick = (
+    shouldYield: ShouldYield,
+    requireMoreTick: RequireMoreTick
+) => void;
+// The return type of `now()`, in ms.
+type TimeSinceOrigin = number;
+export type CollectStatisticOnFrame = () => StatisticDataOnFrame;
+export type StatisticDataOnFrame = {
+    sampleProcessedDataCount: number;
+    samplePipelineStep: number;
+};
+export type RuntimeStatistic = Ticker['_statistic'];
+
+
+export class Ticker {
+
+    private _zr: ZRenderType;
+    private _onTick: OnTick;
+    private _senderPort: MessagePort;
+    private _scheduleOpt: ScheduleOptionInternal;
+
+
+    private _collectStatisticOnFrame: CollectStatisticOnFrame;
+    private _statistic = {
+        lastFrameStartTime: 0,
+        lastFrameCost: 0,
+        lastIdleCost: 0,
+        lastIdleHappened: false,
+        sampleProcessedDataCount: 0,
+        samplePipelineStep: 0,
+        dataProcessedPerFrame: new AverageCounter(1),
+        recentOnTickExeTimeAvg: new AverageCounter(1)
+    };
+
+    constructor(
+        zr: ZRenderType,
+        scheduleOpt: ScheduleOptionInternal,
+        onTick: OnTick,
+        collectStatisticOnFrame: CollectStatisticOnFrame
+    ) {
+        this._zr = zr;
+        this._collectStatisticOnFrame = collectStatisticOnFrame;
+        this._onTick = (shouldYield, requireMoreTick) => {
+            const startTime = now();
+            onTick(shouldYield, requireMoreTick);
+            this._statistic.recentOnTickExeTimeAvg.addData(now() - startTime);
+        };
+        this._scheduleOpt = scheduleOpt || {} as ScheduleOptionInternal;
+    }
+
+    start(): void {
+        if (this._scheduleOpt.ticker === 'messageChannel') {
+            this._startForMessageChannel();
+        }
+        else {
+            this._startForFrame();
+        }
+    }
+
+    private _startForFrame(): void {
+        // In the previous version we use 1ms for long time.
+        const timeQuota = this._scheduleOpt.timeQuota || 1;
+        let startTime: TimeSinceOrigin;
+
+        function shouldYield(): boolean {
+            return now() - startTime > timeQuota;
+        }
+
+        this._zr.animation.on('frame', () => {
+            startTime = now();
+
+            this._statisticOnFrameStart(startTime);
+            this._onTick(shouldYield, noop);
+            this._collectStatistic();
+        });
+    }
+
+    private _startForMessageChannel(): void {
+        this._zr.animation.on('frame', () => {
+            this._statisticOnFrameStart(now());
+            this._collectStatistic();
+        });
+
+        const channel = new MessageChannel();
+        this._senderPort = channel.port2;
+        let doesMoreTickRequired = false;
+
+        // By defualt use the empirical value used by react fiber for long time: 5ms
+        const timeQuota = this._scheduleOpt.timeQuota || 5;
+        let startTime: TimeSinceOrigin;
+
+        function shouldYield(): boolean {
+            return now() - startTime > timeQuota;
+        }
+        function requireMoreTick(): void {
+            doesMoreTickRequired = true;
+        }
+
+        channel.port1.onmessage = () => {
+            startTime = now();
+            doesMoreTickRequired = false;
+
+            this._onTick(shouldYield, requireMoreTick);
+
+            if (doesMoreTickRequired) {
+                this._senderPort.postMessage(null);
+            }
+        };
+
+        this._senderPort.postMessage(null);
+    }
+
+    private _statisticOnFrameStart(frameStartTime: number) {
+        if (!this._statistic.lastIdleHappened) {
+            this._statistic.lastIdleCost = 0;
+        }
+
+        if (this._statistic.lastFrameStartTime) {
+            this._statistic.lastFrameCost = frameStartTime - this._statistic.lastFrameStartTime;
+        }
+        this._statistic.lastFrameStartTime = frameStartTime;
+        this._statistic.lastIdleHappened = false;
+
+        // PENDING: polyfill for safari
+        // @ts-ignore
+        if (typeof requestIdleCallback === 'function') {
+            // @ts-ignore
+            requestIdleCallback(deadline => {
+                this._statistic.lastIdleHappened = true;
+                this._statistic.lastIdleCost = deadline.timeRemaining();
+            });
+        }
+    }
+
+    private _collectStatistic() {
+        const statistic = this._statistic;
+        const {
+            sampleProcessedDataCount,
+            samplePipelineStep
+        } = this._collectStatisticOnFrame();
+        if (statistic.sampleProcessedDataCount != null) {
+            statistic.dataProcessedPerFrame.addData(sampleProcessedDataCount - statistic.sampleProcessedDataCount);
+        }
+        statistic.samplePipelineStep = samplePipelineStep;
+        statistic.sampleProcessedDataCount = sampleProcessedDataCount;
+    }
+
+    getRuntimeStatistic(): RuntimeStatistic {
+        return this._statistic;
+    }
+
+    getRecentFrameCost(): number {
+        return this._statistic.lastFrameCost;
+    }
+
+    getRecentIdleCost(): number {
+        return this._statistic.lastIdleCost;
+    }
+
+}
+
+/**
+ * Return time since a time origin (document start or 19700101) in ms.
+ * So can only be compared with the result returned by this method.
+ */
+const now: (() => TimeSinceOrigin) =
+    (typeof performance === 'object' && isFunction(performance.now))
+        ? () => performance.now()
+        : () => +new Date();
+
+export function isMessageChannelTickerAvailable(): boolean {
+    return typeof MessageChannel === 'function';
+}
+
+class AverageCounter {
+
+    private _lastAvg: number;
+    private _avgSum = 0;
+    private _count = 0;
+    private _avgSize: number;
+
+    constructor(avgSize: number) {
+        this._avgSize = avgSize;
+    }
+
+    addData(data: number): void {
+        this._avgSum += data / this._avgSize;
+        this._count++;
+        if (this._count >= this._avgSize) {
+            this._lastAvg = this._avgSum;
+            this._count = this._avgSum = 0;
+        }
+    }
+
+    getLastAvg(): number {
+        return this._lastAvg;
+    }
+}
diff --git a/src/model/Series.ts b/src/model/Series.ts
index 5a54ad4..3f5d899 100644
--- a/src/model/Series.ts
+++ b/src/model/Series.ts
@@ -479,7 +479,7 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> extends ComponentMode
     /**
      * Get progressive rendering count each step
      */
-    getProgressive(): number | false {
+    getProgressive(): number | false | 'auto' {
         return this.get('progressive');
     }
 
diff --git a/src/util/types.ts b/src/util/types.ts
index 00d3b82..020abbf 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -1563,7 +1563,7 @@ export interface SeriesOption<
     /**
      * Configurations about progressive rendering
      */
-    progressive?: number | false
+    progressive?: number | false | 'auto'
     progressiveThreshold?: number
     progressiveChunkMode?: 'mod'
     /**
diff --git a/test/candlestick-large2.html b/test/candlestick-large2.html
index abcb939..d5cff82 100644
--- a/test/candlestick-large2.html
+++ b/test/candlestick-large2.html
@@ -28,7 +28,7 @@ under the License.
         <script src="lib/jquery.min.js"></script>
         <script src="lib/facePrint.js"></script>
         <script src="lib/testHelper.js"></script>
-        <script src="lib/frameInsight.js"></script>
+        <script src="lib/frameInsight2.js"></script>
         <link rel="stylesheet" href="lib/reset.css" />
     </head>
     <body>
@@ -265,6 +265,7 @@ under the License.
                         {
                             name: 'Data Amount: ' + echarts.format.addCommas(rawDataCount),
                             type: 'candlestick',
+                            // progressiveChunkMode: 'linear',
                             itemStyle: {
                                 color: upColor,
                                 color0: downColor,
@@ -295,6 +296,9 @@ under the License.
 
                 var panel = document.getElementById('panel0');
                 var chart = testHelper.create(echarts, 'main0', {
+                    schedule: {
+                      ticker: 'messageChannel'
+                    },
                     title: [
                         'Progressive by mod',
                         '(1) Check click legend',
diff --git a/test/lib/caseFrame.js b/test/lib/caseFrame.js
index febc9d5..fb2a170 100644
--- a/test/lib/caseFrame.js
+++ b/test/lib/caseFrame.js
@@ -262,7 +262,8 @@
             '__RENDERER__=' + curr.renderer,
             '__USE_DIRTY_RECT__=' + curr.useDirtyRect,
             '__ECDIST__=' + curr.dist,
-            '__FILTER__=' + curr.listFilterName
+            '__FILTER__=' + curr.listFilterName,
+            '__CASE_FRAME__=1'
         ].join('&');
     }
 
diff --git a/test/lib/config.js b/test/lib/config.js
index 67dae59..1c99b35 100644
--- a/test/lib/config.js
+++ b/test/lib/config.js
@@ -36,7 +36,7 @@
 
     // Set echarts source code.
     var ecDistPath;
-    if (params.__ECDIST__) {
+    if (params.__ECDIST__ && !params.__CASE_FRAME__) {
         ecDistPath = ({
             'webpack-req-ec': '../../echarts-boilerplate/echarts-webpack/dist/webpack-req-ec',
             'webpack-req-eclibec': '../../echarts-boilerplate/echarts-webpack/dist/webpack-req-eclibec',
diff --git a/test/lib/frameInsight2.js b/test/lib/frameInsight2.js
new file mode 100644
index 0000000..546f5ac
--- /dev/null
+++ b/test/lib/frameInsight2.js
@@ -0,0 +1,567 @@
+
+/*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*   http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied.  See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+(function (global) {
+
+    var frameInsight = global.frameInsight = {};
+
+    var TIME_UNIT = 1000 / 60;
+    var PERF_LINE_SPAN_TIME = TIME_UNIT * 60;
+    var PERF_WORK_NORMAL_FILL = 'rgba(3, 163, 14, 0.7)';
+    var PERF_WORK_SLOW_FILL = 'rgba(201, 0, 0, 0.7)';
+    var PERF_RIC_DETECTED_IDLE_FILL = '#fff';
+    // var REPF_MESSAGE_CHANNEL_DETECTED_IDLE_FILL = '#fff';
+    // var PERF_MESSAGE_CHANNEL_DETECT_IDLE_SPAN = 0.5; // in ms
+    var SLOW_TIME_THRESHOLD = 50;
+    var LINE_DEFAULT_WORK_COLOR = '#ccffd5';
+    // var LINE_DEFAULT_WORK_COLOR = '#f5dedc';
+    var LIST_MAX = 1000000;
+    var CSS_HEADER_HEIGHT = 40;
+    var CSS_PERF_LINE_HEIGHT = 30;
+    var CSS_PERF_CHART_GAP = 10;
+    var CSS_PERF_CHART_PADDING = 3;
+    var DEFAULT_PERF_CHART_COUNT = 5;
+    var BACKGROUND_COLOR = '#eee';
+
+    var _settings;
+    var _getChart;
+    var _dpr = window && Math.max(window.devicePixelRatio || 1, 1) || 1;
+    var _tickWorkStartList = new TickList();
+    var _tickWorkEndList = new TickList();
+    var _tickFrameStart;
+    var _tickFrameStartCanPaint;
+    // var _tickMessageChannelList = new TickList();
+    var _tickRICStartList = new TickList();
+    var _tickRICEndList = new TickList();
+    var _started = false;
+    var _newestPerfLineIdx = 0;
+    var _timelineStart;
+    var _renderWidth;
+    var _renderHeight;
+    var _renderHeaderHeight;
+    var _renderPerfLineHeight;
+    var _ctx;
+
+    var _original = {
+        setTimeout: global.setTimeout,
+        requestAnimationFrame: global.requestAnimationFrame,
+        addEventListener: global.addEventListener,
+        MessageChannel: global.MessageChannel
+    };
+
+    // performance.now is not mocked in the visual regression test.
+    var now = (typeof performance === 'object' && isFunction(performance.now))
+        ? function () {
+            return performance.now();
+        }
+        : function () {
+            return +new Date();
+        }
+
+    // Make sure call it as early as possible.
+    initListening();
+
+    instrumentEnvironment();
+
+    /**
+     * @public
+     * @param {Object} opt
+     * @param {Object} opt.echarts
+     * @param {string | HTMLElement} opt.perfDOM
+     * @param {string | HTMLElement} opt.lagDOM
+     * @param {string | HTMLElement} opt.statisticDOM
+     * @param {Function} opt.getChart
+     * @param {number} opt.perfChartCount
+     * @param {boolean} opt.dontInstrumentECharts
+     */
+    frameInsight.init = function (opt) {
+        _settings = {
+            echarts: opt.echarts,
+            perfDOM: opt.perfDOM,
+            lagDOM: opt.lagDOM,
+            statisticDOM: opt.statisticDOM,
+            perfChartCount: opt.perfChartCount || DEFAULT_PERF_CHART_COUNT
+        };
+        _getChart = opt.getChart;
+
+        var start = _timelineStart = now();
+
+        !opt.dontInstrumentECharts && instrumentECharts();
+        initResultPanel();
+        initLagPanel();
+        initStatisticPanel();
+        _started = true;
+    };
+
+    function instrumentEnvironment() {
+        doInstrumentRegistrar('setTimeout', 0);
+        doInstrumentRegistrar('requestAnimationFrame', 0);
+        doInstrumentRegistrar('addEventListenter', 1);
+        instrumentMessageChannel();
+    }
+
+    function instrumentECharts() {
+        var echarts = _settings.echarts;
+
+        var dummyDom = document.createElement('div');
+        var dummyChart = echarts.init(dummyDom, null, {width: 10, height: 10});
+        var ECClz = dummyChart.constructor;
+        dummyChart.dispose();
+
+        ECClz.prototype.setOption = doInstrumentHandler(ECClz.prototype.setOption, 'setOption');
+    }
+
+    function doInstrumentRegistrar(name, handlerIndex) {
+        global[name] = function () {
+            var args = [].slice.call(arguments);
+            args[handlerIndex] = doInstrumentHandler(args[handlerIndex], name);
+            return _original[name].apply(this, args);
+        };
+    }
+
+    function doInstrumentHandler(orginalHandler) {
+        return function () {
+            var start = now();
+            var result = orginalHandler.apply(this, arguments);
+            var end = now();
+            _tickWorkStartList.push(start);
+            _tickWorkEndList.push(end);
+            return result;
+        };
+    }
+
+    function instrumentMessageChannel() {
+        global.MessageChannel = function () {
+            this._msgChannel = new _original.MessageChannel();
+            this.port1 = instrumentPort(this._msgChannel.port1);
+            this.port2 = instrumentPort(this._msgChannel.port2);
+        };
+
+        function instrumentPort(originalPort) {
+            var newPort = {
+                postMessage: function () {
+                    originalPort.postMessage.apply(originalPort, arguments);
+                }
+            };
+            Object.defineProperty(newPort, 'onmessage', {
+                set: function (listener) {
+                    originalPort.onmessage = doInstrumentHandler(listener);
+                }
+            });
+            return newPort;
+        }
+    }
+
+    function initListening() {
+        function nextFrame() {
+            if (_started) {
+                // A trick to make fram start line at the top in z-order.
+                _tickFrameStartCanPaint = _tickFrameStart;
+                _tickFrameStart = now();
+                // console.time('a');
+                renderResultPanel();
+                // console.timeEnd('a');
+            }
+            _original.requestAnimationFrame.call(global, nextFrame);
+        }
+        nextFrame();
+
+        function nextIdle(deadline) {
+            if (_started) {
+                var start = now();
+                var timeRemaining = deadline.timeRemaining();
+                _tickRICStartList.push(start);
+                _tickRICEndList.push(start + timeRemaining);
+            }
+            requestIdleCallback(nextIdle);
+        }
+        nextIdle();
+
+        // Fail: too aggressive
+        // Use message channel to detect background long task.
+        // var messageChannel = new MessageChannel();
+        // messageChannel.port1.onmessage = function () {
+        //     if (_started) {
+        //         _tickMessageChannelList.push(now());
+        //     }
+        //     messageChannel.port2.postMessage(null);
+        // };
+        // messageChannel.port2.postMessage(null);
+    }
+
+    function initResultPanel() {
+        var panelEl = isString(_settings.perfDOM)
+            ? document.getElementById(_settings.perfDOM)
+            : _settings.perfDOM;
+
+        var panelElStyle = panelEl.style;
+        panelElStyle.position = 'relative';
+        // panelElStyle.backgroundColor = '#eee';
+        panelElStyle.padding = 0;
+
+        var panelElWidth = getSize(panelEl, 0);
+        var cssCanvasHeight = CSS_HEADER_HEIGHT
+            + (CSS_PERF_LINE_HEIGHT + CSS_PERF_CHART_GAP) * _settings.perfChartCount;
+        _renderWidth = panelElWidth * _dpr;
+        _renderHeight = cssCanvasHeight * _dpr;
+        _renderHeaderHeight = CSS_HEADER_HEIGHT * _dpr;
+        _renderPerfLineHeight = CSS_PERF_LINE_HEIGHT * _dpr;
+
+        var canvas = document.createElement('canvas');
+        panelEl.appendChild(canvas);
+
+        canvas.style.cssText = [
+            'width: 100%',
+            'height: ' + cssCanvasHeight + 'px',
+            'padding: 0',
+            'margin: 0',
+        ].join('; ') + ';';
+
+        canvas.width = _renderWidth;
+        canvas.height = _renderHeight;
+
+        _ctx = canvas.getContext('2d');
+        _ctx.fillStyle = BACKGROUND_COLOR;
+        _ctx.fillRect(0, 0, _renderWidth, _renderHeight);
+
+        initMesureMarkers();
+    }
+
+    function initMesureMarkers() {
+        _ctx.font = '30px serif';
+        _ctx.fillStyle = '#111';
+        _ctx.textAlign = 'start';
+        _ctx.textBaseline = 'top';
+        _ctx.fillText(TIME_UNIT.toFixed(2) + ' ms', 10, 10);
+
+        var measureY = _renderHeaderHeight - 10;
+        renderHorizontalLine(measureY, '#333', 1);
+        var timeExtentStart = getPerfLineExtentStart(0);
+        var timeExtentEnd = getPerfLineExtentEnd(0);
+        for (var measureX = timeExtentStart; measureX < timeExtentEnd; measureX += TIME_UNIT) {
+            var coord = linearMap(measureX, timeExtentStart, timeExtentEnd, 0, _renderWidth);
+            _ctx.strokeStyle = '#333';
+            _ctx.lineWidth = 2;
+            _ctx.beginPath();
+            _ctx.moveTo(coord, measureY - 8);
+            _ctx.lineTo(coord, measureY);
+            _ctx.stroke();
+        }
+
+        for (var i = 0; i < _settings.perfChartCount; i++) {
+            var y = getPerfLineY(i) + _renderPerfLineHeight + 1;
+            renderHorizontalLine(y, '#ccc', 1);
+        }
+
+        function renderHorizontalLine(y, color, lineWidth) {
+            _ctx.strokeStyle = color;
+            _ctx.lineWidth = lineWidth;
+            _ctx.beginPath();
+            _ctx.moveTo(0, y);
+            _ctx.lineTo(_renderWidth, y);
+            _ctx.stroke();
+        }
+    }
+
+    function getPerfLineIndex(timeVal) {
+        var timeOffset = timeVal - _timelineStart;
+        return Math.floor(timeOffset / PERF_LINE_SPAN_TIME);
+    }
+
+    function getPerfLineExtentStart(perfLineIndex) {
+        return _timelineStart + PERF_LINE_SPAN_TIME * perfLineIndex;
+    }
+    function getPerfLineExtentEnd(perfLineIndex) {
+        return _timelineStart + PERF_LINE_SPAN_TIME * (perfLineIndex + 1);
+    }
+
+    function getPerfLineY(perfLineIndex) {
+        var perfLineNumber = getPerfLineNumber(perfLineIndex);
+        return _renderHeaderHeight + perfLineNumber * (_renderPerfLineHeight + CSS_PERF_CHART_GAP * _dpr);
+    }
+
+    function getPerfLineNumber(perfLineIndex) {
+        return perfLineIndex % _settings.perfChartCount;
+    }
+
+    function prepareNextPerfChart(timeVal) {
+        var perfChartExtentEnd = getPerfLineExtentEnd(_newestPerfLineIdx);
+        if (timeVal <= perfChartExtentEnd) {
+            return;
+        }
+
+        _newestPerfLineIdx++;
+        _ctx.fillStyle = BACKGROUND_COLOR;
+        _ctx.fillRect(0, getPerfLineY(_newestPerfLineIdx) - 5, _renderWidth, _renderPerfLineHeight + 5);
+    }
+
+    function renderResultPanel() {
+        renderDefualtWorkForLastFrame();
+        renderRICForLastFrame();
+        renderJSWorkForLastFrame();
+        renderFrameStartForLastFrame();
+        // renderMessageChannelForLastFrame();
+    }
+
+    function renderDefualtWorkForLastFrame() {
+        // By default deem it as busy. only rIC can render idle rect.
+        var timeStart = _tickFrameStartCanPaint != null ? _tickFrameStartCanPaint : _timelineStart;
+        var timeEnd = _tickFrameStart;
+        prepareNextPerfChart(timeStart);
+        prepareNextPerfChart(timeEnd);
+
+        var perfLineIndexStart = getPerfLineIndex(timeStart);
+        var perfLineIndexEnd = getPerfLineIndex(timeEnd);
+
+        renderPerfRect(perfLineIndexStart, LINE_DEFAULT_WORK_COLOR, timeStart, timeEnd);
+        if (perfLineIndexEnd !== perfLineIndexStart) {
+            renderPerfRect(perfLineIndexEnd, LINE_DEFAULT_WORK_COLOR, timeStart, timeEnd);
+        }
+    }
+
+    function renderRICForLastFrame() {
+        for (var i = 0; i < _tickRICStartList.len; i++) {
+            var tickRICStart = _tickRICStartList.list[i];
+            var tickRICEnd = _tickRICEndList.list[i];
+            var perfLineIdxStart = getPerfLineIndex(tickRICStart);
+            var perfLineIdxEnd = getPerfLineIndex(tickRICEnd);
+
+            prepareNextPerfChart(tickRICStart);
+            prepareNextPerfChart(tickRICEnd);
+
+            renderPerfRect(perfLineIdxStart, PERF_RIC_DETECTED_IDLE_FILL, tickRICStart, tickRICEnd);
+            if (perfLineIdxStart !== perfLineIdxEnd) {
+                renderPerfRect(perfLineIdxEnd, PERF_RIC_DETECTED_IDLE_FILL, tickRICStart, tickRICEnd);
+            }
+        };
+        _tickRICStartList.len = 0;
+        _tickRICEndList.len = 0;
+    }
+
+    function renderJSWorkForLastFrame() {
+        for (var i = 0; i < _tickWorkStartList.len; i++) {
+            var tickWorkStart = _tickWorkStartList.list[i];
+            var tickWorkEnd = _tickWorkEndList.list[i];
+            var perfLineIdxStart = getPerfLineIndex(tickWorkStart);
+            var perfLineIdxEnd = getPerfLineIndex(tickWorkEnd);
+
+            prepareNextPerfChart(tickWorkStart);
+            prepareNextPerfChart(tickWorkEnd);
+
+            renderPerfWorkSpan(tickWorkStart, tickWorkEnd, perfLineIdxStart);
+            if (perfLineIdxStart !== perfLineIdxEnd) {
+                renderPerfWorkSpan(tickWorkStart, tickWorkEnd, perfLineIdxEnd);
+            }
+        }
+        _tickWorkStartList.len = 0;
+        _tickWorkEndList.len = 0;
+    }
+
+    function renderFrameStartForLastFrame() {
+        if (_tickFrameStartCanPaint) {
+            prepareNextPerfChart(_tickFrameStartCanPaint);
+
+            var perfLineIndex = getPerfLineIndex(_tickFrameStartCanPaint);
+            var perfLineY = getPerfLineY(perfLineIndex);
+            var perfTimeExtentStart = getPerfLineExtentStart(perfLineIndex);
+            var perfTimeExtentEnd = getPerfLineExtentEnd(perfLineIndex);
+            var coord = linearMap(_tickFrameStartCanPaint, perfTimeExtentStart, perfTimeExtentEnd, 0, _renderWidth);
+            _ctx.lineWidth = 2;
+            _ctx.strokeStyle = '#0037b8';
+            _ctx.beginPath();
+            _ctx.moveTo(coord, perfLineY - 2);
+            _ctx.lineTo(coord, perfLineY + _renderPerfLineHeight);
+            _ctx.stroke();
+        }
+        _tickFrameStartCanPaint = null;
+    }
+
+    // function renderMessageChannelForLastFrame() {
+    //     var tickIdleStart = _tickMessageChannelList.list[0];
+    //     for (var i = 0; i < _tickMessageChannelList.len; i++) {
+    //         var tickMsgVal = _tickMessageChannelList.list[i];
+    //         prepareNextPerfChart(tickMsgVal);
+
+            // PERF_MESSAGE_CHANNEL_DETECT_IDLE_SPAN
+
+            // renderFrameStart(_tickFrameStartCanPaint, perfLineIndex);
+            // var perfLineY = getPerfLineY(perfLineIndex);
+            // var perfTimeExtentStart = getPerfLineExtentStart(perfLineIndex);
+            // var perfTimeExtentEnd = getPerfLineExtentEnd(perfLineIndex);
+            // var coord = linearMap(tickMsgVal, perfTimeExtentStart, perfTimeExtentEnd, 0, _renderWidth);
+            // _ctx.lineWidth = 2;
+            // _ctx.strokeStyle = '#184561';
+            // _ctx.beginPath();
+            // _ctx.moveTo(coord, perfLineY - 2);
+            // _ctx.lineTo(coord, perfLineY + 10);
+            // _ctx.stroke();
+        // }
+    //     _tickMessageChannelList.len = 0;
+    // }
+
+    function renderPerfWorkSpan(timeWorkStart, timeWorkEnd, perfLineIndex) {
+        var timeSlow = timeWorkStart + SLOW_TIME_THRESHOLD;
+
+        renderPerfRect(
+            perfLineIndex,
+            PERF_WORK_NORMAL_FILL,
+            timeWorkStart,
+            Math.min(timeWorkEnd, timeSlow)
+        );
+
+        if (timeWorkEnd > timeSlow) {
+            renderPerfRect(
+                perfLineIndex,
+                PERF_WORK_SLOW_FILL,
+                timeSlow,
+                timeWorkEnd
+            );
+        }
+    }
+
+    function renderPerfRect(perfLineIndex, color, timeStart, timeEnd) {
+        var perfTimeExtentStart = getPerfLineExtentStart(perfLineIndex);
+        var perfTimeExtentEnd = getPerfLineExtentEnd(perfLineIndex);
+        var realTimeStart = Math.max(timeStart, perfTimeExtentStart);
+        var realTimeEnd = Math.min(timeEnd, perfTimeExtentEnd);
+        var coordStart = linearMap(realTimeStart, perfTimeExtentStart, perfTimeExtentEnd, 0, _renderWidth);
+        var coordEnd = linearMap(realTimeEnd, perfTimeExtentStart, perfTimeExtentEnd, 0, _renderWidth);
+
+        if (coordEnd - coordStart > 0.5) {
+            _ctx.fillStyle = color;
+            var perfLineY = getPerfLineY(perfLineIndex);
+            _ctx.fillRect(
+                coordStart,
+                perfLineY + CSS_PERF_CHART_PADDING,
+                coordEnd - coordStart,
+                _renderPerfLineHeight - CSS_PERF_CHART_PADDING
+            );
+        }
+    }
+
+    function initLagPanel() {
+        if (!_settings.lagDOM) {
+            return;
+        }
+        var dom = document.getElementById(_settings.lagDOM);
+        dom.style.fontSize = 26;
+        dom.style.fontFamily = 'Arial';
+        // dom.style.color = '#000';
+        dom.style.padding = '10px';
+
+        function onFrame() {
+            render();
+            _original.requestAnimationFrame.call(global, onFrame);
+        }
+
+        function render() {
+            var time = new Date();
+            var timeStr = time.getMinutes() + ':' + time.getSeconds() + '.' + time.getMilliseconds();
+            dom.innerHTML = timeStr;
+        }
+
+        onFrame();
+    }
+
+    function initStatisticPanel() {
+        if (!_settings.statisticDOM) {
+            return;
+        }
+
+        var dom = document.getElementById(_settings.statisticDOM);
+        dom.style.fontSize = 14;
+        dom.style.fontFamily = 'Arial';
+        dom.style.color = '#000';
+        dom.style.padding = '5px';
+        dom.style.boxShadow = '0 0 5px #000';
+
+        function onFrame() {
+            render();
+            _original.requestAnimationFrame.call(global, onFrame);
+        }
+
+        function render() {
+            var chart = _getChart();
+            var statistic = chart.getRuntimeStatistic();
+            var msg = [
+                'lastFrameStartTime: ' + statistic.lastFrameStartTime,
+                'lastFrameCost: ' + statistic.lastFrameCost,
+                'sampleProcessedDataCount: ' + statistic.sampleProcessedDataCount,
+                'samplePipelineStep: ' + statistic.samplePipelineStep,
+                'dataProcessedPerFrame: ' + statistic.dataProcessedPerFrame.getLastAvg(),
+                'recentOnTickExeTimeAvg: ' + statistic.recentOnTickExeTimeAvg.getLastAvg()
+            ];
+            dom.innerHTML = msg.join('<br>');
+        }
+
+        _original.requestAnimationFrame.call(global, onFrame);
+    }
+
+    function getSize(root, whIdx) {
+        var wh = ['width', 'height'][whIdx];
+        var cwh = ['clientWidth', 'clientHeight'][whIdx];
+        var plt = ['paddingLeft', 'paddingTop'][whIdx];
+        var prb = ['paddingRight', 'paddingBottom'][whIdx];
+
+        // IE8 does not support getComputedStyle, but it use VML.
+        var stl = document.defaultView.getComputedStyle(root);
+
+        return (
+            (root[cwh] || parseInt10(stl[wh]) || parseInt10(root.style[wh]))
+            - (parseInt10(stl[plt]) || 0)
+            - (parseInt10(stl[prb]) || 0)
+        ) | 0;
+    }
+
+    function linearMap(val, domain0, domain1, range0, range1) {
+        var subDomain = domain1 - domain0;
+        var subRange = range1 - range0;
+
+        if (val <= domain0) {
+            return range0;
+        }
+        if (val >= domain1) {
+            return range1;
+        }
+
+        return (val - domain0) / subDomain * subRange + range0;
+    }
+
+    function parseInt10(val) {
+        return parseInt(val, 10);
+    }
+
+    function isString(val) {
+        return typeof val === 'string'
+    }
+
+    function isFunction(val) {
+        return typeof val === 'function';
+    }
+
+    function TickList() {
+        this.list = new Float32Array(LIST_MAX);
+        this.len = 0;
+    }
+    TickList.prototype.push = function (val) {
+        this.list[this.len++] = val;
+    }
+
+})(window);
diff --git a/test/lib/testHelper.js b/test/lib/testHelper.js
index be1d491..ec36270 100644
--- a/test/lib/testHelper.js
+++ b/test/lib/testHelper.js
@@ -54,6 +54,7 @@
      * @param {boolean} [opt.lazyUpdate]
      * @param {boolean} [opt.notMerge]
      * @param {boolean} [opt.autoResize=true]
+     * @param {Object} [opt.scheduleOpt]
      * @param {Array.<Object>|Object} [opt.button] {text: ..., onClick: ...}, or an array of them.
      * @param {Array.<Object>|Object} [opt.buttons] {text: ..., onClick: ...}, or an array of them.
      * @param {boolean} [opt.recordCanvas] 'test/lib/canteen.js' is required.
@@ -224,6 +225,7 @@
      * @param {number} opt.width
      * @param {number} opt.height
      * @param {boolean} opt.draggable
+     * @param {boolean} opt.scheduleOpt
      */
     testHelper.createChart = function (echarts, domOrId, option, opt) {
         if (typeof opt === 'number') {
@@ -243,7 +245,9 @@
                 dom.style.height = opt.height + 'px';
             }
 
-            var chart = echarts.init(dom);
+            var chart = echarts.init(dom, null, {
+                schedule: opt.schedule
+            });
 
             if (opt.draggable) {
                 if (!window.draggable) {
diff --git a/test/scatter-random-stream-layers.html b/test/scatter-random-stream-layers.html
new file mode 100644
index 0000000..5a492ee
--- /dev/null
+++ b/test/scatter-random-stream-layers.html
@@ -0,0 +1,268 @@
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<html>
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="../dist/echarts.js"></script>
+        <script src="lib/jquery.min.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <script src="lib/facePrint.js"></script>
+    </head>
+    <body>
+        <style>
+            html, body, #main {
+                width: 100%;
+                height: 600;
+                margin: 0;
+            }
+
+            #main {
+                /* margin-left: 200px; */
+                /* width: 300px; */
+                width: 90%;
+                margin: 0 auto;
+            }
+
+            #snapshot {
+                position: fixed;
+                right: 10;
+                top: 10;
+                width: 50;
+                height: 50;
+                background: #fff;
+                border: 2px solid rgba(0,0,0,0.5);
+            }
+
+
+        </style>
+
+        <button id="show_layers">Show Layers</button>
+
+        <script>
+            var btn = document.getElementById('show_layers');
+            btn.onclick = function () {
+                var container = document.getElementById('container');
+                container.className = 'container-show-layers';
+                var canvasList = document.getElementsByTagName('canvas');
+                canvasList[0].parentNode.style.cssText = [
+                    // 'perspective:971px;'
+                ].join(';');
+                for (var i = 0; i < canvasList.length; i++) {
+                    var canvasDom = canvasList[i];
+                    canvasDom.style.cssText = [
+                        'transform: translateY(' + (-200 - i * 1350) + 'px) rotateX(82deg) rotateZ(345deg) scaleX(0.3) translateX(-1600px)',
+                        'border: 10px solid #999',
+                        'background: rgba(255,255,255,0.5)'
+                    ].join(';') + ';';
+                }
+            };
+        </script>
+
+
+        <div id="container" data-ec-title="css transform 3d" style="
+        ">
+            <div id="main" style=""></div>
+        </div>
+
+        <script>
+
+            var dataCount = 1e5;
+            // var dataCount = 1e6;
+            var chunkMax = 4;
+            var chunkCount = 0;
+            // var progressive = 2;
+            // var progressive = 5000;
+            // var progressive = 10000;
+            var progressive = 'auto';
+            // var progressive = 100;
+            // var largeThreshold = 500;
+            var largeThreshold = 500;
+            // var largeThreshold = Infinity;
+            var ticker = 'frame';
+            // var ticker = 'messageChannel';
+
+            function genData1(len, offset) {
+                var lngRange = [-10.781327, 131.48];
+                var latRange = [18.252847, 52.33];
+
+                var arr = new Float32Array(len * 2);
+                var off = 0;
+
+                for (var i = 0; i < len; i++) {
+                    var x = +Math.random() * 10;
+                    var y = +Math.sin(x) - x * (len % 2 ? 0.1 : -0.1) * Math.random() + (offset || 0) / 10;
+                    arr[off++] = x;
+                    arr[off++] = y;
+                }
+                return arr;
+            }
+
+            function genData2(count) {
+                var lngRange = [-10.781327, 31.48];
+                var latRange = [-18.252847, 30.33];
+                return genData(count, lngRange, latRange);
+            }
+
+            function genData(count, lngRange, latRange) {
+                lngRange[1] += 5;
+                lngRange[0] -= 10;
+                latRange[1] += 4;
+                var lngExtent = lngRange[1] - lngRange[0];
+                var latExtent = latRange[1] - latRange[0];
+                var data = [];
+                for (var i = 0; i < count; i++) {
+                    data.push([
+                        Math.random() * lngExtent + lngRange[0],
+                        Math.random() * latExtent + latRange[0],
+                        Math.random() * 1000
+                    ]);
+                }
+                return data;
+            }
+
+            var series0Data = genData1(dataCount);
+
+                var chart = echarts.init(document.getElementById('main'), null, {
+                    schedule: {
+                        ticker: ticker
+                    }
+                });
+
+                chart.setOption({
+                    tooltip: {
+                        // trigger: 'axis',
+                        // renderMode: 'richText'
+                    },
+                    toolbox: {
+                        left: 'center',
+                        feature: {
+                            dataZoom: {}
+                        }
+                    },
+                    legend: {
+                        orient: 'vertical',
+                        left: 'left',
+                        data: ['pm2.5' /* ,'pm10' */]
+                    },
+                    // ???
+                    // visualMap: {
+                    //     min: 0,
+                    //     max: 1500,
+                    //     left: 'left',
+                    //     top: 'bottom',
+                    //     text: ['High','Low'],
+                    //     seriesIndex: [1, 2, 3],
+                    //     inRange: {
+                    //         color: ['#006edd', '#e0ffff']
+                    //     },
+                    //     calculable : true
+                    // },
+                    xAxis: [{
+                    }],
+                    yAxis: [{
+                    }],
+                    dataZoom: [{
+                        type: 'inside',
+                        // filterMode: 'none',
+                    }, {
+                        type: 'slider',
+                        // filterMode: 'none',
+                        showDataShadow: false
+                    }],
+                    animation: false,
+                    series : [{
+                        name: 'pm2.5',
+                        type: 'scatter',
+                        progressive: progressive,
+                        data: series0Data,
+                        dimensions: ['x', 'y'],
+                        // symbol: 'rect',
+                        symbolSize: 3,
+                        // symbol: 'rect',
+                        itemStyle: {
+                            // color: '#128de3',
+                            color: '#5470c6',
+                            opacity: 0.2
+                        },
+                        z: 100,
+                        large: true,
+                        // large: {
+                        //     symbolSize: 2
+                        // },
+                        // large: function (params) {
+                        //     if (params.dataCount > 30000) {
+                        //         return {symbolSize: 1};
+                        //     }
+                        //     else if (params.dataCount > 3000) {
+                        //         return {symbolSize: 5};
+                        //     }
+                        // },
+                        largeThreshold: largeThreshold
+                    }, {
+                        type: 'pie',
+                        center: ['50%', '50%'],
+                        radius: '30%',
+                        data: [{
+                            value: 123, name: 'a'
+                        }, {
+                            value: 123, name: 'b'
+                        }, {
+                            value: 123, name: 'c'
+                        }, {
+                            value: 123, name: 'd'
+                        }, {
+                            value: 123, name: 'e'
+                        }, {
+                            value: 23, name: 'f'
+                        }],
+                        z: 121212
+                    }]
+                });
+
+                chart.on('click', function (param) {
+                    alert('asdf');
+                });
+
+                // chart.on('finished', function () {
+                //     console.log('finished');
+                //     var url = chart.getDataURL();
+                //     var snapshotEl = document.getElementById('snapshot');
+                //     snapshotEl.src = url;
+                // });
+
+                window.onresize = chart.resize;
+
+                // next();
+
+                function next() {
+                    if (chunkCount++ < chunkMax) {
+                        var newData = genData1(100000, chunkCount);
+                        chart.appendData({seriesIndex: 0, data: newData});
+                        // console.log('Data loaded');
+                        setTimeout(next, 3000);
+                    }
+                }
+
+
+        </script>
+    </body>
+</html>
\ No newline at end of file
diff --git a/test/scatter-random-stream2.html b/test/scatter-random-stream2.html
new file mode 100644
index 0000000..c9b8050
--- /dev/null
+++ b/test/scatter-random-stream2.html
@@ -0,0 +1,268 @@
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<html>
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script src="lib/simpleRequire.js"></script>
+        <script src="lib/config.js"></script>
+        <script src="lib/jquery.min.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <script src="lib/frameInsight2.js"></script>
+        <script src="lib/facePrint.js"></script>
+    </head>
+    <body>
+        <style>
+            html, body, #main {
+                width: 100%;
+                height: 600;
+                margin: 0;
+            }
+
+            #snapshot {
+                position: fixed;
+                right: 10;
+                top: 10;
+                width: 50;
+                height: 50;
+                background: #fff;
+                border: 2px solid rgba(0,0,0,0.5);
+            }
+
+        </style>
+
+
+        <div id="main"></div>
+        <div id="perf"></div>
+        <div id="lag"></div>
+        <div id="statistic"></div>
+        <img id="snapshot"/>
+
+        <script>
+
+            var dataCount = 5e6;
+            // var dataCount = 1e6;
+            var chunkMax = 4;
+            var chunkCount = 0;
+            // var progressive = 2;
+            // var progressive = 5000;
+            // var progressive = 10000;
+            var progressive = 'auto';
+            // var progressive = 100;
+            // var largeThreshold = 500;
+            var largeThreshold = 500;
+            // var largeThreshold = Infinity;
+            var ticker = 'frame';
+            // var ticker = 'messageChannel';
+
+            function genData1(len, offset) {
+                var lngRange = [-10.781327, 131.48];
+                var latRange = [18.252847, 52.33];
+
+                var arr = new Float32Array(len * 2);
+                var off = 0;
+
+                for (var i = 0; i < len; i++) {
+                    var x = +Math.random() * 10;
+                    var y = +Math.sin(x) - x * (len % 2 ? 0.1 : -0.1) * Math.random() + (offset || 0) / 10;
+                    arr[off++] = x;
+                    arr[off++] = y;
+                }
+                return arr;
+            }
+
+            function genData2(count) {
+                var lngRange = [-10.781327, 31.48];
+                var latRange = [-18.252847, 30.33];
+                return genData(count, lngRange, latRange);
+            }
+
+            function genData(count, lngRange, latRange) {
+                lngRange[1] += 5;
+                lngRange[0] -= 10;
+                latRange[1] += 4;
+                var lngExtent = lngRange[1] - lngRange[0];
+                var latExtent = latRange[1] - latRange[0];
+                var data = [];
+                for (var i = 0; i < count; i++) {
+                    data.push([
+                        Math.random() * lngExtent + lngRange[0],
+                        Math.random() * latExtent + latRange[0],
+                        Math.random() * 1000
+                    ]);
+                }
+                return data;
+            }
+
+            var series0Data = genData1(dataCount);
+
+            require([
+                'echarts'
+            ], function (echarts) {
+
+
+                setTimeout(run, 5000);
+
+                function run() {
+
+                    frameInsight.init({
+                        echarts: echarts,
+                        perfDOM: 'perf',
+                        lagDOM: 'lag',
+                        statisticDOM: 'statistic',
+                        dontInstrumentECharts: true,
+                        perfChartCount: 4,
+                        getChart: function () {
+                            return chart;
+                        }
+                    });
+
+                    var chart = echarts.init(document.getElementById('main'), null, {
+                        schedule: {
+                            ticker: ticker
+                        }
+                    });
+
+                    var option = {
+                        tooltip: {
+                            // trigger: 'axis',
+                            // renderMode: 'richText'
+                        },
+                        toolbox: {
+                            left: 'center',
+                            feature: {
+                                dataZoom: {}
+                            }
+                        },
+                        legend: {
+                            orient: 'vertical',
+                            left: 'left',
+                            data: ['series1' /* ,'pm10' */]
+                        },
+                        // ???
+                        // visualMap: {
+                        //     min: 0,
+                        //     max: 1500,
+                        //     left: 'left',
+                        //     top: 'bottom',
+                        //     text: ['High','Low'],
+                        //     seriesIndex: [1, 2, 3],
+                        //     inRange: {
+                        //         color: ['#006edd', '#e0ffff']
+                        //     },
+                        //     calculable : true
+                        // },
+                        xAxis: [{
+                        }],
+                        yAxis: [{
+                        }],
+                        dataZoom: [{
+                            type: 'inside',
+                            // filterMode: 'none',
+                        }, {
+                            type: 'slider',
+                            // filterMode: 'none',
+                            showDataShadow: false
+                        }],
+                        animation: false,
+                        series : [{
+                            name: 'series1',
+                            type: 'scatter',
+                            progressive: progressive,
+                            data: series0Data,
+                            dimensions: ['x', 'y'],
+                            // symbol: 'rect',
+                            symbolSize: 3,
+                            // symbol: 'rect',
+                            itemStyle: {
+                                // color: '#128de3',
+                                color: '#5470c6',
+                                opacity: 0.2
+                            },
+                            z: 100,
+                            large: true,
+                            // large: {
+                            //     symbolSize: 2
+                            // },
+                            // large: function (params) {
+                            //     if (params.dataCount > 30000) {
+                            //         return {symbolSize: 1};
+                            //     }
+                            //     else if (params.dataCount > 3000) {
+                            //         return {symbolSize: 5};
+                            //     }
+                            // },
+                            largeThreshold: largeThreshold
+                        // }, {
+                        //     type: 'pie',
+                        //     center: ['50%', '50%'],
+                        //     data: [{
+                        //         value: 123, name: 'a'
+                        //     }, {
+                        //         value: 123, name: 'b'
+                        //     }, {
+                        //         value: 123, name: 'c'
+                        //     }, {
+                        //         value: 123, name: 'd'
+                        //     }, {
+                        //         value: 123, name: 'e'
+                        //     }, {
+                        //         value: 23, name: 'f'
+                        //     }],
+                        //     z: 121212
+                        }]
+                    };
+
+                    chart.setOption(option);
+
+
+                    chart.on('click', function (param) {
+                        alert('asdf');
+                    });
+
+                    chart.on('finished', function () {
+                        console.log('finished');
+                        var url = chart.getDataURL();
+                        var snapshotEl = document.getElementById('snapshot');
+                        snapshotEl.src = url;
+                    });
+
+                    window.onresize = chart.resize;
+
+                    // next();
+
+                    function next() {
+                        if (chunkCount++ < chunkMax) {
+                            var newData = genData1(100000, chunkCount);
+                            chart.appendData({seriesIndex: 0, data: newData});
+                            // console.log('Data loaded');
+                            setTimeout(next, 3000);
+                        }
+                    }
+
+                }
+
+            });
+
+
+        </script>
+    </body>
+</html>
\ No newline at end of file

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org