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