blob: efa35bd17d0bf8a88e72aeb62103db32c3d01231 [file] [log] [blame]
// Copyright (C) 2020 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} from '../base/time';
import {featureFlags} from './feature_flags';
import {FlowDirection, Flow} from './flow_types';
import {asSliceSqlId} from '../trace_processor/sql_utils/core_types';
import {LONG, NUM, STR_NULL} from '../trace_processor/query_result';
import {
ACTUAL_FRAMES_SLICE_TRACK_KIND,
SLICE_TRACK_KIND,
} from '../public/track_kinds';
import {TrackDescriptor, TrackManager} from '../public/track';
import {AreaSelection, Selection, SelectionManager} from '../public/selection';
import {raf} from './raf_scheduler';
import {Engine} from '../trace_processor/engine';
const SHOW_INDIRECT_PRECEDING_FLOWS_FLAG = featureFlags.register({
id: 'showIndirectPrecedingFlows',
name: 'Show indirect preceding flows',
description:
'Show indirect preceding flows (connected through ancestor ' +
'slices) when a slice is selected.',
defaultValue: false,
});
export class FlowManager {
private _connectedFlows: Flow[] = [];
private _selectedFlows: Flow[] = [];
private _curSelection?: Selection;
private _focusedFlowIdLeft = -1;
private _focusedFlowIdRight = -1;
private _visibleCategories = new Map<string, boolean>();
private _initialized = false;
constructor(
private engine: Engine,
private trackMgr: TrackManager,
private selectionMgr: SelectionManager,
) {}
// 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.engine.query(`
SELECT CREATE_FUNCTION(
'CHROME_CUSTOM_SLICE_NAME(slice_id LONG)',
'STRING',
'select case
when name="Receive mojo message" then
printf("Receive mojo message (interface=%s, hash=%s)",
EXTRACT_ARG(arg_set_id,
"chrome_mojo_event_info.mojo_interface_tag"),
EXTRACT_ARG(arg_set_id, "chrome_mojo_event_info.ipc_hash"))
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'
);`);
}
async queryFlowEvents(query: string, callback: (flows: Flow[]) => void) {
const result = await this.engine.query(query);
const flows: Flow[] = [];
const it = result.iter({
beginSliceId: NUM,
beginTrackId: NUM,
beginSliceName: STR_NULL,
beginSliceChromeCustomName: STR_NULL,
beginSliceCategory: STR_NULL,
beginSliceStartTs: LONG,
beginSliceEndTs: LONG,
beginDepth: NUM,
beginThreadName: STR_NULL,
beginProcessName: STR_NULL,
endSliceId: NUM,
endTrackId: NUM,
endSliceName: STR_NULL,
endSliceChromeCustomName: STR_NULL,
endSliceCategory: STR_NULL,
endSliceStartTs: LONG,
endSliceEndTs: LONG,
endDepth: NUM,
endThreadName: STR_NULL,
endProcessName: STR_NULL,
name: STR_NULL,
category: STR_NULL,
id: NUM,
flowToDescendant: NUM,
});
const nullToStr = (s: null | string): string => {
return s === null ? 'NULL' : s;
};
const nullToUndefined = (s: null | string): undefined | string => {
return s === null ? undefined : s;
};
const nodes = [];
for (; it.valid(); it.next()) {
// Category and name present only in version 1 flow events
// It is most likelly NULL for all other versions
const category = nullToUndefined(it.category);
const name = nullToUndefined(it.name);
const id = it.id;
const begin = {
trackId: it.beginTrackId,
sliceId: asSliceSqlId(it.beginSliceId),
sliceName: nullToStr(it.beginSliceName),
sliceChromeCustomName: nullToUndefined(it.beginSliceChromeCustomName),
sliceCategory: nullToStr(it.beginSliceCategory),
sliceStartTs: Time.fromRaw(it.beginSliceStartTs),
sliceEndTs: Time.fromRaw(it.beginSliceEndTs),
depth: it.beginDepth,
threadName: nullToStr(it.beginThreadName),
processName: nullToStr(it.beginProcessName),
};
const end = {
trackId: it.endTrackId,
sliceId: asSliceSqlId(it.endSliceId),
sliceName: nullToStr(it.endSliceName),
sliceChromeCustomName: nullToUndefined(it.endSliceChromeCustomName),
sliceCategory: nullToStr(it.endSliceCategory),
sliceStartTs: Time.fromRaw(it.endSliceStartTs),
sliceEndTs: Time.fromRaw(it.endSliceEndTs),
depth: it.endDepth,
threadName: nullToStr(it.endThreadName),
processName: nullToStr(it.endProcessName),
};
nodes.push(begin);
nodes.push(end);
flows.push({
id,
begin,
end,
dur: it.endSliceStartTs - it.beginSliceEndTs,
category,
name,
flowToDescendant: !!it.flowToDescendant,
});
}
// Everything below here is a horrible hack to support flows for
// async slice tracks.
// In short the issue is this:
// - For most slice tracks there is a one-to-one mapping between
// the track in the UI and the track in the TP. n.b. Even in this
// case the UI 'trackId' and the TP 'track.id' may not be the
// same. In this case 'depth' in the TP is the exact depth in the
// UI.
// - In the case of aysnc tracks however the mapping is
// one-to-many. Each async slice track in the UI is 'backed' but
// multiple TP tracks. In order to render this track we need
// to adjust depth to avoid overlapping slices. In the render
// path we use experimental_slice_layout for this purpose. This
// is a virtual table in the TP which, for an arbitrary collection
// of TP trackIds, computes for each slice a 'layout_depth'.
// - Everything above in this function and its callers doesn't
// know anything about layout_depth.
//
// So if we stopped here we would have incorrect rendering for
// async slice tracks. Instead we want to 'fix' depth for these
// cases. We do this in two passes.
// - First we collect all the information we need in 'Info' POJOs
// - Secondly we loop over those Infos querying
// the database to find the layout_depth for each sliceId
// TODO(hjd): This should not be needed after TracksV2 lands.
// We end up with one Info POJOs for each UI async slice track
// which has at least one flow {begin,end}ing in one of its slices.
interface Info {
siblingTrackIds: number[];
sliceIds: number[];
nodes: Array<{
sliceId: number;
depth: number;
}>;
}
const trackUriToInfo = new Map<string, null | Info>();
const trackIdToInfo = new Map<number, null | Info>();
const trackIdToTrack = new Map<number, TrackDescriptor>();
this.trackMgr
.getAllTracks()
.forEach((trackDescriptor) =>
trackDescriptor.tags?.trackIds?.forEach((trackId) =>
trackIdToTrack.set(trackId, trackDescriptor),
),
);
const getInfo = (trackId: number): null | Info => {
let info = trackIdToInfo.get(trackId);
if (info !== undefined) {
return info;
}
const trackDescriptor = trackIdToTrack.get(trackId);
if (trackDescriptor === undefined) {
trackIdToInfo.set(trackId, null);
return null;
}
info = trackUriToInfo.get(trackDescriptor.uri);
if (info !== undefined) {
return info;
}
// If 'trackIds' is undefined this is not an async slice track so
// we don't need to do anything. We also don't need to do
// anything if there is only one TP track in this async track. In
// that case experimental_slice_layout is just an expensive way
// to find out depth === layout_depth.
const trackIds = trackDescriptor?.tags?.trackIds;
if (trackIds === undefined || trackIds.length <= 1) {
trackUriToInfo.set(trackDescriptor.uri, null);
trackIdToInfo.set(trackId, null);
return null;
}
const newInfo = {
siblingTrackIds: [...trackIds],
sliceIds: [],
nodes: [],
};
trackUriToInfo.set(trackDescriptor.uri, newInfo);
trackIdToInfo.set(trackId, newInfo);
return newInfo;
};
// First pass, collect:
// - all slices that belong to async slice track
// - grouped by the async slice track in question
for (const node of nodes) {
const info = getInfo(node.trackId);
if (info !== null) {
info.sliceIds.push(node.sliceId);
info.nodes.push(node);
}
}
// Second pass, for each async track:
// - Query to find the layout_depth for each relevant sliceId
// - Iterate through the nodes updating the depth in place
for (const info of trackUriToInfo.values()) {
if (info === null) {
continue;
}
const r = await this.engine.query(`
SELECT
id,
layout_depth as depth
FROM
experimental_slice_layout
WHERE
filter_track_ids = '${info.siblingTrackIds.join(',')}'
AND id in (${info.sliceIds.join(', ')})
`);
// Create the sliceId -> new depth map:
const it = r.iter({
id: NUM,
depth: NUM,
});
const sliceIdToDepth = new Map<number, number>();
for (; it.valid(); it.next()) {
sliceIdToDepth.set(it.id, it.depth);
}
// For each begin/end from an async track update the depth:
for (const node of info.nodes) {
const newDepth = sliceIdToDepth.get(node.sliceId);
if (newDepth !== undefined) {
node.depth = newDepth;
}
}
}
callback(flows);
}
sliceSelected(sliceId: number) {
const connectedFlows = SHOW_INDIRECT_PRECEDING_FLOWS_FLAG.get()
? `(
select * from directly_connected_flow(${sliceId})
union
select * from preceding_flow(${sliceId})
)`
: `directly_connected_flow(${sliceId})`;
const query = `
-- Include slices.flow to initialise indexes on 'flow.slice_in' and 'flow.slice_out'.
INCLUDE PERFETTO MODULE slices.flow;
select
f.slice_out as beginSliceId,
t1.track_id as beginTrackId,
t1.name as beginSliceName,
CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName,
t1.category as beginSliceCategory,
t1.ts as beginSliceStartTs,
(t1.ts+t1.dur) as beginSliceEndTs,
t1.depth as beginDepth,
(thread_out.name || ' ' || thread_out.tid) as beginThreadName,
(process_out.name || ' ' || process_out.pid) as beginProcessName,
f.slice_in as endSliceId,
t2.track_id as endTrackId,
t2.name as endSliceName,
CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName,
t2.category as endSliceCategory,
t2.ts as endSliceStartTs,
(t2.ts+t2.dur) as endSliceEndTs,
t2.depth as endDepth,
(thread_in.name || ' ' || thread_in.tid) as endThreadName,
(process_in.name || ' ' || process_in.pid) as endProcessName,
extract_arg(f.arg_set_id, 'cat') as category,
extract_arg(f.arg_set_id, 'name') as name,
f.id as id,
slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant
from ${connectedFlows} f
join slice t1 on f.slice_out = t1.slice_id
join slice t2 on f.slice_in = t2.slice_id
left join thread_track track_out on track_out.id = t1.track_id
left join thread thread_out on thread_out.utid = track_out.utid
left join thread_track track_in on track_in.id = t2.track_id
left join thread thread_in on thread_in.utid = track_in.utid
left join process process_out on process_out.upid = thread_out.upid
left join process process_in on process_in.upid = thread_in.upid
`;
this.queryFlowEvents(query, (flows: Flow[]) =>
this.setConnectedFlows(flows),
);
}
private areaSelected(area: AreaSelection) {
const trackIds: number[] = [];
for (const trackInfo of area.tracks) {
const kind = trackInfo?.tags?.kind;
if (
kind === SLICE_TRACK_KIND ||
kind === ACTUAL_FRAMES_SLICE_TRACK_KIND
) {
if (trackInfo?.tags?.trackIds) {
for (const trackId of trackInfo.tags.trackIds) {
trackIds.push(trackId);
}
}
}
}
const tracks = `(${trackIds.join(',')})`;
const startNs = area.start;
const endNs = area.end;
const query = `
select
f.slice_out as beginSliceId,
t1.track_id as beginTrackId,
t1.name as beginSliceName,
CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName,
t1.category as beginSliceCategory,
t1.ts as beginSliceStartTs,
(t1.ts+t1.dur) as beginSliceEndTs,
t1.depth as beginDepth,
NULL as beginThreadName,
NULL as beginProcessName,
f.slice_in as endSliceId,
t2.track_id as endTrackId,
t2.name as endSliceName,
CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName,
t2.category as endSliceCategory,
t2.ts as endSliceStartTs,
(t2.ts+t2.dur) as endSliceEndTs,
t2.depth as endDepth,
NULL as endThreadName,
NULL as endProcessName,
extract_arg(f.arg_set_id, 'cat') as category,
extract_arg(f.arg_set_id, 'name') as name,
f.id as id,
slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant
from flow f
join slice t1 on f.slice_out = t1.slice_id
join slice t2 on f.slice_in = t2.slice_id
where
(t1.track_id in ${tracks}
and (t1.ts+t1.dur <= ${endNs} and t1.ts+t1.dur >= ${startNs}))
or
(t2.track_id in ${tracks}
and (t2.ts <= ${endNs} and t2.ts >= ${startNs}))
`;
this.queryFlowEvents(query, (flows: Flow[]) =>
this.setSelectedFlows(flows),
);
}
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 === 'track_event') {
const sliceId = this._curSelection.eventId;
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();
this._curSelection = selection;
if (selection.kind === 'empty') {
this.setConnectedFlows([]);
this.setSelectedFlows([]);
return;
}
// TODO(b/155483804): This is a hack as annotation slices don't contain
// flows. We should tidy this up when fixing this bug.
if (selection.kind === 'track_event' && selection.tableName === 'slice') {
this.sliceSelected(selection.eventId);
} else {
this.setConnectedFlows([]);
}
if (selection.kind === 'area') {
this.areaSelected(selection);
} else {
this.setConnectedFlows([]);
}
}
// Change focus to the next flow event (matching the direction)
focusOtherFlow(direction: FlowDirection) {
const currentSelection = this._curSelection;
if (!currentSelection || currentSelection.kind !== 'track_event') {
return;
}
const sliceId = currentSelection.eventId;
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();
}
// Select the slice connected to the flow in focus
moveByFocusedFlow(direction: FlowDirection): void {
const currentSelection = this._curSelection;
if (!currentSelection || currentSelection.kind !== 'track_event') {
return;
}
const sliceId = currentSelection.eventId;
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;
this.selectionMgr.selectSqlEvent('slice', flowPoint.sliceId, {
scrollToSelection: true,
});
}
}
}
get connectedFlows() {
return this._connectedFlows;
}
get selectedFlows() {
return this._selectedFlows;
}
get focusedFlowIdLeft() {
return this._focusedFlowIdLeft;
}
get focusedFlowIdRight() {
return this._focusedFlowIdRight;
}
get visibleCategories(): ReadonlyMap<string, boolean> {
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;
}