xref: /aosp_15_r20/external/perfetto/ui/src/core/raf_scheduler.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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 {PerfStats} from './perf_stats';
16import m from 'mithril';
17import {featureFlags} from './feature_flags';
18
19export type AnimationCallback = (lastFrameMs: number) => void;
20export type RedrawCallback = () => void;
21
22export const AUTOREDRAW_FLAG = featureFlags.register({
23  id: 'mithrilAutoredraw',
24  name: 'Enable Mithril autoredraw',
25  description: 'Turns calls to schedulefullRedraw() a no-op',
26  defaultValue: false,
27});
28
29// This class orchestrates all RAFs in the UI. It ensures that there is only
30// one animation frame handler overall and that callbacks are called in
31// predictable order. There are two types of callbacks here:
32// - actions (e.g. pan/zoon animations), which will alter the "fast"
33//  (main-thread-only) state (e.g. update visible time bounds @ 60 fps).
34// - redraw callbacks that will repaint canvases.
35// This class guarantees that, on each frame, redraw callbacks are called after
36// all action callbacks.
37export class RafScheduler {
38  // These happen at the beginning of any animation frame. Used by Animation.
39  private animationCallbacks = new Set<AnimationCallback>();
40
41  // These happen during any animaton frame, after the (optional) DOM redraw.
42  private canvasRedrawCallbacks = new Set<RedrawCallback>();
43
44  // These happen at the end of full (DOM) animation frames.
45  private postRedrawCallbacks = new Array<RedrawCallback>();
46  private hasScheduledNextFrame = false;
47  private requestedFullRedraw = false;
48  private isRedrawing = false;
49  private _shutdown = false;
50  private recordPerfStats = false;
51  private mounts = new Map<Element, m.ComponentTypes>();
52
53  readonly perfStats = {
54    rafActions: new PerfStats(),
55    rafCanvas: new PerfStats(),
56    rafDom: new PerfStats(),
57    rafTotal: new PerfStats(),
58    domRedraw: new PerfStats(),
59  };
60
61  constructor() {
62    // Patch m.redraw() to our RAF full redraw.
63    const origSync = m.redraw.sync;
64    const redrawFn = () => this.scheduleFullRedraw('force');
65    redrawFn.sync = origSync;
66    m.redraw = redrawFn;
67
68    m.mount = this.mount.bind(this);
69  }
70
71  // Schedule re-rendering of virtual DOM and canvas.
72  // If a callback is passed it will be executed after the DOM redraw has
73  // completed.
74  scheduleFullRedraw(force?: 'force', cb?: RedrawCallback) {
75    // If we are using autoredraw mode, make this function a no-op unless
76    // 'force' is passed.
77    if (AUTOREDRAW_FLAG.get() && force !== 'force') return;
78    this.requestedFullRedraw = true;
79    cb && this.postRedrawCallbacks.push(cb);
80    this.maybeScheduleAnimationFrame(true);
81  }
82
83  // Schedule re-rendering of canvas only.
84  scheduleCanvasRedraw() {
85    this.maybeScheduleAnimationFrame(true);
86  }
87
88  startAnimation(cb: AnimationCallback) {
89    this.animationCallbacks.add(cb);
90    this.maybeScheduleAnimationFrame();
91  }
92
93  stopAnimation(cb: AnimationCallback) {
94    this.animationCallbacks.delete(cb);
95  }
96
97  addCanvasRedrawCallback(cb: RedrawCallback): Disposable {
98    this.canvasRedrawCallbacks.add(cb);
99    const canvasRedrawCallbacks = this.canvasRedrawCallbacks;
100    return {
101      [Symbol.dispose]() {
102        canvasRedrawCallbacks.delete(cb);
103      },
104    };
105  }
106
107  mount(element: Element, component: m.ComponentTypes | null): void {
108    const mounts = this.mounts;
109    if (component === null) {
110      mounts.delete(element);
111    } else {
112      mounts.set(element, component);
113    }
114    this.syncDomRedrawMountEntry(element, component);
115  }
116
117  shutdown() {
118    this._shutdown = true;
119  }
120
121  setPerfStatsEnabled(enabled: boolean) {
122    this.recordPerfStats = enabled;
123    this.scheduleFullRedraw();
124  }
125
126  get hasPendingRedraws(): boolean {
127    return this.isRedrawing || this.hasScheduledNextFrame;
128  }
129
130  private syncDomRedraw() {
131    const redrawStart = performance.now();
132
133    for (const [element, component] of this.mounts.entries()) {
134      this.syncDomRedrawMountEntry(element, component);
135    }
136
137    if (this.recordPerfStats) {
138      this.perfStats.domRedraw.addValue(performance.now() - redrawStart);
139    }
140  }
141
142  private syncDomRedrawMountEntry(
143    element: Element,
144    component: m.ComponentTypes | null,
145  ) {
146    // Mithril's render() function takes a third argument which tells us if a
147    // further redraw is needed (e.g. due to managed event handler). This allows
148    // us to implement auto-redraw. The redraw argument is documented in the
149    // official Mithril docs but is just not part of the @types/mithril package.
150    const mithrilRender = m.render as (
151      el: Element,
152      vnodes: m.Children,
153      redraw?: () => void,
154    ) => void;
155
156    mithrilRender(
157      element,
158      component !== null ? m(component) : null,
159      AUTOREDRAW_FLAG.get() ? () => raf.scheduleFullRedraw('force') : undefined,
160    );
161  }
162
163  private syncCanvasRedraw() {
164    const redrawStart = performance.now();
165    if (this.isRedrawing) return;
166    this.isRedrawing = true;
167    this.canvasRedrawCallbacks.forEach((cb) => cb());
168    this.isRedrawing = false;
169    if (this.recordPerfStats) {
170      this.perfStats.rafCanvas.addValue(performance.now() - redrawStart);
171    }
172  }
173
174  private maybeScheduleAnimationFrame(force = false) {
175    if (this.hasScheduledNextFrame) return;
176    if (this.animationCallbacks.size !== 0 || force) {
177      this.hasScheduledNextFrame = true;
178      window.requestAnimationFrame(this.onAnimationFrame.bind(this));
179    }
180  }
181
182  private onAnimationFrame(lastFrameMs: number) {
183    if (this._shutdown) return;
184    this.hasScheduledNextFrame = false;
185    const doFullRedraw = this.requestedFullRedraw;
186    this.requestedFullRedraw = false;
187
188    const tStart = performance.now();
189    this.animationCallbacks.forEach((cb) => cb(lastFrameMs));
190    const tAnim = performance.now();
191    doFullRedraw && this.syncDomRedraw();
192    const tDom = performance.now();
193    this.syncCanvasRedraw();
194    const tCanvas = performance.now();
195
196    const animTime = tAnim - tStart;
197    const domTime = tDom - tAnim;
198    const canvasTime = tCanvas - tDom;
199    const totalTime = tCanvas - tStart;
200    this.updatePerfStats(animTime, domTime, canvasTime, totalTime);
201    this.maybeScheduleAnimationFrame();
202
203    if (doFullRedraw && this.postRedrawCallbacks.length > 0) {
204      const pendingCbs = this.postRedrawCallbacks.splice(0); // splice = clear.
205      pendingCbs.forEach((cb) => cb());
206    }
207  }
208
209  private updatePerfStats(
210    actionsTime: number,
211    domTime: number,
212    canvasTime: number,
213    totalRafTime: number,
214  ) {
215    if (!this.recordPerfStats) return;
216    this.perfStats.rafActions.addValue(actionsTime);
217    this.perfStats.rafDom.addValue(domTime);
218    this.perfStats.rafCanvas.addValue(canvasTime);
219    this.perfStats.rafTotal.addValue(totalRafTime);
220  }
221}
222
223export const raf = new RafScheduler();
224