Deepanjan Roy | 9d95a25 | 2018-08-09 10:10:19 -0400 | [diff] [blame] | 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 | |
Deepanjan Roy | 9a906ed | 2018-09-20 11:06:00 -0400 | [diff] [blame] | 15 | import * as m from 'mithril'; |
| 16 | |
| 17 | import {assertTrue} from '../base/logging'; |
| 18 | |
| 19 | import { |
| 20 | debugNow, |
| 21 | measure, |
| 22 | perfDebug, |
| 23 | perfDisplay, |
| 24 | RunningStatistics, |
| 25 | runningStatStr |
| 26 | } from './perf'; |
| 27 | |
| 28 | function 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 | |
| 37 | function 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 | |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 46 | export type ActionCallback = (nowMs: number) => void; |
| 47 | export type RedrawCallback = (nowMs: number) => void; |
Deepanjan Roy | 9d95a25 | 2018-08-09 10:10:19 -0400 | [diff] [blame] | 48 | |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 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. |
| 57 | export class RafScheduler { |
| 58 | private actionCallbacks = new Set<ActionCallback>(); |
Deepanjan Roy | f190cb2 | 2018-08-28 10:43:07 -0400 | [diff] [blame] | 59 | private canvasRedrawCallbacks = new Set<RedrawCallback>(); |
| 60 | private _syncDomRedraw: RedrawCallback = _ => {}; |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 61 | private hasScheduledNextFrame = false; |
Deepanjan Roy | f190cb2 | 2018-08-28 10:43:07 -0400 | [diff] [blame] | 62 | private requestedFullRedraw = false; |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 63 | private isRedrawing = false; |
| 64 | |
Deepanjan Roy | 9a906ed | 2018-09-20 11:06:00 -0400 | [diff] [blame] | 65 | private perfStats = { |
| 66 | rafActions: new RunningStatistics(), |
| 67 | rafCanvas: new RunningStatistics(), |
| 68 | rafDom: new RunningStatistics(), |
| 69 | rafTotal: new RunningStatistics(), |
| 70 | domRedraw: new RunningStatistics(), |
| 71 | }; |
| 72 | |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 73 | start(cb: ActionCallback) { |
| 74 | this.actionCallbacks.add(cb); |
| 75 | this.maybeScheduleAnimationFrame(); |
Deepanjan Roy | 9d95a25 | 2018-08-09 10:10:19 -0400 | [diff] [blame] | 76 | } |
| 77 | |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 78 | stop(cb: ActionCallback) { |
| 79 | this.actionCallbacks.delete(cb); |
| 80 | } |
| 81 | |
| 82 | addRedrawCallback(cb: RedrawCallback) { |
Deepanjan Roy | f190cb2 | 2018-08-28 10:43:07 -0400 | [diff] [blame] | 83 | this.canvasRedrawCallbacks.add(cb); |
Deepanjan Roy | 9d95a25 | 2018-08-09 10:10:19 -0400 | [diff] [blame] | 84 | } |
| 85 | |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 86 | removeRedrawCallback(cb: RedrawCallback) { |
Deepanjan Roy | f190cb2 | 2018-08-28 10:43:07 -0400 | [diff] [blame] | 87 | this.canvasRedrawCallbacks.delete(cb); |
Deepanjan Roy | 9d95a25 | 2018-08-09 10:10:19 -0400 | [diff] [blame] | 88 | } |
| 89 | |
Deepanjan Roy | f190cb2 | 2018-08-28 10:43:07 -0400 | [diff] [blame] | 90 | scheduleRedraw() { |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 91 | this.maybeScheduleAnimationFrame(true); |
Deepanjan Roy | 9d95a25 | 2018-08-09 10:10:19 -0400 | [diff] [blame] | 92 | } |
| 93 | |
Deepanjan Roy | f190cb2 | 2018-08-28 10:43:07 -0400 | [diff] [blame] | 94 | set domRedraw(cb: RedrawCallback|null) { |
| 95 | this._syncDomRedraw = cb || (_ => {}); |
| 96 | } |
| 97 | |
| 98 | scheduleFullRedraw() { |
| 99 | this.requestedFullRedraw = true; |
| 100 | this.maybeScheduleAnimationFrame(true); |
| 101 | } |
| 102 | |
Deepanjan Roy | 9a906ed | 2018-09-20 11:06:00 -0400 | [diff] [blame] | 103 | syncDomRedraw(nowMs: number) { |
| 104 | const redrawStart = debugNow(); |
| 105 | this._syncDomRedraw(nowMs); |
| 106 | if (perfDebug()) { |
| 107 | this.perfStats.domRedraw.addValue(debugNow() - redrawStart); |
| 108 | } |
| 109 | } |
| 110 | |
Deepanjan Roy | f190cb2 | 2018-08-28 10:43:07 -0400 | [diff] [blame] | 111 | private syncCanvasRedraw(nowMs: number) { |
Deepanjan Roy | 9a906ed | 2018-09-20 11:06:00 -0400 | [diff] [blame] | 112 | const redrawStart = debugNow(); |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 113 | if (this.isRedrawing) return; |
| 114 | this.isRedrawing = true; |
Deepanjan Roy | f190cb2 | 2018-08-28 10:43:07 -0400 | [diff] [blame] | 115 | for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs); |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 116 | this.isRedrawing = false; |
Deepanjan Roy | 9a906ed | 2018-09-20 11:06:00 -0400 | [diff] [blame] | 117 | if (perfDebug()) { |
| 118 | this.perfStats.rafCanvas.addValue(debugNow() - redrawStart); |
| 119 | } |
Deepanjan Roy | 9d95a25 | 2018-08-09 10:10:19 -0400 | [diff] [blame] | 120 | } |
| 121 | |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 122 | private maybeScheduleAnimationFrame(force = false) { |
| 123 | if (this.hasScheduledNextFrame) return; |
Deepanjan Roy | f190cb2 | 2018-08-28 10:43:07 -0400 | [diff] [blame] | 124 | if (this.actionCallbacks.size !== 0 || force) { |
| 125 | this.hasScheduledNextFrame = true; |
| 126 | window.requestAnimationFrame(this.onAnimationFrame.bind(this)); |
| 127 | } |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 128 | } |
| 129 | |
| 130 | private onAnimationFrame(nowMs: number) { |
Deepanjan Roy | 9a906ed | 2018-09-20 11:06:00 -0400 | [diff] [blame] | 131 | const rafStart = debugNow(); |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 132 | this.hasScheduledNextFrame = false; |
Deepanjan Roy | f190cb2 | 2018-08-28 10:43:07 -0400 | [diff] [blame] | 133 | |
| 134 | const doFullRedraw = this.requestedFullRedraw; |
| 135 | this.requestedFullRedraw = false; |
| 136 | |
Deepanjan Roy | 9a906ed | 2018-09-20 11:06:00 -0400 | [diff] [blame] | 137 | const actionTime = measure(() => { |
| 138 | for (const action of this.actionCallbacks) action(nowMs); |
| 139 | }); |
| 140 | |
| 141 | const domTime = measure(() => { |
| 142 | if (doFullRedraw) this.syncDomRedraw(nowMs); |
| 143 | }); |
| 144 | const canvasTime = measure(() => this.syncCanvasRedraw(nowMs)); |
| 145 | |
| 146 | const totalRafTime = debugNow() - rafStart; |
| 147 | this.updatePerfStats(actionTime, domTime, canvasTime, totalRafTime); |
| 148 | perfDisplay.renderPerfStats(); |
Deepanjan Roy | f190cb2 | 2018-08-28 10:43:07 -0400 | [diff] [blame] | 149 | |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 150 | this.maybeScheduleAnimationFrame(); |
Deepanjan Roy | 9d95a25 | 2018-08-09 10:10:19 -0400 | [diff] [blame] | 151 | } |
Deepanjan Roy | 9a906ed | 2018-09-20 11:06:00 -0400 | [diff] [blame] | 152 | |
| 153 | private updatePerfStats( |
| 154 | actionsTime: number, domTime: number, canvasTime: number, |
| 155 | totalRafTime: number) { |
| 156 | if (!perfDebug()) return; |
| 157 | this.perfStats.rafActions.addValue(actionsTime); |
| 158 | this.perfStats.rafDom.addValue(domTime); |
| 159 | this.perfStats.rafCanvas.addValue(canvasTime); |
| 160 | this.perfStats.rafTotal.addValue(totalRafTime); |
| 161 | } |
| 162 | |
| 163 | renderPerfStats() { |
| 164 | assertTrue(perfDebug()); |
| 165 | return m( |
| 166 | 'div', |
| 167 | m('div', |
| 168 | [ |
| 169 | m('button', |
| 170 | {onclick: () => this.scheduleRedraw()}, |
| 171 | 'Do Canvas Redraw'), |
| 172 | ' | ', |
| 173 | m('button', |
| 174 | {onclick: () => this.scheduleFullRedraw()}, |
| 175 | 'Do Full Redraw'), |
| 176 | ]), |
| 177 | m('div', |
| 178 | 'Raf Timing ' + |
| 179 | '(Total may not add up due to imprecision)'), |
| 180 | m('table', |
| 181 | statTableHeader(), |
| 182 | statTableRow('Actions', this.perfStats.rafActions), |
| 183 | statTableRow('Dom', this.perfStats.rafDom), |
| 184 | statTableRow('Canvas', this.perfStats.rafCanvas), |
| 185 | statTableRow('Total', this.perfStats.rafTotal), ), |
| 186 | m('div', |
| 187 | 'Dom redraw: ' + |
| 188 | `Count: ${this.perfStats.domRedraw.count} | ` + |
| 189 | runningStatStr(this.perfStats.domRedraw)), ); |
| 190 | } |
Deepanjan Roy | 9d95a25 | 2018-08-09 10:10:19 -0400 | [diff] [blame] | 191 | } |