Merge changes I59ff0260,Iae793df8 into main

* changes:
  ui: remove unnecessary full redraws
  ui: clean up raf_scheduler.ts and perf.ts
diff --git a/ui/src/common/track_helper.ts b/ui/src/common/track_helper.ts
index 3087228..e9ef6fb 100644
--- a/ui/src/common/track_helper.ts
+++ b/ui/src/common/track_helper.ts
@@ -95,6 +95,6 @@
     const {start, end} = this.latestTimespan;
     const resolution = this.latestResolution;
     this.data_ = await this.doFetch(start, end, resolution);
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 }
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts
index 3336045..7662755 100644
--- a/ui/src/core/app_impl.ts
+++ b/ui/src/core/app_impl.ts
@@ -32,7 +32,7 @@
 import {createProxy, getOrCreate} from '../base/utils';
 import {PageManagerImpl} from './page_manager';
 import {PageHandler} from '../public/page';
-import {setPerfHooks} from './perf';
+import {PerfManager} from './perf_manager';
 import {ServiceWorkerController} from '../frontend/service_worker_controller';
 import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
 import {featureFlags} from './feature_flags';
@@ -59,6 +59,7 @@
   readonly pageMgr = new PageManagerImpl();
   readonly sidebarMgr: SidebarManagerImpl;
   readonly pluginMgr: PluginManagerImpl;
+  readonly perfMgr = new PerfManager();
   readonly analytics: AnalyticsInternal;
   readonly serviceWorkerController: ServiceWorkerController;
   httpRpc = {
@@ -67,7 +68,6 @@
   };
   initialRouteArgs: RouteArgs;
   isLoadingTrace = false; // Set when calling openTrace().
-  perfDebugging = false; // Enables performance debugging of tracks/panels.
   readonly initArgs: AppInitArgs;
   readonly embeddedMode: boolean;
   readonly testingMode: boolean;
@@ -296,17 +296,8 @@
     return this.appCtx.extraSqlPackages;
   }
 
-  get perfDebugging(): boolean {
-    return this.appCtx.perfDebugging;
-  }
-
-  setPerfDebuggingEnabled(enabled: boolean) {
-    this.appCtx.perfDebugging = enabled;
-    setPerfHooks(
-      () => this.perfDebugging,
-      () => this.setPerfDebuggingEnabled(!this.perfDebugging),
-    );
-    raf.scheduleFullRedraw();
+  get perfDebugging(): PerfManager {
+    return this.appCtx.perfMgr;
   }
 
   get serviceWorkerController(): ServiceWorkerController {
diff --git a/ui/src/core/perf.ts b/ui/src/core/perf.ts
deleted file mode 100644
index 6e9afaf..0000000
--- a/ui/src/core/perf.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-const hooks = {
-  isDebug: () => false,
-  toggleDebug: () => {},
-};
-
-export function setPerfHooks(isDebug: () => boolean, toggleDebug: () => void) {
-  hooks.isDebug = isDebug;
-  hooks.toggleDebug = toggleDebug;
-}
-
-// Shorthand for if globals perf debug mode is on.
-export const perfDebug = () => hooks.isDebug();
-
-// Returns performance.now() if perfDebug is enabled, otherwise 0.
-// This is needed because calling performance.now is generally expensive
-// and should not be done for every frame.
-export const debugNow = () => (perfDebug() ? performance.now() : 0);
-
-// Returns execution time of |fn| if perf debug mode is on. Returns 0 otherwise.
-export function measure(fn: () => void): number {
-  const start = debugNow();
-  fn();
-  return debugNow() - start;
-}
-
-// Stores statistics about samples, and keeps a fixed size buffer of most recent
-// samples.
-export class RunningStatistics {
-  private _count = 0;
-  private _mean = 0;
-  private _lastValue = 0;
-  private _ptr = 0;
-
-  private buffer: number[] = [];
-
-  constructor(private _maxBufferSize = 10) {}
-
-  addValue(value: number) {
-    this._lastValue = value;
-    if (this.buffer.length >= this._maxBufferSize) {
-      this.buffer[this._ptr++] = value;
-      if (this._ptr >= this.buffer.length) {
-        this._ptr -= this.buffer.length;
-      }
-    } else {
-      this.buffer.push(value);
-    }
-
-    this._mean = (this._mean * this._count + value) / (this._count + 1);
-    this._count++;
-  }
-
-  get mean() {
-    return this._mean;
-  }
-  get count() {
-    return this._count;
-  }
-  get bufferMean() {
-    return this.buffer.reduce((sum, v) => sum + v, 0) / this.buffer.length;
-  }
-  get bufferSize() {
-    return this.buffer.length;
-  }
-  get maxBufferSize() {
-    return this._maxBufferSize;
-  }
-  get last() {
-    return this._lastValue;
-  }
-}
-
-// Returns a summary string representation of a RunningStatistics object.
-export function runningStatStr(stat: RunningStatistics) {
-  return (
-    `Last: ${stat.last.toFixed(2)}ms | ` +
-    `Avg: ${stat.mean.toFixed(2)}ms | ` +
-    `Avg${stat.maxBufferSize}: ${stat.bufferMean.toFixed(2)}ms`
-  );
-}
-
-export interface PerfStatsSource {
-  renderPerfStats(): m.Children;
-}
-
-// Globals singleton class that renders performance stats for the whole app.
-class PerfDisplay {
-  private containers: PerfStatsSource[] = [];
-
-  addContainer(container: PerfStatsSource) {
-    this.containers.push(container);
-  }
-
-  removeContainer(container: PerfStatsSource) {
-    const i = this.containers.indexOf(container);
-    this.containers.splice(i, 1);
-  }
-
-  renderPerfStats(src: PerfStatsSource) {
-    if (!perfDebug()) return;
-    const perfDisplayEl = document.querySelector('.perf-stats');
-    if (!perfDisplayEl) return;
-    m.render(perfDisplayEl, [
-      m('section', src.renderPerfStats()),
-      m(
-        'button.close-button',
-        {
-          onclick: hooks.toggleDebug,
-        },
-        m('i.material-icons', 'close'),
-      ),
-      this.containers.map((c, i) =>
-        m('section', m('div', `Panel Container ${i + 1}`), c.renderPerfStats()),
-      ),
-    ]);
-  }
-}
-
-export const perfDisplay = new PerfDisplay();
diff --git a/ui/src/core/perf_manager.ts b/ui/src/core/perf_manager.ts
new file mode 100644
index 0000000..e63e7e8
--- /dev/null
+++ b/ui/src/core/perf_manager.ts
@@ -0,0 +1,145 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {raf} from './raf_scheduler';
+import {PerfStats, PerfStatsContainer, runningStatStr} from './perf_stats';
+
+export class PerfManager {
+  private _enabled = false;
+  readonly containers: PerfStatsContainer[] = [];
+
+  get enabled(): boolean {
+    return this._enabled;
+  }
+
+  set enabled(enabled: boolean) {
+    this._enabled = enabled;
+    raf.setPerfStatsEnabled(true);
+    this.containers.forEach((c) => c.setPerfStatsEnabled(enabled));
+  }
+
+  addContainer(container: PerfStatsContainer): Disposable {
+    this.containers.push(container);
+    return {
+      [Symbol.dispose]: () => {
+        const i = this.containers.indexOf(container);
+        this.containers.splice(i, 1);
+      },
+    };
+  }
+
+  renderPerfStats(): m.Children {
+    if (!this._enabled) return;
+    // The rendering of the perf stats UI is atypical. The main issue is that we
+    // want to redraw the mithril component even if there is no full DOM redraw
+    // happening (and we don't want to force redraws as a side effect). So we
+    // return here just a container and handle its rendering ourselves.
+    const perfMgr = this;
+    let removed = false;
+    return m('.perf-stats', {
+      oncreate(vnode: m.VnodeDOM) {
+        const animationFrame = (dom: Element) => {
+          if (removed) return;
+          m.render(dom, m(PerfStatsUi, {perfMgr}));
+          requestAnimationFrame(() => animationFrame(dom));
+        };
+        animationFrame(vnode.dom);
+      },
+      onremove() {
+        removed = true;
+      },
+    });
+  }
+}
+
+// The mithril component that draws the contents of the perf stats box.
+
+interface PerfStatsUiAttrs {
+  perfMgr: PerfManager;
+}
+
+class PerfStatsUi implements m.ClassComponent<PerfStatsUiAttrs> {
+  view({attrs}: m.Vnode<PerfStatsUiAttrs>) {
+    return m(
+      '.perf-stats',
+      {},
+      m('section', this.renderRafSchedulerStats()),
+      m(
+        'button.close-button',
+        {
+          onclick: () => (attrs.perfMgr.enabled = false),
+        },
+        m('i.material-icons', 'close'),
+      ),
+      attrs.perfMgr.containers.map((c, i) =>
+        m('section', m('div', `Panel Container ${i + 1}`), c.renderPerfStats()),
+      ),
+    );
+  }
+
+  renderRafSchedulerStats() {
+    return m(
+      'div',
+      m('div', [
+        m(
+          'button',
+          {onclick: () => raf.scheduleCanvasRedraw()},
+          'Do Canvas Redraw',
+        ),
+        '   |   ',
+        m(
+          'button',
+          {onclick: () => raf.scheduleFullRedraw()},
+          'Do Full Redraw',
+        ),
+      ]),
+      m('div', 'Raf Timing ' + '(Total may not add up due to imprecision)'),
+      m(
+        'table',
+        this.statTableHeader(),
+        this.statTableRow('Actions', raf.perfStats.rafActions),
+        this.statTableRow('Dom', raf.perfStats.rafDom),
+        this.statTableRow('Canvas', raf.perfStats.rafCanvas),
+        this.statTableRow('Total', raf.perfStats.rafTotal),
+      ),
+      m(
+        'div',
+        'Dom redraw: ' +
+          `Count: ${raf.perfStats.domRedraw.count} | ` +
+          runningStatStr(raf.perfStats.domRedraw),
+      ),
+    );
+  }
+
+  statTableHeader() {
+    return m(
+      'tr',
+      m('th', ''),
+      m('th', 'Last (ms)'),
+      m('th', 'Avg (ms)'),
+      m('th', 'Avg-10 (ms)'),
+    );
+  }
+
+  statTableRow(title: string, stat: PerfStats) {
+    return m(
+      'tr',
+      m('td', title),
+      m('td', stat.last.toFixed(2)),
+      m('td', stat.mean.toFixed(2)),
+      m('td', stat.bufferMean.toFixed(2)),
+    );
+  }
+}
diff --git a/ui/src/core/perf_stats.ts b/ui/src/core/perf_stats.ts
new file mode 100644
index 0000000..3f1eda0
--- /dev/null
+++ b/ui/src/core/perf_stats.ts
@@ -0,0 +1,78 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+// The interface that every container (e.g. Track Panels) that exposes granular
+// per-container masurements implements to be perf-stats-aware.
+export interface PerfStatsContainer {
+  setPerfStatsEnabled(enable: boolean): void;
+  renderPerfStats(): m.Children;
+}
+
+// Stores statistics about samples, and keeps a fixed size buffer of most recent
+// samples.
+export class PerfStats {
+  private _count = 0;
+  private _mean = 0;
+  private _lastValue = 0;
+  private _ptr = 0;
+
+  private buffer: number[] = [];
+
+  constructor(private _maxBufferSize = 10) {}
+
+  addValue(value: number) {
+    this._lastValue = value;
+    if (this.buffer.length >= this._maxBufferSize) {
+      this.buffer[this._ptr++] = value;
+      if (this._ptr >= this.buffer.length) {
+        this._ptr -= this.buffer.length;
+      }
+    } else {
+      this.buffer.push(value);
+    }
+
+    this._mean = (this._mean * this._count + value) / (this._count + 1);
+    this._count++;
+  }
+
+  get mean() {
+    return this._mean;
+  }
+  get count() {
+    return this._count;
+  }
+  get bufferMean() {
+    return this.buffer.reduce((sum, v) => sum + v, 0) / this.buffer.length;
+  }
+  get bufferSize() {
+    return this.buffer.length;
+  }
+  get maxBufferSize() {
+    return this._maxBufferSize;
+  }
+  get last() {
+    return this._lastValue;
+  }
+}
+
+// Returns a summary string representation of a RunningStatistics object.
+export function runningStatStr(stat: PerfStats) {
+  return (
+    `Last: ${stat.last.toFixed(2)}ms | ` +
+    `Avg: ${stat.mean.toFixed(2)}ms | ` +
+    `Avg${stat.maxBufferSize}: ${stat.bufferMean.toFixed(2)}ms`
+  );
+}
diff --git a/ui/src/core/perf_unittest.ts b/ui/src/core/perf_stats_unittest.ts
similarity index 86%
rename from ui/src/core/perf_unittest.ts
rename to ui/src/core/perf_stats_unittest.ts
index 5ba357c..1b24bf5 100644
--- a/ui/src/core/perf_unittest.ts
+++ b/ui/src/core/perf_stats_unittest.ts
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {RunningStatistics} from './perf';
+import {PerfStats} from './perf_stats';
 
 test('buffer size is accurate before reaching max capacity', () => {
-  const buf = new RunningStatistics(10);
+  const buf = new PerfStats(10);
 
   for (let i = 0; i < 10; i++) {
     buf.addValue(i);
@@ -24,7 +24,7 @@
 });
 
 test('buffer size is accurate after reaching max capacity', () => {
-  const buf = new RunningStatistics(10);
+  const buf = new PerfStats(10);
 
   for (let i = 0; i < 10; i++) {
     buf.addValue(i);
@@ -37,7 +37,7 @@
 });
 
 test('buffer mean is accurate before reaching max capacity', () => {
-  const buf = new RunningStatistics(10);
+  const buf = new PerfStats(10);
 
   buf.addValue(1);
   buf.addValue(2);
@@ -47,7 +47,7 @@
 });
 
 test('buffer mean is accurate after reaching max capacity', () => {
-  const buf = new RunningStatistics(10);
+  const buf = new PerfStats(10);
 
   for (let i = 0; i < 20; i++) {
     buf.addValue(2);
diff --git a/ui/src/core/raf_scheduler.ts b/ui/src/core/raf_scheduler.ts
index c6ca0fc..b23379f 100644
--- a/ui/src/core/raf_scheduler.ts
+++ b/ui/src/core/raf_scheduler.ts
@@ -12,39 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import m from 'mithril';
-import {
-  debugNow,
-  measure,
-  perfDebug,
-  perfDisplay,
-  PerfStatsSource,
-  RunningStatistics,
-  runningStatStr,
-} from './perf';
+import {PerfStats} from './perf_stats';
 
-function statTableHeader() {
-  return m(
-    'tr',
-    m('th', ''),
-    m('th', 'Last (ms)'),
-    m('th', 'Avg (ms)'),
-    m('th', 'Avg-10 (ms)'),
-  );
-}
-
-function statTableRow(title: string, stat: RunningStatistics) {
-  return m(
-    'tr',
-    m('td', title),
-    m('td', stat.last.toFixed(2)),
-    m('td', stat.mean.toFixed(2)),
-    m('td', stat.bufferMean.toFixed(2)),
-  );
-}
-
-export type ActionCallback = (nowMs: number) => void;
-export type RedrawCallback = (nowMs: number) => void;
+export type AnimationCallback = (lastFrameMs: number) => void;
+export type RedrawCallback = () => void;
 
 // This class orchestrates all RAFs in the UI. It ensures that there is only
 // one animation frame handler overall and that callbacks are called in
@@ -54,146 +25,134 @@
 // - redraw callbacks that will repaint canvases.
 // This class guarantees that, on each frame, redraw callbacks are called after
 // all action callbacks.
-export class RafScheduler implements PerfStatsSource {
-  private actionCallbacks = new Set<ActionCallback>();
+export class RafScheduler {
+  // These happen at the beginning of any animation frame. Used by Animation.
+  private animationCallbacks = new Set<AnimationCallback>();
+
+  // These happen during any animaton frame, after the (optional) DOM redraw.
   private canvasRedrawCallbacks = new Set<RedrawCallback>();
-  private _syncDomRedraw: RedrawCallback = (_) => {};
+
+  // These happen at the end of full (DOM) animation frames.
+  private postRedrawCallbacks = new Array<RedrawCallback>();
+  private syncDomRedrawFn: () => void = () => {};
   private hasScheduledNextFrame = false;
   private requestedFullRedraw = false;
   private isRedrawing = false;
   private _shutdown = false;
-  private _beforeRedraw: () => void = () => {};
-  private _afterRedraw: () => void = () => {};
-  private _pendingCallbacks: RedrawCallback[] = [];
+  private recordPerfStats = false;
 
-  private perfStats = {
-    rafActions: new RunningStatistics(),
-    rafCanvas: new RunningStatistics(),
-    rafDom: new RunningStatistics(),
-    rafTotal: new RunningStatistics(),
-    domRedraw: new RunningStatistics(),
+  readonly perfStats = {
+    rafActions: new PerfStats(),
+    rafCanvas: new PerfStats(),
+    rafDom: new PerfStats(),
+    rafTotal: new PerfStats(),
+    domRedraw: new PerfStats(),
   };
 
-  start(cb: ActionCallback) {
-    this.actionCallbacks.add(cb);
-    this.maybeScheduleAnimationFrame();
+  // Called by frontend/index.ts. syncDomRedrawFn is a function that invokes
+  // m.render() of the root UiMain component.
+  initialize(syncDomRedrawFn: () => void) {
+    this.syncDomRedrawFn = syncDomRedrawFn;
   }
 
-  stop(cb: ActionCallback) {
-    this.actionCallbacks.delete(cb);
-  }
-
-  addRedrawCallback(cb: RedrawCallback) {
-    this.canvasRedrawCallbacks.add(cb);
-  }
-
-  removeRedrawCallback(cb: RedrawCallback) {
-    this.canvasRedrawCallbacks.delete(cb);
-  }
-
-  addPendingCallback(cb: RedrawCallback) {
-    this._pendingCallbacks.push(cb);
+  // Schedule re-rendering of virtual DOM and canvas.
+  // If a callback is passed it will be executed after the DOM redraw has
+  // completed.
+  scheduleFullRedraw(cb?: RedrawCallback) {
+    this.requestedFullRedraw = true;
+    cb && this.postRedrawCallbacks.push(cb);
+    this.maybeScheduleAnimationFrame(true);
   }
 
   // Schedule re-rendering of canvas only.
-  scheduleRedraw() {
+  scheduleCanvasRedraw() {
     this.maybeScheduleAnimationFrame(true);
   }
 
+  startAnimation(cb: AnimationCallback) {
+    this.animationCallbacks.add(cb);
+    this.maybeScheduleAnimationFrame();
+  }
+
+  stopAnimation(cb: AnimationCallback) {
+    this.animationCallbacks.delete(cb);
+  }
+
+  addCanvasRedrawCallback(cb: RedrawCallback): Disposable {
+    this.canvasRedrawCallbacks.add(cb);
+    const canvasRedrawCallbacks = this.canvasRedrawCallbacks;
+    return {
+      [Symbol.dispose]() {
+        canvasRedrawCallbacks.delete(cb);
+      },
+    };
+  }
+
   shutdown() {
     this._shutdown = true;
   }
 
-  set domRedraw(cb: RedrawCallback) {
-    this._syncDomRedraw = cb;
-  }
-
-  set beforeRedraw(cb: () => void) {
-    this._beforeRedraw = cb;
-  }
-
-  set afterRedraw(cb: () => void) {
-    this._afterRedraw = cb;
-  }
-
-  // Schedule re-rendering of virtual DOM and canvas.
-  scheduleFullRedraw() {
-    this.requestedFullRedraw = true;
-    this.maybeScheduleAnimationFrame(true);
-  }
-
-  // Schedule a full redraw to happen after a short delay (50 ms).
-  // This is done to prevent flickering / visual noise and allow the UI to fetch
-  // the initial data from the Trace Processor.
-  // There is a chance that someone else schedules a full redraw in the
-  // meantime, forcing the flicker, but in practice it works quite well and
-  // avoids a lot of complexity for the callers.
-  scheduleDelayedFullRedraw() {
-    // 50ms is half of the responsiveness threshold (100ms):
-    // https://web.dev/rail/#response-process-events-in-under-50ms
-    const delayMs = 50;
-    setTimeout(() => this.scheduleFullRedraw(), delayMs);
-  }
-
-  syncDomRedraw(nowMs: number) {
-    const redrawStart = debugNow();
-    this._syncDomRedraw(nowMs);
-    if (perfDebug()) {
-      this.perfStats.domRedraw.addValue(debugNow() - redrawStart);
-    }
+  setPerfStatsEnabled(enabled: boolean) {
+    this.recordPerfStats = enabled;
+    this.scheduleFullRedraw();
   }
 
   get hasPendingRedraws(): boolean {
     return this.isRedrawing || this.hasScheduledNextFrame;
   }
 
-  private syncCanvasRedraw(nowMs: number) {
-    const redrawStart = debugNow();
-    if (this.isRedrawing) return;
-    this._beforeRedraw();
-    this.isRedrawing = true;
-    for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs);
-    this.isRedrawing = false;
-    this._afterRedraw();
-    for (const cb of this._pendingCallbacks) {
-      cb(nowMs);
+  private syncDomRedraw() {
+    const redrawStart = performance.now();
+    this.syncDomRedrawFn();
+    if (this.recordPerfStats) {
+      this.perfStats.domRedraw.addValue(performance.now() - redrawStart);
     }
-    this._pendingCallbacks.splice(0, this._pendingCallbacks.length);
-    if (perfDebug()) {
-      this.perfStats.rafCanvas.addValue(debugNow() - redrawStart);
+  }
+
+  private syncCanvasRedraw() {
+    const redrawStart = performance.now();
+    if (this.isRedrawing) return;
+    this.isRedrawing = true;
+    this.canvasRedrawCallbacks.forEach((cb) => cb());
+    this.isRedrawing = false;
+    if (this.recordPerfStats) {
+      this.perfStats.rafCanvas.addValue(performance.now() - redrawStart);
     }
   }
 
   private maybeScheduleAnimationFrame(force = false) {
     if (this.hasScheduledNextFrame) return;
-    if (this.actionCallbacks.size !== 0 || force) {
+    if (this.animationCallbacks.size !== 0 || force) {
       this.hasScheduledNextFrame = true;
       window.requestAnimationFrame(this.onAnimationFrame.bind(this));
     }
   }
 
-  private onAnimationFrame(nowMs: number) {
+  private onAnimationFrame(lastFrameMs: number) {
     if (this._shutdown) return;
-    const rafStart = debugNow();
     this.hasScheduledNextFrame = false;
-
     const doFullRedraw = this.requestedFullRedraw;
     this.requestedFullRedraw = false;
 
-    const actionTime = measure(() => {
-      for (const action of this.actionCallbacks) action(nowMs);
-    });
+    const tStart = performance.now();
+    this.animationCallbacks.forEach((cb) => cb(lastFrameMs));
+    const tAnim = performance.now();
+    doFullRedraw && this.syncDomRedraw();
+    const tDom = performance.now();
+    this.syncCanvasRedraw();
+    const tCanvas = performance.now();
 
-    const domTime = measure(() => {
-      if (doFullRedraw) this.syncDomRedraw(nowMs);
-    });
-    const canvasTime = measure(() => this.syncCanvasRedraw(nowMs));
-
-    const totalRafTime = debugNow() - rafStart;
-    this.updatePerfStats(actionTime, domTime, canvasTime, totalRafTime);
-    perfDisplay.renderPerfStats(this);
-
+    const animTime = tAnim - tStart;
+    const domTime = tDom - tAnim;
+    const canvasTime = tCanvas - tDom;
+    const totalTime = tCanvas - tStart;
+    this.updatePerfStats(animTime, domTime, canvasTime, totalTime);
     this.maybeScheduleAnimationFrame();
+
+    if (doFullRedraw && this.postRedrawCallbacks.length > 0) {
+      const pendingCbs = this.postRedrawCallbacks.splice(0); // splice = clear.
+      pendingCbs.forEach((cb) => cb());
+    }
   }
 
   private updatePerfStats(
@@ -202,42 +161,12 @@
     canvasTime: number,
     totalRafTime: number,
   ) {
-    if (!perfDebug()) return;
+    if (!this.recordPerfStats) return;
     this.perfStats.rafActions.addValue(actionsTime);
     this.perfStats.rafDom.addValue(domTime);
     this.perfStats.rafCanvas.addValue(canvasTime);
     this.perfStats.rafTotal.addValue(totalRafTime);
   }
-
-  renderPerfStats() {
-    return m(
-      'div',
-      m('div', [
-        m('button', {onclick: () => this.scheduleRedraw()}, 'Do Canvas Redraw'),
-        '   |   ',
-        m(
-          'button',
-          {onclick: () => this.scheduleFullRedraw()},
-          'Do Full Redraw',
-        ),
-      ]),
-      m('div', 'Raf Timing ' + '(Total may not add up due to imprecision)'),
-      m(
-        'table',
-        statTableHeader(),
-        statTableRow('Actions', this.perfStats.rafActions),
-        statTableRow('Dom', this.perfStats.rafDom),
-        statTableRow('Canvas', this.perfStats.rafCanvas),
-        statTableRow('Total', this.perfStats.rafTotal),
-      ),
-      m(
-        'div',
-        'Dom redraw: ' +
-          `Count: ${this.perfStats.domRedraw.count} | ` +
-          runningStatStr(this.perfStats.domRedraw),
-      ),
-    );
-  }
 }
 
 export const raf = new RafScheduler();
diff --git a/ui/src/core/scroll_helper.ts b/ui/src/core/scroll_helper.ts
index 59b7b11..c732b91 100644
--- a/ui/src/core/scroll_helper.ts
+++ b/ui/src/core/scroll_helper.ts
@@ -35,7 +35,7 @@
   // See comments in ScrollToArgs for the intended semantics.
   scrollTo(args: ScrollToArgs) {
     const {time, track} = args;
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
 
     if (time !== undefined) {
       if (time.end === undefined) {
diff --git a/ui/src/core/timeline.ts b/ui/src/core/timeline.ts
index d91503c..bc8a613 100644
--- a/ui/src/core/timeline.ts
+++ b/ui/src/core/timeline.ts
@@ -46,7 +46,7 @@
 
   set highlightedSliceId(x) {
     this._highlightedSliceId = x;
-    raf.scheduleFullRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   get hoveredNoteTimestamp() {
@@ -55,7 +55,7 @@
 
   set hoveredNoteTimestamp(x) {
     this._hoveredNoteTimestamp = x;
-    raf.scheduleFullRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   get hoveredUtid() {
@@ -64,7 +64,7 @@
 
   set hoveredUtid(x) {
     this._hoveredUtid = x;
-    raf.scheduleFullRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   get hoveredPid() {
@@ -73,7 +73,7 @@
 
   set hoveredPid(x) {
     this._hoveredPid = x;
-    raf.scheduleFullRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   // This is used to calculate the tracks within a Y range for area selection.
@@ -95,7 +95,7 @@
       .scale(ratio, centerPoint, MIN_DURATION)
       .fitWithin(this.traceInfo.start, this.traceInfo.end);
 
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   panVisibleWindow(delta: number) {
@@ -103,7 +103,7 @@
       .translate(delta)
       .fitWithin(this.traceInfo.start, this.traceInfo.end);
 
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   // Given a timestamp, if |ts| is not currently in view move the view to
@@ -136,7 +136,7 @@
 
   deselectArea() {
     this._selectedArea = undefined;
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   get selectedArea(): Area | undefined {
@@ -160,7 +160,7 @@
       .clampDuration(MIN_DURATION)
       .fitWithin(this.traceInfo.start, this.traceInfo.end);
 
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   // Get the bounds of the visible window as a high-precision time span
@@ -174,7 +174,7 @@
 
   set hoverCursorTimestamp(t: time | undefined) {
     this._hoverCursorTimestamp = t;
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   // Offset between t=0 and the configured time domain.
diff --git a/ui/src/core/trace_impl.ts b/ui/src/core/trace_impl.ts
index 2ae2d16..abed7f5 100644
--- a/ui/src/core/trace_impl.ts
+++ b/ui/src/core/trace_impl.ts
@@ -50,6 +50,7 @@
 import {featureFlags} from './feature_flags';
 import {SerializedAppState} from './state_serialization_schema';
 import {PostedTrace} from './trace_source';
+import {PerfManager} from './perf_manager';
 
 /**
  * Handles the per-trace state of the UI
@@ -460,6 +461,10 @@
     }
   }
 
+  get perfDebugging(): PerfManager {
+    return this.appImpl.perfDebugging;
+  }
+
   get trash(): DisposableStack {
     return this.traceCtx.trash;
   }
diff --git a/ui/src/frontend/animation.ts b/ui/src/frontend/animation.ts
index c8428c4..74cf065 100644
--- a/ui/src/frontend/animation.ts
+++ b/ui/src/frontend/animation.ts
@@ -31,12 +31,12 @@
     }
     this.startMs = nowMs;
     this.endMs = nowMs + durationMs;
-    raf.start(this.boundOnAnimationFrame);
+    raf.startAnimation(this.boundOnAnimationFrame);
   }
 
   stop() {
     this.endMs = 0;
-    raf.stop(this.boundOnAnimationFrame);
+    raf.stopAnimation(this.boundOnAnimationFrame);
   }
 
   get startTimeMs(): number {
@@ -45,7 +45,7 @@
 
   private onAnimationFrame(nowMs: number) {
     if (nowMs >= this.endMs) {
-      raf.stop(this.boundOnAnimationFrame);
+      raf.stopAnimation(this.boundOnAnimationFrame);
       return;
     }
     this.onAnimationStep(Math.max(Math.round(nowMs - this.startMs), 0));
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index c09ccbc..b5d57fa 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -867,7 +867,7 @@
     this.countersKey = countersKey;
     this.counters = data;
 
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   private async createTableAndFetchLimits(
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 7ef1cd4..0ca6c01 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -694,7 +694,7 @@
     this.onUpdatedSlices(slices);
     this.slices = slices;
 
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   private rowToSliceInternal(row: RowT): CastInternal<SliceT> {
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 67aa000..4c87e4d 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -63,19 +63,18 @@
 });
 
 function routeChange(route: Route) {
-  raf.scheduleFullRedraw();
-  maybeOpenTraceFromRoute(route);
-  if (route.fragment) {
-    // This needs to happen after the next redraw call. It's not enough
-    // to use setTimeout(..., 0); since that may occur before the
-    // redraw scheduled above.
-    raf.addPendingCallback(() => {
+  raf.scheduleFullRedraw(() => {
+    if (route.fragment) {
+      // This needs to happen after the next redraw call. It's not enough
+      // to use setTimeout(..., 0); since that may occur before the
+      // redraw scheduled above.
       const e = document.getElementById(route.fragment);
       if (e) {
         e.scrollIntoView();
       }
-    });
-  }
+    }
+  });
+  maybeOpenTraceFromRoute(route);
 }
 
 function setupContentSecurityPolicy() {
@@ -226,12 +225,12 @@
   const router = new Router();
   router.onRouteChanged = routeChange;
 
-  raf.domRedraw = () => {
+  raf.initialize(() =>
     m.render(
       document.body,
       m(UiMain, pages.renderPageForCurrentRoute(AppImpl.instance.trace)),
-    );
-  };
+    ),
+  );
 
   if (
     (location.origin.startsWith('http://localhost:') ||
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 21dc29a..ac5b015 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -89,11 +89,11 @@
         onmousemove: (e: MouseEvent) => {
           this.mouseDragging = true;
           this.hoveredX = currentTargetOffset(e).x - TRACK_SHELL_WIDTH;
-          raf.scheduleRedraw();
+          raf.scheduleCanvasRedraw();
         },
         onmouseenter: (e: MouseEvent) => {
           this.hoveredX = currentTargetOffset(e).x - TRACK_SHELL_WIDTH;
-          raf.scheduleRedraw();
+          raf.scheduleCanvasRedraw();
         },
         onmouseout: () => {
           this.hoveredX = null;
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index e3798e1..8eb41ec 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -241,7 +241,7 @@
 
     const cb = (vizTime: HighPrecisionTimeSpan) => {
       this.trace.timeline.updateVisibleTimeHP(vizTime);
-      raf.scheduleRedraw();
+      raf.scheduleCanvasRedraw();
     };
     const pixelBounds = this.extractBounds(this.timeScale);
     const timeScale = this.timeScale;
@@ -445,6 +445,6 @@
         this.overviewData.get(key)!.push(value);
       }
     }
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 }
diff --git a/ui/src/frontend/pan_and_zoom_handler.ts b/ui/src/frontend/pan_and_zoom_handler.ts
index 4536b9e..0009335 100644
--- a/ui/src/frontend/pan_and_zoom_handler.ts
+++ b/ui/src/frontend/pan_and_zoom_handler.ts
@@ -259,12 +259,12 @@
   private onWheel(e: WheelEvent) {
     if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
       this.onPanned(e.deltaX * HORIZONTAL_WHEEL_PAN_SPEED);
-      raf.scheduleRedraw();
+      raf.scheduleCanvasRedraw();
     } else if (e.ctrlKey && this.mousePositionX !== null) {
       const sign = e.deltaY < 0 ? -1 : 1;
       const deltaY = sign * Math.log2(1 + Math.abs(e.deltaY));
       this.onZoomed(this.mousePositionX, deltaY * WHEEL_ZOOM_SPEED);
-      raf.scheduleRedraw();
+      raf.scheduleCanvasRedraw();
     }
   }
 
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index ab6de73..760e098 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -16,13 +16,10 @@
 import {findRef, toHTMLElement} from '../base/dom_utils';
 import {assertExists, assertFalse} from '../base/logging';
 import {
-  PerfStatsSource,
-  RunningStatistics,
-  debugNow,
-  perfDebug,
-  perfDisplay,
+  PerfStats,
+  PerfStatsContainer,
   runningStatStr,
-} from '../core/perf';
+} from '../core/perf_stats';
 import {raf} from '../core/raf_scheduler';
 import {SimpleResizeObserver} from '../base/resize_observer';
 import {canvasClip} from '../base/canvas_utils';
@@ -94,7 +91,7 @@
 }
 
 export class PanelContainer
-  implements m.ClassComponent<PanelContainerAttrs>, PerfStatsSource
+  implements m.ClassComponent<PanelContainerAttrs>, PerfStatsContainer
 {
   private readonly trace: TraceImpl;
   private attrs: PanelContainerAttrs;
@@ -105,11 +102,12 @@
   // Updated every render cycle in the oncreate/onupdate hook
   private panelInfos: PanelInfo[] = [];
 
-  private panelPerfStats = new WeakMap<Panel, RunningStatistics>();
+  private perfStatsEnabled = false;
+  private panelPerfStats = new WeakMap<Panel, PerfStats>();
   private perfStats = {
     totalPanels: 0,
     panelsOnCanvas: 0,
-    renderStats: new RunningStatistics(10),
+    renderStats: new PerfStats(10),
   };
 
   private ctx?: CanvasRenderingContext2D;
@@ -122,16 +120,8 @@
   constructor({attrs}: m.CVnode<PanelContainerAttrs>) {
     this.attrs = attrs;
     this.trace = attrs.trace;
-    const onRedraw = () => this.renderCanvas();
-    raf.addRedrawCallback(onRedraw);
-    this.trash.defer(() => {
-      raf.removeRedrawCallback(onRedraw);
-    });
-
-    perfDisplay.addContainer(this);
-    this.trash.defer(() => {
-      perfDisplay.removeContainer(this);
-    });
+    this.trash.use(raf.addCanvasRedrawCallback(() => this.renderCanvas()));
+    this.trash.use(attrs.trace.perfDebugging.addContainer(this));
   }
 
   getPanelsInRegion(
@@ -352,7 +342,7 @@
 
     const ctx = this.ctx;
     const vc = this.virtualCanvas;
-    const redrawStart = debugNow();
+    const redrawStart = performance.now();
 
     ctx.resetTransform();
     ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -367,7 +357,7 @@
     this.drawTopLayerOnCanvas(ctx, vc);
 
     // Collect performance as the last thing we do.
-    const redrawDur = debugNow() - redrawStart;
+    const redrawDur = performance.now() - redrawStart;
     this.updatePerfStats(
       redrawDur,
       this.panelInfos.length,
@@ -407,12 +397,12 @@
         ctx.save();
         ctx.translate(0, panelTop);
         canvasClip(ctx, 0, 0, panelWidth, panelHeight);
-        const beforeRender = debugNow();
+        const beforeRender = performance.now();
         panel.renderCanvas(ctx, panelSize);
         this.updatePanelStats(
           i,
           panel,
-          debugNow() - beforeRender,
+          performance.now() - beforeRender,
           ctx,
           panelSize,
         );
@@ -505,10 +495,10 @@
     ctx: CanvasRenderingContext2D,
     size: Size2D,
   ) {
-    if (!perfDebug()) return;
+    if (!this.perfStatsEnabled) return;
     let renderStats = this.panelPerfStats.get(panel);
     if (renderStats === undefined) {
-      renderStats = new RunningStatistics();
+      renderStats = new PerfStats();
       this.panelPerfStats.set(panel, renderStats);
     }
     renderStats.addValue(renderTime);
@@ -537,12 +527,16 @@
     totalPanels: number,
     panelsOnCanvas: number,
   ) {
-    if (!perfDebug()) return;
+    if (!this.perfStatsEnabled) return;
     this.perfStats.renderStats.addValue(renderTime);
     this.perfStats.totalPanels = totalPanels;
     this.perfStats.panelsOnCanvas = panelsOnCanvas;
   }
 
+  setPerfStatsEnabled(enable: boolean): void {
+    this.perfStatsEnabled = enable;
+  }
+
   renderPerfStats() {
     return [
       m(
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 7814674..fcae30c 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -133,15 +133,15 @@
           ...pos,
           timescale,
         });
-        raf.scheduleRedraw();
+        raf.scheduleCanvasRedraw();
       },
       onTrackContentMouseOut: () => {
         trackRenderer?.track.onMouseOut?.();
-        raf.scheduleRedraw();
+        raf.scheduleCanvasRedraw();
       },
       onTrackContentClick: (pos, bounds) => {
         const timescale = this.getTimescaleForBounds(bounds);
-        raf.scheduleRedraw();
+        raf.scheduleCanvasRedraw();
         return (
           trackRenderer?.track.onMouseClick?.({
             ...pos,
diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts
index 954da1a..c67f5b7 100644
--- a/ui/src/frontend/ui_main.ts
+++ b/ui/src/frontend/ui_main.ts
@@ -171,7 +171,8 @@
       {
         id: 'perfetto.TogglePerformanceMetrics',
         name: 'Toggle performance metrics',
-        callback: () => app.setPerfDebuggingEnabled(!app.perfDebugging),
+        callback: () =>
+          (app.perfDebugging.enabled = !app.perfDebugging.enabled),
       },
       {
         id: 'perfetto.ShareTrace',
@@ -652,7 +653,7 @@
         children,
         m(CookieConsent),
         maybeRenderFullscreenModalDialog(),
-        AppImpl.instance.perfDebugging && m('.perf-stats'),
+        AppImpl.instance.perfDebugging.renderPerfStats(),
       ),
     );
   }
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 75f2aba..9509c1d 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -145,7 +145,7 @@
         const rect = dom.getBoundingClientRect();
         const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH);
         timeline.zoomVisibleWindow(1 - zoomRatio, centerPoint);
-        raf.scheduleRedraw();
+        raf.scheduleCanvasRedraw();
       },
       editSelection: (currentPx: number) => {
         if (this.timelineWidthPx === undefined) return false;
@@ -257,7 +257,7 @@
           }
           this.showPanningHint = true;
         }
-        raf.scheduleRedraw();
+        raf.scheduleCanvasRedraw();
       },
       endSelection: (edit: boolean) => {
         this.selectedContainer = undefined;
diff --git a/ui/src/frontend/widgets/sql/table/state.ts b/ui/src/frontend/widgets/sql/table/state.ts
index 1f513b8..4540214 100644
--- a/ui/src/frontend/widgets/sql/table/state.ts
+++ b/ui/src/frontend/widgets/sql/table/state.ts
@@ -331,8 +331,15 @@
       this.rowCount = undefined;
     }
 
-    // Run a delayed UI update to avoid flickering if the query returns quickly.
-    raf.scheduleDelayedFullRedraw();
+    // Schedule a full redraw to happen after a short delay (50 ms).
+    // This is done to prevent flickering / visual noise and allow the UI to fetch
+    // the initial data from the Trace Processor.
+    // There is a chance that someone else schedules a full redraw in the
+    // meantime, forcing the flicker, but in practice it works quite well and
+    // avoids a lot of complexity for the callers.
+    // 50ms is half of the responsiveness threshold (100ms):
+    // https://web.dev/rail/#response-process-events-in-under-50ms
+    setTimeout(() => raf.scheduleFullRedraw(), 50);
 
     if (!filtersMatch) {
       this.rowCount = await this.loadRowCount();