Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions ui/src/components/details/connected_flows.ts
Original file line number Diff line number Diff line change
@@ -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<DirectlyConnectedFlows> {
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};
}
62 changes: 31 additions & 31 deletions ui/src/components/details/thread_slice_details_tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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 ?? {};
}

Expand All @@ -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;
Expand Down Expand Up @@ -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 =
Expand All @@ -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)),
]),
}),
);
Expand All @@ -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 =
Expand All @@ -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)),
]),
}),
);
Expand All @@ -423,7 +423,7 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel {
}

private getThreadNameForFlow(
flow: FlowPoint,
flow: FlowRow,
includeProcessName: boolean,
): string {
return includeProcessName
Expand Down
2 changes: 1 addition & 1 deletion ui/src/core/embedder/default_plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 0 additions & 10 deletions ui/src/core/trace_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<void>();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
28 changes: 0 additions & 28 deletions ui/src/core_plugins/dev.perfetto.CoreCommands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FlowEventsAreaSelectedPanelAttrs> {
Expand All @@ -32,8 +34,7 @@ export class FlowEventsAreaSelectedPanel implements m.ClassComponent<FlowEventsA
return;
}

const {trace} = attrs;
const {flows} = trace;
const {flows} = attrs;

const categoryToFlowsNum = new Map<string, number>();

Expand Down
Loading
Loading