• 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 {Arg, Args} from '../common/arg_types';
16import {Engine} from '../common/engine';
17import {
18  NUM,
19  singleRow,
20  singleRowUntyped,
21  slowlyCountRows,
22  STR
23} from '../common/query_iterator';
24import {ChromeSliceSelection} from '../common/state';
25import {translateState} from '../common/thread_state';
26import {fromNs, toNs} from '../common/time';
27import {
28  CounterDetails,
29  SliceDetails,
30  ThreadStateDetails
31} from '../frontend/globals';
32import {SLICE_TRACK_KIND} from '../tracks/chrome_slices/common';
33
34import {parseArgs} from './args_parser';
35import {Controller} from './controller';
36import {globals} from './globals';
37
38export interface SelectionControllerArgs {
39  engine: Engine;
40}
41
42// This class queries the TP for the details on a specific slice that has
43// been clicked.
44export class SelectionController extends Controller<'main'> {
45  private lastSelectedId?: number|string;
46  private lastSelectedKind?: string;
47  constructor(private args: SelectionControllerArgs) {
48    super('main');
49  }
50
51  run() {
52    const selection = globals.state.currentSelection;
53    if (!selection || selection.kind === 'AREA') return;
54
55    const selectWithId =
56        ['SLICE', 'COUNTER', 'CHROME_SLICE', 'HEAP_PROFILE', 'THREAD_STATE'];
57    if (!selectWithId.includes(selection.kind) ||
58        (selectWithId.includes(selection.kind) &&
59         selection.id === this.lastSelectedId &&
60         selection.kind === this.lastSelectedKind)) {
61      return;
62    }
63    const selectedId = selection.id;
64    const selectedKind = selection.kind;
65    this.lastSelectedId = selectedId;
66    this.lastSelectedKind = selectedKind;
67
68    if (selectedId === undefined) return;
69
70    if (selection.kind === 'COUNTER') {
71      const selected: CounterDetails = {};
72      this.counterDetails(selection.leftTs, selection.rightTs, selection.id)
73          .then(results => {
74            if (results !== undefined && selection &&
75                selection.kind === selectedKind &&
76                selection.id === selectedId) {
77              Object.assign(selected, results);
78              globals.publish('CounterDetails', selected);
79            }
80          });
81    } else if (selection.kind === 'SLICE') {
82      this.sliceDetails(selectedId as number);
83    } else if (selection.kind === 'THREAD_STATE') {
84      this.threadStateDetails(selection.id);
85    } else if (selection.kind === 'CHROME_SLICE') {
86      this.chromeSliceDetails(selection);
87    }
88  }
89
90  async chromeSliceDetails(selection: ChromeSliceSelection) {
91    const selectedId = selection.id;
92    const table = selection.table;
93
94    let leafTable: string;
95    let promisedDescription: Promise<Map<string, string>>;
96    let promisedArgs: Promise<Args>;
97    // TODO(b/155483804): This is a hack to ensure annotation slices are
98    // selectable for now. We should tidy this up when improving this class.
99    if (table === 'annotation') {
100      leafTable = 'annotation_slice';
101      promisedDescription = Promise.resolve(new Map());
102      promisedArgs = Promise.resolve(new Map());
103    } else {
104      const typeResult = singleRow(
105          {
106            leafTable: STR,
107            argSetId: NUM,
108          },
109          await this.args.engine.query(`
110        SELECT
111          type as leafTable,
112          arg_set_id as argSetId
113        FROM slice WHERE id = ${selectedId}`));
114
115      if (typeResult === undefined) {
116        return;
117      }
118
119      leafTable = typeResult.leafTable;
120      const argSetId = typeResult.argSetId;
121      promisedDescription = this.describeSlice(selectedId);
122      promisedArgs = this.getArgs(argSetId);
123    }
124
125    const promisedDetails = this.args.engine.query(`
126      SELECT * FROM ${leafTable} WHERE id = ${selectedId};
127    `);
128
129    const [details, args, description] =
130        await Promise.all([promisedDetails, promisedArgs, promisedDescription]);
131
132    const row = singleRowUntyped(details);
133    if (row === undefined) {
134      return;
135    }
136
137    // A few columns are hard coded as part of the SliceDetails interface.
138    // Long term these should be handled generically as args but for now
139    // handle them specially:
140    let ts = undefined;
141    let dur = undefined;
142    let name = undefined;
143    let category = undefined;
144
145    for (const [k, v] of Object.entries(row)) {
146      switch (k) {
147        case 'id':
148          break;
149        case 'ts':
150          ts = fromNs(Number(v)) - globals.state.traceTime.startSec;
151          break;
152        case 'name':
153          name = `${v}`;
154          break;
155        case 'dur':
156          dur = fromNs(Number(v));
157          break;
158        case 'category':
159        case 'cat':
160          category = `${v}`;
161          break;
162        default:
163          args.set(k, `${v}`);
164      }
165    }
166
167    const argsTree = parseArgs(args);
168    const selected: SliceDetails = {
169      id: selectedId,
170      ts,
171      dur,
172      name,
173      category,
174      args,
175      argsTree,
176      description,
177    };
178
179    // Check selection is still the same on completion of query.
180    if (selection === globals.state.currentSelection) {
181      globals.publish('SliceDetails', selected);
182    }
183  }
184
185  async describeSlice(id: number): Promise<Map<string, string>> {
186    const map = new Map<string, string>();
187    if (id === -1) return map;
188    const query = `
189      select description, doc_link
190      from describe_slice
191      where slice_id = ${id}
192    `;
193    const result = await this.args.engine.query(query);
194    for (let i = 0; i < slowlyCountRows(result); i++) {
195      const description = result.columns[0].stringValues![i];
196      const docLink = result.columns[1].stringValues![i];
197      map.set('Description', description);
198      map.set('Documentation', docLink);
199    }
200    return map;
201  }
202
203  async getArgs(argId: number): Promise<Args> {
204    const args = new Map<string, Arg>();
205    const query = `
206      select
207        key AS name,
208        CAST(COALESCE(int_value, string_value, real_value) AS text) AS value
209      FROM args
210      WHERE arg_set_id = ${argId}
211    `;
212    const result = await this.args.engine.query(query);
213    for (let i = 0; i < slowlyCountRows(result); i++) {
214      const name = result.columns[0].stringValues![i];
215      const value = result.columns[1].stringValues![i];
216      if (name === 'destination slice id' && !isNaN(Number(value))) {
217        const destTrackId = await this.getDestTrackId(value);
218        args.set(
219            'Destination Slice',
220            {kind: 'SLICE', trackId: destTrackId, sliceId: Number(value)});
221      } else {
222        args.set(name, value);
223      }
224    }
225    return args;
226  }
227
228  async getDestTrackId(sliceId: string): Promise<string> {
229    const trackIdQuery = `select track_id from slice
230    where slice_id = ${sliceId}`;
231    const destResult = await this.args.engine.query(trackIdQuery);
232    const trackIdTp = destResult.columns[0].longValues![0];
233    // TODO(hjd): If we had a consistent mapping from TP track_id
234    // UI track id for slice tracks this would be unnecessary.
235    let trackId = '';
236    for (const track of Object.values(globals.state.tracks)) {
237      if (track.kind === SLICE_TRACK_KIND &&
238          (track.config as {trackId: number}).trackId === Number(trackIdTp)) {
239        trackId = track.id;
240        break;
241      }
242    }
243    return trackId;
244  }
245
246  async threadStateDetails(id: number) {
247    const query = `
248      SELECT
249        ts,
250        thread_state.dur,
251        state,
252        io_wait,
253        thread_state.utid,
254        thread_state.cpu,
255        sched.id,
256        thread_state.blocked_function
257      from thread_state
258      left join sched using(ts) where thread_state.id = ${id}
259    `;
260    this.args.engine.query(query).then(result => {
261      const selection = globals.state.currentSelection;
262      const cols = result.columns;
263      if (slowlyCountRows(result) === 1 && selection) {
264        const ts = cols[0].longValues![0];
265        const timeFromStart = fromNs(ts) - globals.state.traceTime.startSec;
266        const dur = fromNs(cols[1].longValues![0]);
267        const stateStr = cols[2].stringValues![0];
268        const ioWait =
269            cols[3].isNulls![0] ? undefined : !!cols[3].longValues![0];
270        const state = translateState(stateStr, ioWait);
271        const utid = cols[4].longValues![0];
272        const cpu = cols[5].isNulls![0] ? undefined : cols[5].longValues![0];
273        const sliceId =
274            cols[6].isNulls![0] ? undefined : cols[6].longValues![0];
275        const blockedFunction =
276            cols[7].isNulls![0] ? undefined : cols[7].stringValues![0];
277        const selected: ThreadStateDetails = {
278          ts: timeFromStart,
279          dur,
280          state,
281          utid,
282          cpu,
283          sliceId,
284          blockedFunction
285        };
286        globals.publish('ThreadStateDetails', selected);
287      }
288    });
289  }
290
291  async sliceDetails(id: number) {
292    const sqlQuery = `SELECT ts, dur, priority, end_state, utid, cpu,
293    thread_state.id FROM sched join thread_state using(ts, utid, dur, cpu)
294    WHERE sched.id = ${id}`;
295    this.args.engine.query(sqlQuery).then(result => {
296      // Check selection is still the same on completion of query.
297      const selection = globals.state.currentSelection;
298      if (slowlyCountRows(result) === 1 && selection) {
299        const ts = result.columns[0].longValues![0];
300        const timeFromStart = fromNs(ts) - globals.state.traceTime.startSec;
301        const dur = fromNs(result.columns[1].longValues![0]);
302        const priority = result.columns[2].longValues![0];
303        const endState = result.columns[3].stringValues![0];
304        const utid = result.columns[4].longValues![0];
305        const cpu = result.columns[5].longValues![0];
306        const threadStateId = result.columns[6].longValues![0];
307        const selected: SliceDetails = {
308          ts: timeFromStart,
309          dur,
310          priority,
311          endState,
312          cpu,
313          id,
314          utid,
315          threadStateId
316        };
317        this.schedulingDetails(ts, utid)
318            .then(wakeResult => {
319              Object.assign(selected, wakeResult);
320            })
321            .finally(() => {
322              globals.publish('SliceDetails', selected);
323            });
324      }
325    });
326  }
327
328  async counterDetails(ts: number, rightTs: number, id: number) {
329    const counter = await this.args.engine.query(
330        `SELECT value, track_id FROM counter WHERE id = ${id}`);
331    const value = counter.columns[0].doubleValues![0];
332    const trackId = counter.columns[1].longValues![0];
333    // Finding previous value. If there isn't previous one, it will return 0 for
334    // ts and value.
335    const previous = await this.args.engine.query(
336        `SELECT MAX(ts), value FROM counter WHERE ts < ${ts} and track_id = ${
337            trackId}`);
338    const previousValue = previous.columns[1].doubleValues![0];
339    const endTs =
340        rightTs !== -1 ? rightTs : toNs(globals.state.traceTime.endSec);
341    const delta = value - previousValue;
342    const duration = endTs - ts;
343    const startTime = fromNs(ts) - globals.state.traceTime.startSec;
344    return {startTime, value, delta, duration};
345  }
346
347  async schedulingDetails(ts: number, utid: number|Long) {
348    let event = 'sched_waking';
349    const waking = await this.args.engine.query(
350        `select * from instants where name = 'sched_waking' limit 1`);
351    const wakeup = await this.args.engine.query(
352        `select * from instants where name = 'sched_wakeup' limit 1`);
353    if (slowlyCountRows(waking) === 0) {
354      if (slowlyCountRows(wakeup) === 0) return undefined;
355      // Only use sched_wakeup if waking is not in the trace.
356      event = 'sched_wakeup';
357    }
358
359    // Find the ts of the first sched_wakeup before the current slice.
360    const queryWakeupTs = `select ts from instants where name = '${event}'
361    and ref = ${utid} and ts < ${ts} order by ts desc limit 1`;
362    const wakeupRow = await this.args.engine.queryOneRow(queryWakeupTs);
363    // Find the previous sched slice for the current utid.
364    const queryPrevSched = `select ts from sched where utid = ${utid}
365    and ts < ${ts} order by ts desc limit 1`;
366    const prevSchedRow = await this.args.engine.queryOneRow(queryPrevSched);
367    // If this is the first sched slice for this utid or if the wakeup found
368    // was after the previous slice then we know the wakeup was for this slice.
369    if (wakeupRow[0] === undefined ||
370        (prevSchedRow[0] !== undefined && wakeupRow[0] < prevSchedRow[0])) {
371      return undefined;
372    }
373    const wakeupTs = wakeupRow[0];
374    // Find the sched slice with the utid of the waker running when the
375    // sched wakeup occurred. This is the waker.
376    const queryWaker = `select utid, cpu from sched where utid =
377    (select utid from raw where name = '${event}' and ts = ${wakeupTs})
378    and ts < ${wakeupTs} and ts + dur >= ${wakeupTs};`;
379    const wakerRow = await this.args.engine.queryOneRow(queryWaker);
380    if (wakerRow) {
381      return {
382        wakeupTs: fromNs(wakeupTs),
383        wakerUtid: wakerRow[0],
384        wakerCpu: wakerRow[1]
385      };
386    } else {
387      return undefined;
388    }
389  }
390}
391