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