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 { 18 debugNow, 19 measure, 20 perfDebug, 21 perfDisplay, 22 PerfStatsSource, 23 RunningStatistics, 24 runningStatStr, 25} from './perf'; 26 27function statTableHeader() { 28 return m( 29 'tr', 30 m('th', ''), 31 m('th', 'Last (ms)'), 32 m('th', 'Avg (ms)'), 33 m('th', 'Avg-10 (ms)'), 34 ); 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} 46 47export type ActionCallback = (nowMs: number) => void; 48export type RedrawCallback = (nowMs: number) => void; 49 50// This class orchestrates all RAFs in the UI. It ensures that there is only 51// one animation frame handler overall and that callbacks are called in 52// predictable order. There are two types of callbacks here: 53// - actions (e.g. pan/zoon animations), which will alter the "fast" 54// (main-thread-only) state (e.g. update visible time bounds @ 60 fps). 55// - redraw callbacks that will repaint canvases. 56// This class guarantees that, on each frame, redraw callbacks are called after 57// all action callbacks. 58export class RafScheduler implements PerfStatsSource { 59 private actionCallbacks = new Set<ActionCallback>(); 60 private canvasRedrawCallbacks = new Set<RedrawCallback>(); 61 private _syncDomRedraw: RedrawCallback = (_) => {}; 62 private hasScheduledNextFrame = false; 63 private requestedFullRedraw = false; 64 private isRedrawing = false; 65 private _shutdown = false; 66 private _beforeRedraw: () => void = () => {}; 67 private _afterRedraw: () => void = () => {}; 68 private _pendingCallbacks: RedrawCallback[] = []; 69 70 private perfStats = { 71 rafActions: new RunningStatistics(), 72 rafCanvas: new RunningStatistics(), 73 rafDom: new RunningStatistics(), 74 rafTotal: new RunningStatistics(), 75 domRedraw: new RunningStatistics(), 76 }; 77 78 start(cb: ActionCallback) { 79 this.actionCallbacks.add(cb); 80 this.maybeScheduleAnimationFrame(); 81 } 82 83 stop(cb: ActionCallback) { 84 this.actionCallbacks.delete(cb); 85 } 86 87 addRedrawCallback(cb: RedrawCallback) { 88 this.canvasRedrawCallbacks.add(cb); 89 } 90 91 removeRedrawCallback(cb: RedrawCallback) { 92 this.canvasRedrawCallbacks.delete(cb); 93 } 94 95 addPendingCallback(cb: RedrawCallback) { 96 this._pendingCallbacks.push(cb); 97 } 98 99 // Schedule re-rendering of canvas only. 100 scheduleRedraw() { 101 this.maybeScheduleAnimationFrame(true); 102 } 103 104 shutdown() { 105 this._shutdown = true; 106 } 107 108 set domRedraw(cb: RedrawCallback) { 109 this._syncDomRedraw = cb; 110 } 111 112 set beforeRedraw(cb: () => void) { 113 this._beforeRedraw = cb; 114 } 115 116 set afterRedraw(cb: () => void) { 117 this._afterRedraw = cb; 118 } 119 120 // Schedule re-rendering of virtual DOM and canvas. 121 scheduleFullRedraw() { 122 this.requestedFullRedraw = true; 123 this.maybeScheduleAnimationFrame(true); 124 } 125 126 // Schedule a full redraw to happen after a short delay (50 ms). 127 // This is done to prevent flickering / visual noise and allow the UI to fetch 128 // the initial data from the Trace Processor. 129 // There is a chance that someone else schedules a full redraw in the 130 // meantime, forcing the flicker, but in practice it works quite well and 131 // avoids a lot of complexity for the callers. 132 scheduleDelayedFullRedraw() { 133 // 50ms is half of the responsiveness threshold (100ms): 134 // https://web.dev/rail/#response-process-events-in-under-50ms 135 const delayMs = 50; 136 setTimeout(() => this.scheduleFullRedraw(), delayMs); 137 } 138 139 syncDomRedraw(nowMs: number) { 140 const redrawStart = debugNow(); 141 this._syncDomRedraw(nowMs); 142 if (perfDebug()) { 143 this.perfStats.domRedraw.addValue(debugNow() - redrawStart); 144 } 145 } 146 147 get hasPendingRedraws(): boolean { 148 return this.isRedrawing || this.hasScheduledNextFrame; 149 } 150 151 private syncCanvasRedraw(nowMs: number) { 152 const redrawStart = debugNow(); 153 if (this.isRedrawing) return; 154 this._beforeRedraw(); 155 this.isRedrawing = true; 156 for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs); 157 this.isRedrawing = false; 158 this._afterRedraw(); 159 for (const cb of this._pendingCallbacks) { 160 cb(nowMs); 161 } 162 this._pendingCallbacks.splice(0, this._pendingCallbacks.length); 163 if (perfDebug()) { 164 this.perfStats.rafCanvas.addValue(debugNow() - redrawStart); 165 } 166 } 167 168 private maybeScheduleAnimationFrame(force = false) { 169 if (this.hasScheduledNextFrame) return; 170 if (this.actionCallbacks.size !== 0 || force) { 171 this.hasScheduledNextFrame = true; 172 window.requestAnimationFrame(this.onAnimationFrame.bind(this)); 173 } 174 } 175 176 private onAnimationFrame(nowMs: number) { 177 if (this._shutdown) return; 178 const rafStart = debugNow(); 179 this.hasScheduledNextFrame = false; 180 181 const doFullRedraw = this.requestedFullRedraw; 182 this.requestedFullRedraw = false; 183 184 const actionTime = measure(() => { 185 for (const action of this.actionCallbacks) action(nowMs); 186 }); 187 188 const domTime = measure(() => { 189 if (doFullRedraw) this.syncDomRedraw(nowMs); 190 }); 191 const canvasTime = measure(() => this.syncCanvasRedraw(nowMs)); 192 193 const totalRafTime = debugNow() - rafStart; 194 this.updatePerfStats(actionTime, domTime, canvasTime, totalRafTime); 195 perfDisplay.renderPerfStats(this); 196 197 this.maybeScheduleAnimationFrame(); 198 } 199 200 private updatePerfStats( 201 actionsTime: number, 202 domTime: number, 203 canvasTime: number, 204 totalRafTime: number, 205 ) { 206 if (!perfDebug()) return; 207 this.perfStats.rafActions.addValue(actionsTime); 208 this.perfStats.rafDom.addValue(domTime); 209 this.perfStats.rafCanvas.addValue(canvasTime); 210 this.perfStats.rafTotal.addValue(totalRafTime); 211 } 212 213 renderPerfStats() { 214 return m( 215 'div', 216 m('div', [ 217 m('button', {onclick: () => this.scheduleRedraw()}, 'Do Canvas Redraw'), 218 ' | ', 219 m( 220 'button', 221 {onclick: () => this.scheduleFullRedraw()}, 222 'Do Full Redraw', 223 ), 224 ]), 225 m('div', 'Raf Timing ' + '(Total may not add up due to imprecision)'), 226 m( 227 'table', 228 statTableHeader(), 229 statTableRow('Actions', this.perfStats.rafActions), 230 statTableRow('Dom', this.perfStats.rafDom), 231 statTableRow('Canvas', this.perfStats.rafCanvas), 232 statTableRow('Total', this.perfStats.rafTotal), 233 ), 234 m( 235 'div', 236 'Dom redraw: ' + 237 `Count: ${this.perfStats.domRedraw.count} | ` + 238 runningStatStr(this.perfStats.domRedraw), 239 ), 240 ); 241 } 242} 243 244export const raf = new RafScheduler(); 245