blob: bfe201749e577b57b124588c74cdc824e0a449d6 [file] [log] [blame]
Deepanjan Roy9d95a252018-08-09 10:10:19 -04001// 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 Roy9a906ed2018-09-20 11:06:00 -040015import * as m from 'mithril';
16
17import {assertTrue} from '../base/logging';
18
19import {
20 debugNow,
21 measure,
22 perfDebug,
23 perfDisplay,
24 RunningStatistics,
25 runningStatStr
26} from './perf';
27
28function 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
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
Primiano Tuccif30cd9c2018-08-13 01:53:26 +020046export type ActionCallback = (nowMs: number) => void;
47export type RedrawCallback = (nowMs: number) => void;
Deepanjan Roy9d95a252018-08-09 10:10:19 -040048
Primiano Tuccif30cd9c2018-08-13 01:53:26 +020049// 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.
57export class RafScheduler {
58 private actionCallbacks = new Set<ActionCallback>();
Deepanjan Royf190cb22018-08-28 10:43:07 -040059 private canvasRedrawCallbacks = new Set<RedrawCallback>();
60 private _syncDomRedraw: RedrawCallback = _ => {};
Primiano Tuccif30cd9c2018-08-13 01:53:26 +020061 private hasScheduledNextFrame = false;
Deepanjan Royf190cb22018-08-28 10:43:07 -040062 private requestedFullRedraw = false;
Primiano Tuccif30cd9c2018-08-13 01:53:26 +020063 private isRedrawing = false;
64
Deepanjan Roy9a906ed2018-09-20 11:06:00 -040065 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 Tuccif30cd9c2018-08-13 01:53:26 +020073 start(cb: ActionCallback) {
74 this.actionCallbacks.add(cb);
75 this.maybeScheduleAnimationFrame();
Deepanjan Roy9d95a252018-08-09 10:10:19 -040076 }
77
Primiano Tuccif30cd9c2018-08-13 01:53:26 +020078 stop(cb: ActionCallback) {
79 this.actionCallbacks.delete(cb);
80 }
81
82 addRedrawCallback(cb: RedrawCallback) {
Deepanjan Royf190cb22018-08-28 10:43:07 -040083 this.canvasRedrawCallbacks.add(cb);
Deepanjan Roy9d95a252018-08-09 10:10:19 -040084 }
85
Primiano Tuccif30cd9c2018-08-13 01:53:26 +020086 removeRedrawCallback(cb: RedrawCallback) {
Deepanjan Royf190cb22018-08-28 10:43:07 -040087 this.canvasRedrawCallbacks.delete(cb);
Deepanjan Roy9d95a252018-08-09 10:10:19 -040088 }
89
Deepanjan Royf190cb22018-08-28 10:43:07 -040090 scheduleRedraw() {
Primiano Tuccif30cd9c2018-08-13 01:53:26 +020091 this.maybeScheduleAnimationFrame(true);
Deepanjan Roy9d95a252018-08-09 10:10:19 -040092 }
93
Deepanjan Royf190cb22018-08-28 10:43:07 -040094 set domRedraw(cb: RedrawCallback|null) {
95 this._syncDomRedraw = cb || (_ => {});
96 }
97
98 scheduleFullRedraw() {
99 this.requestedFullRedraw = true;
100 this.maybeScheduleAnimationFrame(true);
101 }
102
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400103 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 Royf190cb22018-08-28 10:43:07 -0400111 private syncCanvasRedraw(nowMs: number) {
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400112 const redrawStart = debugNow();
Primiano Tuccif30cd9c2018-08-13 01:53:26 +0200113 if (this.isRedrawing) return;
114 this.isRedrawing = true;
Deepanjan Royf190cb22018-08-28 10:43:07 -0400115 for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs);
Primiano Tuccif30cd9c2018-08-13 01:53:26 +0200116 this.isRedrawing = false;
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400117 if (perfDebug()) {
118 this.perfStats.rafCanvas.addValue(debugNow() - redrawStart);
119 }
Deepanjan Roy9d95a252018-08-09 10:10:19 -0400120 }
121
Primiano Tuccif30cd9c2018-08-13 01:53:26 +0200122 private maybeScheduleAnimationFrame(force = false) {
123 if (this.hasScheduledNextFrame) return;
Deepanjan Royf190cb22018-08-28 10:43:07 -0400124 if (this.actionCallbacks.size !== 0 || force) {
125 this.hasScheduledNextFrame = true;
126 window.requestAnimationFrame(this.onAnimationFrame.bind(this));
127 }
Primiano Tuccif30cd9c2018-08-13 01:53:26 +0200128 }
129
130 private onAnimationFrame(nowMs: number) {
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400131 const rafStart = debugNow();
Primiano Tuccif30cd9c2018-08-13 01:53:26 +0200132 this.hasScheduledNextFrame = false;
Deepanjan Royf190cb22018-08-28 10:43:07 -0400133
134 const doFullRedraw = this.requestedFullRedraw;
135 this.requestedFullRedraw = false;
136
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400137 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 Royf190cb22018-08-28 10:43:07 -0400149
Primiano Tuccif30cd9c2018-08-13 01:53:26 +0200150 this.maybeScheduleAnimationFrame();
Deepanjan Roy9d95a252018-08-09 10:10:19 -0400151 }
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400152
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 Roy9d95a252018-08-09 10:10:19 -0400191}