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();