From cf95cb604a925033456443f40a78e8b3bd345fab Mon Sep 17 00:00:00 2001 From: Primiano Tucci Date: Thu, 14 Nov 2024 15:47:14 +0000 Subject: [PATCH 1/2] ui: clean up raf_scheduler.ts and perf.ts - Improve naming and semantic of code in RafScheduler. - Remove coupling between the Perf debugging UI and RafScheduler. - Remove global state in perf.ts - Just use performance.now() unconditionally. The existing comment that says that it is slow is a lie. Bug: 379040490 Change-Id: Iae793df8d83d712720bb7d24c89db3c00eb56f22 --- ui/src/common/track_helper.ts | 2 +- ui/src/core/app_impl.ts | 17 +- ui/src/core/perf.ts | 135 ---------- ui/src/core/perf_manager.ts | 145 +++++++++++ ui/src/core/perf_stats.ts | 78 ++++++ ...erf_unittest.ts => perf_stats_unittest.ts} | 10 +- ui/src/core/raf_scheduler.ts | 237 ++++++------------ ui/src/core/scroll_helper.ts | 2 +- ui/src/core/timeline.ts | 10 +- ui/src/core/trace_impl.ts | 5 + ui/src/frontend/animation.ts | 6 +- ui/src/frontend/base_counter_track.ts | 2 +- ui/src/frontend/base_slice_track.ts | 2 +- ui/src/frontend/index.ts | 23 +- ui/src/frontend/notes_panel.ts | 4 +- ui/src/frontend/overview_timeline_panel.ts | 4 +- ui/src/frontend/pan_and_zoom_handler.ts | 4 +- ui/src/frontend/panel_container.ts | 46 ++-- ui/src/frontend/track_panel.ts | 6 +- ui/src/frontend/ui_main.ts | 5 +- ui/src/frontend/viewer_page.ts | 4 +- ui/src/frontend/widgets/sql/table/state.ts | 11 +- 22 files changed, 386 insertions(+), 372 deletions(-) delete mode 100644 ui/src/core/perf.ts create mode 100644 ui/src/core/perf_manager.ts create mode 100644 ui/src/core/perf_stats.ts rename ui/src/core/{perf_unittest.ts => perf_stats_unittest.ts} (86%) diff --git a/ui/src/common/track_helper.ts b/ui/src/common/track_helper.ts index 3087228884..e9ef6fbaa8 100644 --- a/ui/src/common/track_helper.ts +++ b/ui/src/common/track_helper.ts @@ -95,6 +95,6 @@ export class TimelineFetcher implements Disposable { 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 3336045658..766275501e 100644 --- a/ui/src/core/app_impl.ts +++ b/ui/src/core/app_impl.ts @@ -32,7 +32,7 @@ import {AnalyticsInternal, initAnalytics} from './analytics_impl'; 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 @@ export class AppContext { 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 @@ export class AppContext { }; 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 @@ export class AppImpl implements App { 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 6e9afaf2cd..0000000000 --- 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 0000000000..e63e7e8eda --- /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 { + view({attrs}: m.Vnode) { + 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 0000000000..3f1eda07b3 --- /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 5ba357c2d2..1b24bf51e0 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 before reaching max capacity', () => { }); 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 size is accurate after reaching max capacity', () => { }); 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 before reaching max capacity', () => { }); 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 c6ca0fcdda..b23379ffa2 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'; - -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)), - ); -} +import {PerfStats} from './perf_stats'; -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 @@ export type RedrawCallback = (nowMs: number) => void; // - 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(); +export class RafScheduler { + // These happen at the beginning of any animation frame. Used by Animation. + private animationCallbacks = new Set(); + + // These happen during any animaton frame, after the (optional) DOM redraw. private canvasRedrawCallbacks = new Set(); - private _syncDomRedraw: RedrawCallback = (_) => {}; + + // These happen at the end of full (DOM) animation frames. + private postRedrawCallbacks = new Array(); + 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 perfStats = { - rafActions: new RunningStatistics(), - rafCanvas: new RunningStatistics(), - rafDom: new RunningStatistics(), - rafTotal: new RunningStatistics(), - domRedraw: new RunningStatistics(), + private recordPerfStats = false; + + 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(); - } - - stop(cb: ActionCallback) { - this.actionCallbacks.delete(cb); - } - - addRedrawCallback(cb: RedrawCallback) { - this.canvasRedrawCallbacks.add(cb); - } - - removeRedrawCallback(cb: RedrawCallback) { - this.canvasRedrawCallbacks.delete(cb); + // Called by frontend/index.ts. syncDomRedrawFn is a function that invokes + // m.render() of the root UiMain component. + initialize(syncDomRedrawFn: () => void) { + this.syncDomRedrawFn = syncDomRedrawFn; } - 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); } - shutdown() { - this._shutdown = true; + startAnimation(cb: AnimationCallback) { + this.animationCallbacks.add(cb); + this.maybeScheduleAnimationFrame(); } - set domRedraw(cb: RedrawCallback) { - this._syncDomRedraw = cb; + stopAnimation(cb: AnimationCallback) { + this.animationCallbacks.delete(cb); } - set beforeRedraw(cb: () => void) { - this._beforeRedraw = cb; + addCanvasRedrawCallback(cb: RedrawCallback): Disposable { + this.canvasRedrawCallbacks.add(cb); + const canvasRedrawCallbacks = this.canvasRedrawCallbacks; + return { + [Symbol.dispose]() { + canvasRedrawCallbacks.delete(cb); + }, + }; } - set afterRedraw(cb: () => void) { - this._afterRedraw = cb; + shutdown() { + this._shutdown = true; } - // Schedule re-rendering of virtual DOM and canvas. - scheduleFullRedraw() { - this.requestedFullRedraw = true; - this.maybeScheduleAnimationFrame(true); + setPerfStatsEnabled(enabled: boolean) { + this.recordPerfStats = enabled; + this.scheduleFullRedraw(); } - // 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); + get hasPendingRedraws(): boolean { + return this.isRedrawing || this.hasScheduledNextFrame; } - syncDomRedraw(nowMs: number) { - const redrawStart = debugNow(); - this._syncDomRedraw(nowMs); - if (perfDebug()) { - this.perfStats.domRedraw.addValue(debugNow() - redrawStart); + private syncDomRedraw() { + const redrawStart = performance.now(); + this.syncDomRedrawFn(); + if (this.recordPerfStats) { + this.perfStats.domRedraw.addValue(performance.now() - redrawStart); } } - get hasPendingRedraws(): boolean { - return this.isRedrawing || this.hasScheduledNextFrame; - } - - private syncCanvasRedraw(nowMs: number) { - const redrawStart = debugNow(); + private syncCanvasRedraw() { + const redrawStart = performance.now(); if (this.isRedrawing) return; - this._beforeRedraw(); this.isRedrawing = true; - for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs); + this.canvasRedrawCallbacks.forEach((cb) => cb()); this.isRedrawing = false; - this._afterRedraw(); - for (const cb of this._pendingCallbacks) { - cb(nowMs); - } - this._pendingCallbacks.splice(0, this._pendingCallbacks.length); - if (perfDebug()) { - this.perfStats.rafCanvas.addValue(debugNow() - redrawStart); + 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 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 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 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 @@ export class RafScheduler implements PerfStatsSource { 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 59b7b11138..c732b91ccd 100644 --- a/ui/src/core/scroll_helper.ts +++ b/ui/src/core/scroll_helper.ts @@ -35,7 +35,7 @@ export class ScrollHelper { // 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 d91503c003..fc7de35268 100644 --- a/ui/src/core/timeline.ts +++ b/ui/src/core/timeline.ts @@ -95,7 +95,7 @@ export class TimelineImpl implements Timeline { .scale(ratio, centerPoint, MIN_DURATION) .fitWithin(this.traceInfo.start, this.traceInfo.end); - raf.scheduleRedraw(); + raf.scheduleCanvasRedraw(); } panVisibleWindow(delta: number) { @@ -103,7 +103,7 @@ export class TimelineImpl implements Timeline { .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 @@ export class TimelineImpl implements Timeline { deselectArea() { this._selectedArea = undefined; - raf.scheduleRedraw(); + raf.scheduleCanvasRedraw(); } get selectedArea(): Area | undefined { @@ -160,7 +160,7 @@ export class TimelineImpl implements Timeline { .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 @@ export class TimelineImpl implements Timeline { 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 2ae2d166ef..abed7f5828 100644 --- a/ui/src/core/trace_impl.ts +++ b/ui/src/core/trace_impl.ts @@ -50,6 +50,7 @@ import {FeatureFlagManager, FlagSettings} from '../public/feature_flag'; 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 @@ export class TraceImpl implements Trace { } } + 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 c8428c4fc4..74cf06506a 100644 --- a/ui/src/frontend/animation.ts +++ b/ui/src/frontend/animation.ts @@ -31,12 +31,12 @@ export class Animation { } 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 @@ export class Animation { 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 c09ccbce40..b5d57fae16 100644 --- a/ui/src/frontend/base_counter_track.ts +++ b/ui/src/frontend/base_counter_track.ts @@ -867,7 +867,7 @@ export abstract class BaseCounterTrack implements Track { 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 7ef1cd4665..0ca6c01b54 100644 --- a/ui/src/frontend/base_slice_track.ts +++ b/ui/src/frontend/base_slice_track.ts @@ -694,7 +694,7 @@ export abstract class BaseSliceTrack< this.onUpdatedSlices(slices); this.slices = slices; - raf.scheduleRedraw(); + raf.scheduleCanvasRedraw(); } private rowToSliceInternal(row: RowT): CastInternal { diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts index 67aa000ce0..4c87e4dbd6 100644 --- a/ui/src/frontend/index.ts +++ b/ui/src/frontend/index.ts @@ -63,19 +63,18 @@ const CSP_WS_PERMISSIVE_PORT = featureFlags.register({ }); 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 @@ function onCssLoaded() { 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 21dc29afef..ac5b015931 100644 --- a/ui/src/frontend/notes_panel.ts +++ b/ui/src/frontend/notes_panel.ts @@ -89,11 +89,11 @@ export class NotesPanel implements Panel { 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 e3798e187e..8eb41ecdb4 100644 --- a/ui/src/frontend/overview_timeline_panel.ts +++ b/ui/src/frontend/overview_timeline_panel.ts @@ -241,7 +241,7 @@ export class OverviewTimelinePanel implements Panel { 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 @@ class OverviewDataLoader { 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 4536b9e6fd..00093350b8 100644 --- a/ui/src/frontend/pan_and_zoom_handler.ts +++ b/ui/src/frontend/pan_and_zoom_handler.ts @@ -259,12 +259,12 @@ export class PanAndZoomHandler implements Disposable { 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 ab6de73ab2..760e0980e7 100644 --- a/ui/src/frontend/panel_container.ts +++ b/ui/src/frontend/panel_container.ts @@ -16,13 +16,10 @@ import m from 'mithril'; 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 interface RenderedPanelInfo { } export class PanelContainer - implements m.ClassComponent, PerfStatsSource + implements m.ClassComponent, PerfStatsContainer { private readonly trace: TraceImpl; private attrs: PanelContainerAttrs; @@ -105,11 +102,12 @@ export class PanelContainer // Updated every render cycle in the oncreate/onupdate hook private panelInfos: PanelInfo[] = []; - private panelPerfStats = new WeakMap(); + private perfStatsEnabled = false; + private panelPerfStats = new WeakMap(); private perfStats = { totalPanels: 0, panelsOnCanvas: 0, - renderStats: new RunningStatistics(10), + renderStats: new PerfStats(10), }; private ctx?: CanvasRenderingContext2D; @@ -122,16 +120,8 @@ export class PanelContainer constructor({attrs}: m.CVnode) { 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 @@ export class PanelContainer 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 @@ export class PanelContainer 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 @@ export class PanelContainer 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 @@ export class PanelContainer 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 @@ export class PanelContainer 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 7814674e82..fcae30c7ac 100644 --- a/ui/src/frontend/track_panel.ts +++ b/ui/src/frontend/track_panel.ts @@ -133,15 +133,15 @@ export class TrackPanel implements Panel { ...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 954da1a8da..c67f5b721c 100644 --- a/ui/src/frontend/ui_main.ts +++ b/ui/src/frontend/ui_main.ts @@ -171,7 +171,8 @@ export class UiMainPerTrace implements m.ClassComponent { { 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 @@ export class UiMainPerTrace implements m.ClassComponent { 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 75f2aba74a..9509c1dab9 100644 --- a/ui/src/frontend/viewer_page.ts +++ b/ui/src/frontend/viewer_page.ts @@ -145,7 +145,7 @@ export class ViewerPage implements m.ClassComponent { 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 @@ export class ViewerPage implements m.ClassComponent { } 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 1f513b8678..45402149ca 100644 --- a/ui/src/frontend/widgets/sql/table/state.ts +++ b/ui/src/frontend/widgets/sql/table/state.ts @@ -331,8 +331,15 @@ export class SqlTableState { 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(); From 33e2344d71f032ee5d173cad81b3c418f3589fea Mon Sep 17 00:00:00 2001 From: Primiano Tucci Date: Thu, 14 Nov 2024 17:04:57 +0000 Subject: [PATCH 2/2] ui: remove unnecessary full redraws note/pid hovering affects only the canvas and doesn't require full redraws. This reduces the number of full redraws while panning. Change-Id: I59ff02603b71bb95b007fe17e483704c308666a6 --- ui/src/core/timeline.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/core/timeline.ts b/ui/src/core/timeline.ts index fc7de35268..bc8a61367a 100644 --- a/ui/src/core/timeline.ts +++ b/ui/src/core/timeline.ts @@ -46,7 +46,7 @@ export class TimelineImpl implements Timeline { set highlightedSliceId(x) { this._highlightedSliceId = x; - raf.scheduleFullRedraw(); + raf.scheduleCanvasRedraw(); } get hoveredNoteTimestamp() { @@ -55,7 +55,7 @@ export class TimelineImpl implements Timeline { set hoveredNoteTimestamp(x) { this._hoveredNoteTimestamp = x; - raf.scheduleFullRedraw(); + raf.scheduleCanvasRedraw(); } get hoveredUtid() { @@ -64,7 +64,7 @@ export class TimelineImpl implements Timeline { set hoveredUtid(x) { this._hoveredUtid = x; - raf.scheduleFullRedraw(); + raf.scheduleCanvasRedraw(); } get hoveredPid() { @@ -73,7 +73,7 @@ export class TimelineImpl implements Timeline { set hoveredPid(x) { this._hoveredPid = x; - raf.scheduleFullRedraw(); + raf.scheduleCanvasRedraw(); } // This is used to calculate the tracks within a Y range for area selection.