feat: experiment of auto step for progressive rendering.
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 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 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 @@
     progressiveEnabled: boolean,
     blockIndex: number,
     step: number,
+    useAutoStep: boolean;
     count: number,
     currentTask?: GeneralTask,
     context?: PipelineContext
@@ -99,6 +109,10 @@
     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 @@
     // 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 @@
         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 @@
         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 @@
                 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 @@
         });
     }
 
+    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 @@
         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 @@
         this._performStageTasks(this._visualHandlers, ecModel, payload, opt);
     }
 
+    getRuntimeStatistic(): RuntimeStatistic {
+        return this._ticker.getRuntimeStatistic();
+    }
+
     private _performStageTasks(
         stageHandlers: StageHandlerInternal[],
         ecModel: GlobalModel,
@@ -686,4 +776,23 @@
     /* 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 {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 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 @@
     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 @@
     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 @@
         // 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 @@
         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 @@
         // 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 @@
         // 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 @@
                 // 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 @@
         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 @@
             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 @@
                 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 @@
                 // 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 @@
 
                 // 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 @@
     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 @@
         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 @@
     /**
      * 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 @@
     /**
      * 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 @@
         <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 @@
                         {
                             name: 'Data Amount: ' + echarts.format.addCommas(rawDataCount),
                             type: 'candlestick',
+                            // progressiveChunkMode: 'linear',
                             itemStyle: {
                                 color: upColor,
                                 color0: downColor,
@@ -295,6 +296,9 @@
 
                 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