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