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