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