• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2019 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this 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 {assertTrue} from '../base/logging';
16import {Time, time} from '../base/time';
17import {Args, ArgValue} from '../common/arg_types';
18import {
19  SelectionKind,
20  ThreadSliceSelection,
21  getLegacySelection,
22} from '../common/state';
23import {THREAD_SLICE_TRACK_KIND} from '../core/track_kinds';
24import {globals, SliceDetails, ThreadStateDetails} from '../frontend/globals';
25import {
26  publishSliceDetails,
27  publishThreadStateDetails,
28} from '../frontend/publish';
29import {Engine} from '../trace_processor/engine';
30import {
31  durationFromSql,
32  LONG,
33  NUM,
34  NUM_NULL,
35  STR,
36  STR_NULL,
37  timeFromSql,
38} from '../trace_processor/query_result';
39
40import {Controller} from './controller';
41
42export interface SelectionControllerArgs {
43  engine: Engine;
44}
45
46interface ThreadDetails {
47  tid: number;
48  threadName?: string;
49}
50
51interface ProcessDetails {
52  pid?: number;
53  processName?: string;
54  uid?: number;
55  packageName?: string;
56  versionCode?: number;
57}
58
59// This class queries the TP for the details on a specific slice that has
60// been clicked.
61export class SelectionController extends Controller<'main'> {
62  private lastSelectedId?: number | string;
63  private lastSelectedKind?: string;
64  constructor(private args: SelectionControllerArgs) {
65    super('main');
66  }
67
68  run() {
69    const selection = getLegacySelection(globals.state);
70    if (!selection) return;
71
72    const selectWithId: SelectionKind[] = [
73      'SLICE',
74      'SCHED_SLICE',
75      'HEAP_PROFILE',
76      'THREAD_STATE',
77    ];
78    if (
79      !selectWithId.includes(selection.kind) ||
80      (selectWithId.includes(selection.kind) &&
81        selection.id === this.lastSelectedId &&
82        selection.kind === this.lastSelectedKind)
83    ) {
84      return;
85    }
86    const selectedId = selection.id;
87    const selectedKind = selection.kind;
88    this.lastSelectedId = selectedId;
89    this.lastSelectedKind = selectedKind;
90
91    if (selectedId === undefined) return;
92
93    if (selection.kind === 'SCHED_SLICE') {
94      this.schedSliceDetails(selectedId as number);
95    } else if (selection.kind === 'THREAD_STATE') {
96      this.threadStateDetails(selection.id);
97    } else if (selection.kind === 'SLICE') {
98      this.sliceDetails(selection);
99    }
100  }
101
102  async sliceDetails(selection: ThreadSliceSelection) {
103    const selectedId = selection.id;
104    const table = selection.table;
105
106    let leafTable: string;
107    let promisedArgs: Promise<Args>;
108    // TODO(b/155483804): This is a hack to ensure annotation slices are
109    // selectable for now. We should tidy this up when improving this class.
110    if (table === 'annotation') {
111      leafTable = 'annotation_slice';
112      promisedArgs = Promise.resolve(new Map());
113    } else {
114      const result = await this.args.engine.query(`
115        SELECT
116          type as leafTable,
117          arg_set_id as argSetId
118        FROM slice WHERE id = ${selectedId}`);
119
120      if (result.numRows() === 0) {
121        return;
122      }
123
124      const row = result.firstRow({
125        leafTable: STR,
126        argSetId: NUM,
127      });
128
129      leafTable = row.leafTable;
130      const argSetId = row.argSetId;
131      promisedArgs = this.getArgs(argSetId);
132    }
133
134    const promisedDetails = this.args.engine.query(`
135      SELECT *, ABS_TIME_STR(ts) as absTime FROM ${leafTable} WHERE id = ${selectedId};
136    `);
137
138    const [details, args] = await Promise.all([promisedDetails, promisedArgs]);
139
140    if (details.numRows() <= 0) return;
141    const rowIter = details.iter({});
142    assertTrue(rowIter.valid());
143
144    // A few columns are hard coded as part of the SliceDetails interface.
145    // Long term these should be handled generically as args but for now
146    // handle them specially:
147    let ts = undefined;
148    let absTime = undefined;
149    let dur = undefined;
150    let name = undefined;
151    let category = undefined;
152    let threadDur = undefined;
153    let threadTs = undefined;
154    let trackId = undefined;
155
156    // We select all columns from the leafTable to ensure that we include
157    // additional fields from the child tables (like `thread_dur` from
158    // `thread_slice` or `frame_number` from `frame_slice`).
159    // However, this also includes some basic columns (especially from `slice`)
160    // that are not interesting (i.e. `arg_set_id`, which has already been used
161    // to resolve and show the arguments) and should not be shown to the user.
162    const ignoredColumns = [
163      'type',
164      'depth',
165      'parent_id',
166      'stack_id',
167      'parent_stack_id',
168      'arg_set_id',
169      'thread_instruction_count',
170      'thread_instruction_delta',
171    ];
172
173    for (const k of details.columns()) {
174      const v = rowIter.get(k);
175      switch (k) {
176        case 'id':
177          break;
178        case 'ts':
179          ts = timeFromSql(v);
180          break;
181        case 'thread_ts':
182          threadTs = timeFromSql(v);
183          break;
184        case 'absTime':
185          /* eslint-disable @typescript-eslint/strict-boolean-expressions */
186          if (v) absTime = `${v}`;
187          /* eslint-enable */
188          break;
189        case 'name':
190          name = `${v}`;
191          break;
192        case 'dur':
193          dur = durationFromSql(v);
194          break;
195        case 'thread_dur':
196          threadDur = durationFromSql(v);
197          break;
198        case 'category':
199        case 'cat':
200          category = `${v}`;
201          break;
202        case 'track_id':
203          trackId = Number(v);
204          break;
205        default:
206          if (!ignoredColumns.includes(k)) args.set(k, `${v}`);
207      }
208    }
209
210    const selected: SliceDetails = {
211      id: selectedId,
212      ts,
213      threadTs,
214      absTime,
215      dur,
216      threadDur,
217      name,
218      category,
219      args,
220    };
221
222    if (trackId !== undefined) {
223      const columnInfo = (
224        await this.args.engine.query(`
225        WITH
226           leafTrackTable AS (SELECT type FROM track WHERE id = ${trackId}),
227           cols AS (
228                SELECT name
229                FROM pragma_table_info((SELECT type FROM leafTrackTable))
230            )
231        SELECT
232           type as leafTrackTable,
233          'upid' in cols AS hasUpid,
234          'utid' in cols AS hasUtid
235        FROM leafTrackTable
236      `)
237      ).firstRow({hasUpid: NUM, hasUtid: NUM, leafTrackTable: STR});
238      const hasUpid = columnInfo.hasUpid !== 0;
239      const hasUtid = columnInfo.hasUtid !== 0;
240
241      if (hasUtid) {
242        const utid = (
243          await this.args.engine.query(`
244            SELECT utid
245            FROM ${columnInfo.leafTrackTable}
246            WHERE id = ${trackId};
247        `)
248        ).firstRow({
249          utid: NUM,
250        }).utid;
251        Object.assign(selected, await this.computeThreadDetails(utid));
252      } else if (hasUpid) {
253        const upid = (
254          await this.args.engine.query(`
255            SELECT upid
256            FROM ${columnInfo.leafTrackTable}
257            WHERE id = ${trackId};
258        `)
259        ).firstRow({
260          upid: NUM,
261        }).upid;
262        Object.assign(selected, await this.computeProcessDetails(upid));
263      }
264    }
265
266    // Check selection is still the same on completion of query.
267    if (selection === getLegacySelection(globals.state)) {
268      publishSliceDetails(selected);
269    }
270  }
271
272  async getArgs(argId: number): Promise<Args> {
273    const args = new Map<string, ArgValue>();
274    const query = `
275      select
276        key AS name,
277        display_value AS value
278      FROM args
279      WHERE arg_set_id = ${argId}
280    `;
281    const result = await this.args.engine.query(query);
282    const it = result.iter({
283      name: STR,
284      value: STR_NULL,
285    });
286    for (; it.valid(); it.next()) {
287      const name = it.name;
288      const value = it.value || 'NULL';
289      if (name === 'destination slice id' && !isNaN(Number(value))) {
290        const destTrackId = await this.getDestTrackId(value);
291        args.set('Destination Slice', {
292          kind: 'SCHED_SLICE',
293          trackId: destTrackId,
294          sliceId: Number(value),
295          rawValue: value,
296        });
297      } else {
298        args.set(name, value);
299      }
300    }
301    return args;
302  }
303
304  async getDestTrackId(sliceId: string): Promise<string> {
305    const trackIdQuery = `select track_id as trackId from slice
306    where slice_id = ${sliceId}`;
307    const result = await this.args.engine.query(trackIdQuery);
308    const trackId = result.firstRow({trackId: NUM}).trackId;
309    // TODO(hjd): If we had a consistent mapping from TP track_id
310    // UI track id for slice tracks this would be unnecessary.
311    let trackKey = '';
312    for (const track of Object.values(globals.state.tracks)) {
313      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
314      if (trackInfo?.kind === THREAD_SLICE_TRACK_KIND) {
315        const trackIds = trackInfo?.trackIds;
316        if (trackIds && trackIds.length > 0 && trackIds[0] === trackId) {
317          trackKey = track.key;
318          break;
319        }
320      }
321    }
322    return trackKey;
323  }
324
325  // TODO(altimin): We currently rely on the ThreadStateDetails for supporting
326  // marking the area (the rest goes is handled by ThreadStateTab
327  // directly. Refactor it to be plugin-friendly and remove this.
328  async threadStateDetails(id: number) {
329    const query = `
330      SELECT
331        ts,
332        thread_state.dur as dur
333      from thread_state
334      where thread_state.id = ${id}
335    `;
336    const result = await this.args.engine.query(query);
337
338    const selection = getLegacySelection(globals.state);
339    if (result.numRows() > 0 && selection) {
340      const row = result.firstRow({
341        ts: LONG,
342        dur: LONG,
343      });
344      const selected: ThreadStateDetails = {
345        ts: Time.fromRaw(row.ts),
346        dur: row.dur,
347      };
348      publishThreadStateDetails(selected);
349    }
350  }
351
352  async schedSliceDetails(id: number) {
353    const sqlQuery = `SELECT
354      sched.ts,
355      sched.dur,
356      sched.priority,
357      sched.end_state as endState,
358      sched.utid,
359      sched.cpu,
360      thread_state.id as threadStateId
361    FROM sched left join thread_state using(ts, utid, cpu)
362    WHERE sched.id = ${id}`;
363    const result = await this.args.engine.query(sqlQuery);
364    // Check selection is still the same on completion of query.
365    const selection = getLegacySelection(globals.state);
366    if (result.numRows() > 0 && selection) {
367      const row = result.firstRow({
368        ts: LONG,
369        dur: LONG,
370        priority: NUM,
371        endState: STR_NULL,
372        utid: NUM,
373        cpu: NUM,
374        threadStateId: NUM_NULL,
375      });
376      const ts = Time.fromRaw(row.ts);
377      const dur = row.dur;
378      const priority = row.priority;
379      const endState = row.endState;
380      const utid = row.utid;
381      const cpu = row.cpu;
382      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
383      const threadStateId = row.threadStateId || undefined;
384      const selected: SliceDetails = {
385        ts,
386        dur,
387        priority,
388        endState,
389        cpu,
390        id,
391        utid,
392        threadStateId,
393      };
394      Object.assign(selected, await this.computeThreadDetails(utid));
395
396      this.schedulingDetails(ts, utid)
397        .then((wakeResult) => {
398          Object.assign(selected, wakeResult);
399        })
400        .finally(() => {
401          publishSliceDetails(selected);
402        });
403    }
404  }
405
406  async schedulingDetails(ts: time, utid: number) {
407    // Find the ts of the first wakeup before the current slice.
408    const wakeResult = await this.args.engine.query(`
409      select ts, waker_utid as wakerUtid
410      from thread_state
411      where utid = ${utid} and ts < ${ts} and state = 'R'
412      order by ts desc
413      limit 1
414    `);
415    if (wakeResult.numRows() === 0) {
416      return undefined;
417    }
418
419    const wakeFirstRow = wakeResult.firstRow({ts: LONG, wakerUtid: NUM_NULL});
420    const wakeupTs = wakeFirstRow.ts;
421    const wakerUtid = wakeFirstRow.wakerUtid;
422    if (wakerUtid === null) {
423      return undefined;
424    }
425
426    // Find the previous sched slice for the current utid.
427    const prevSchedResult = await this.args.engine.query(`
428      select ts
429      from sched
430      where utid = ${utid} and ts < ${ts}
431      order by ts desc
432      limit 1
433    `);
434
435    // If this is the first sched slice for this utid or if the wakeup found
436    // was after the previous slice then we know the wakeup was for this slice.
437    if (
438      prevSchedResult.numRows() !== 0 &&
439      wakeupTs < prevSchedResult.firstRow({ts: LONG}).ts
440    ) {
441      return undefined;
442    }
443
444    // Find the sched slice with the utid of the waker running when the
445    // sched wakeup occurred. This is the waker.
446    const wakerResult = await this.args.engine.query(`
447      select cpu
448      from sched
449      where
450        utid = ${wakerUtid} and
451        ts < ${wakeupTs} and
452        ts + dur >= ${wakeupTs};
453    `);
454    if (wakerResult.numRows() === 0) {
455      return undefined;
456    }
457
458    const wakerRow = wakerResult.firstRow({cpu: NUM});
459    return {wakeupTs, wakerUtid, wakerCpu: wakerRow.cpu};
460  }
461
462  async computeThreadDetails(
463    utid: number,
464  ): Promise<ThreadDetails & ProcessDetails> {
465    const threadInfo = (
466      await this.args.engine.query(`
467          SELECT tid, name, upid
468          FROM thread
469          WHERE utid = ${utid};
470      `)
471    ).firstRow({tid: NUM, name: STR_NULL, upid: NUM_NULL});
472    const threadDetails = {
473      tid: threadInfo.tid,
474      threadName: threadInfo.name || undefined,
475    };
476    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
477    if (threadInfo.upid) {
478      return Object.assign(
479        {},
480        threadDetails,
481        await this.computeProcessDetails(threadInfo.upid),
482      );
483    }
484    return threadDetails;
485  }
486
487  async computeProcessDetails(upid: number): Promise<ProcessDetails> {
488    const details: ProcessDetails = {};
489    const processResult = (
490      await this.args.engine.query(`
491                SELECT pid, name, uid FROM process WHERE upid = ${upid};
492              `)
493    ).firstRow({pid: NUM, name: STR_NULL, uid: NUM_NULL});
494    details.pid = processResult.pid;
495    details.processName = processResult.name || undefined;
496    if (processResult.uid === null) {
497      return details;
498    }
499    details.uid = processResult.uid;
500
501    const packageResult = await this.args.engine.query(`
502                  SELECT
503                    package_name as packageName,
504                    version_code as versionCode
505                  FROM package_list WHERE uid = ${details.uid};
506                `);
507    // The package_list table is not populated in some traces so we need to
508    // check if the result has returned any rows.
509    if (packageResult.numRows() > 0) {
510      const packageDetails = packageResult.firstRow({
511        packageName: STR,
512        versionCode: NUM,
513      });
514      details.packageName = packageDetails.packageName;
515      details.versionCode = packageDetails.versionCode;
516    }
517    return details;
518  }
519}
520