| // Copyright (C) 2018 The Android Open Source Project |
| // |
| // Licensed 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. |
| |
| import {PerfStats} from './perf_stats'; |
| import m from 'mithril'; |
| import {featureFlags} from './feature_flags'; |
| |
| export type AnimationCallback = (lastFrameMs: number) => void; |
| export type RedrawCallback = () => void; |
| |
| export const AUTOREDRAW_FLAG = featureFlags.register({ |
| id: 'mithrilAutoredraw', |
| name: 'Enable Mithril autoredraw', |
| description: 'Turns calls to schedulefullRedraw() a no-op', |
| defaultValue: false, |
| }); |
| |
| // This class orchestrates all RAFs in the UI. It ensures that there is only |
| // one animation frame handler overall and that callbacks are called in |
| // predictable order. There are two types of callbacks here: |
| // - actions (e.g. pan/zoon animations), which will alter the "fast" |
| // (main-thread-only) state (e.g. update visible time bounds @ 60 fps). |
| // - redraw callbacks that will repaint canvases. |
| // This class guarantees that, on each frame, redraw callbacks are called after |
| // all action callbacks. |
| export class RafScheduler { |
| // These happen at the beginning of any animation frame. Used by Animation. |
| private animationCallbacks = new Set<AnimationCallback>(); |
| |
| // These happen during any animaton frame, after the (optional) DOM redraw. |
| private canvasRedrawCallbacks = new Set<RedrawCallback>(); |
| |
| // These happen at the end of full (DOM) animation frames. |
| private postRedrawCallbacks = new Array<RedrawCallback>(); |
| private hasScheduledNextFrame = false; |
| private requestedFullRedraw = false; |
| private isRedrawing = false; |
| private _shutdown = false; |
| private recordPerfStats = false; |
| private mounts = new Map<Element, m.ComponentTypes>(); |
| |
| readonly perfStats = { |
| rafActions: new PerfStats(), |
| rafCanvas: new PerfStats(), |
| rafDom: new PerfStats(), |
| rafTotal: new PerfStats(), |
| domRedraw: new PerfStats(), |
| }; |
| |
| constructor() { |
| // Patch m.redraw() to our RAF full redraw. |
| const origSync = m.redraw.sync; |
| const redrawFn = () => this.scheduleFullRedraw('force'); |
| redrawFn.sync = origSync; |
| m.redraw = redrawFn; |
| |
| m.mount = this.mount.bind(this); |
| } |
| |
| // Schedule re-rendering of virtual DOM and canvas. |
| // If a callback is passed it will be executed after the DOM redraw has |
| // completed. |
| scheduleFullRedraw(force?: 'force', cb?: RedrawCallback) { |
| // If we are using autoredraw mode, make this function a no-op unless |
| // 'force' is passed. |
| if (AUTOREDRAW_FLAG.get() && force !== 'force') return; |
| this.requestedFullRedraw = true; |
| cb && this.postRedrawCallbacks.push(cb); |
| this.maybeScheduleAnimationFrame(true); |
| } |
| |
| // Schedule re-rendering of canvas only. |
| scheduleCanvasRedraw() { |
| this.maybeScheduleAnimationFrame(true); |
| } |
| |
| startAnimation(cb: AnimationCallback) { |
| this.animationCallbacks.add(cb); |
| this.maybeScheduleAnimationFrame(); |
| } |
| |
| stopAnimation(cb: AnimationCallback) { |
| this.animationCallbacks.delete(cb); |
| } |
| |
| addCanvasRedrawCallback(cb: RedrawCallback): Disposable { |
| this.canvasRedrawCallbacks.add(cb); |
| const canvasRedrawCallbacks = this.canvasRedrawCallbacks; |
| return { |
| [Symbol.dispose]() { |
| canvasRedrawCallbacks.delete(cb); |
| }, |
| }; |
| } |
| |
| mount(element: Element, component: m.ComponentTypes | null): void { |
| const mounts = this.mounts; |
| if (component === null) { |
| mounts.delete(element); |
| } else { |
| mounts.set(element, component); |
| } |
| this.syncDomRedrawMountEntry(element, component); |
| } |
| |
| shutdown() { |
| this._shutdown = true; |
| } |
| |
| setPerfStatsEnabled(enabled: boolean) { |
| this.recordPerfStats = enabled; |
| this.scheduleFullRedraw(); |
| } |
| |
| get hasPendingRedraws(): boolean { |
| return this.isRedrawing || this.hasScheduledNextFrame; |
| } |
| |
| private syncDomRedraw() { |
| const redrawStart = performance.now(); |
| |
| for (const [element, component] of this.mounts.entries()) { |
| this.syncDomRedrawMountEntry(element, component); |
| } |
| |
| if (this.recordPerfStats) { |
| this.perfStats.domRedraw.addValue(performance.now() - redrawStart); |
| } |
| } |
| |
| private syncDomRedrawMountEntry( |
| element: Element, |
| component: m.ComponentTypes | null, |
| ) { |
| // Mithril's render() function takes a third argument which tells us if a |
| // further redraw is needed (e.g. due to managed event handler). This allows |
| // us to implement auto-redraw. The redraw argument is documented in the |
| // official Mithril docs but is just not part of the @types/mithril package. |
| const mithrilRender = m.render as ( |
| el: Element, |
| vnodes: m.Children, |
| redraw?: () => void, |
| ) => void; |
| |
| mithrilRender( |
| element, |
| component !== null ? m(component) : null, |
| AUTOREDRAW_FLAG.get() ? () => raf.scheduleFullRedraw('force') : undefined, |
| ); |
| } |
| |
| private syncCanvasRedraw() { |
| const redrawStart = performance.now(); |
| if (this.isRedrawing) return; |
| this.isRedrawing = true; |
| this.canvasRedrawCallbacks.forEach((cb) => cb()); |
| this.isRedrawing = false; |
| if (this.recordPerfStats) { |
| this.perfStats.rafCanvas.addValue(performance.now() - redrawStart); |
| } |
| } |
| |
| private maybeScheduleAnimationFrame(force = false) { |
| if (this.hasScheduledNextFrame) return; |
| if (this.animationCallbacks.size !== 0 || force) { |
| this.hasScheduledNextFrame = true; |
| window.requestAnimationFrame(this.onAnimationFrame.bind(this)); |
| } |
| } |
| |
| private onAnimationFrame(lastFrameMs: number) { |
| if (this._shutdown) return; |
| this.hasScheduledNextFrame = false; |
| const doFullRedraw = this.requestedFullRedraw; |
| this.requestedFullRedraw = false; |
| |
| const tStart = performance.now(); |
| this.animationCallbacks.forEach((cb) => cb(lastFrameMs)); |
| const tAnim = performance.now(); |
| doFullRedraw && this.syncDomRedraw(); |
| const tDom = performance.now(); |
| this.syncCanvasRedraw(); |
| const tCanvas = performance.now(); |
| |
| const animTime = tAnim - tStart; |
| const domTime = tDom - tAnim; |
| const canvasTime = tCanvas - tDom; |
| const totalTime = tCanvas - tStart; |
| this.updatePerfStats(animTime, domTime, canvasTime, totalTime); |
| this.maybeScheduleAnimationFrame(); |
| |
| if (doFullRedraw && this.postRedrawCallbacks.length > 0) { |
| const pendingCbs = this.postRedrawCallbacks.splice(0); // splice = clear. |
| pendingCbs.forEach((cb) => cb()); |
| } |
| } |
| |
| private updatePerfStats( |
| actionsTime: number, |
| domTime: number, |
| canvasTime: number, |
| totalRafTime: number, |
| ) { |
| if (!this.recordPerfStats) return; |
| this.perfStats.rafActions.addValue(actionsTime); |
| this.perfStats.rafDom.addValue(domTime); |
| this.perfStats.rafCanvas.addValue(canvasTime); |
| this.perfStats.rafTotal.addValue(totalRafTime); |
| } |
| } |
| |
| export const raf = new RafScheduler(); |