xref: /aosp_15_r20/external/perfetto/ui/src/core/flow_manager.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2020 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use size file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {Time} from '../base/time';
16import {featureFlags} from './feature_flags';
17import {FlowDirection, Flow} from './flow_types';
18import {asSliceSqlId} from '../components/sql_utils/core_types';
19import {LONG, NUM, STR_NULL} from '../trace_processor/query_result';
20import {
21  ACTUAL_FRAMES_SLICE_TRACK_KIND,
22  SLICE_TRACK_KIND,
23} from '../public/track_kinds';
24import {TrackDescriptor, TrackManager} from '../public/track';
25import {AreaSelection, Selection, SelectionManager} from '../public/selection';
26import {raf} from './raf_scheduler';
27import {Engine} from '../trace_processor/engine';
28
29const SHOW_INDIRECT_PRECEDING_FLOWS_FLAG = featureFlags.register({
30  id: 'showIndirectPrecedingFlows',
31  name: 'Show indirect preceding flows',
32  description:
33    'Show indirect preceding flows (connected through ancestor ' +
34    'slices) when a slice is selected.',
35  defaultValue: false,
36});
37
38export class FlowManager {
39  private _connectedFlows: Flow[] = [];
40  private _selectedFlows: Flow[] = [];
41  private _curSelection?: Selection;
42  private _focusedFlowIdLeft = -1;
43  private _focusedFlowIdRight = -1;
44  private _visibleCategories = new Map<string, boolean>();
45  private _initialized = false;
46
47  constructor(
48    private engine: Engine,
49    private trackMgr: TrackManager,
50    private selectionMgr: SelectionManager,
51  ) {}
52
53  // TODO(primiano): the only reason why this is not done in the constructor is
54  // because when loading the UI with no trace, we initialize globals with a
55  // FakeTraceImpl with a FakeEngine, which crashes when issuing queries.
56  // This can be moved in the ctor once globals go away.
57  private initialize() {
58    if (this._initialized) return;
59    this._initialized = true;
60    // Create |CHROME_CUSTOME_SLICE_NAME| helper, which combines slice name
61    // and args for some slices (scheduler tasks and mojo messages) for more
62    // helpful messages.
63    // In the future, it should be replaced with this a more scalable and
64    // customisable solution.
65    // Note that a function here is significantly faster than a join.
66    this.engine.query(`
67      SELECT CREATE_FUNCTION(
68        'CHROME_CUSTOM_SLICE_NAME(slice_id LONG)',
69        'STRING',
70        'select case
71           when name="Receive mojo message" then
72            printf("Receive mojo message (interface=%s, hash=%s)",
73              EXTRACT_ARG(arg_set_id,
74                          "chrome_mojo_event_info.mojo_interface_tag"),
75              EXTRACT_ARG(arg_set_id, "chrome_mojo_event_info.ipc_hash"))
76           when name="ThreadControllerImpl::RunTask" or
77                name="ThreadPool_RunTask" then
78            printf("RunTask(posted_from=%s:%s)",
79             EXTRACT_ARG(arg_set_id, "task.posted_from.file_name"),
80             EXTRACT_ARG(arg_set_id, "task.posted_from.function_name"))
81         end
82         from slice where id=$slice_id'
83    );`);
84  }
85
86  async queryFlowEvents(query: string): Promise<Flow[]> {
87    const result = await this.engine.query(query);
88    const flows: Flow[] = [];
89
90    const it = result.iter({
91      beginSliceId: NUM,
92      beginTrackId: NUM,
93      beginSliceName: STR_NULL,
94      beginSliceChromeCustomName: STR_NULL,
95      beginSliceCategory: STR_NULL,
96      beginSliceStartTs: LONG,
97      beginSliceEndTs: LONG,
98      beginDepth: NUM,
99      beginThreadName: STR_NULL,
100      beginProcessName: STR_NULL,
101      endSliceId: NUM,
102      endTrackId: NUM,
103      endSliceName: STR_NULL,
104      endSliceChromeCustomName: STR_NULL,
105      endSliceCategory: STR_NULL,
106      endSliceStartTs: LONG,
107      endSliceEndTs: LONG,
108      endDepth: NUM,
109      endThreadName: STR_NULL,
110      endProcessName: STR_NULL,
111      name: STR_NULL,
112      category: STR_NULL,
113      id: NUM,
114      flowToDescendant: NUM,
115    });
116
117    const nullToStr = (s: null | string): string => {
118      return s === null ? 'NULL' : s;
119    };
120
121    const nullToUndefined = (s: null | string): undefined | string => {
122      return s === null ? undefined : s;
123    };
124
125    const nodes = [];
126
127    for (; it.valid(); it.next()) {
128      // Category and name present only in version 1 flow events
129      // It is most likelly NULL for all other versions
130      const category = nullToUndefined(it.category);
131      const name = nullToUndefined(it.name);
132      const id = it.id;
133
134      const begin = {
135        trackId: it.beginTrackId,
136        sliceId: asSliceSqlId(it.beginSliceId),
137        sliceName: nullToStr(it.beginSliceName),
138        sliceChromeCustomName: nullToUndefined(it.beginSliceChromeCustomName),
139        sliceCategory: nullToStr(it.beginSliceCategory),
140        sliceStartTs: Time.fromRaw(it.beginSliceStartTs),
141        sliceEndTs: Time.fromRaw(it.beginSliceEndTs),
142        depth: it.beginDepth,
143        threadName: nullToStr(it.beginThreadName),
144        processName: nullToStr(it.beginProcessName),
145      };
146
147      const end = {
148        trackId: it.endTrackId,
149        sliceId: asSliceSqlId(it.endSliceId),
150        sliceName: nullToStr(it.endSliceName),
151        sliceChromeCustomName: nullToUndefined(it.endSliceChromeCustomName),
152        sliceCategory: nullToStr(it.endSliceCategory),
153        sliceStartTs: Time.fromRaw(it.endSliceStartTs),
154        sliceEndTs: Time.fromRaw(it.endSliceEndTs),
155        depth: it.endDepth,
156        threadName: nullToStr(it.endThreadName),
157        processName: nullToStr(it.endProcessName),
158      };
159
160      nodes.push(begin);
161      nodes.push(end);
162
163      flows.push({
164        id,
165        begin,
166        end,
167        dur: it.endSliceStartTs - it.beginSliceEndTs,
168        category,
169        name,
170        flowToDescendant: !!it.flowToDescendant,
171      });
172    }
173
174    // Everything below here is a horrible hack to support flows for
175    // async slice tracks.
176    // In short the issue is this:
177    // - For most slice tracks there is a one-to-one mapping between
178    //   the track in the UI and the track in the TP. n.b. Even in this
179    //   case the UI 'trackId' and the TP 'track.id' may not be the
180    //   same. In this case 'depth' in the TP is the exact depth in the
181    //   UI.
182    // - In the case of aysnc tracks however the mapping is
183    //   one-to-many. Each async slice track in the UI is 'backed' but
184    //   multiple TP tracks. In order to render this track we need
185    //   to adjust depth to avoid overlapping slices. In the render
186    //   path we use experimental_slice_layout for this purpose. This
187    //   is a virtual table in the TP which, for an arbitrary collection
188    //   of TP trackIds, computes for each slice a 'layout_depth'.
189    // - Everything above in this function and its callers doesn't
190    //   know anything about layout_depth.
191    //
192    // So if we stopped here we would have incorrect rendering for
193    // async slice tracks. Instead we want to 'fix' depth for these
194    // cases. We do this in two passes.
195    // - First we collect all the information we need in 'Info' POJOs
196    // - Secondly we loop over those Infos querying
197    //   the database to find the layout_depth for each sliceId
198    // TODO(hjd): This should not be needed after TracksV2 lands.
199
200    // We end up with one Info POJOs for each UI async slice track
201    // which has at least  one flow {begin,end}ing in one of its slices.
202    interface Info {
203      siblingTrackIds: number[];
204      sliceIds: number[];
205      nodes: Array<{
206        sliceId: number;
207        depth: number;
208      }>;
209    }
210
211    const trackUriToInfo = new Map<string, null | Info>();
212    const trackIdToInfo = new Map<number, null | Info>();
213
214    const trackIdToTrack = new Map<number, TrackDescriptor>();
215    this.trackMgr
216      .getAllTracks()
217      .forEach((trackDescriptor) =>
218        trackDescriptor.tags?.trackIds?.forEach((trackId) =>
219          trackIdToTrack.set(trackId, trackDescriptor),
220        ),
221      );
222
223    const getInfo = (trackId: number): null | Info => {
224      let info = trackIdToInfo.get(trackId);
225      if (info !== undefined) {
226        return info;
227      }
228
229      const trackDescriptor = trackIdToTrack.get(trackId);
230      if (trackDescriptor === undefined) {
231        trackIdToInfo.set(trackId, null);
232        return null;
233      }
234
235      info = trackUriToInfo.get(trackDescriptor.uri);
236      if (info !== undefined) {
237        return info;
238      }
239
240      // If 'trackIds' is undefined this is not an async slice track so
241      // we don't need to do anything. We also don't need to do
242      // anything if there is only one TP track in this async track. In
243      // that case experimental_slice_layout is just an expensive way
244      // to find out depth === layout_depth.
245      const trackIds = trackDescriptor?.tags?.trackIds;
246      if (trackIds === undefined || trackIds.length <= 1) {
247        trackUriToInfo.set(trackDescriptor.uri, null);
248        trackIdToInfo.set(trackId, null);
249        return null;
250      }
251
252      const newInfo = {
253        siblingTrackIds: [...trackIds],
254        sliceIds: [],
255        nodes: [],
256      };
257
258      trackUriToInfo.set(trackDescriptor.uri, newInfo);
259      trackIdToInfo.set(trackId, newInfo);
260
261      return newInfo;
262    };
263
264    // First pass, collect:
265    // - all slices that belong to async slice track
266    // - grouped by the async slice track in question
267    for (const node of nodes) {
268      const info = getInfo(node.trackId);
269      if (info !== null) {
270        info.sliceIds.push(node.sliceId);
271        info.nodes.push(node);
272      }
273    }
274
275    // Second pass, for each async track:
276    // - Query to find the layout_depth for each relevant sliceId
277    // - Iterate through the nodes updating the depth in place
278    for (const info of trackUriToInfo.values()) {
279      if (info === null) {
280        continue;
281      }
282      const r = await this.engine.query(`
283        SELECT
284          id,
285          layout_depth as depth
286        FROM
287          experimental_slice_layout
288        WHERE
289          filter_track_ids = '${info.siblingTrackIds.join(',')}'
290          AND id in (${info.sliceIds.join(', ')})
291      `);
292
293      // Create the sliceId -> new depth map:
294      const it = r.iter({
295        id: NUM,
296        depth: NUM,
297      });
298      const sliceIdToDepth = new Map<number, number>();
299      for (; it.valid(); it.next()) {
300        sliceIdToDepth.set(it.id, it.depth);
301      }
302
303      // For each begin/end from an async track update the depth:
304      for (const node of info.nodes) {
305        const newDepth = sliceIdToDepth.get(node.sliceId);
306        if (newDepth !== undefined) {
307          node.depth = newDepth;
308        }
309      }
310    }
311
312    // Fill in the track uris if available
313    flows.forEach((flow) => {
314      flow.begin.trackUri = trackIdToTrack.get(flow.begin.trackId)?.uri;
315      flow.end.trackUri = trackIdToTrack.get(flow.end.trackId)?.uri;
316    });
317
318    return flows;
319  }
320
321  sliceSelected(sliceId: number) {
322    const connectedFlows = SHOW_INDIRECT_PRECEDING_FLOWS_FLAG.get()
323      ? `(
324           select * from directly_connected_flow(${sliceId})
325           union
326           select * from preceding_flow(${sliceId})
327         )`
328      : `directly_connected_flow(${sliceId})`;
329
330    const query = `
331    -- Include slices.flow to initialise indexes on 'flow.slice_in' and 'flow.slice_out'.
332    INCLUDE PERFETTO MODULE slices.flow;
333
334    select
335      f.slice_out as beginSliceId,
336      t1.track_id as beginTrackId,
337      t1.name as beginSliceName,
338      CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName,
339      t1.category as beginSliceCategory,
340      t1.ts as beginSliceStartTs,
341      (t1.ts+t1.dur) as beginSliceEndTs,
342      t1.depth as beginDepth,
343      (thread_out.name || ' ' || thread_out.tid) as beginThreadName,
344      (process_out.name || ' ' || process_out.pid) as beginProcessName,
345      f.slice_in as endSliceId,
346      t2.track_id as endTrackId,
347      t2.name as endSliceName,
348      CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName,
349      t2.category as endSliceCategory,
350      t2.ts as endSliceStartTs,
351      (t2.ts+t2.dur) as endSliceEndTs,
352      t2.depth as endDepth,
353      (thread_in.name || ' ' || thread_in.tid) as endThreadName,
354      (process_in.name || ' ' || process_in.pid) as endProcessName,
355      extract_arg(f.arg_set_id, 'cat') as category,
356      extract_arg(f.arg_set_id, 'name') as name,
357      f.id as id,
358      slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant
359    from ${connectedFlows} f
360    join slice t1 on f.slice_out = t1.slice_id
361    join slice t2 on f.slice_in = t2.slice_id
362    left join thread_track track_out on track_out.id = t1.track_id
363    left join thread thread_out on thread_out.utid = track_out.utid
364    left join thread_track track_in on track_in.id = t2.track_id
365    left join thread thread_in on thread_in.utid = track_in.utid
366    left join process process_out on process_out.upid = thread_out.upid
367    left join process process_in on process_in.upid = thread_in.upid
368    `;
369    this.queryFlowEvents(query).then((flows) => this.setConnectedFlows(flows));
370  }
371
372  private areaSelected(area: AreaSelection) {
373    const trackIds: number[] = [];
374
375    for (const trackInfo of area.tracks) {
376      const kind = trackInfo?.tags?.kind;
377      if (
378        kind === SLICE_TRACK_KIND ||
379        kind === ACTUAL_FRAMES_SLICE_TRACK_KIND
380      ) {
381        if (trackInfo?.tags?.trackIds) {
382          for (const trackId of trackInfo.tags.trackIds) {
383            trackIds.push(trackId);
384          }
385        }
386      }
387    }
388
389    const tracks = `(${trackIds.join(',')})`;
390
391    const startNs = area.start;
392    const endNs = area.end;
393
394    const query = `
395    select
396      f.slice_out as beginSliceId,
397      t1.track_id as beginTrackId,
398      t1.name as beginSliceName,
399      CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName,
400      t1.category as beginSliceCategory,
401      t1.ts as beginSliceStartTs,
402      (t1.ts+t1.dur) as beginSliceEndTs,
403      t1.depth as beginDepth,
404      NULL as beginThreadName,
405      NULL as beginProcessName,
406      f.slice_in as endSliceId,
407      t2.track_id as endTrackId,
408      t2.name as endSliceName,
409      CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName,
410      t2.category as endSliceCategory,
411      t2.ts as endSliceStartTs,
412      (t2.ts+t2.dur) as endSliceEndTs,
413      t2.depth as endDepth,
414      NULL as endThreadName,
415      NULL as endProcessName,
416      extract_arg(f.arg_set_id, 'cat') as category,
417      extract_arg(f.arg_set_id, 'name') as name,
418      f.id as id,
419      slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant
420    from flow f
421    join slice t1 on f.slice_out = t1.slice_id
422    join slice t2 on f.slice_in = t2.slice_id
423    where
424      (t1.track_id in ${tracks}
425        and (t1.ts+t1.dur <= ${endNs} and t1.ts+t1.dur >= ${startNs}))
426      or
427      (t2.track_id in ${tracks}
428        and (t2.ts <= ${endNs} and t2.ts >= ${startNs}))
429    `;
430    this.queryFlowEvents(query).then((flows) => this.setSelectedFlows(flows));
431  }
432
433  private setConnectedFlows(connectedFlows: Flow[]) {
434    this._connectedFlows = connectedFlows;
435    // If a chrome slice is selected and we have any flows in connectedFlows
436    // we will find the flows on the right and left of that slice to set a default
437    // focus. In all other cases the focusedFlowId(Left|Right) will be set to -1.
438    this._focusedFlowIdLeft = -1;
439    this._focusedFlowIdRight = -1;
440    if (this._curSelection?.kind === 'track_event') {
441      const sliceId = this._curSelection.eventId;
442      for (const flow of connectedFlows) {
443        if (flow.begin.sliceId === sliceId) {
444          this._focusedFlowIdRight = flow.id;
445        }
446        if (flow.end.sliceId === sliceId) {
447          this._focusedFlowIdLeft = flow.id;
448        }
449      }
450    }
451    raf.scheduleFullRedraw();
452  }
453
454  private setSelectedFlows(selectedFlows: Flow[]) {
455    this._selectedFlows = selectedFlows;
456    raf.scheduleFullRedraw();
457  }
458
459  updateFlows(selection: Selection) {
460    this.initialize();
461    this._curSelection = selection;
462
463    if (selection.kind === 'empty') {
464      this.setConnectedFlows([]);
465      this.setSelectedFlows([]);
466      return;
467    }
468
469    // TODO(b/155483804): This is a hack as annotation slices don't contain
470    // flows. We should tidy this up when fixing this bug.
471    if (selection.kind === 'track_event' && selection.tableName === 'slice') {
472      this.sliceSelected(selection.eventId);
473    } else {
474      this.setConnectedFlows([]);
475    }
476
477    if (selection.kind === 'area') {
478      this.areaSelected(selection);
479    } else {
480      this.setConnectedFlows([]);
481    }
482  }
483
484  // Change focus to the next flow event (matching the direction)
485  focusOtherFlow(direction: FlowDirection) {
486    const currentSelection = this._curSelection;
487    if (!currentSelection || currentSelection.kind !== 'track_event') {
488      return;
489    }
490    const sliceId = currentSelection.eventId;
491    if (sliceId === -1) {
492      return;
493    }
494
495    const boundFlows = this._connectedFlows.filter(
496      (flow) =>
497        (flow.begin.sliceId === sliceId && direction === 'Forward') ||
498        (flow.end.sliceId === sliceId && direction === 'Backward'),
499    );
500
501    if (direction === 'Backward') {
502      const nextFlowId = findAnotherFlowExcept(
503        boundFlows,
504        this._focusedFlowIdLeft,
505      );
506      this._focusedFlowIdLeft = nextFlowId;
507    } else {
508      const nextFlowId = findAnotherFlowExcept(
509        boundFlows,
510        this._focusedFlowIdRight,
511      );
512      this._focusedFlowIdRight = nextFlowId;
513    }
514    raf.scheduleFullRedraw();
515  }
516
517  // Select the slice connected to the flow in focus
518  moveByFocusedFlow(direction: FlowDirection): void {
519    const currentSelection = this._curSelection;
520    if (!currentSelection || currentSelection.kind !== 'track_event') {
521      return;
522    }
523
524    const sliceId = currentSelection.eventId;
525    const flowId =
526      direction === 'Backward'
527        ? this._focusedFlowIdLeft
528        : this._focusedFlowIdRight;
529
530    if (sliceId === -1 || flowId === -1) {
531      return;
532    }
533
534    // Find flow that is in focus and select corresponding slice
535    for (const flow of this._connectedFlows) {
536      if (flow.id === flowId) {
537        const flowPoint = direction === 'Backward' ? flow.begin : flow.end;
538        this.selectionMgr.selectSqlEvent('slice', flowPoint.sliceId, {
539          scrollToSelection: true,
540        });
541      }
542    }
543  }
544
545  get connectedFlows() {
546    return this._connectedFlows;
547  }
548
549  get selectedFlows() {
550    return this._selectedFlows;
551  }
552
553  get focusedFlowIdLeft() {
554    return this._focusedFlowIdLeft;
555  }
556  get focusedFlowIdRight() {
557    return this._focusedFlowIdRight;
558  }
559
560  get visibleCategories(): ReadonlyMap<string, boolean> {
561    return this._visibleCategories;
562  }
563
564  setCategoryVisible(name: string, value: boolean) {
565    this._visibleCategories.set(name, value);
566    raf.scheduleFullRedraw();
567  }
568}
569
570// Search |boundFlows| for |flowId| and return the id following it.
571// Returns the first flow id if nothing was found or |flowId| was the last flow
572// in |boundFlows|, and -1 if |boundFlows| is empty
573function findAnotherFlowExcept(boundFlows: Flow[], flowId: number): number {
574  let selectedFlowFound = false;
575
576  if (boundFlows.length === 0) {
577    return -1;
578  }
579
580  for (const flow of boundFlows) {
581    if (selectedFlowFound) {
582      return flow.id;
583    }
584
585    if (flow.id === flowId) {
586      selectedFlowFound = true;
587    }
588  }
589  return boundFlows[0].id;
590}
591