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