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 {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 scheduleRedraw() { 94 this.maybeScheduleAnimationFrame(true); 95 } 96 97 shutdown() { 98 this._shutdown = true; 99 } 100 101 set domRedraw(cb: RedrawCallback|null) { 102 this._syncDomRedraw = cb || (_ => {}); 103 } 104 105 scheduleFullRedraw() { 106 this.requestedFullRedraw = true; 107 this.maybeScheduleAnimationFrame(true); 108 } 109 110 syncDomRedraw(nowMs: number) { 111 const redrawStart = debugNow(); 112 this._syncDomRedraw(nowMs); 113 if (perfDebug()) { 114 this.perfStats.domRedraw.addValue(debugNow() - redrawStart); 115 } 116 } 117 118 private syncCanvasRedraw(nowMs: number) { 119 const redrawStart = debugNow(); 120 if (this.isRedrawing) return; 121 globals.frontendLocalState.clearVisibleTracks(); 122 this.isRedrawing = true; 123 for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs); 124 this.isRedrawing = false; 125 globals.frontendLocalState.sendVisibleTracks(); 126 if (perfDebug()) { 127 this.perfStats.rafCanvas.addValue(debugNow() - redrawStart); 128 } 129 } 130 131 private maybeScheduleAnimationFrame(force = false) { 132 if (this.hasScheduledNextFrame) return; 133 if (this.actionCallbacks.size !== 0 || force) { 134 this.hasScheduledNextFrame = true; 135 window.requestAnimationFrame(this.onAnimationFrame.bind(this)); 136 } 137 } 138 139 private onAnimationFrame(nowMs: number) { 140 if (this._shutdown) return; 141 const rafStart = debugNow(); 142 this.hasScheduledNextFrame = false; 143 144 const doFullRedraw = this.requestedFullRedraw; 145 this.requestedFullRedraw = false; 146 147 const actionTime = measure(() => { 148 for (const action of this.actionCallbacks) action(nowMs); 149 }); 150 151 const domTime = measure(() => { 152 if (doFullRedraw) this.syncDomRedraw(nowMs); 153 }); 154 const canvasTime = measure(() => this.syncCanvasRedraw(nowMs)); 155 156 const totalRafTime = debugNow() - rafStart; 157 this.updatePerfStats(actionTime, domTime, canvasTime, totalRafTime); 158 perfDisplay.renderPerfStats(); 159 160 this.maybeScheduleAnimationFrame(); 161 } 162 163 private updatePerfStats( 164 actionsTime: number, domTime: number, canvasTime: number, 165 totalRafTime: number) { 166 if (!perfDebug()) return; 167 this.perfStats.rafActions.addValue(actionsTime); 168 this.perfStats.rafDom.addValue(domTime); 169 this.perfStats.rafCanvas.addValue(canvasTime); 170 this.perfStats.rafTotal.addValue(totalRafTime); 171 } 172 173 renderPerfStats() { 174 assertTrue(perfDebug()); 175 return m( 176 'div', 177 m('div', 178 [ 179 m('button', 180 {onclick: () => this.scheduleRedraw()}, 181 'Do Canvas Redraw'), 182 ' | ', 183 m('button', 184 {onclick: () => this.scheduleFullRedraw()}, 185 'Do Full Redraw'), 186 ]), 187 m('div', 188 'Raf Timing ' + 189 '(Total may not add up due to imprecision)'), 190 m('table', 191 statTableHeader(), 192 statTableRow('Actions', this.perfStats.rafActions), 193 statTableRow('Dom', this.perfStats.rafDom), 194 statTableRow('Canvas', this.perfStats.rafCanvas), 195 statTableRow('Total', this.perfStats.rafTotal), ), 196 m('div', 197 'Dom redraw: ' + 198 `Count: ${this.perfStats.domRedraw.count} | ` + 199 runningStatStr(this.perfStats.domRedraw)), ); 200 } 201} 202