• 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 m from 'mithril';
16
17import {
18  debugNow,
19  measure,
20  perfDebug,
21  perfDisplay,
22  PerfStatsSource,
23  RunningStatistics,
24  runningStatStr,
25} from './perf';
26
27function statTableHeader() {
28  return m(
29    'tr',
30    m('th', ''),
31    m('th', 'Last (ms)'),
32    m('th', 'Avg (ms)'),
33    m('th', 'Avg-10 (ms)'),
34  );
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}
46
47export type ActionCallback = (nowMs: number) => void;
48export type RedrawCallback = (nowMs: number) => void;
49
50// This class orchestrates all RAFs in the UI. It ensures that there is only
51// one animation frame handler overall and that callbacks are called in
52// predictable order. There are two types of callbacks here:
53// - actions (e.g. pan/zoon animations), which will alter the "fast"
54//  (main-thread-only) state (e.g. update visible time bounds @ 60 fps).
55// - redraw callbacks that will repaint canvases.
56// This class guarantees that, on each frame, redraw callbacks are called after
57// all action callbacks.
58export class RafScheduler implements PerfStatsSource {
59  private actionCallbacks = new Set<ActionCallback>();
60  private canvasRedrawCallbacks = new Set<RedrawCallback>();
61  private _syncDomRedraw: RedrawCallback = (_) => {};
62  private hasScheduledNextFrame = false;
63  private requestedFullRedraw = false;
64  private isRedrawing = false;
65  private _shutdown = false;
66  private _beforeRedraw: () => void = () => {};
67  private _afterRedraw: () => void = () => {};
68  private _pendingCallbacks: RedrawCallback[] = [];
69
70  private perfStats = {
71    rafActions: new RunningStatistics(),
72    rafCanvas: new RunningStatistics(),
73    rafDom: new RunningStatistics(),
74    rafTotal: new RunningStatistics(),
75    domRedraw: new RunningStatistics(),
76  };
77
78  start(cb: ActionCallback) {
79    this.actionCallbacks.add(cb);
80    this.maybeScheduleAnimationFrame();
81  }
82
83  stop(cb: ActionCallback) {
84    this.actionCallbacks.delete(cb);
85  }
86
87  addRedrawCallback(cb: RedrawCallback) {
88    this.canvasRedrawCallbacks.add(cb);
89  }
90
91  removeRedrawCallback(cb: RedrawCallback) {
92    this.canvasRedrawCallbacks.delete(cb);
93  }
94
95  addPendingCallback(cb: RedrawCallback) {
96    this._pendingCallbacks.push(cb);
97  }
98
99  // Schedule re-rendering of canvas only.
100  scheduleRedraw() {
101    this.maybeScheduleAnimationFrame(true);
102  }
103
104  shutdown() {
105    this._shutdown = true;
106  }
107
108  set domRedraw(cb: RedrawCallback) {
109    this._syncDomRedraw = cb;
110  }
111
112  set beforeRedraw(cb: () => void) {
113    this._beforeRedraw = cb;
114  }
115
116  set afterRedraw(cb: () => void) {
117    this._afterRedraw = cb;
118  }
119
120  // Schedule re-rendering of virtual DOM and canvas.
121  scheduleFullRedraw() {
122    this.requestedFullRedraw = true;
123    this.maybeScheduleAnimationFrame(true);
124  }
125
126  // Schedule a full redraw to happen after a short delay (50 ms).
127  // This is done to prevent flickering / visual noise and allow the UI to fetch
128  // the initial data from the Trace Processor.
129  // There is a chance that someone else schedules a full redraw in the
130  // meantime, forcing the flicker, but in practice it works quite well and
131  // avoids a lot of complexity for the callers.
132  scheduleDelayedFullRedraw() {
133    // 50ms is half of the responsiveness threshold (100ms):
134    // https://web.dev/rail/#response-process-events-in-under-50ms
135    const delayMs = 50;
136    setTimeout(() => this.scheduleFullRedraw(), delayMs);
137  }
138
139  syncDomRedraw(nowMs: number) {
140    const redrawStart = debugNow();
141    this._syncDomRedraw(nowMs);
142    if (perfDebug()) {
143      this.perfStats.domRedraw.addValue(debugNow() - redrawStart);
144    }
145  }
146
147  get hasPendingRedraws(): boolean {
148    return this.isRedrawing || this.hasScheduledNextFrame;
149  }
150
151  private syncCanvasRedraw(nowMs: number) {
152    const redrawStart = debugNow();
153    if (this.isRedrawing) return;
154    this._beforeRedraw();
155    this.isRedrawing = true;
156    for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs);
157    this.isRedrawing = false;
158    this._afterRedraw();
159    for (const cb of this._pendingCallbacks) {
160      cb(nowMs);
161    }
162    this._pendingCallbacks.splice(0, this._pendingCallbacks.length);
163    if (perfDebug()) {
164      this.perfStats.rafCanvas.addValue(debugNow() - redrawStart);
165    }
166  }
167
168  private maybeScheduleAnimationFrame(force = false) {
169    if (this.hasScheduledNextFrame) return;
170    if (this.actionCallbacks.size !== 0 || force) {
171      this.hasScheduledNextFrame = true;
172      window.requestAnimationFrame(this.onAnimationFrame.bind(this));
173    }
174  }
175
176  private onAnimationFrame(nowMs: number) {
177    if (this._shutdown) return;
178    const rafStart = debugNow();
179    this.hasScheduledNextFrame = false;
180
181    const doFullRedraw = this.requestedFullRedraw;
182    this.requestedFullRedraw = false;
183
184    const actionTime = measure(() => {
185      for (const action of this.actionCallbacks) action(nowMs);
186    });
187
188    const domTime = measure(() => {
189      if (doFullRedraw) this.syncDomRedraw(nowMs);
190    });
191    const canvasTime = measure(() => this.syncCanvasRedraw(nowMs));
192
193    const totalRafTime = debugNow() - rafStart;
194    this.updatePerfStats(actionTime, domTime, canvasTime, totalRafTime);
195    perfDisplay.renderPerfStats(this);
196
197    this.maybeScheduleAnimationFrame();
198  }
199
200  private updatePerfStats(
201    actionsTime: number,
202    domTime: number,
203    canvasTime: number,
204    totalRafTime: number,
205  ) {
206    if (!perfDebug()) return;
207    this.perfStats.rafActions.addValue(actionsTime);
208    this.perfStats.rafDom.addValue(domTime);
209    this.perfStats.rafCanvas.addValue(canvasTime);
210    this.perfStats.rafTotal.addValue(totalRafTime);
211  }
212
213  renderPerfStats() {
214    return m(
215      'div',
216      m('div', [
217        m('button', {onclick: () => this.scheduleRedraw()}, 'Do Canvas Redraw'),
218        '   |   ',
219        m(
220          'button',
221          {onclick: () => this.scheduleFullRedraw()},
222          'Do Full Redraw',
223        ),
224      ]),
225      m('div', 'Raf Timing ' + '(Total may not add up due to imprecision)'),
226      m(
227        'table',
228        statTableHeader(),
229        statTableRow('Actions', this.perfStats.rafActions),
230        statTableRow('Dom', this.perfStats.rafDom),
231        statTableRow('Canvas', this.perfStats.rafCanvas),
232        statTableRow('Total', this.perfStats.rafTotal),
233      ),
234      m(
235        'div',
236        'Dom redraw: ' +
237          `Count: ${this.perfStats.domRedraw.count} | ` +
238          runningStatStr(this.perfStats.domRedraw),
239      ),
240    );
241  }
242}
243
244export const raf = new RafScheduler();
245