From d450c5bd8353d1a8b3111f2e19158df5828d9fd1 Mon Sep 17 00:00:00 2001 From: Steve Golton Date: Fri, 7 Aug 2026 16:37:27 +0100 Subject: [PATCH] ui: Consolidate flow events into single plugin and extract connected flows Merge FlowEventsPanel into FlowEvents, move flow navigation commands and overlay rendering into the plugin, and remove flow state from core TraceImpl. Extract connected flow queries into a standalone helper to decouple ThreadSliceDetailsPanel from core flow state. --- ui/src/components/details/connected_flows.ts | 97 ++++++++++++++++++ .../details/thread_slice_details_tab.ts | 62 ++++++------ ui/src/core/embedder/default_plugins.ts | 2 +- ui/src/core/trace_impl.ts | 10 -- .../dev.perfetto.CoreCommands/index.ts | 28 ------ .../flow_events_panel.ts | 7 +- .../flow_events_renderer.ts | 21 ++-- .../dev.perfetto.FlowEvents}/flow_manager.ts | 31 +++--- .../dev.perfetto.FlowEvents}/flow_types.ts | 4 +- .../dev.perfetto.FlowEvents/index.ts | 99 +++++++++++++++++++ .../dev.perfetto.FlowEventsPanel/index.ts | 46 --------- .../dev.perfetto.Timeline/track_tree_view.ts | 4 - 12 files changed, 265 insertions(+), 146 deletions(-) create mode 100644 ui/src/components/details/connected_flows.ts rename ui/src/core_plugins/{dev.perfetto.FlowEventsPanel => dev.perfetto.FlowEvents}/flow_events_panel.ts (96%) rename ui/src/core_plugins/{dev.perfetto.Timeline => dev.perfetto.FlowEvents}/flow_events_renderer.ts (93%) rename ui/src/{core => core_plugins/dev.perfetto.FlowEvents}/flow_manager.ts (95%) rename ui/src/{core => core_plugins/dev.perfetto.FlowEvents}/flow_types.ts (94%) create mode 100644 ui/src/core_plugins/dev.perfetto.FlowEvents/index.ts delete mode 100644 ui/src/core_plugins/dev.perfetto.FlowEventsPanel/index.ts diff --git a/ui/src/components/details/connected_flows.ts b/ui/src/components/details/connected_flows.ts new file mode 100644 index 00000000000..a6870352eb0 --- /dev/null +++ b/ui/src/components/details/connected_flows.ts @@ -0,0 +1,97 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use size 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 {Time, type time} from '../../base/time'; +import type {Engine} from '../../trace_processor/engine'; +import {LONG, NUM, STR_NULL} from '../../trace_processor/query_result'; +import {asSliceSqlId, type SliceSqlId} from '../sql_utils/core_types'; + +export interface FlowRow { + readonly id: number; + readonly sliceId: SliceSqlId; + readonly sliceName: string; + readonly sliceChromeCustomName?: string; + readonly sliceStartTs: time; + readonly sliceEndTs: time; + readonly threadName: string; + readonly processName: string; +} + +export interface DirectlyConnectedFlows { + readonly preceding: readonly FlowRow[]; + readonly following: readonly FlowRow[]; +} + +export async function getConnectedFlows( + engine: Engine, + sliceId: number, +): Promise { + const query = ` + INCLUDE PERFETTO MODULE slices.flow; + + select + f.id as id, + f.slice_out as sliceOut, + f.slice_in as sliceIn, + t.id as otherSliceId, + t.name as otherSliceName, + t.ts as otherSliceStartTs, + (t.ts + t.dur) as otherSliceEndTs, + (thread.name || ' ' || thread.tid) as otherThreadName, + (process.name || ' ' || process.pid) as otherProcessName + from directly_connected_flow(${sliceId}) f + join slice t on (case when f.slice_in = ${sliceId} then f.slice_out else f.slice_in end) = t.id + left join thread_track track on track.id = t.track_id + left join thread using (utid) + left join process using (upid) + `; + + const result = await engine.query(query); + const preceding: FlowRow[] = []; + const following: FlowRow[] = []; + + const it = result.iter({ + id: NUM, + sliceOut: NUM, + sliceIn: NUM, + otherSliceId: NUM, + otherSliceName: STR_NULL, + otherSliceStartTs: LONG, + otherSliceEndTs: LONG, + otherThreadName: STR_NULL, + otherProcessName: STR_NULL, + }); + + const nullToStr = (s: null | string): string => (s === null ? 'NULL' : s); + + for (; it.valid(); it.next()) { + const row: FlowRow = { + id: it.id, + sliceId: asSliceSqlId(it.otherSliceId), + sliceName: nullToStr(it.otherSliceName), + sliceStartTs: Time.fromRaw(it.otherSliceStartTs), + sliceEndTs: Time.fromRaw(it.otherSliceEndTs), + threadName: nullToStr(it.otherThreadName), + processName: nullToStr(it.otherProcessName), + }; + + if (it.sliceIn === sliceId) { + preceding.push(row); + } else { + following.push(row); + } + } + + return {preceding, following}; +} diff --git a/ui/src/components/details/thread_slice_details_tab.ts b/ui/src/components/details/thread_slice_details_tab.ts index d29ecbacb90..b15305cc83c 100644 --- a/ui/src/components/details/thread_slice_details_tab.ts +++ b/ui/src/components/details/thread_slice_details_tab.ts @@ -23,7 +23,6 @@ import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout'; import {MenuItem, PopupMenu} from '../../widgets/menu'; import {Section} from '../../widgets/section'; import {Tree} from '../../widgets/tree'; -import type {FlowPoint} from '../../core/flow_types'; import {hasArgs} from './args'; import { type DistributionScope, @@ -40,12 +39,10 @@ import { import {asSliceSqlId} from '../sql_utils/core_types'; import {DurationWidget} from '../widgets/duration'; import {Grid, GridCell, GridHeaderCell} from '../../widgets/grid'; -import {ensureIsInstance} from '../../base/assert'; import type {Trace} from '../../public/trace'; import type {TrackEventDetailsPanel} from '../../public/details_panel'; import type {TrackEventSelection} from '../../public/selection'; import {extensions} from '../extensions'; -import {TraceImpl} from '../../core/trace_impl'; import {renderSliceArguments} from './slice_args'; import {SLICE_TABLE} from '../widgets/sql/table_definitions'; import {TrackEventRef} from '../widgets/track_event_ref'; @@ -59,6 +56,11 @@ import { titleWithHelp, } from '../distribution_panel'; import type {Dataset} from '../../trace_processor/dataset'; +import { + type DirectlyConnectedFlows, + type FlowRow, + getConnectedFlows, +} from './connected_flows'; interface ContextMenuItem { name: string; @@ -221,17 +223,18 @@ export interface ThreadSliceDetailsPanelAttrs { export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel { private sliceDetails?: SliceDetails; private breakdownByThreadState?: BreakdownByThreadState; + private connectedFlows: DirectlyConnectedFlows = { + preceding: [], + following: [], + }; private distributionLoaded = false; private distributionScope: DistributionScope = 'track'; private cachedWholeTraceDataset?: {dataset: Dataset | undefined}; - private readonly trace: TraceImpl; + private readonly trace: Trace; private readonly attrs: ThreadSliceDetailsPanelAttrs; constructor(trace: Trace, attrs?: ThreadSliceDetailsPanelAttrs) { - // Rationale for the assertIsInstance: ThreadSliceDetailsPanel requires a - // TraceImpl (because of flows) but here we must take a Trace interface, - // because this track is exposed to plugins (which see only Trace). - this.trace = ensureIsInstance(trace, TraceImpl); + this.trace = trace; this.attrs = attrs ?? {}; } @@ -240,16 +243,15 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel { const {eventId} = selection; const details = await getSliceDetails(trace.engine, eventId); - if ( - details !== undefined && - details.thread !== undefined && - details.dur > 0 - ) { - this.breakdownByThreadState = await breakDownIntervalByThreadState( - trace.engine, - TimeSpan.fromTimeAndDuration(details.ts, details.dur), - details.thread.utid, - ); + if (details !== undefined) { + this.connectedFlows = await getConnectedFlows(trace.engine, details.id); + if (details.thread !== undefined && details.dur > 0) { + this.breakdownByThreadState = await breakDownIntervalByThreadState( + trace.engine, + TimeSpan.fromTimeAndDuration(details.ts, details.dur), + details.thread.utid, + ); + } } this.sliceDetails = details; @@ -335,8 +337,7 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel { } private renderPrecedingFlows(slice: SliceDetails): m.Children { - const flows = this.trace.flows.connectedFlows; - const inFlows = flows.filter(({end}) => end.sliceId === slice.id); + const inFlows = this.connectedFlows.preceding; if (inFlows.length > 0) { const isRunTask = @@ -358,18 +359,18 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel { m(TrackEventRef, { trace: this.trace, table: 'slice', - id: flow.begin.sliceId, - name: flow.begin.sliceChromeCustomName ?? flow.begin.sliceName, + id: flow.sliceId, + name: flow.sliceChromeCustomName ?? flow.sliceName, }), ), m( GridCell, m(DurationWidget, { trace: this.trace, - dur: flow.end.sliceStartTs - flow.begin.sliceEndTs, + dur: slice.ts - flow.sliceEndTs, }), ), - m(GridCell, this.getThreadNameForFlow(flow.begin, !isRunTask)), + m(GridCell, this.getThreadNameForFlow(flow, !isRunTask)), ]), }), ); @@ -379,8 +380,7 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel { } private renderFollowingFlows(slice: SliceDetails): m.Children { - const flows = this.trace.flows.connectedFlows; - const outFlows = flows.filter(({begin}) => begin.sliceId === slice.id); + const outFlows = this.connectedFlows.following; if (outFlows.length > 0) { const isPostTask = @@ -402,18 +402,18 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel { m(TrackEventRef, { trace: this.trace, table: 'slice', - id: flow.end.sliceId, - name: flow.end.sliceChromeCustomName ?? flow.end.sliceName, + id: flow.sliceId, + name: flow.sliceChromeCustomName ?? flow.sliceName, }), ), m( GridCell, m(DurationWidget, { trace: this.trace, - dur: flow.end.sliceStartTs - flow.begin.sliceEndTs, + dur: flow.sliceStartTs - (slice.ts + slice.dur), }), ), - m(GridCell, this.getThreadNameForFlow(flow.end, !isPostTask)), + m(GridCell, this.getThreadNameForFlow(flow, !isPostTask)), ]), }), ); @@ -423,7 +423,7 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel { } private getThreadNameForFlow( - flow: FlowPoint, + flow: FlowRow, includeProcessName: boolean, ): string { return includeProcessName diff --git a/ui/src/core/embedder/default_plugins.ts b/ui/src/core/embedder/default_plugins.ts index 0008eac4cc9..4370fd8137d 100644 --- a/ui/src/core/embedder/default_plugins.ts +++ b/ui/src/core/embedder/default_plugins.ts @@ -64,7 +64,7 @@ export const defaultPlugins = [ 'dev.perfetto.ExtensionServers', 'dev.perfetto.FlagsPage', 'dev.perfetto.FirefoxProfilerMarkers', - 'dev.perfetto.FlowEventsPanel', + 'dev.perfetto.FlowEvents', 'dev.perfetto.Frames', 'dev.perfetto.Ftrace', 'dev.perfetto.GlobalGroups', diff --git a/ui/src/core/trace_impl.ts b/ui/src/core/trace_impl.ts index c9a357881ef..3297b913165 100644 --- a/ui/src/core/trace_impl.ts +++ b/ui/src/core/trace_impl.ts @@ -32,7 +32,6 @@ import type {SidebarMenuItem} from '../public/sidebar'; import {ScrollHelper} from './scroll_helper'; import type {Selection, SelectionOpts} from '../public/selection'; import type {SearchResult} from '../public/search'; -import {FlowManager} from './flow_manager'; import type {AppImpl, OpenTraceArrayBufArgs} from './app_impl'; import type {PluginManagerImpl} from './plugin_manager'; import type {RouteArgs} from '../public/route_schema'; @@ -76,7 +75,6 @@ export class TraceImpl implements Trace, Disposable { readonly tracks = new TrackManagerImpl(); readonly workspaces = new WorkspaceManagerImpl(); readonly notes = new NoteManagerImpl(); - readonly flows: FlowManager; readonly scrollHelper: ScrollHelper; readonly trash = new DisposableStack(); readonly onTraceReady = new EvtSource(); @@ -126,12 +124,6 @@ export class TraceImpl implements Trace, Disposable { } }; - this.flows = new FlowManager( - engine.getProxy('FlowManager'), - this.tracks, - this.selection, - ); - this.search = new SearchManagerImpl({ timeline: this.timeline, trackManager: this.tracks, @@ -209,8 +201,6 @@ export class TraceImpl implements Trace, Disposable { if (switchToCurrentSelectionTab && selection.kind !== 'empty') { this.tabs.showCurrentSelectionTab(); } - - this.flows.updateFlows(selection); } private onResultStep(searchResult: SearchResult) { diff --git a/ui/src/core_plugins/dev.perfetto.CoreCommands/index.ts b/ui/src/core_plugins/dev.perfetto.CoreCommands/index.ts index 3c7e62df006..5683a644286 100644 --- a/ui/src/core_plugins/dev.perfetto.CoreCommands/index.ts +++ b/ui/src/core_plugins/dev.perfetto.CoreCommands/index.ts @@ -617,34 +617,6 @@ export default class CoreCommands implements PerfettoPlugin { defaultHotkey: 'Escape', }); - ctx.commands.registerCommand({ - id: 'dev.perfetto.NextFlow', - name: 'Next flow', - callback: () => ctx.flows.focusOtherFlow('Forward'), - defaultHotkey: 'Mod+]', - }); - - ctx.commands.registerCommand({ - id: 'dev.perfetto.PrevFlow', - name: 'Prev flow', - callback: () => ctx.flows.focusOtherFlow('Backward'), - defaultHotkey: 'Mod+[', - }); - - ctx.commands.registerCommand({ - id: 'dev.perfetto.MoveNextFlow', - name: 'Move next flow', - callback: () => ctx.flows.moveByFocusedFlow('Forward'), - defaultHotkey: ']', - }); - - ctx.commands.registerCommand({ - id: 'dev.perfetto.MovePrevFlow', - name: 'Move prev flow', - callback: () => ctx.flows.moveByFocusedFlow('Backward'), - defaultHotkey: '[', - }); - // Provides a test bed for resolving events using a SQL table name and ID // which is used in deep-linking, amongst other places. ctx.commands.registerCommand({ diff --git a/ui/src/core_plugins/dev.perfetto.FlowEventsPanel/flow_events_panel.ts b/ui/src/core_plugins/dev.perfetto.FlowEvents/flow_events_panel.ts similarity index 96% rename from ui/src/core_plugins/dev.perfetto.FlowEventsPanel/flow_events_panel.ts rename to ui/src/core_plugins/dev.perfetto.FlowEvents/flow_events_panel.ts index 87be6f8f5ca..f7edb3b858c 100644 --- a/ui/src/core_plugins/dev.perfetto.FlowEventsPanel/flow_events_panel.ts +++ b/ui/src/core_plugins/dev.perfetto.FlowEvents/flow_events_panel.ts @@ -18,11 +18,13 @@ import {Checkbox} from '../../widgets/checkbox'; import {Intent} from '../../widgets/common'; import {Icon} from '../../widgets/icon'; import {Tooltip} from '../../widgets/tooltip'; -import {ALL_CATEGORIES, getFlowCategories} from '../../core/flow_types'; +import {ALL_CATEGORIES, getFlowCategories} from './flow_types'; +import type {FlowManager} from './flow_manager'; import type {TraceImpl} from '../../core/trace_impl'; export interface FlowEventsAreaSelectedPanelAttrs { trace: TraceImpl; + flows: FlowManager; } export class FlowEventsAreaSelectedPanel implements m.ClassComponent { @@ -32,8 +34,7 @@ export class FlowEventsAreaSelectedPanel implements m.ClassComponent(); diff --git a/ui/src/core_plugins/dev.perfetto.Timeline/flow_events_renderer.ts b/ui/src/core_plugins/dev.perfetto.FlowEvents/flow_events_renderer.ts similarity index 93% rename from ui/src/core_plugins/dev.perfetto.Timeline/flow_events_renderer.ts rename to ui/src/core_plugins/dev.perfetto.FlowEvents/flow_events_renderer.ts index 785bd8145b3..62480c39c82 100644 --- a/ui/src/core_plugins/dev.perfetto.Timeline/flow_events_renderer.ts +++ b/ui/src/core_plugins/dev.perfetto.FlowEvents/flow_events_renderer.ts @@ -25,7 +25,8 @@ import { ALL_CATEGORIES, type Flow, getFlowCategories, -} from '../../core/flow_types'; +} from './flow_types'; +import type {FlowManager} from './flow_manager'; import type {TraceImpl} from '../../core/trace_impl'; import type {TrackNode} from '../../public/workspace'; @@ -56,10 +57,11 @@ export interface TrackInfo { * Renders the flows overlay on top of the timeline, given the set of panels and * a canvas to draw on. * - * Note: the actual flow data is retrieved from trace.flows, which are produced + * Note: the actual flow data is retrieved from flows, which are produced * by FlowManager. * - * @param trace - The Trace instance, which holds onto the FlowManager. + * @param trace - The Trace instance. + * @param flows - The FlowManager instance. * @param ctx - The canvas to draw on. * @param size - The size of the canvas. * @param tracks - A list of tracks and their vertical positions on the canvas. @@ -71,6 +73,7 @@ export interface TrackInfo { */ export function renderFlows( trace: TraceImpl, + flows: FlowManager, ctx: CanvasRenderingContext2D, size: Size2D, tracks: ReadonlyArray, @@ -107,8 +110,8 @@ export function renderFlows( flow.end.sliceId === trace.timeline.highlightedSliceId || flow.begin.sliceId === trace.timeline.highlightedSliceId; const focused = - flow.id === trace.flows.focusedFlowIdLeft || - flow.id === trace.flows.focusedFlowIdRight; + flow.id === flows.focusedFlowIdLeft || + flow.id === flows.focusedFlowIdRight; let intensity = DEFAULT_FLOW_INTENSITY; let width = DEFAULT_FLOW_WIDTH; @@ -188,17 +191,17 @@ export function renderFlows( }; // Render the connected flows - trace.flows.connectedFlows.forEach((flow) => { + flows.connectedFlows.forEach((flow) => { drawFlow(flow, CONNECTED_FLOW_HUE); }); // Render the selected flows - trace.flows.selectedFlows.forEach((flow) => { + flows.selectedFlows.forEach((flow) => { const categories = getFlowCategories(flow); for (const cat of categories) { if ( - trace.flows.visibleCategories.get(cat) || - trace.flows.visibleCategories.get(ALL_CATEGORIES) + flows.visibleCategories.get(cat) || + flows.visibleCategories.get(ALL_CATEGORIES) ) { drawFlow(flow, SELECTED_FLOW_HUE); break; diff --git a/ui/src/core/flow_manager.ts b/ui/src/core_plugins/dev.perfetto.FlowEvents/flow_manager.ts similarity index 95% rename from ui/src/core/flow_manager.ts rename to ui/src/core_plugins/dev.perfetto.FlowEvents/flow_manager.ts index d13018b0fb4..b153ecc0200 100644 --- a/ui/src/core/flow_manager.ts +++ b/ui/src/core_plugins/dev.perfetto.FlowEvents/flow_manager.ts @@ -12,18 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {Time} from '../base/time'; -import {featureFlags} from './feature_flags'; +import {Time} from '../../base/time'; +import {featureFlags} from '../../core/feature_flags'; import type {FlowDirection, Flow} from './flow_types'; -import {asSliceSqlId} from '../components/sql_utils/core_types'; -import {LONG, NUM, STR_NULL} from '../trace_processor/query_result'; -import type {Track, TrackManager} from '../public/track'; +import {asSliceSqlId} from '../../components/sql_utils/core_types'; +import {LONG, NUM, STR_NULL} from '../../trace_processor/query_result'; +import type {Track, TrackManager} from '../../public/track'; import type { AreaSelection, Selection, SelectionManager, -} from '../public/selection'; -import type {Engine} from '../trace_processor/engine'; +} from '../../public/selection'; +import type {Engine} from '../../trace_processor/engine'; +import type {Raf} from '../../public/raf'; const SHOW_INDIRECT_PRECEDING_FLOWS_FLAG = featureFlags.register({ id: 'showIndirectPrecedingFlows', @@ -47,6 +48,7 @@ export class FlowManager { private engine: Engine, private trackMgr: TrackManager, private selectionMgr: SelectionManager, + private raf?: Raf, ) {} // TODO(primiano): the only reason why this is not done in the constructor is @@ -75,10 +77,10 @@ export class FlowManager { when name="ThreadControllerImpl::RunTask" or name="ThreadPool_RunTask" then printf("RunTask(posted_from=%s:%s)", - EXTRACT_ARG(arg_set_id, "task.posted_from.file_name"), - EXTRACT_ARG(arg_set_id, "task.posted_from.function_name")) - end - from slice where id=$slice_id' + EXTRACT_ARG(arg_set_id, "task.posted_from.file_name"), + EXTRACT_ARG(arg_set_id, "task.posted_from.function_name")) + end + from slice where id=$slice_id' );`); } @@ -448,13 +450,18 @@ export class FlowManager { } } } + this.raf?.scheduleCanvasRedraw(); } private setSelectedFlows(selectedFlows: Flow[]) { this._selectedFlows = selectedFlows; + this.raf?.scheduleCanvasRedraw(); } updateFlows(selection: Selection) { + if (this._curSelection === selection) { + return; + } this.initialize(); this._curSelection = selection; @@ -477,7 +484,7 @@ export class FlowManager { if (selection.kind === 'area') { this.areaSelected(selection); } else { - this.setConnectedFlows([]); + this.setSelectedFlows([]); } } diff --git a/ui/src/core/flow_types.ts b/ui/src/core_plugins/dev.perfetto.FlowEvents/flow_types.ts similarity index 94% rename from ui/src/core/flow_types.ts rename to ui/src/core_plugins/dev.perfetto.FlowEvents/flow_types.ts index d5f73e2fc44..9f97a933e65 100644 --- a/ui/src/core/flow_types.ts +++ b/ui/src/core_plugins/dev.perfetto.FlowEvents/flow_types.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import type {time, duration} from '../base/time'; -import type {SliceSqlId} from '../components/sql_utils/core_types'; +import type {time, duration} from '../../base/time'; +import type {SliceSqlId} from '../../components/sql_utils/core_types'; export interface Flow { id: number; diff --git a/ui/src/core_plugins/dev.perfetto.FlowEvents/index.ts b/ui/src/core_plugins/dev.perfetto.FlowEvents/index.ts new file mode 100644 index 00000000000..9692b55fca8 --- /dev/null +++ b/ui/src/core_plugins/dev.perfetto.FlowEvents/index.ts @@ -0,0 +1,99 @@ +// Copyright (C) 2026 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 type {TraceImpl} from '../../core/trace_impl'; +import type {PerfettoPlugin} from '../../public/plugin'; +import {FlowEventsAreaSelectedPanel} from './flow_events_panel'; +import {renderFlows} from './flow_events_renderer'; +import {FlowManager} from './flow_manager'; + +// Any track that wants to use this plugin to render flows should +export const FLOWS_SLICE_TRACK = 'flows_slice_track'; + +export default class FlowEventsPlugin implements PerfettoPlugin { + static readonly id = 'dev.perfetto.FlowEvents'; + static readonly description = + 'Handles rendering flows on top of slice tracks originating from the ' + + 'slice table, and for the flows area selection panel.'; + + async onTraceLoad(trace: TraceImpl): Promise { + const flows = new FlowManager( + trace.engine, + trace.tracks, + trace.selection, + trace.raf, + ); + + trace.tracks.registerOverlay({ + render(ctx, timescale, size, tracks) { + flows.updateFlows(trace.selection.selection); + renderFlows( + trace, + flows, + ctx, + size, + tracks, + trace.workspaces.currentWorkspace.tracks, + timescale, + ); + }, + }); + + // This type assertion is allowed because we're a core plugin. + trace.selection.registerAreaSelectionTab({ + id: 'flow_events', + name: 'Flow Events', + priority: -100, + render() { + if (flows.selectedFlows.length > 0) { + return { + isLoading: false, + content: m(FlowEventsAreaSelectedPanel, {trace, flows}), + }; + } else { + return undefined; + } + }, + }); + + trace.commands.registerCommand({ + id: 'dev.perfetto.NextFlow', + name: 'Next flow', + callback: () => flows.focusOtherFlow('Forward'), + defaultHotkey: 'Mod+]', + }); + + trace.commands.registerCommand({ + id: 'dev.perfetto.PrevFlow', + name: 'Prev flow', + callback: () => flows.focusOtherFlow('Backward'), + defaultHotkey: 'Mod+[', + }); + + trace.commands.registerCommand({ + id: 'dev.perfetto.MoveNextFlow', + name: 'Move next flow', + callback: () => flows.moveByFocusedFlow('Forward'), + defaultHotkey: ']', + }); + + trace.commands.registerCommand({ + id: 'dev.perfetto.MovePrevFlow', + name: 'Move prev flow', + callback: () => flows.moveByFocusedFlow('Backward'), + defaultHotkey: '[', + }); + } +} diff --git a/ui/src/core_plugins/dev.perfetto.FlowEventsPanel/index.ts b/ui/src/core_plugins/dev.perfetto.FlowEventsPanel/index.ts deleted file mode 100644 index 89d46266904..00000000000 --- a/ui/src/core_plugins/dev.perfetto.FlowEventsPanel/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (C) 2025 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 type {PerfettoPlugin} from '../../public/plugin'; -import type {TraceImpl} from '../../core/trace_impl'; -import {FlowEventsAreaSelectedPanel} from './flow_events_panel'; - -/** - * This plugin is a core plugin because for now flows are stored in the core and - * not exposed to plugins. In the future once we normalize how flows should - * work, we can reassess this and move it into wherever it needs to be. - */ -export default class implements PerfettoPlugin { - static readonly id = 'dev.perfetto.FlowEventsPanel'; - - async onTraceLoad(trace: TraceImpl): Promise { - // This type assertion is allowed because we're a core plugin. - trace.selection.registerAreaSelectionTab({ - id: 'flow_events', - name: 'Flow Events', - priority: -100, - render() { - if (trace.flows.selectedFlows.length > 0) { - return { - isLoading: false, - content: m(FlowEventsAreaSelectedPanel, {trace}), - }; - } else { - return undefined; - } - }, - }); - } -} diff --git a/ui/src/core_plugins/dev.perfetto.Timeline/track_tree_view.ts b/ui/src/core_plugins/dev.perfetto.Timeline/track_tree_view.ts index e611d9a879d..8c85aa14bb9 100644 --- a/ui/src/core_plugins/dev.perfetto.Timeline/track_tree_view.ts +++ b/ui/src/core_plugins/dev.perfetto.Timeline/track_tree_view.ts @@ -60,7 +60,6 @@ import { COLOR_TIMELINE_OVERLAY, TRACK_SHELL_WIDTH, } from '../../frontend/css_constants'; -import {renderFlows} from './flow_events_renderer'; import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper'; import { shiftDragPanInteraction, @@ -341,7 +340,6 @@ export class TrackTreeView implements m.ClassComponent { virtualCanvasSize, renderedTracks, canvasRect, - rootNode, renderer, ); @@ -429,7 +427,6 @@ export class TrackTreeView implements m.ClassComponent { size: Size2D, renderedTracks: ReadonlyArray, floatingCanvasRect: Rect2D, - rootNode: TrackNode, renderer: Renderer, ) { const timelineRect = new Rect2D({ @@ -480,7 +477,6 @@ export class TrackTreeView implements m.ClassComponent { renderer, ); - renderFlows(this.trace, ctx, size, renderedTracks, rootNode, timescale); this.drawHoveredNoteVertical(ctx, timescale, size); this.drawHoveredCursorVertical(ctx, timescale, size); this.drawNoteVerticals(ctx, timescale, size);