• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
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
46export type ActionCallback = (nowMs: number) => void;
47export type RedrawCallback = (nowMs: number) => void;
48
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.
57export class RafScheduler {
58  private actionCallbacks = new Set<ActionCallback>();
59  private canvasRedrawCallbacks = new Set<RedrawCallback>();
60  private _syncDomRedraw: RedrawCallback = _ => {};
61  private hasScheduledNextFrame = false;
62  private requestedFullRedraw = false;
63  private isRedrawing = false;
64  private _shutdown = false;
65
66  private perfStats = {
67    rafActions: new RunningStatistics(),
68    rafCanvas: new RunningStatistics(),
69    rafDom: new RunningStatistics(),
70    rafTotal: new RunningStatistics(),
71    domRedraw: new RunningStatistics(),
72  };
73
74  start(cb: ActionCallback) {
75    this.actionCallbacks.add(cb);
76    this.maybeScheduleAnimationFrame();
77  }
78
79  stop(cb: ActionCallback) {
80    this.actionCallbacks.delete(cb);
81  }
82
83  addRedrawCallback(cb: RedrawCallback) {
84    this.canvasRedrawCallbacks.add(cb);
85  }
86
87  removeRedrawCallback(cb: RedrawCallback) {
88    this.canvasRedrawCallbacks.delete(cb);
89  }
90
91  scheduleRedraw() {
92    this.maybeScheduleAnimationFrame(true);
93  }
94
95  shutdown() {
96    this._shutdown = true;
97  }
98
99  set domRedraw(cb: RedrawCallback|null) {
100    this._syncDomRedraw = cb || (_ => {});
101  }
102
103  scheduleFullRedraw() {
104    this.requestedFullRedraw = true;
105    this.maybeScheduleAnimationFrame(true);
106  }
107
108  syncDomRedraw(nowMs: number) {
109    const redrawStart = debugNow();
110    this._syncDomRedraw(nowMs);
111    if (perfDebug()) {
112      this.perfStats.domRedraw.addValue(debugNow() - redrawStart);
113    }
114  }
115
116  private syncCanvasRedraw(nowMs: number) {
117    const redrawStart = debugNow();
118    if (this.isRedrawing) return;
119    this.isRedrawing = true;
120    for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs);
121    this.isRedrawing = false;
122    if (perfDebug()) {
123      this.perfStats.rafCanvas.addValue(debugNow() - redrawStart);
124    }
125  }
126
127  private maybeScheduleAnimationFrame(force = false) {
128    if (this.hasScheduledNextFrame) return;
129    if (this.actionCallbacks.size !== 0 || force) {
130      this.hasScheduledNextFrame = true;
131      window.requestAnimationFrame(this.onAnimationFrame.bind(this));
132    }
133  }
134
135  private onAnimationFrame(nowMs: number) {
136    if (this._shutdown) return;
137    const rafStart = debugNow();
138    this.hasScheduledNextFrame = false;
139
140    const doFullRedraw = this.requestedFullRedraw;
141    this.requestedFullRedraw = false;
142
143    const actionTime = measure(() => {
144      for (const action of this.actionCallbacks) action(nowMs);
145    });
146
147    const domTime = measure(() => {
148      if (doFullRedraw) this.syncDomRedraw(nowMs);
149    });
150    const canvasTime = measure(() => this.syncCanvasRedraw(nowMs));
151
152    const totalRafTime = debugNow() - rafStart;
153    this.updatePerfStats(actionTime, domTime, canvasTime, totalRafTime);
154    perfDisplay.renderPerfStats();
155
156    this.maybeScheduleAnimationFrame();
157  }
158
159  private updatePerfStats(
160      actionsTime: number, domTime: number, canvasTime: number,
161      totalRafTime: number) {
162    if (!perfDebug()) return;
163    this.perfStats.rafActions.addValue(actionsTime);
164    this.perfStats.rafDom.addValue(domTime);
165    this.perfStats.rafCanvas.addValue(canvasTime);
166    this.perfStats.rafTotal.addValue(totalRafTime);
167  }
168
169  renderPerfStats() {
170    assertTrue(perfDebug());
171    return m(
172        'div',
173        m('div',
174          [
175            m('button',
176              {onclick: () => this.scheduleRedraw()},
177              'Do Canvas Redraw'),
178            '   |   ',
179            m('button',
180              {onclick: () => this.scheduleFullRedraw()},
181              'Do Full Redraw'),
182          ]),
183        m('div',
184          'Raf Timing ' +
185              '(Total may not add up due to imprecision)'),
186        m('table',
187          statTableHeader(),
188          statTableRow('Actions', this.perfStats.rafActions),
189          statTableRow('Dom', this.perfStats.rafDom),
190          statTableRow('Canvas', this.perfStats.rafCanvas),
191          statTableRow('Total', this.perfStats.rafTotal), ),
192        m('div',
193          'Dom redraw: ' +
194              `Count: ${this.perfStats.domRedraw.count} | ` +
195              runningStatStr(this.perfStats.domRedraw)), );
196  }
197}
198