From e3dc89fbfc4dd8030097c7831479eb18cf23d8eb Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Thu, 12 Nov 2020 00:52:29 -0300 Subject: [PATCH] feat(core): queueMacroTask (#8904) --- packages/core/animation-frame/index.ts | 65 +++++++++++++++----- packages/core/index.ts | 3 +- packages/core/utils/index.d.ts | 7 +++ packages/core/utils/macrotask-scheduler.d.ts | 5 ++ packages/core/utils/macrotask-scheduler.ts | 27 ++++++++ packages/core/utils/utils-common.ts | 1 + 6 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 packages/core/utils/macrotask-scheduler.d.ts create mode 100644 packages/core/utils/macrotask-scheduler.ts diff --git a/packages/core/animation-frame/index.ts b/packages/core/animation-frame/index.ts index c7ef43e37..e39983ae0 100644 --- a/packages/core/animation-frame/index.ts +++ b/packages/core/animation-frame/index.ts @@ -1,12 +1,18 @@ +import { queueMacrotask } from '../utils/macrotask-scheduler'; import { FPSCallback } from '../fps-meter/fps-native'; import { getTimeInFrameBase } from './animation-native'; +import { Trace } from 'trace'; export interface FrameRequestCallback { (time: number): void; } +type AnimationFrameCallbacks = { [key: string]: FrameRequestCallback }; + let animationId = 0; -let nextFrameAnimationCallbacks: { [key: string]: FrameRequestCallback } = {}; +let currentFrameAnimationCallbacks: AnimationFrameCallbacks = {}; // requests that were scheduled in this frame and must be called ASAP +let currentFrameScheduled = false; +let nextFrameAnimationCallbacks: AnimationFrameCallbacks = {}; // requests there were scheduled in another request and must be called in the next frame let shouldStop = true; let inAnimationFrame = false; let fpsCallback: FPSCallback; @@ -23,33 +29,59 @@ function ensureNative() { fpsCallback = new FPSCallback(doFrame); } +function callAnimationCallbacks(thisFrameCbs: AnimationFrameCallbacks, frameTime: number): void { + inAnimationFrame = true; + for (const animationId in thisFrameCbs) { + if (thisFrameCbs[animationId]) { + try { + thisFrameCbs[animationId](frameTime); + } catch (err) { + const msg = err ? err.stack || err : err; + Trace.write(`Error in requestAnimationFrame: ${msg}`, Trace.categories.Error, Trace.messageType.error); + } + } + } + inAnimationFrame = false; +} + +function doCurrentFrame() { + // if we're not getting accurate frame times + // set last frame time as the current time + if (!fpsCallback || !fpsCallback.running) { + lastFrameTime = getTimeInFrameBase(); + } + currentFrameScheduled = false; + const thisFrameCbs = currentFrameAnimationCallbacks; + currentFrameAnimationCallbacks = {}; + callAnimationCallbacks(thisFrameCbs, lastFrameTime); +} + function doFrame(currentTimeMillis: number) { lastFrameTime = currentTimeMillis; shouldStop = true; const thisFrameCbs = nextFrameAnimationCallbacks; nextFrameAnimationCallbacks = {}; - inAnimationFrame = true; - for (const animationId in thisFrameCbs) { - if (thisFrameCbs[animationId]) { - thisFrameCbs[animationId](lastFrameTime); - } - } - inAnimationFrame = false; + callAnimationCallbacks(thisFrameCbs, lastFrameTime); if (shouldStop) { fpsCallback.stop(); // TODO: check performance without stopping to allow consistent frame times } } -export function requestAnimationFrame(cb: FrameRequestCallback): number { - if (!inAnimationFrame) { - inAnimationFrame = true; - zonedCallback(cb)(getTimeInFrameBase()); // TODO: store and use lastFrameTime - inAnimationFrame = false; +function ensureCurrentFrameScheduled() { + if (!currentFrameScheduled) { + currentFrameScheduled = true; + queueMacrotask(doCurrentFrame); + } +} - return getNewId(); +export function requestAnimationFrame(cb: FrameRequestCallback): number { + const animId = getNewId(); + if (!inAnimationFrame) { + ensureCurrentFrameScheduled(); + currentFrameAnimationCallbacks[animId] = zonedCallback(cb) as FrameRequestCallback; + return animId; } ensureNative(); - const animId = getNewId(); nextFrameAnimationCallbacks[animId] = zonedCallback(cb) as FrameRequestCallback; shouldStop = false; fpsCallback.start(); @@ -57,6 +89,7 @@ export function requestAnimationFrame(cb: FrameRequestCallback): number { return animId; } -export function cancelAnimationFrame(id: number) { +export function cancelAnimationFrame(id: number): void { + delete currentFrameAnimationCallbacks[id]; delete nextFrameAnimationCallbacks[id]; } diff --git a/packages/core/index.ts b/packages/core/index.ts index 77be3d80a..65879ead4 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -110,7 +110,7 @@ export * from './trace'; export * from './ui'; -import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, RESOURCE_PREFIX, FILE_PREFIX } from './utils'; +import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, queueMacrotask, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, RESOURCE_PREFIX, FILE_PREFIX } from './utils'; import { ClassInfo, getClass, getBaseClasses, getClassInfo, isBoolean, isDefined, isFunction, isNullOrUndefined, isNumber, isObject, isString, isUndefined, toUIString, verifyCallback } from './utils/types'; export const Utils = { @@ -124,6 +124,7 @@ export const Utils = { mainThreadify, isMainThread, dispatchToMainThread, + queueMacrotask, releaseNativeObject, getModuleName, diff --git a/packages/core/utils/index.d.ts b/packages/core/utils/index.d.ts index bb86bdcee..08ddfe79e 100644 --- a/packages/core/utils/index.d.ts +++ b/packages/core/utils/index.d.ts @@ -1,6 +1,7 @@ import { dip, px } from '../ui/core/view'; export * from './mainthread-helper'; +export * from './macrotask-scheduler'; export { Source } from './debug'; export * from './native-helper'; @@ -192,6 +193,12 @@ export function GC(); */ export function releaseNativeObject(object: any /*java.lang.Object | NSObject*/); +/** + * Queues the passed function to be ran in a macroTask + * @param task the function to execute as a macroTask + */ +export function queueMacrotask(task: () => void): void; + /** * Checks if the current thread is the main thread. Directly calls the passed function * if it is, or dispatches it to the main thread otherwise. diff --git a/packages/core/utils/macrotask-scheduler.d.ts b/packages/core/utils/macrotask-scheduler.d.ts new file mode 100644 index 000000000..5e1cb3fb5 --- /dev/null +++ b/packages/core/utils/macrotask-scheduler.d.ts @@ -0,0 +1,5 @@ +/** + * Queues the passed function to be ran in a macroTask + * @param task the function to execute as a macroTask + */ +export function queueMacrotask(task: () => void): void; diff --git a/packages/core/utils/macrotask-scheduler.ts b/packages/core/utils/macrotask-scheduler.ts new file mode 100644 index 000000000..dfd477e9d --- /dev/null +++ b/packages/core/utils/macrotask-scheduler.ts @@ -0,0 +1,27 @@ +import { Trace } from 'trace'; +import { dispatchToMainThread } from './mainthread-helper'; + +let scheduled = false; +let macroTaskQueue: Array<() => void> = []; + +function drainMacrotaskQueue() { + const currentQueue = macroTaskQueue; + macroTaskQueue = []; + scheduled = false; + currentQueue.forEach((task) => { + try { + task(); + } catch (err) { + const msg = err ? err.stack || err : err; + Trace.write(`Error in macroTask: ${msg}`, Trace.categories.Error, Trace.messageType.error); + } + }); +} + +export function queueMacrotask(task: () => void): void { + macroTaskQueue.push(task); + if (!scheduled) { + scheduled = true; + dispatchToMainThread(drainMacrotaskQueue); + } +} diff --git a/packages/core/utils/utils-common.ts b/packages/core/utils/utils-common.ts index c5d20ff96..b453d19c9 100644 --- a/packages/core/utils/utils-common.ts +++ b/packages/core/utils/utils-common.ts @@ -5,6 +5,7 @@ import * as layout from './layout-helper'; export { layout }; export * from './mainthread-helper'; +export * from './macrotask-scheduler'; export const RESOURCE_PREFIX = 'res://'; export const FILE_PREFIX = 'file:///';