• 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 {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  get hasPendingRedraws(): boolean {
119    return this.isRedrawing || this.hasScheduledNextFrame;
120  }
121
122  private syncCanvasRedraw(nowMs: number) {
123    const redrawStart = debugNow();
124    if (this.isRedrawing) return;
125    globals.frontendLocalState.clearVisibleTracks();
126    this.isRedrawing = true;
127    for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs);
128    this.isRedrawing = false;
129    globals.frontendLocalState.sendVisibleTracks();
130    if (perfDebug()) {
131      this.perfStats.rafCanvas.addValue(debugNow() - redrawStart);
132    }
133  }
134
135  private maybeScheduleAnimationFrame(force = false) {
136    if (this.hasScheduledNextFrame) return;
137    if (this.actionCallbacks.size !== 0 || force) {
138      this.hasScheduledNextFrame = true;
139      window.requestAnimationFrame(this.onAnimationFrame.bind(this));
140    }
141  }
142
143  private onAnimationFrame(nowMs: number) {
144    if (this._shutdown) return;
145    const rafStart = debugNow();
146    this.hasScheduledNextFrame = false;
147
148    const doFullRedraw = this.requestedFullRedraw;
149    this.requestedFullRedraw = false;
150
151    const actionTime = measure(() => {
152      for (const action of this.actionCallbacks) action(nowMs);
153    });
154
155    const domTime = measure(() => {
156      if (doFullRedraw) this.syncDomRedraw(nowMs);
157    });
158    const canvasTime = measure(() => this.syncCanvasRedraw(nowMs));
159
160    const totalRafTime = debugNow() - rafStart;
161    this.updatePerfStats(actionTime, domTime, canvasTime, totalRafTime);
162    perfDisplay.renderPerfStats();
163
164    this.maybeScheduleAnimationFrame();
165  }
166
167  private updatePerfStats(
168      actionsTime: number, domTime: number, canvasTime: number,
169      totalRafTime: number) {
170    if (!perfDebug()) return;
171    this.perfStats.rafActions.addValue(actionsTime);
172    this.perfStats.rafDom.addValue(domTime);
173    this.perfStats.rafCanvas.addValue(canvasTime);
174    this.perfStats.rafTotal.addValue(totalRafTime);
175  }
176
177  renderPerfStats() {
178    assertTrue(perfDebug());
179    return m(
180        'div',
181        m('div',
182          [
183            m('button',
184              {onclick: () => this.scheduleRedraw()},
185              'Do Canvas Redraw'),
186            '   |   ',
187            m('button',
188              {onclick: () => this.scheduleFullRedraw()},
189              'Do Full Redraw'),
190          ]),
191        m('div',
192          'Raf Timing ' +
193              '(Total may not add up due to imprecision)'),
194        m('table',
195          statTableHeader(),
196          statTableRow('Actions', this.perfStats.rafActions),
197          statTableRow('Dom', this.perfStats.rafDom),
198          statTableRow('Canvas', this.perfStats.rafCanvas),
199          statTableRow('Total', this.perfStats.rafTotal), ),
200        m('div',
201          'Dom redraw: ' +
202              `Count: ${this.perfStats.domRedraw.count} | ` +
203              runningStatStr(this.perfStats.domRedraw)), );
204  }
205}
206