• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2023 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 m from 'mithril';
16
17import {duration, Time, time} from '../../base/time';
18import {raf} from '../../core/raf_scheduler';
19import {BottomTab, NewBottomTabArgs} from '../bottom_tab';
20import {GenericSliceDetailsTabConfig} from '../generic_slice_details_tab';
21import {hasArgs, renderArguments} from '../slice_args';
22import {getSlice, SliceDetails, sliceRef} from '../sql/slice';
23import {asSliceSqlId, Utid} from '../sql_types';
24import {getProcessName, getThreadName} from '../thread_and_process_info';
25import {getThreadState, ThreadState, threadStateRef} from '../thread_state';
26import {DurationWidget} from '../widgets/duration';
27import {Timestamp} from '../widgets/timestamp';
28import {
29  ColumnType,
30  durationFromSql,
31  LONG,
32  STR,
33  timeFromSql,
34} from '../../trace_processor/query_result';
35import {sqlValueToReadableString} from '../../trace_processor/sql_utils';
36import {DetailsShell} from '../../widgets/details_shell';
37import {GridLayout} from '../../widgets/grid_layout';
38import {Section} from '../../widgets/section';
39import {dictToTree, dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree';
40
41export const ARG_PREFIX = 'arg_';
42
43function sqlValueToNumber(value?: ColumnType): number | undefined {
44  if (typeof value === 'bigint') return Number(value);
45  if (typeof value !== 'number') return undefined;
46  return value;
47}
48
49function sqlValueToUtid(value?: ColumnType): Utid | undefined {
50  if (typeof value === 'bigint') return Number(value) as Utid;
51  if (typeof value !== 'number') return undefined;
52  return value as Utid;
53}
54
55function renderTreeContents(dict: {[key: string]: m.Child}): m.Child[] {
56  const children: m.Child[] = [];
57  for (const key of Object.keys(dict)) {
58    if (dict[key] === null || dict[key] === undefined) continue;
59    children.push(
60      m(TreeNode, {
61        left: key,
62        right: dict[key],
63      }),
64    );
65  }
66  return children;
67}
68
69export class DebugSliceDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> {
70  static readonly kind = 'dev.perfetto.DebugSliceDetailsTab';
71
72  data?: {
73    name: string;
74    ts: time;
75    dur: duration;
76    args: {[key: string]: ColumnType};
77  };
78  // We will try to interpret the arguments as references into well-known
79  // tables. These values will be set if the relevant columns exist and
80  // are consistent (e.g. 'ts' and 'dur' for this slice correspond to values
81  // in these well-known tables).
82  threadState?: ThreadState;
83  slice?: SliceDetails;
84
85  static create(
86    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
87  ): DebugSliceDetailsTab {
88    return new DebugSliceDetailsTab(args);
89  }
90
91  private async maybeLoadThreadState(
92    id: number | undefined,
93    ts: time,
94    dur: duration,
95    table: string | undefined,
96    utid?: Utid,
97  ): Promise<ThreadState | undefined> {
98    if (id === undefined) return undefined;
99    if (utid === undefined) return undefined;
100
101    const threadState = await getThreadState(this.engine, id);
102    if (threadState === undefined) return undefined;
103    if (
104      table === 'thread_state' ||
105      (threadState.ts === ts &&
106        threadState.dur === dur &&
107        threadState.thread?.utid === utid)
108    ) {
109      return threadState;
110    } else {
111      return undefined;
112    }
113  }
114
115  private renderThreadStateInfo(): m.Child {
116    if (this.threadState === undefined) return null;
117    return m(
118      TreeNode,
119      {
120        left: threadStateRef(this.threadState),
121        right: '',
122      },
123      renderTreeContents({
124        Thread: getThreadName(this.threadState.thread),
125        Process: getProcessName(this.threadState.thread?.process),
126        State: this.threadState.state,
127      }),
128    );
129  }
130
131  private async maybeLoadSlice(
132    id: number | undefined,
133    ts: time,
134    dur: duration,
135    table: string | undefined,
136    trackId?: number,
137  ): Promise<SliceDetails | undefined> {
138    if (id === undefined) return undefined;
139    if (table !== 'slice' && trackId === undefined) return undefined;
140
141    const slice = await getSlice(this.engine, asSliceSqlId(id));
142    if (slice === undefined) return undefined;
143    if (
144      table === 'slice' ||
145      (slice.ts === ts && slice.dur === dur && slice.trackId === trackId)
146    ) {
147      return slice;
148    } else {
149      return undefined;
150    }
151  }
152
153  private renderSliceInfo(): m.Child {
154    if (this.slice === undefined) return null;
155    return m(
156      TreeNode,
157      {
158        left: sliceRef(this.slice, 'Slice'),
159        right: '',
160      },
161      m(TreeNode, {
162        left: 'Name',
163        right: this.slice.name,
164      }),
165      m(TreeNode, {
166        left: 'Thread',
167        right: getThreadName(this.slice.thread),
168      }),
169      m(TreeNode, {
170        left: 'Process',
171        right: getProcessName(this.slice.process),
172      }),
173      hasArgs(this.slice.args) &&
174        m(
175          TreeNode,
176          {
177            left: 'Args',
178          },
179          renderArguments(this.engine, this.slice.args),
180        ),
181    );
182  }
183
184  private async loadData() {
185    const queryResult = await this.engine.query(
186      `select * from ${this.config.sqlTableName} where id = ${this.config.id}`,
187    );
188    const row = queryResult.firstRow({
189      ts: LONG,
190      dur: LONG,
191      name: STR,
192    });
193    this.data = {
194      name: row.name,
195      ts: Time.fromRaw(row.ts),
196      dur: row.dur,
197      args: {},
198    };
199
200    for (const key of Object.keys(row)) {
201      if (key.startsWith(ARG_PREFIX)) {
202        this.data.args[key.substr(ARG_PREFIX.length)] = (
203          row as {[key: string]: ColumnType}
204        )[key];
205      }
206    }
207
208    this.threadState = await this.maybeLoadThreadState(
209      sqlValueToNumber(this.data.args['id']),
210      this.data.ts,
211      this.data.dur,
212      sqlValueToReadableString(this.data.args['table_name']),
213      sqlValueToUtid(this.data.args['utid']),
214    );
215
216    this.slice = await this.maybeLoadSlice(
217      sqlValueToNumber(this.data.args['id']) ??
218        sqlValueToNumber(this.data.args['slice_id']),
219      this.data.ts,
220      this.data.dur,
221      sqlValueToReadableString(this.data.args['table_name']),
222      sqlValueToNumber(this.data.args['track_id']),
223    );
224
225    raf.scheduleRedraw();
226  }
227
228  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
229    super(args);
230    this.loadData();
231  }
232
233  viewTab() {
234    if (this.data === undefined) {
235      return m('h2', 'Loading');
236    }
237    const details = dictToTreeNodes({
238      'Name': this.data['name'] as string,
239      'Start time': m(Timestamp, {ts: timeFromSql(this.data['ts'])}),
240      'Duration': m(DurationWidget, {dur: durationFromSql(this.data['dur'])}),
241      'Debug slice id': `${this.config.sqlTableName}[${this.config.id}]`,
242    });
243    details.push(this.renderThreadStateInfo());
244    details.push(this.renderSliceInfo());
245
246    const args: {[key: string]: m.Child} = {};
247    for (const key of Object.keys(this.data.args)) {
248      args[key] = sqlValueToReadableString(this.data.args[key]);
249    }
250
251    return m(
252      DetailsShell,
253      {
254        title: 'Debug Slice',
255      },
256      m(
257        GridLayout,
258        m(Section, {title: 'Details'}, m(Tree, details)),
259        m(Section, {title: 'Arguments'}, dictToTree(args)),
260      ),
261    );
262  }
263
264  getTitle(): string {
265    return `Current Selection`;
266  }
267
268  isLoading() {
269    return this.data === undefined;
270  }
271}
272