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