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 * as m from 'mithril'; 16 17import {assertTrue} from '../base/logging'; 18 19import { 20 debugNow, 21 measure, 22 perfDebug, 23 perfDisplay, 24 RunningStatistics, 25 runningStatStr 26} from './perf'; 27 28function statTableHeader() { 29 return m( 30 'tr', 31 m('th', ''), 32 m('th', 'Last (ms)'), 33 m('th', 'Avg (ms)'), 34 m('th', 'Avg-10 (ms)'), ); 35} 36 37function statTableRow(title: string, stat: RunningStatistics) { 38 return m( 39 'tr', 40 m('td', title), 41 m('td', stat.last.toFixed(2)), 42 m('td', stat.mean.toFixed(2)), 43 m('td', stat.bufferMean.toFixed(2)), ); 44} 45 46export type ActionCallback = (nowMs: number) => void; 47export type RedrawCallback = (nowMs: number) => void; 48 49// This class orchestrates all RAFs in the UI. It ensures that there is only 50// one animation frame handler overall and that callbacks are called in 51// predictable order. There are two types of callbacks here: 52// - actions (e.g. pan/zoon animations), which will alter the "fast" 53// (main-thread-only) state (e.g. update visible time bounds @ 60 fps). 54// - redraw callbacks that will repaint canvases. 55// This class guarantees that, on each frame, redraw callbacks are called after 56// all action callbacks. 57export class RafScheduler { 58 private actionCallbacks = new Set<ActionCallback>(); 59 private canvasRedrawCallbacks = new Set<RedrawCallback>(); 60 private _syncDomRedraw: RedrawCallback = _ => {}; 61 private hasScheduledNextFrame = false; 62 private requestedFullRedraw = false; 63 private isRedrawing = false; 64 private _shutdown = false; 65 66 private perfStats = { 67 rafActions: new RunningStatistics(), 68 rafCanvas: new RunningStatistics(), 69 rafDom: new RunningStatistics(), 70 rafTotal: new RunningStatistics(), 71 domRedraw: new RunningStatistics(), 72 }; 73 74 start(cb: ActionCallback) { 75 this.actionCallbacks.add(cb); 76 this.maybeScheduleAnimationFrame(); 77 } 78 79 stop(cb: ActionCallback) { 80 this.actionCallbacks.delete(cb); 81 } 82 83 addRedrawCallback(cb: RedrawCallback) { 84 this.canvasRedrawCallbacks.add(cb); 85 } 86 87 removeRedrawCallback(cb: RedrawCallback) { 88 this.canvasRedrawCallbacks.delete(cb); 89 } 90 91 scheduleRedraw() { 92 this.maybeScheduleAnimationFrame(true); 93 } 94 95 shutdown() { 96 this._shutdown = true; 97 } 98 99 set domRedraw(cb: RedrawCallback|null) { 100 this._syncDomRedraw = cb || (_ => {}); 101 } 102 103 scheduleFullRedraw() { 104 this.requestedFullRedraw = true; 105 this.maybeScheduleAnimationFrame(true); 106 } 107 108 syncDomRedraw(nowMs: number) { 109 const redrawStart = debugNow(); 110 this._syncDomRedraw(nowMs); 111 if (perfDebug()) { 112 this.perfStats.domRedraw.addValue(debugNow() - redrawStart); 113 } 114 } 115 116 private syncCanvasRedraw(nowMs: number) { 117 const redrawStart = debugNow(); 118 if (this.isRedrawing) return; 119 this.isRedrawing = true; 120 for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs); 121 this.isRedrawing = false; 122 if (perfDebug()) { 123 this.perfStats.rafCanvas.addValue(debugNow() - redrawStart); 124 } 125 } 126 127 private maybeScheduleAnimationFrame(force = false) { 128 if (this.hasScheduledNextFrame) return; 129 if (this.actionCallbacks.size !== 0 || force) { 130 this.hasScheduledNextFrame = true; 131 window.requestAnimationFrame(this.onAnimationFrame.bind(this)); 132 } 133 } 134 135 private onAnimationFrame(nowMs: number) { 136 if (this._shutdown) return; 137 const rafStart = debugNow(); 138 this.hasScheduledNextFrame = false; 139 140 const doFullRedraw = this.requestedFullRedraw; 141 this.requestedFullRedraw = false; 142 143 const actionTime = measure(() => { 144 for (const action of this.actionCallbacks) action(nowMs); 145 }); 146 147 const domTime = measure(() => { 148 if (doFullRedraw) this.syncDomRedraw(nowMs); 149 }); 150 const canvasTime = measure(() => this.syncCanvasRedraw(nowMs)); 151 152 const totalRafTime = debugNow() - rafStart; 153 this.updatePerfStats(actionTime, domTime, canvasTime, totalRafTime); 154 perfDisplay.renderPerfStats(); 155 156 this.maybeScheduleAnimationFrame(); 157 } 158 159 private updatePerfStats( 160 actionsTime: number, domTime: number, canvasTime: number, 161 totalRafTime: number) { 162 if (!perfDebug()) return; 163 this.perfStats.rafActions.addValue(actionsTime); 164 this.perfStats.rafDom.addValue(domTime); 165 this.perfStats.rafCanvas.addValue(canvasTime); 166 this.perfStats.rafTotal.addValue(totalRafTime); 167 } 168 169 renderPerfStats() { 170 assertTrue(perfDebug()); 171 return m( 172 'div', 173 m('div', 174 [ 175 m('button', 176 {onclick: () => this.scheduleRedraw()}, 177 'Do Canvas Redraw'), 178 ' | ', 179 m('button', 180 {onclick: () => this.scheduleFullRedraw()}, 181 'Do Full Redraw'), 182 ]), 183 m('div', 184 'Raf Timing ' + 185 '(Total may not add up due to imprecision)'), 186 m('table', 187 statTableHeader(), 188 statTableRow('Actions', this.perfStats.rafActions), 189 statTableRow('Dom', this.perfStats.rafDom), 190 statTableRow('Canvas', this.perfStats.rafCanvas), 191 statTableRow('Total', this.perfStats.rafTotal), ), 192 m('div', 193 'Dom redraw: ' + 194 `Count: ${this.perfStats.domRedraw.count} | ` + 195 runningStatStr(this.perfStats.domRedraw)), ); 196 } 197} 198