diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts index 8005679739..7a4b1a7b0f 100644 --- a/ui/src/common/actions.ts +++ b/ui/src/common/actions.ts @@ -281,14 +281,6 @@ export const StateActions = { state.highlightedSliceId = args.sliceId; }, - setHighlightedFlowLeftId(state: StateDraft, args: {flowId: number}) { - state.focusedFlowIdLeft = args.flowId; - }, - - setHighlightedFlowRightId(state: StateDraft, args: {flowId: number}) { - state.focusedFlowIdRight = args.flowId; - }, - setHoverCursorTimestamp(state: StateDraft, args: {ts: time}) { state.hoverCursorTimestamp = args.ts; }, diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts index 965141edb4..2eccd3e962 100644 --- a/ui/src/common/empty_state.ts +++ b/ui/src/common/empty_state.ts @@ -66,8 +66,6 @@ export function createEmptyState(): State { hoverCursorTimestamp: Time.INVALID, hoveredNoteTimestamp: Time.INVALID, highlightedSliceId: -1, - focusedFlowIdLeft: -1, - focusedFlowIdRight: -1, recordingInProgress: false, recordingCancelled: false, diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts index c814817945..6440a484e8 100644 --- a/ui/src/common/state.ts +++ b/ui/src/common/state.ts @@ -205,8 +205,6 @@ export interface State { hoverCursorTimestamp: time; hoveredNoteTimestamp: time; highlightedSliceId: number; - focusedFlowIdLeft: number; - focusedFlowIdRight: number; /** * Trace recording diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts index 749e3f40ad..ebaf7333ed 100644 --- a/ui/src/controller/trace_controller.ts +++ b/ui/src/controller/trace_controller.ts @@ -51,10 +51,6 @@ import { } from '../trace_processor/wasm_engine_proxy'; import {showModal} from '../widgets/modal'; import {Child, Children, Controller} from './controller'; -import { - FlowEventsController, - FlowEventsControllerArgs, -} from './flow_events_controller'; import {LoadingManager} from './loading_manager'; import {TraceErrorController} from './trace_error_controller'; import { @@ -235,11 +231,6 @@ export class TraceController extends Controller { const engine = assertExists(this.engine); const childControllers: Children = []; - const flowEventsArgs: FlowEventsControllerArgs = {engine}; - childControllers.push( - Child('flowEvents', FlowEventsController, flowEventsArgs), - ); - childControllers.push( Child('traceError', TraceErrorController, {engine}), ); diff --git a/ui/src/core/app_trace_impl.ts b/ui/src/core/app_trace_impl.ts index 2e26407b67..edd2ac12e5 100644 --- a/ui/src/core/app_trace_impl.ts +++ b/ui/src/core/app_trace_impl.ts @@ -40,6 +40,7 @@ import {Selection, SelectionOpts} from '../public/selection'; import {SearchResult} from '../public/search'; import {raf} from './raf_scheduler'; import {PivotTableManager} from './pivot_table_manager'; +import {FlowManager} from './flow_manager'; // The pseudo plugin id used for the core instance of AppImpl. export const CORE_PLUGIN_ID = '__core__'; @@ -178,6 +179,7 @@ class TraceContext implements Disposable { readonly trackMgr = new TrackManagerImpl(); readonly workspaceMgr = new WorkspaceManagerImpl(); readonly noteMgr = new NoteManagerImpl(); + readonly flowMgr: FlowManager; readonly pluginSerializableState = createStore<{[key: string]: {}}>({}); readonly scrollHelper: ScrollHelper; readonly pivotTableMgr; @@ -218,6 +220,13 @@ class TraceContext implements Disposable { engine.getProxy('PivotTableManager'), ); + this.flowMgr = new FlowManager( + engine.getProxy('FlowManager'), + this.trackMgr, + this.selectionMgr, + () => this.workspaceMgr.currentWorkspace, + ); + this.searchMgr = new SearchManagerImpl({ timeline: this.timeline, trackManager: this.trackMgr, @@ -243,6 +252,8 @@ class TraceContext implements Disposable { this.pivotTableMgr.setSelectionArea(selection); } + this.flowMgr.updateFlows(selection); + // TODO(primiano): this is temporarily necessary until we kill // controllers. The flow controller needs to be re-kicked when we change // the selection. @@ -391,6 +402,10 @@ export class TraceImpl implements Trace { return this.traceCtx.pivotTableMgr; } + get flows() { + return this.traceCtx.flowMgr; + } + // App interface implementation. get pluginId(): string { diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/core/flow_manager.ts similarity index 71% rename from ui/src/controller/flow_events_controller.ts rename to ui/src/core/flow_manager.ts index e2dc6cb104..27074076eb 100644 --- a/ui/src/controller/flow_events_controller.ts +++ b/ui/src/core/flow_manager.ts @@ -13,24 +13,24 @@ // limitations under the License. import {Time} from '../base/time'; -import {featureFlags} from '../core/feature_flags'; -import {Flow, globals} from '../frontend/globals'; -import {publishConnectedFlows, publishSelectedFlows} from '../frontend/publish'; +import {featureFlags} from './feature_flags'; +import {FlowDirection, Flow} from './flow_types'; import {asSliceSqlId} from '../trace_processor/sql_utils/core_types'; -import {Engine} from '../trace_processor/engine'; import {LONG, NUM, STR_NULL} from '../trace_processor/query_result'; -import {Controller} from './controller'; -import {Monitor} from '../base/monitor'; import { ACTUAL_FRAMES_SLICE_TRACK_KIND, THREAD_SLICE_TRACK_KIND, } from '../public/track_kinds'; -import {TrackDescriptor} from '../public/track'; -import {AreaSelection} from '../public/selection'; - -export interface FlowEventsControllerArgs { - engine: Engine; -} +import {TrackDescriptor, TrackManager} from '../public/track'; +import { + AreaSelection, + LegacySelection, + Selection, + SelectionManager, +} from '../public/selection'; +import {raf} from './raf_scheduler'; +import {Engine} from '../trace_processor/engine'; +import {Workspace} from '../public/workspace'; const SHOW_INDIRECT_PRECEDING_FLOWS_FLAG = featureFlags.register({ id: 'showIndirectPrecedingFlows', @@ -41,21 +41,36 @@ const SHOW_INDIRECT_PRECEDING_FLOWS_FLAG = featureFlags.register({ defaultValue: false, }); -export class FlowEventsController extends Controller<'main'> { - private readonly monitor = new Monitor([ - () => globals.selectionManager.selection, - ]); - - constructor(private args: FlowEventsControllerArgs) { - super('main'); - +export class FlowManager { + private _connectedFlows: Flow[] = []; + private _selectedFlows: Flow[] = []; + private _curSelection?: LegacySelection; + private _focusedFlowIdLeft = -1; + private _focusedFlowIdRight = -1; + private _visibleCategories = new Map(); + private _initialized = false; + + constructor( + private engine: Engine, + private trackMgr: TrackManager, + private selectionMgr: SelectionManager, + private getCurWorkspace: () => Workspace, + ) {} + + // TODO(primiano): the only reason why this is not done in the constructor is + // because when loading the UI with no trace, we initialize globals with a + // FakeTraceImpl with a FakeEngine, which crashes when issuing queries. + // This can be moved in the ctor once globals go away. + private initialize() { + if (this._initialized) return; + this._initialized = true; // Create |CHROME_CUSTOME_SLICE_NAME| helper, which combines slice name // and args for some slices (scheduler tasks and mojo messages) for more // helpful messages. // In the future, it should be replaced with this a more scalable and // customisable solution. // Note that a function here is significantly faster than a join. - this.args.engine.query(` + this.engine.query(` SELECT CREATE_FUNCTION( 'CHROME_CUSTOM_SLICE_NAME(slice_id LONG)', 'STRING', @@ -76,7 +91,7 @@ export class FlowEventsController extends Controller<'main'> { } async queryFlowEvents(query: string, callback: (flows: Flow[]) => void) { - const result = await this.args.engine.query(query); + const result = await this.engine.query(query); const flows: Flow[] = []; const it = result.iter({ @@ -204,7 +219,7 @@ export class FlowEventsController extends Controller<'main'> { const trackIdToInfo = new Map(); const trackIdToTrack = new Map(); - globals.trackManager + this.trackMgr .getAllTracks() .forEach((trackDescriptor) => trackDescriptor.tags?.trackIds?.forEach((trackId) => @@ -271,7 +286,7 @@ export class FlowEventsController extends Controller<'main'> { if (info === null) { continue; } - const r = await this.args.engine.query(` + const r = await this.engine.query(` SELECT id, layout_depth as depth @@ -353,7 +368,7 @@ export class FlowEventsController extends Controller<'main'> { left join process process_in on process_in.upid = thread_in.upid `; this.queryFlowEvents(query, (flows: Flow[]) => - publishConnectedFlows(flows), + this.setConnectedFlows(flows), ); } @@ -415,22 +430,49 @@ export class FlowEventsController extends Controller<'main'> { (t2.track_id in ${tracks} and (t2.ts <= ${endNs} and t2.ts >= ${startNs})) `; - this.queryFlowEvents(query, (flows: Flow[]) => publishSelectedFlows(flows)); + this.queryFlowEvents(query, (flows: Flow[]) => + this.setSelectedFlows(flows), + ); } - refreshVisibleFlows() { - if (!this.monitor.ifStateChanged()) { - return; + private setConnectedFlows(connectedFlows: Flow[]) { + this._connectedFlows = connectedFlows; + // If a chrome slice is selected and we have any flows in connectedFlows + // we will find the flows on the right and left of that slice to set a default + // focus. In all other cases the focusedFlowId(Left|Right) will be set to -1. + this._focusedFlowIdLeft = -1; + this._focusedFlowIdRight = -1; + if (this._curSelection?.kind === 'SLICE') { + const sliceId = this._curSelection.id; + for (const flow of connectedFlows) { + if (flow.begin.sliceId === sliceId) { + this._focusedFlowIdRight = flow.id; + } + if (flow.end.sliceId === sliceId) { + this._focusedFlowIdLeft = flow.id; + } + } } + raf.scheduleFullRedraw(); + } + + private setSelectedFlows(selectedFlows: Flow[]) { + this._selectedFlows = selectedFlows; + raf.scheduleFullRedraw(); + } + + updateFlows(selection: Selection) { + this.initialize(); + const legacySelection = + selection.kind === 'legacy' ? selection.legacySelection : undefined; + this._curSelection = legacySelection; - const selection = globals.selectionManager.selection; if (selection.kind === 'empty') { - publishConnectedFlows([]); - publishSelectedFlows([]); + this.setConnectedFlows([]); + this.setSelectedFlows([]); return; } - const legacySelection = globals.selectionManager.legacySelection; // TODO(b/155483804): This is a hack as annotation slices don't contain // flows. We should tidy this up when fixing this bug. if ( @@ -440,17 +482,128 @@ export class FlowEventsController extends Controller<'main'> { ) { this.sliceSelected(legacySelection.id); } else { - publishConnectedFlows([]); + this.setConnectedFlows([]); } if (selection.kind === 'area') { this.areaSelected(selection); } else { - publishSelectedFlows([]); + this.setConnectedFlows([]); + } + } + + // Change focus to the next flow event (matching the direction) + focusOtherFlow(direction: FlowDirection) { + const currentSelection = this._curSelection; + if (!currentSelection || currentSelection.kind !== 'SLICE') { + return; + } + const sliceId = currentSelection.id; + if (sliceId === -1) { + return; + } + + const boundFlows = this._connectedFlows.filter( + (flow) => + (flow.begin.sliceId === sliceId && direction === 'Forward') || + (flow.end.sliceId === sliceId && direction === 'Backward'), + ); + + if (direction === 'Backward') { + const nextFlowId = findAnotherFlowExcept( + boundFlows, + this._focusedFlowIdLeft, + ); + this._focusedFlowIdLeft = nextFlowId; + } else { + const nextFlowId = findAnotherFlowExcept( + boundFlows, + this._focusedFlowIdRight, + ); + this._focusedFlowIdRight = nextFlowId; } + raf.scheduleFullRedraw(); } - run() { - this.refreshVisibleFlows(); + // Select the slice connected to the flow in focus + moveByFocusedFlow(direction: FlowDirection): void { + const currentSelection = this._curSelection; + if (!currentSelection || currentSelection.kind !== 'SLICE') { + return; + } + + const sliceId = currentSelection.id; + const flowId = + direction === 'Backward' + ? this._focusedFlowIdLeft + : this._focusedFlowIdRight; + + if (sliceId === -1 || flowId === -1) { + return; + } + + // Find flow that is in focus and select corresponding slice + for (const flow of this._connectedFlows) { + if (flow.id === flowId) { + const flowPoint = direction === 'Backward' ? flow.begin : flow.end; + const track = this.getCurWorkspace().flatTracks.find((t) => { + if (t.uri === undefined) return false; + return this.trackMgr + .getTrack(t.uri) + ?.tags?.trackIds?.includes(flowPoint.trackId); + }); + if (track) { + this.selectionMgr.selectSqlEvent('slice', flowPoint.sliceId, { + pendingScrollId: flowPoint.sliceId, + }); + } + } + } + } + + get connectedFlows() { + return this._connectedFlows; + } + + get selectedFlows() { + return this._selectedFlows; + } + + get focusedFlowIdLeft() { + return this._focusedFlowIdLeft; + } + get focusedFlowIdRight() { + return this._focusedFlowIdRight; + } + + get visibleCategories(): ReadonlyMap { + return this._visibleCategories; + } + + setCategoryVisible(name: string, value: boolean) { + this._visibleCategories.set(name, value); + raf.scheduleFullRedraw(); + } +} + +// Search |boundFlows| for |flowId| and return the id following it. +// Returns the first flow id if nothing was found or |flowId| was the last flow +// in |boundFlows|, and -1 if |boundFlows| is empty +function findAnotherFlowExcept(boundFlows: Flow[], flowId: number): number { + let selectedFlowFound = false; + + if (boundFlows.length === 0) { + return -1; + } + + for (const flow of boundFlows) { + if (selectedFlowFound) { + return flow.id; + } + + if (flow.id === flowId) { + selectedFlowFound = true; + } } + return boundFlows[0].id; } diff --git a/ui/src/core/flow_types.ts b/ui/src/core/flow_types.ts new file mode 100644 index 0000000000..bed9dae612 --- /dev/null +++ b/ui/src/core/flow_types.ts @@ -0,0 +1,55 @@ +// 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 {time, duration} from '../base/time'; +import {SliceSqlId} from '../trace_processor/sql_utils/core_types'; + +export interface Flow { + id: number; + + begin: FlowPoint; + end: FlowPoint; + dur: duration; + + // Whether this flow connects a slice with its descendant. + flowToDescendant: boolean; + + category?: string; + name?: string; +} + +export interface FlowPoint { + trackId: number; + + sliceName: string; + sliceCategory: string; + sliceId: SliceSqlId; + sliceStartTs: time; + sliceEndTs: time; + // Thread and process info. Only set in sliceSelected not in areaSelected as + // the latter doesn't display per-flow info and it'd be a waste to join + // additional tables for undisplayed info in that case. Nothing precludes + // adding this in a future iteration however. + threadName: string; + processName: string; + + depth: number; + + // TODO(altimin): Ideally we should have a generic mechanism for allowing to + // customise the name here, but for now we are hardcording a few + // Chrome-specific bits in the query here. + sliceChromeCustomName?: string; +} + +export type FlowDirection = 'Forward' | 'Backward'; diff --git a/ui/src/frontend/aggregation_panel.ts b/ui/src/frontend/aggregation_panel.ts index ad72c9e1bc..8f7c4dfb20 100644 --- a/ui/src/frontend/aggregation_panel.ts +++ b/ui/src/frontend/aggregation_panel.ts @@ -20,7 +20,6 @@ import { isEmptyData, } from '../public/aggregation'; import {colorForState} from '../core/colorizer'; -import {globals} from './globals'; import {DurationWidget} from './widgets/duration'; import {EmptyState} from '../widgets/empty_state'; import {Anchor} from '../widgets/anchor'; @@ -37,6 +36,12 @@ export interface AggregationPanelAttrs { export class AggregationPanel implements m.ClassComponent { + private trace: TraceImpl; + + constructor({attrs}: m.CVnode) { + this.trace = attrs.trace; + } + view({attrs}: m.CVnode) { if (!attrs.data || isEmptyData(attrs.data)) { return m( @@ -50,7 +55,7 @@ export class AggregationPanel { icon: Icons.ChangeTab, onclick: () => { - globals.tabManager.showCurrentSelectionTab(); + this.trace.tabs.showCurrentSelectionTab(); }, }, 'Go to current selection tab', @@ -144,7 +149,7 @@ export class AggregationPanel } showTimeRange() { - const selection = globals.selectionManager.selection; + const selection = this.trace.selection.selection; if (selection.kind !== 'area') return undefined; const duration = selection.end - selection.start; return m( diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts index 2eedad2952..a5a8ba3d7d 100644 --- a/ui/src/frontend/aggregation_tab.ts +++ b/ui/src/frontend/aggregation_tab.ts @@ -14,7 +14,6 @@ import m from 'mithril'; import {AggregationPanel} from './aggregation_panel'; -import {globals} from './globals'; import {isEmptyData} from '../public/aggregation'; import {DetailsShell} from '../widgets/details_shell'; import {Button, ButtonBar} from '../widgets/button'; @@ -48,16 +47,20 @@ interface View { export type AreaDetailsPanelAttrs = {trace: TraceImpl}; class AreaDetailsPanel implements m.ClassComponent { - private readonly monitor = new Monitor([ - () => globals.selectionManager.selection, - ]); + private trace: TraceImpl; + private monitor: Monitor; private currentTab: string | undefined = undefined; private cpuProfileFlamegraphAttrs?: QueryFlamegraphAttrs; private perfSampleFlamegraphAttrs?: QueryFlamegraphAttrs; private sliceFlamegraphAttrs?: QueryFlamegraphAttrs; - private getCurrentView(trace: TraceImpl): string | undefined { - const types = this.getViews(trace).map(({key}) => key); + constructor({attrs}: m.CVnode) { + this.trace = attrs.trace; + this.monitor = new Monitor([() => this.trace.selection.selection]); + } + + private getCurrentView(): string | undefined { + const types = this.getViews().map(({key}) => key); if (types.length === 0) { return undefined; @@ -74,22 +77,27 @@ class AreaDetailsPanel implements m.ClassComponent { return this.currentTab; } - private getViews(trace: TraceImpl): View[] { + private getViews(): View[] { const views: View[] = []; - for (const aggregator of trace.selection.aggregation.aggregators) { + for (const aggregator of this.trace.selection.aggregation.aggregators) { const aggregatorId = aggregator.id; - const value = trace.selection.aggregation.getAggregatedData(aggregatorId); + const value = + this.trace.selection.aggregation.getAggregatedData(aggregatorId); if (value !== undefined && !isEmptyData(value)) { views.push({ key: value.tabName, name: value.tabName, - content: m(AggregationPanel, {aggregatorId, data: value, trace}), + content: m(AggregationPanel, { + aggregatorId, + data: value, + trace: this.trace, + }), }); } } - const pivotTableState = trace.pivotTable.state; + const pivotTableState = this.trace.pivotTable.state; const tree = pivotTableState.queryResult?.tree; if ( pivotTableState.selectionArea != undefined && @@ -99,29 +107,29 @@ class AreaDetailsPanel implements m.ClassComponent { key: 'pivot_table', name: 'Pivot Table', content: m(PivotTable, { - trace, + trace: this.trace, selectionArea: pivotTableState.selectionArea, }), }); } - this.addFlamegraphView(trace, this.monitor.ifStateChanged(), views); + this.addFlamegraphView(this.trace, this.monitor.ifStateChanged(), views); // Add this after all aggregation panels, to make it appear after 'Slices' - if (globals.selectedFlows.length > 0) { + if (this.trace.flows.selectedFlows.length > 0) { views.push({ key: 'selected_flows', name: 'Flow Events', - content: m(FlowEventsAreaSelectedPanel), + content: m(FlowEventsAreaSelectedPanel, {trace: this.trace}), }); } return views; } - view({attrs}: m.CVnode): m.Children { - const views = this.getViews(attrs.trace); - const currentViewKey = this.getCurrentView(attrs.trace); + view(): m.Children { + const views = this.getViews(); + const currentViewKey = this.getCurrentView(); const aggregationButtons = views.map(({key, name}) => { return m(Button, { @@ -202,7 +210,7 @@ class AreaDetailsPanel implements m.ClassComponent { } private computeCpuProfileFlamegraphAttrs(trace: Trace, isChanged: boolean) { - const currentSelection = globals.selectionManager.selection; + const currentSelection = trace.selection.selection; if (currentSelection.kind !== 'area') { return undefined; } @@ -262,7 +270,7 @@ class AreaDetailsPanel implements m.ClassComponent { } private computePerfSampleFlamegraphAttrs(trace: Trace, isChanged: boolean) { - const currentSelection = globals.selectionManager.selection; + const currentSelection = trace.selection.selection; if (currentSelection.kind !== 'area') { return undefined; } @@ -310,7 +318,7 @@ class AreaDetailsPanel implements m.ClassComponent { } private computeSliceFlamegraphAttrs(trace: Trace, isChanged: boolean) { - const currentSelection = globals.selectionManager.selection; + const currentSelection = trace.selection.selection; if (currentSelection.kind !== 'area') { return undefined; } @@ -371,7 +379,7 @@ export class AggregationsTabs implements Disposable { private trash = new DisposableStack(); constructor(trace: TraceImpl) { - const unregister = globals.tabManager.registerDetailsPanel({ + const unregister = trace.tabs.registerDetailsPanel({ render(selection) { if (selection.kind === 'area') { return m(AreaDetailsPanel, {trace}); diff --git a/ui/src/frontend/flow_events_panel.ts b/ui/src/frontend/flow_events_panel.ts index 7c86c25f63..a4acfefc57 100644 --- a/ui/src/frontend/flow_events_panel.ts +++ b/ui/src/frontend/flow_events_panel.ts @@ -14,11 +14,9 @@ import m from 'mithril'; import {Icons} from '../base/semantic_icons'; -import {Actions} from '../common/actions'; import {raf} from '../core/raf_scheduler'; -import {Flow, globals} from './globals'; -import {DurationWidget} from './widgets/duration'; -import {EmptyState} from '../widgets/empty_state'; +import {Flow} from '../core/flow_types'; +import {TraceImpl} from '../core/app_trace_impl'; export const ALL_CATEGORIES = '_all_'; @@ -35,116 +33,15 @@ export function getFlowCategories(flow: Flow): string[] { return categories; } -export class FlowEventsPanel implements m.ClassComponent { - view() { - const selection = globals.selectionManager.legacySelection; - if (!selection) { - return m( - EmptyState, - { - className: 'pf-noselection', - title: 'Nothing selected', - }, - 'Flow data will appear here', - ); - } - - if (selection.kind !== 'SLICE') { - return m( - EmptyState, - { - className: 'pf-noselection', - title: 'No flow data', - icon: 'warning', - }, - `Flows are not applicable to the selection kind: '${selection.kind}'`, - ); - } - - const flowClickHandler = (sliceId: number, trackId: number) => { - const track = globals.trackManager.findTrack((td) => - td.tags?.trackIds?.includes(trackId), - ); - if (track) { - globals.selectionManager.selectSqlEvent('slice', sliceId, { - switchToCurrentSelectionTab: false, - }); - } - }; - - // Can happen only for flow events version 1 - const haveCategories = - globals.connectedFlows.filter((flow) => flow.category).length > 0; - - const columns = [ - m('th', 'Direction'), - m('th', 'Duration'), - m('th', 'Connected Slice ID'), - m('th', 'Connected Slice Name'), - m('th', 'Thread Out'), - m('th', 'Thread In'), - m('th', 'Process Out'), - m('th', 'Process In'), - ]; - - if (haveCategories) { - columns.push(m('th', 'Flow Category')); - columns.push(m('th', 'Flow Name')); - } - - const rows = [m('tr', columns)]; - - // Fill the table with all the directly connected flow events - globals.connectedFlows.forEach((flow) => { - if ( - selection.id !== flow.begin.sliceId && - selection.id !== flow.end.sliceId - ) { - return; - } - - const outgoing = selection.id === flow.begin.sliceId; - const otherEnd = outgoing ? flow.end : flow.begin; - - const args = { - onclick: () => flowClickHandler(otherEnd.sliceId, otherEnd.trackId), - onmousemove: () => - globals.dispatch( - Actions.setHighlightedSliceId({sliceId: otherEnd.sliceId}), - ), - onmouseleave: () => - globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1})), - }; - - const data = [ - m('td.flow-link', args, outgoing ? 'Outgoing' : 'Incoming'), - m('td.flow-link', args, m(DurationWidget, {dur: flow.dur})), - m('td.flow-link', args, otherEnd.sliceId.toString()), - m('td.flow-link', args, otherEnd.sliceName), - m('td.flow-link', args, flow.begin.threadName), - m('td.flow-link', args, flow.end.threadName), - m('td.flow-link', args, flow.begin.processName), - m('td.flow-link', args, flow.end.processName), - ]; - - if (haveCategories) { - data.push(m('td.flow-info', flow.category ?? '-')); - data.push(m('td.flow-info', flow.name ?? '-')); - } - - rows.push(m('tr', data)); - }); - - return m('.details-panel', [ - m('.details-panel-heading', m('h2', `Flow events`)), - m('.flow-events-table', m('table', rows)), - ]); - } +export interface FlowEventsAreaSelectedPanelAttrs { + trace: TraceImpl; } -export class FlowEventsAreaSelectedPanel implements m.ClassComponent { - view() { - const selection = globals.selectionManager.selection; +export class FlowEventsAreaSelectedPanel + implements m.ClassComponent +{ + view({attrs}: m.CVnode) { + const selection = attrs.trace.selection.selection; if (selection.kind !== 'area') { return; } @@ -170,7 +67,8 @@ export class FlowEventsAreaSelectedPanel implements m.ClassComponent { const categoryToFlowsNum = new Map(); - globals.selectedFlows.forEach((flow) => { + const flows = attrs.trace.flows; + flows.selectedFlows.forEach((flow) => { const categories = getFlowCategories(flow); categories.forEach((cat) => { if (!categoryToFlowsNum.has(cat)) { @@ -180,11 +78,11 @@ export class FlowEventsAreaSelectedPanel implements m.ClassComponent { }); }); - const allWasChecked = globals.visibleFlowCategories.get(ALL_CATEGORIES); + const allWasChecked = flows.visibleCategories.get(ALL_CATEGORIES); rows.push( m('tr.sum', [ m('td.sum-data', 'All'), - m('td.sum-data', globals.selectedFlows.length), + m('td.sum-data', flows.selectedFlows.length), m( 'td.sum-data', m( @@ -192,17 +90,15 @@ export class FlowEventsAreaSelectedPanel implements m.ClassComponent { { onclick: () => { if (allWasChecked) { - globals.visibleFlowCategories.clear(); + for (const k of flows.visibleCategories.keys()) { + flows.setCategoryVisible(k, false); + } } else { categoryToFlowsNum.forEach((_, cat) => { - globals.visibleFlowCategories.set(cat, true); + flows.setCategoryVisible(cat, true); }); } - globals.visibleFlowCategories.set( - ALL_CATEGORIES, - !allWasChecked, - ); - raf.scheduleFullRedraw(); + flows.setCategoryVisible(ALL_CATEGORIES, !allWasChecked); }, }, allWasChecked ? Icons.Checkbox : Icons.BlankCheckbox, @@ -213,8 +109,8 @@ export class FlowEventsAreaSelectedPanel implements m.ClassComponent { categoryToFlowsNum.forEach((num, cat) => { const wasChecked = - globals.visibleFlowCategories.get(cat) || - globals.visibleFlowCategories.get(ALL_CATEGORIES); + flows.visibleCategories.get(cat) || + flows.visibleCategories.get(ALL_CATEGORIES); const data = [ m('td.flow-info', cat), m('td.flow-info', num), @@ -225,9 +121,9 @@ export class FlowEventsAreaSelectedPanel implements m.ClassComponent { { onclick: () => { if (wasChecked) { - globals.visibleFlowCategories.set(ALL_CATEGORIES, false); + flows.setCategoryVisible(ALL_CATEGORIES, false); } - globals.visibleFlowCategories.set(cat, !wasChecked); + flows.setCategoryVisible(cat, !wasChecked); raf.scheduleFullRedraw(); }, }, diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts index e02d12ea00..e69de9db7b 100644 --- a/ui/src/frontend/flow_events_renderer.ts +++ b/ui/src/frontend/flow_events_renderer.ts @@ -16,10 +16,12 @@ import {ArrowHeadStyle, drawBezierArrow} from '../base/canvas/bezier_arrow'; import {Size2D, Point2D, HorizontalBounds} from '../base/geom'; import {Optional} from '../base/utils'; import {ALL_CATEGORIES, getFlowCategories} from './flow_events_panel'; -import {Flow, globals} from './globals'; +import {globals} from './globals'; +import {Flow} from 'src/core/flow_types'; import {RenderedPanelInfo} from './panel_container'; import {TimeScale} from '../base/time_scale'; import {TrackNode} from '../public/workspace'; +import {TraceImpl} from '../core/app_trace_impl'; const TRACK_GROUP_CONNECTION_OFFSET = 5; const TRIANGLE_SIZE = 5; @@ -44,19 +46,21 @@ type VerticalEdgeOrPoint = * 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 globals, which are produced by - * the flow events controller. + * Note: the actual flow data is retrieved from trace.flows, which are produced + * by FlowManager. * + * @param trace - The Trace instance, which holds onto the FlowManager. * @param ctx - The canvas to draw on. * @param size - The size of the canvas. * @param panels - A list of panels and their locations on the canvas. */ export function renderFlows( + trace: TraceImpl, ctx: CanvasRenderingContext2D, size: Size2D, panels: ReadonlyArray, ): void { - const timescale = new TimeScale(globals.timeline.visibleWindow, { + const timescale = new TimeScale(trace.timeline.visibleWindow, { left: 0, right: size.width, }); @@ -72,9 +76,9 @@ export function renderFlows( // the tree to find containing groups. const sqlTrackIdToTrack = new Map(); - globals.workspace.flatTracks.forEach((track) => + trace.workspace.flatTracks.forEach((track) => track.uri - ? globals.trackManager + ? trace.tracks .getTrack(track.uri) ?.tags?.trackIds?.forEach((trackId) => sqlTrackIdToTrack.set(trackId, track), @@ -106,8 +110,8 @@ export function renderFlows( flow.end.sliceId === globals.state.highlightedSliceId || flow.begin.sliceId === globals.state.highlightedSliceId; const focused = - flow.id === globals.state.focusedFlowIdLeft || - flow.id === globals.state.focusedFlowIdRight; + flow.id === globals.trace.flows.focusedFlowIdLeft || + flow.id === globals.trace.flows.focusedFlowIdRight; let intensity = DEFAULT_FLOW_INTENSITY; let width = DEFAULT_FLOW_WIDTH; @@ -181,17 +185,17 @@ export function renderFlows( }; // Render the connected flows - globals.connectedFlows.forEach((flow) => { + globals.trace.flows.connectedFlows.forEach((flow) => { drawFlow(flow, CONNECTED_FLOW_HUE); }); // Render the selected flows - globals.selectedFlows.forEach((flow) => { + globals.trace.flows.selectedFlows.forEach((flow) => { const categories = getFlowCategories(flow); for (const cat of categories) { if ( - globals.visibleFlowCategories.get(cat) || - globals.visibleFlowCategories.get(ALL_CATEGORIES) + globals.trace.flows.visibleCategories.get(cat) || + globals.trace.flows.visibleCategories.get(ALL_CATEGORIES) ) { drawFlow(flow, SELECTED_FLOW_HUE); break; diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts index 57fd4924a4..e0b7e7f878 100644 --- a/ui/src/frontend/globals.ts +++ b/ui/src/frontend/globals.ts @@ -14,7 +14,7 @@ import {assertExists} from '../base/logging'; import {createStore, Store} from '../base/store'; -import {duration, Time, time} from '../base/time'; +import {Time, time} from '../base/time'; import {Actions, DeferredAction} from '../common/actions'; import {CommandManagerImpl} from '../core/command_manager'; import { @@ -30,7 +30,6 @@ import {ServiceWorkerController} from './service_worker_controller'; import {EngineBase} from '../trace_processor/engine'; import {HttpRpcState} from '../trace_processor/http_rpc_engine'; import type {Analytics} from './analytics'; -import {SliceSqlId} from '../trace_processor/sql_utils/core_types'; import {SerializedAppState} from '../common/state_serialization_schema'; import {getServingRoot} from '../base/http_utils'; import {Workspace} from '../public/workspace'; @@ -45,43 +44,6 @@ import {createFakeTraceImpl} from '../common/fake_trace_impl'; type DispatchMultiple = (actions: DeferredAction[]) => void; type TrackDataStore = Map; -export interface FlowPoint { - trackId: number; - - sliceName: string; - sliceCategory: string; - sliceId: SliceSqlId; - sliceStartTs: time; - sliceEndTs: time; - // Thread and process info. Only set in sliceSelected not in areaSelected as - // the latter doesn't display per-flow info and it'd be a waste to join - // additional tables for undisplayed info in that case. Nothing precludes - // adding this in a future iteration however. - threadName: string; - processName: string; - - depth: number; - - // TODO(altimin): Ideally we should have a generic mechanism for allowing to - // customise the name here, but for now we are hardcording a few - // Chrome-specific bits in the query here. - sliceChromeCustomName?: string; -} - -export interface Flow { - id: number; - - begin: FlowPoint; - end: FlowPoint; - dur: duration; - - // Whether this flow connects a slice with its descendant. - flowToDescendant: boolean; - - category?: string; - name?: string; -} - export interface QuantizedLoad { start: time; end: time; @@ -127,9 +89,6 @@ class Globals { private _trackDataStore?: TrackDataStore = undefined; private _overviewStore?: OverviewStore = undefined; private _threadMap?: ThreadMap = undefined; - private _connectedFlows?: Flow[] = undefined; - private _selectedFlows?: Flow[] = undefined; - private _visibleFlowCategories?: Map = undefined; private _numQueriesQueued = 0; private _bufferUsage?: number = undefined; private _recordingLog?: string = undefined; @@ -221,9 +180,6 @@ class Globals { this._trackDataStore = new Map(); this._overviewStore = new Map(); this._threadMap = new Map(); - this._connectedFlows = []; - this._selectedFlows = []; - this._visibleFlowCategories = new Map(); this.engines.clear(); } @@ -292,30 +248,6 @@ class Globals { return assertExists(this._threadMap); } - get connectedFlows() { - return assertExists(this._connectedFlows); - } - - set connectedFlows(connectedFlows: Flow[]) { - this._connectedFlows = assertExists(connectedFlows); - } - - get selectedFlows() { - return assertExists(this._selectedFlows); - } - - set selectedFlows(selectedFlows: Flow[]) { - this._selectedFlows = assertExists(selectedFlows); - } - - get visibleFlowCategories() { - return assertExists(this._visibleFlowCategories); - } - - set visibleFlowCategories(visibleFlowCategories: Map) { - this._visibleFlowCategories = assertExists(visibleFlowCategories); - } - get traceErrors() { return this._traceErrors; } diff --git a/ui/src/frontend/keyboard_event_handler.ts b/ui/src/frontend/keyboard_event_handler.ts deleted file mode 100644 index 72184a5555..0000000000 --- a/ui/src/frontend/keyboard_event_handler.ts +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (C) 2019 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 {Actions} from '../common/actions'; -import {Flow, globals} from './globals'; - -type Direction = 'Forward' | 'Backward'; - -// Search |boundFlows| for |flowId| and return the id following it. -// Returns the first flow id if nothing was found or |flowId| was the last flow -// in |boundFlows|, and -1 if |boundFlows| is empty -function findAnotherFlowExcept(boundFlows: Flow[], flowId: number): number { - let selectedFlowFound = false; - - if (boundFlows.length === 0) { - return -1; - } - - for (const flow of boundFlows) { - if (selectedFlowFound) { - return flow.id; - } - - if (flow.id === flowId) { - selectedFlowFound = true; - } - } - return boundFlows[0].id; -} - -// Change focus to the next flow event (matching the direction) -export function focusOtherFlow(direction: Direction) { - const currentSelection = globals.selectionManager.legacySelection; - if (!currentSelection || currentSelection.kind !== 'SLICE') { - return; - } - const sliceId = currentSelection.id; - if (sliceId === -1) { - return; - } - - const boundFlows = globals.connectedFlows.filter( - (flow) => - (flow.begin.sliceId === sliceId && direction === 'Forward') || - (flow.end.sliceId === sliceId && direction === 'Backward'), - ); - - if (direction === 'Backward') { - const nextFlowId = findAnotherFlowExcept( - boundFlows, - globals.state.focusedFlowIdLeft, - ); - globals.dispatch(Actions.setHighlightedFlowLeftId({flowId: nextFlowId})); - } else { - const nextFlowId = findAnotherFlowExcept( - boundFlows, - globals.state.focusedFlowIdRight, - ); - globals.dispatch(Actions.setHighlightedFlowRightId({flowId: nextFlowId})); - } -} - -// Select the slice connected to the flow in focus -export function moveByFocusedFlow(direction: Direction): void { - const currentSelection = globals.selectionManager.legacySelection; - if (!currentSelection || currentSelection.kind !== 'SLICE') { - return; - } - - const sliceId = currentSelection.id; - const flowId = - direction === 'Backward' - ? globals.state.focusedFlowIdLeft - : globals.state.focusedFlowIdRight; - - if (sliceId === -1 || flowId === -1) { - return; - } - - // Find flow that is in focus and select corresponding slice - for (const flow of globals.connectedFlows) { - if (flow.id === flowId) { - const flowPoint = direction === 'Backward' ? flow.begin : flow.end; - const track = globals.workspace.flatTracks.find((t) => { - if (t.uri === undefined) return false; - return globals.trackManager - .getTrack(t.uri) - ?.tags?.trackIds?.includes(flowPoint.trackId); - }); - if (track) { - globals.selectionManager.selectSqlEvent('slice', flowPoint.sliceId, { - pendingScrollId: flowPoint.sliceId, - }); - } - } - } -} diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts index b12456bd4b..ba02af607a 100644 --- a/ui/src/frontend/publish.ts +++ b/ui/src/frontend/publish.ts @@ -12,11 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {Actions} from '../common/actions'; import {ConversionJobStatusUpdate} from '../common/conversion_jobs'; import {raf} from '../core/raf_scheduler'; import {HttpRpcState} from '../trace_processor/http_rpc_engine'; -import {Flow, globals, QuantizedLoad, ThreadDesc} from './globals'; +import {globals, QuantizedLoad, ThreadDesc} from './globals'; export function publishOverviewData(data: { [key: string]: QuantizedLoad | QuantizedLoad[]; @@ -44,11 +43,6 @@ export function publishTrackData(args: {id: string; data: {}}) { raf.scheduleRedraw(); } -export function publishSelectedFlows(selectedFlows: Flow[]) { - globals.selectedFlows = selectedFlows; - globals.publishRedraw(); -} - export function publishHttpRpcState(httpRpcState: HttpRpcState) { globals.httpRpcState = httpRpcState; raf.scheduleFullRedraw(); @@ -101,29 +95,6 @@ export function publishThreads(data: ThreadDesc[]) { globals.publishRedraw(); } -export function publishConnectedFlows(connectedFlows: Flow[]) { - globals.connectedFlows = connectedFlows; - // If a chrome slice is selected and we have any flows in connectedFlows - // we will find the flows on the right and left of that slice to set a default - // focus. In all other cases the focusedFlowId(Left|Right) will be set to -1. - globals.dispatch(Actions.setHighlightedFlowLeftId({flowId: -1})); - globals.dispatch(Actions.setHighlightedFlowRightId({flowId: -1})); - const currentSelection = globals.selectionManager.legacySelection; - if (currentSelection?.kind === 'SLICE') { - const sliceId = currentSelection.id; - for (const flow of globals.connectedFlows) { - if (flow.begin.sliceId === sliceId) { - globals.dispatch(Actions.setHighlightedFlowRightId({flowId: flow.id})); - } - if (flow.end.sliceId === sliceId) { - globals.dispatch(Actions.setHighlightedFlowLeftId({flowId: flow.id})); - } - } - } - - globals.publishRedraw(); -} - export function publishShowPanningHint() { globals.showPanningHint = true; globals.publishRedraw(); diff --git a/ui/src/frontend/thread_slice_details_tab.ts b/ui/src/frontend/thread_slice_details_tab.ts index cc81ca0549..02fe65a097 100644 --- a/ui/src/frontend/thread_slice_details_tab.ts +++ b/ui/src/frontend/thread_slice_details_tab.ts @@ -27,7 +27,8 @@ import {Section} from '../widgets/section'; import {Tree} from '../widgets/tree'; import {BottomTab, NewBottomTabArgs} from '../public/lib/bottom_tab'; import {addDebugSliceTrack} from '../public/lib/debug_tracks/debug_tracks'; -import {Flow, FlowPoint, globals} from './globals'; +import {globals} from './globals'; +import {Flow, FlowPoint} from 'src/core/flow_types'; import {addQueryResultsTab} from '../public/lib/query_table/query_result_tab'; import {hasArgs, renderArguments} from './slice_args'; import {renderDetails} from './slice_details'; @@ -340,7 +341,7 @@ export class ThreadSliceDetailsTab extends BottomTab end.sliceId === slice.id); if (inFlows.length > 0) { @@ -387,7 +388,7 @@ export class ThreadSliceDetailsTab extends BottomTab begin.sliceId === slice.id); if (outFlows.length > 0) { diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts index 06f1651756..e11d9f4589 100644 --- a/ui/src/frontend/ui_main.ts +++ b/ui/src/frontend/ui_main.ts @@ -40,7 +40,6 @@ import {Sidebar} from './sidebar'; import {Topbar} from './topbar'; import {shareTrace} from './trace_attrs'; import {AggregationsTabs} from './aggregation_tab'; -import {focusOtherFlow, moveByFocusedFlow} from './keyboard_event_handler'; import {publishPermalinkHash} from './publish'; import {OmniboxMode} from '../core/omnibox_manager'; import {PromptOption} from '../public/omnibox'; @@ -307,25 +306,25 @@ export class UiMainPerTrace implements m.ClassComponent { { id: 'perfetto.NextFlow', name: 'Next flow', - callback: () => focusOtherFlow('Forward'), + callback: () => trace.flows.focusOtherFlow('Forward'), defaultHotkey: 'Mod+]', }, { id: 'perfetto.PrevFlow', name: 'Prev flow', - callback: () => focusOtherFlow('Backward'), + callback: () => trace.flows.focusOtherFlow('Backward'), defaultHotkey: 'Mod+[', }, { id: 'perfetto.MoveNextFlow', name: 'Move next flow', - callback: () => moveByFocusedFlow('Forward'), + callback: () => trace.flows.moveByFocusedFlow('Forward'), defaultHotkey: ']', }, { id: 'perfetto.MovePrevFlow', name: 'Move prev flow', - callback: () => moveByFocusedFlow('Backward'), + callback: () => trace.flows.moveByFocusedFlow('Backward'), defaultHotkey: '[', }, { diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts index f7e273f8e0..06068774d1 100644 --- a/ui/src/frontend/viewer_page.ts +++ b/ui/src/frontend/viewer_page.ts @@ -355,7 +355,9 @@ function renderOverlay( using _ = canvasSave(ctx); ctx.translate(TRACK_SHELL_WIDTH, 0); canvasClip(ctx, 0, 0, size.width, size.height); - renderFlows(ctx, size, panels); + + // TODO(primiano): plumb the TraceImpl obj throughout the viwer page. + renderFlows(globals.trace, ctx, size, panels); const timewindow = globals.timeline.visibleWindow; const timescale = new TimeScale(timewindow, {left: 0, right: size.width});