• 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 {AreaSelection, getLegacySelection} from '../common/state';
17import {featureFlags} from '../core/feature_flags';
18import {Flow, globals} from '../frontend/globals';
19import {publishConnectedFlows, publishSelectedFlows} from '../frontend/publish';
20import {asSliceSqlId} from '../frontend/sql_types';
21import {Engine} from '../trace_processor/engine';
22import {LONG, NUM, STR_NULL} from '../trace_processor/query_result';
23
24import {Controller} from './controller';
25import {Monitor} from '../base/monitor';
26import {
27  ACTUAL_FRAMES_SLICE_TRACK_KIND,
28  THREAD_SLICE_TRACK_KIND,
29} from '../core/track_kinds';
30
31export interface FlowEventsControllerArgs {
32  engine: Engine;
33}
34
35const SHOW_INDIRECT_PRECEDING_FLOWS_FLAG = featureFlags.register({
36  id: 'showIndirectPrecedingFlows',
37  name: 'Show indirect preceding flows',
38  description:
39    'Show indirect preceding flows (connected through ancestor ' +
40    'slices) when a slice is selected.',
41  defaultValue: false,
42});
43
44export class FlowEventsController extends Controller<'main'> {
45  private readonly monitor = new Monitor([() => globals.state.selection]);
46
47  constructor(private args: FlowEventsControllerArgs) {
48    super('main');
49
50    // Create |CHROME_CUSTOME_SLICE_NAME| helper, which combines slice name
51    // and args for some slices (scheduler tasks and mojo messages) for more
52    // helpful messages.
53    // In the future, it should be replaced with this a more scalable and
54    // customisable solution.
55    // Note that a function here is significantly faster than a join.
56    this.args.engine.query(`
57      SELECT CREATE_FUNCTION(
58        'CHROME_CUSTOM_SLICE_NAME(slice_id LONG)',
59        'STRING',
60        'select case
61           when name="Receive mojo message" then
62            printf("Receive mojo message (interface=%s, hash=%s)",
63              EXTRACT_ARG(arg_set_id,
64                          "chrome_mojo_event_info.mojo_interface_tag"),
65              EXTRACT_ARG(arg_set_id, "chrome_mojo_event_info.ipc_hash"))
66           when name="ThreadControllerImpl::RunTask" or
67                name="ThreadPool_RunTask" then
68            printf("RunTask(posted_from=%s:%s)",
69             EXTRACT_ARG(arg_set_id, "task.posted_from.file_name"),
70             EXTRACT_ARG(arg_set_id, "task.posted_from.function_name"))
71         end
72         from slice where id=$slice_id'
73    );`);
74  }
75
76  async queryFlowEvents(query: string, callback: (flows: Flow[]) => void) {
77    const result = await this.args.engine.query(query);
78    const flows: Flow[] = [];
79
80    const it = result.iter({
81      beginSliceId: NUM,
82      beginTrackId: NUM,
83      beginSliceName: STR_NULL,
84      beginSliceChromeCustomName: STR_NULL,
85      beginSliceCategory: STR_NULL,
86      beginSliceStartTs: LONG,
87      beginSliceEndTs: LONG,
88      beginDepth: NUM,
89      beginThreadName: STR_NULL,
90      beginProcessName: STR_NULL,
91      endSliceId: NUM,
92      endTrackId: NUM,
93      endSliceName: STR_NULL,
94      endSliceChromeCustomName: STR_NULL,
95      endSliceCategory: STR_NULL,
96      endSliceStartTs: LONG,
97      endSliceEndTs: LONG,
98      endDepth: NUM,
99      endThreadName: STR_NULL,
100      endProcessName: STR_NULL,
101      name: STR_NULL,
102      category: STR_NULL,
103      id: NUM,
104      flowToDescendant: NUM,
105    });
106
107    const nullToStr = (s: null | string): string => {
108      return s === null ? 'NULL' : s;
109    };
110
111    const nullToUndefined = (s: null | string): undefined | string => {
112      return s === null ? undefined : s;
113    };
114
115    const nodes = [];
116
117    for (; it.valid(); it.next()) {
118      // Category and name present only in version 1 flow events
119      // It is most likelly NULL for all other versions
120      const category = nullToUndefined(it.category);
121      const name = nullToUndefined(it.name);
122      const id = it.id;
123
124      const begin = {
125        trackId: it.beginTrackId,
126        sliceId: asSliceSqlId(it.beginSliceId),
127        sliceName: nullToStr(it.beginSliceName),
128        sliceChromeCustomName: nullToUndefined(it.beginSliceChromeCustomName),
129        sliceCategory: nullToStr(it.beginSliceCategory),
130        sliceStartTs: Time.fromRaw(it.beginSliceStartTs),
131        sliceEndTs: Time.fromRaw(it.beginSliceEndTs),
132        depth: it.beginDepth,
133        threadName: nullToStr(it.beginThreadName),
134        processName: nullToStr(it.beginProcessName),
135      };
136
137      const end = {
138        trackId: it.endTrackId,
139        sliceId: asSliceSqlId(it.endSliceId),
140        sliceName: nullToStr(it.endSliceName),
141        sliceChromeCustomName: nullToUndefined(it.endSliceChromeCustomName),
142        sliceCategory: nullToStr(it.endSliceCategory),
143        sliceStartTs: Time.fromRaw(it.endSliceStartTs),
144        sliceEndTs: Time.fromRaw(it.endSliceEndTs),
145        depth: it.endDepth,
146        threadName: nullToStr(it.endThreadName),
147        processName: nullToStr(it.endProcessName),
148      };
149
150      nodes.push(begin);
151      nodes.push(end);
152
153      flows.push({
154        id,
155        begin,
156        end,
157        dur: it.endSliceStartTs - it.beginSliceEndTs,
158        category,
159        name,
160        flowToDescendant: !!it.flowToDescendant,
161      });
162    }
163
164    // Everything below here is a horrible hack to support flows for
165    // async slice tracks.
166    // In short the issue is this:
167    // - For most slice tracks there is a one-to-one mapping between
168    //   the track in the UI and the track in the TP. n.b. Even in this
169    //   case the UI 'trackId' and the TP 'track.id' may not be the
170    //   same. In this case 'depth' in the TP is the exact depth in the
171    //   UI.
172    // - In the case of aysnc tracks however the mapping is
173    //   one-to-many. Each async slice track in the UI is 'backed' but
174    //   multiple TP tracks. In order to render this track we need
175    //   to adjust depth to avoid overlapping slices. In the render
176    //   path we use experimental_slice_layout for this purpose. This
177    //   is a virtual table in the TP which, for an arbitrary collection
178    //   of TP trackIds, computes for each slice a 'layout_depth'.
179    // - Everything above in this function and its callers doesn't
180    //   know anything about layout_depth.
181    //
182    // So if we stopped here we would have incorrect rendering for
183    // async slice tracks. Instead we want to 'fix' depth for these
184    // cases. We do this in two passes.
185    // - First we collect all the information we need in 'Info' POJOs
186    // - Secondly we loop over those Infos querying
187    //   the database to find the layout_depth for each sliceId
188    // TODO(hjd): This should not be needed after TracksV2 lands.
189
190    // We end up with one Info POJOs for each UI async slice track
191    // which has at least  one flow {begin,end}ing in one of its slices.
192    interface Info {
193      uiTrackId: string;
194      siblingTrackIds: number[];
195      sliceIds: number[];
196      nodes: Array<{
197        sliceId: number;
198        depth: number;
199      }>;
200    }
201
202    const uiTrackIdToInfo = new Map<string, null | Info>();
203    const trackIdToInfo = new Map<number, null | Info>();
204
205    const trackIdToUiTrackId = globals.trackManager.trackKeyByTrackId;
206    const tracks = globals.state.tracks;
207
208    const getInfo = (trackId: number): null | Info => {
209      let info = trackIdToInfo.get(trackId);
210      if (info !== undefined) {
211        return info;
212      }
213
214      const uiTrackId = trackIdToUiTrackId.get(trackId);
215      if (uiTrackId === undefined) {
216        trackIdToInfo.set(trackId, null);
217        return null;
218      }
219
220      const track = tracks[uiTrackId];
221      if (track === undefined) {
222        trackIdToInfo.set(trackId, null);
223        return null;
224      }
225
226      info = uiTrackIdToInfo.get(uiTrackId);
227      if (info !== undefined) {
228        return info;
229      }
230
231      // If 'trackIds' is undefined this is not an async slice track so
232      // we don't need to do anything. We also don't need to do
233      // anything if there is only one TP track in this async track. In
234      // that case experimental_slice_layout is just an expensive way
235      // to find out depth === layout_depth.
236      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
237      const trackIds = trackInfo?.trackIds;
238      if (trackIds === undefined || trackIds.length <= 1) {
239        uiTrackIdToInfo.set(uiTrackId, null);
240        trackIdToInfo.set(trackId, null);
241        return null;
242      }
243
244      const newInfo = {
245        uiTrackId,
246        siblingTrackIds: trackIds,
247        sliceIds: [],
248        nodes: [],
249      };
250
251      uiTrackIdToInfo.set(uiTrackId, newInfo);
252      trackIdToInfo.set(trackId, newInfo);
253
254      return newInfo;
255    };
256
257    // First pass, collect:
258    // - all slices that belong to async slice track
259    // - grouped by the async slice track in question
260    for (const node of nodes) {
261      const info = getInfo(node.trackId);
262      if (info !== null) {
263        info.sliceIds.push(node.sliceId);
264        info.nodes.push(node);
265      }
266    }
267
268    // Second pass, for each async track:
269    // - Query to find the layout_depth for each relevant sliceId
270    // - Iterate through the nodes updating the depth in place
271    for (const info of uiTrackIdToInfo.values()) {
272      if (info === null) {
273        continue;
274      }
275      const r = await this.args.engine.query(`
276        SELECT
277          id,
278          layout_depth as depth
279        FROM
280          experimental_slice_layout
281        WHERE
282          filter_track_ids = '${info.siblingTrackIds.join(',')}'
283          AND id in (${info.sliceIds.join(', ')})
284      `);
285
286      // Create the sliceId -> new depth map:
287      const it = r.iter({
288        id: NUM,
289        depth: NUM,
290      });
291      const sliceIdToDepth = new Map<number, number>();
292      for (; it.valid(); it.next()) {
293        sliceIdToDepth.set(it.id, it.depth);
294      }
295
296      // For each begin/end from an async track update the depth:
297      for (const node of info.nodes) {
298        const newDepth = sliceIdToDepth.get(node.sliceId);
299        if (newDepth !== undefined) {
300          node.depth = newDepth;
301        }
302      }
303    }
304
305    callback(flows);
306  }
307
308  sliceSelected(sliceId: number) {
309    const connectedFlows = SHOW_INDIRECT_PRECEDING_FLOWS_FLAG.get()
310      ? `(
311           select * from directly_connected_flow(${sliceId})
312           union
313           select * from preceding_flow(${sliceId})
314         )`
315      : `directly_connected_flow(${sliceId})`;
316
317    const query = `
318    select
319      f.slice_out as beginSliceId,
320      t1.track_id as beginTrackId,
321      t1.name as beginSliceName,
322      CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName,
323      t1.category as beginSliceCategory,
324      t1.ts as beginSliceStartTs,
325      (t1.ts+t1.dur) as beginSliceEndTs,
326      t1.depth as beginDepth,
327      (thread_out.name || ' ' || thread_out.tid) as beginThreadName,
328      (process_out.name || ' ' || process_out.pid) as beginProcessName,
329      f.slice_in as endSliceId,
330      t2.track_id as endTrackId,
331      t2.name as endSliceName,
332      CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName,
333      t2.category as endSliceCategory,
334      t2.ts as endSliceStartTs,
335      (t2.ts+t2.dur) as endSliceEndTs,
336      t2.depth as endDepth,
337      (thread_in.name || ' ' || thread_in.tid) as endThreadName,
338      (process_in.name || ' ' || process_in.pid) as endProcessName,
339      extract_arg(f.arg_set_id, 'cat') as category,
340      extract_arg(f.arg_set_id, 'name') as name,
341      f.id as id,
342      slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant
343    from ${connectedFlows} f
344    join slice t1 on f.slice_out = t1.slice_id
345    join slice t2 on f.slice_in = t2.slice_id
346    left join thread_track track_out on track_out.id = t1.track_id
347    left join thread thread_out on thread_out.utid = track_out.utid
348    left join thread_track track_in on track_in.id = t2.track_id
349    left join thread thread_in on thread_in.utid = track_in.utid
350    left join process process_out on process_out.upid = thread_out.upid
351    left join process process_in on process_in.upid = thread_in.upid
352    `;
353    this.queryFlowEvents(query, (flows: Flow[]) =>
354      publishConnectedFlows(flows),
355    );
356  }
357
358  private areaSelected(area: AreaSelection) {
359    const trackIds: number[] = [];
360
361    for (const uiTrackId of area.tracks) {
362      const track = globals.state.tracks[uiTrackId];
363      if (track?.uri !== undefined) {
364        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
365        const kind = trackInfo?.kind;
366        if (
367          kind === THREAD_SLICE_TRACK_KIND ||
368          kind === ACTUAL_FRAMES_SLICE_TRACK_KIND
369        ) {
370          if (trackInfo?.trackIds) {
371            for (const trackId of trackInfo.trackIds) {
372              trackIds.push(trackId);
373            }
374          }
375        }
376      }
377    }
378
379    const tracks = `(${trackIds.join(',')})`;
380
381    const startNs = area.start;
382    const endNs = area.end;
383
384    const query = `
385    select
386      f.slice_out as beginSliceId,
387      t1.track_id as beginTrackId,
388      t1.name as beginSliceName,
389      CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName,
390      t1.category as beginSliceCategory,
391      t1.ts as beginSliceStartTs,
392      (t1.ts+t1.dur) as beginSliceEndTs,
393      t1.depth as beginDepth,
394      NULL as beginThreadName,
395      NULL as beginProcessName,
396      f.slice_in as endSliceId,
397      t2.track_id as endTrackId,
398      t2.name as endSliceName,
399      CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName,
400      t2.category as endSliceCategory,
401      t2.ts as endSliceStartTs,
402      (t2.ts+t2.dur) as endSliceEndTs,
403      t2.depth as endDepth,
404      NULL as endThreadName,
405      NULL as endProcessName,
406      extract_arg(f.arg_set_id, 'cat') as category,
407      extract_arg(f.arg_set_id, 'name') as name,
408      f.id as id,
409      slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant
410    from flow f
411    join slice t1 on f.slice_out = t1.slice_id
412    join slice t2 on f.slice_in = t2.slice_id
413    where
414      (t1.track_id in ${tracks}
415        and (t1.ts+t1.dur <= ${endNs} and t1.ts+t1.dur >= ${startNs}))
416      or
417      (t2.track_id in ${tracks}
418        and (t2.ts <= ${endNs} and t2.ts >= ${startNs}))
419    `;
420    this.queryFlowEvents(query, (flows: Flow[]) => publishSelectedFlows(flows));
421  }
422
423  refreshVisibleFlows() {
424    if (!this.monitor.ifStateChanged()) {
425      return;
426    }
427
428    const selection = globals.state.selection;
429    if (selection.kind === 'empty') {
430      publishConnectedFlows([]);
431      publishSelectedFlows([]);
432      return;
433    }
434
435    const legacySelection = getLegacySelection(globals.state);
436    // TODO(b/155483804): This is a hack as annotation slices don't contain
437    // flows. We should tidy this up when fixing this bug.
438    if (
439      legacySelection &&
440      legacySelection.kind === 'SLICE' &&
441      legacySelection.table !== 'annotation'
442    ) {
443      this.sliceSelected(legacySelection.id);
444    } else {
445      publishConnectedFlows([]);
446    }
447
448    if (selection.kind === 'area') {
449      this.areaSelected(selection);
450    } else {
451      publishSelectedFlows([]);
452    }
453  }
454
455  run() {
456    this.refreshVisibleFlows();
457  }
458}
459