1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {PerfStats} from './perf_stats'; 16import m from 'mithril'; 17import {featureFlags} from './feature_flags'; 18 19export type AnimationCallback = (lastFrameMs: number) => void; 20export type RedrawCallback = () => void; 21 22export const AUTOREDRAW_FLAG = featureFlags.register({ 23 id: 'mithrilAutoredraw', 24 name: 'Enable Mithril autoredraw', 25 description: 'Turns calls to schedulefullRedraw() a no-op', 26 defaultValue: false, 27}); 28 29// This class orchestrates all RAFs in the UI. It ensures that there is only 30// one animation frame handler overall and that callbacks are called in 31// predictable order. There are two types of callbacks here: 32// - actions (e.g. pan/zoon animations), which will alter the "fast" 33// (main-thread-only) state (e.g. update visible time bounds @ 60 fps). 34// - redraw callbacks that will repaint canvases. 35// This class guarantees that, on each frame, redraw callbacks are called after 36// all action callbacks. 37export class RafScheduler { 38 // These happen at the beginning of any animation frame. Used by Animation. 39 private animationCallbacks = new Set<AnimationCallback>(); 40 41 // These happen during any animaton frame, after the (optional) DOM redraw. 42 private canvasRedrawCallbacks = new Set<RedrawCallback>(); 43 44 // These happen at the end of full (DOM) animation frames. 45 private postRedrawCallbacks = new Array<RedrawCallback>(); 46 private hasScheduledNextFrame = false; 47 private requestedFullRedraw = false; 48 private isRedrawing = false; 49 private _shutdown = false; 50 private recordPerfStats = false; 51 private mounts = new Map<Element, m.ComponentTypes>(); 52 53 readonly perfStats = { 54 rafActions: new PerfStats(), 55 rafCanvas: new PerfStats(), 56 rafDom: new PerfStats(), 57 rafTotal: new PerfStats(), 58 domRedraw: new PerfStats(), 59 }; 60 61 constructor() { 62 // Patch m.redraw() to our RAF full redraw. 63 const origSync = m.redraw.sync; 64 const redrawFn = () => this.scheduleFullRedraw('force'); 65 redrawFn.sync = origSync; 66 m.redraw = redrawFn; 67 68 m.mount = this.mount.bind(this); 69 } 70 71 // Schedule re-rendering of virtual DOM and canvas. 72 // If a callback is passed it will be executed after the DOM redraw has 73 // completed. 74 scheduleFullRedraw(force?: 'force', cb?: RedrawCallback) { 75 // If we are using autoredraw mode, make this function a no-op unless 76 // 'force' is passed. 77 if (AUTOREDRAW_FLAG.get() && force !== 'force') return; 78 this.requestedFullRedraw = true; 79 cb && this.postRedrawCallbacks.push(cb); 80 this.maybeScheduleAnimationFrame(true); 81 } 82 83 // Schedule re-rendering of canvas only. 84 scheduleCanvasRedraw() { 85 this.maybeScheduleAnimationFrame(true); 86 } 87 88 startAnimation(cb: AnimationCallback) { 89 this.animationCallbacks.add(cb); 90 this.maybeScheduleAnimationFrame(); 91 } 92 93 stopAnimation(cb: AnimationCallback) { 94 this.animationCallbacks.delete(cb); 95 } 96 97 addCanvasRedrawCallback(cb: RedrawCallback): Disposable { 98 this.canvasRedrawCallbacks.add(cb); 99 const canvasRedrawCallbacks = this.canvasRedrawCallbacks; 100 return { 101 [Symbol.dispose]() { 102 canvasRedrawCallbacks.delete(cb); 103 }, 104 }; 105 } 106 107 mount(element: Element, component: m.ComponentTypes | null): void { 108 const mounts = this.mounts; 109 if (component === null) { 110 mounts.delete(element); 111 } else { 112 mounts.set(element, component); 113 } 114 this.syncDomRedrawMountEntry(element, component); 115 } 116 117 shutdown() { 118 this._shutdown = true; 119 } 120 121 setPerfStatsEnabled(enabled: boolean) { 122 this.recordPerfStats = enabled; 123 this.scheduleFullRedraw(); 124 } 125 126 get hasPendingRedraws(): boolean { 127 return this.isRedrawing || this.hasScheduledNextFrame; 128 } 129 130 private syncDomRedraw() { 131 const redrawStart = performance.now(); 132 133 for (const [element, component] of this.mounts.entries()) { 134 this.syncDomRedrawMountEntry(element, component); 135 } 136 137 if (this.recordPerfStats) { 138 this.perfStats.domRedraw.addValue(performance.now() - redrawStart); 139 } 140 } 141 142 private syncDomRedrawMountEntry( 143 element: Element, 144 component: m.ComponentTypes | null, 145 ) { 146 // Mithril's render() function takes a third argument which tells us if a 147 // further redraw is needed (e.g. due to managed event handler). This allows 148 // us to implement auto-redraw. The redraw argument is documented in the 149 // official Mithril docs but is just not part of the @types/mithril package. 150 const mithrilRender = m.render as ( 151 el: Element, 152 vnodes: m.Children, 153 redraw?: () => void, 154 ) => void; 155 156 mithrilRender( 157 element, 158 component !== null ? m(component) : null, 159 AUTOREDRAW_FLAG.get() ? () => raf.scheduleFullRedraw('force') : undefined, 160 ); 161 } 162 163 private syncCanvasRedraw() { 164 const redrawStart = performance.now(); 165 if (this.isRedrawing) return; 166 this.isRedrawing = true; 167 this.canvasRedrawCallbacks.forEach((cb) => cb()); 168 this.isRedrawing = false; 169 if (this.recordPerfStats) { 170 this.perfStats.rafCanvas.addValue(performance.now() - redrawStart); 171 } 172 } 173 174 private maybeScheduleAnimationFrame(force = false) { 175 if (this.hasScheduledNextFrame) return; 176 if (this.animationCallbacks.size !== 0 || force) { 177 this.hasScheduledNextFrame = true; 178 window.requestAnimationFrame(this.onAnimationFrame.bind(this)); 179 } 180 } 181 182 private onAnimationFrame(lastFrameMs: number) { 183 if (this._shutdown) return; 184 this.hasScheduledNextFrame = false; 185 const doFullRedraw = this.requestedFullRedraw; 186 this.requestedFullRedraw = false; 187 188 const tStart = performance.now(); 189 this.animationCallbacks.forEach((cb) => cb(lastFrameMs)); 190 const tAnim = performance.now(); 191 doFullRedraw && this.syncDomRedraw(); 192 const tDom = performance.now(); 193 this.syncCanvasRedraw(); 194 const tCanvas = performance.now(); 195 196 const animTime = tAnim - tStart; 197 const domTime = tDom - tAnim; 198 const canvasTime = tCanvas - tDom; 199 const totalTime = tCanvas - tStart; 200 this.updatePerfStats(animTime, domTime, canvasTime, totalTime); 201 this.maybeScheduleAnimationFrame(); 202 203 if (doFullRedraw && this.postRedrawCallbacks.length > 0) { 204 const pendingCbs = this.postRedrawCallbacks.splice(0); // splice = clear. 205 pendingCbs.forEach((cb) => cb()); 206 } 207 } 208 209 private updatePerfStats( 210 actionsTime: number, 211 domTime: number, 212 canvasTime: number, 213 totalRafTime: number, 214 ) { 215 if (!this.recordPerfStats) return; 216 this.perfStats.rafActions.addValue(actionsTime); 217 this.perfStats.rafDom.addValue(domTime); 218 this.perfStats.rafCanvas.addValue(canvasTime); 219 this.perfStats.rafTotal.addValue(totalRafTime); 220 } 221} 222 223export const raf = new RafScheduler(); 224