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