blob: 61001f08799dc4dbe80fe537343022bd5e578f9e [file] [log] [blame]
/*
* 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;
}
}