• 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 size 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 {Time, time} from '../base/time';
18import {raf} from '../core/raf_scheduler';
19import {Anchor} from '../widgets/anchor';
20import {Button} from '../widgets/button';
21import {DetailsShell} from '../widgets/details_shell';
22import {GridLayout} from '../widgets/grid_layout';
23import {Section} from '../widgets/section';
24import {SqlRef} from '../widgets/sql_ref';
25import {Tree, TreeNode} from '../widgets/tree';
26import {Intent} from '../widgets/common';
27
28import {BottomTab, NewBottomTabArgs} from './bottom_tab';
29import {SchedSqlId, ThreadStateSqlId} from './sql_types';
30import {
31  getFullThreadName,
32  getProcessName,
33  getThreadName,
34  ThreadInfo,
35} from './thread_and_process_info';
36import {
37  getThreadState,
38  getThreadStateFromConstraints,
39  goToSchedSlice,
40  ThreadState,
41  ThreadStateRef,
42} from './thread_state';
43import {DurationWidget, renderDuration} from './widgets/duration';
44import {Timestamp} from './widgets/timestamp';
45import {addDebugSliceTrack} from './debug_tracks/debug_tracks';
46import {globals} from './globals';
47
48interface ThreadStateTabConfig {
49  // Id into |thread_state| sql table.
50  readonly id: ThreadStateSqlId;
51}
52
53interface RelatedThreadStates {
54  prev?: ThreadState;
55  next?: ThreadState;
56  waker?: ThreadState;
57  wakee?: ThreadState[];
58}
59
60export class ThreadStateTab extends BottomTab<ThreadStateTabConfig> {
61  static readonly kind = 'dev.perfetto.ThreadStateTab';
62
63  state?: ThreadState;
64  relatedStates?: RelatedThreadStates;
65  loaded: boolean = false;
66
67  static create(args: NewBottomTabArgs<ThreadStateTabConfig>): ThreadStateTab {
68    return new ThreadStateTab(args);
69  }
70
71  constructor(args: NewBottomTabArgs<ThreadStateTabConfig>) {
72    super(args);
73
74    this.load().then(() => {
75      this.loaded = true;
76      raf.scheduleFullRedraw();
77    });
78  }
79
80  async load() {
81    this.state = await getThreadState(this.engine, this.config.id);
82
83    if (!this.state) {
84      return;
85    }
86
87    const relatedStates: RelatedThreadStates = {};
88    relatedStates.prev = (
89      await getThreadStateFromConstraints(this.engine, {
90        filters: [
91          `ts + dur = ${this.state.ts}`,
92          `utid = ${this.state.thread?.utid}`,
93        ],
94        limit: 1,
95      })
96    )[0];
97    relatedStates.next = (
98      await getThreadStateFromConstraints(this.engine, {
99        filters: [
100          `ts = ${this.state.ts + this.state.dur}`,
101          `utid = ${this.state.thread?.utid}`,
102        ],
103        limit: 1,
104      })
105    )[0];
106    if (this.state.wakerThread?.utid !== undefined) {
107      relatedStates.waker = (
108        await getThreadStateFromConstraints(this.engine, {
109          filters: [
110            `utid = ${this.state.wakerThread?.utid}`,
111            `ts <= ${this.state.ts}`,
112            `ts + dur >= ${this.state.ts}`,
113          ],
114        })
115      )[0];
116    }
117    relatedStates.wakee = await getThreadStateFromConstraints(this.engine, {
118      filters: [
119        `waker_utid = ${this.state.thread?.utid}`,
120        `state = 'R'`,
121        `ts >= ${this.state.ts}`,
122        `ts <= ${this.state.ts + this.state.dur}`,
123      ],
124    });
125
126    this.relatedStates = relatedStates;
127  }
128
129  getTitle() {
130    // TODO(altimin): Support dynamic titles here.
131    return 'Current Selection';
132  }
133
134  viewTab() {
135    // TODO(altimin/stevegolton): Differentiate between "Current Selection" and
136    // "Pinned" views in DetailsShell.
137    return m(
138      DetailsShell,
139      {title: 'Thread State', description: this.renderLoadingText()},
140      m(
141        GridLayout,
142        m(
143          Section,
144          {title: 'Details'},
145          this.state && this.renderTree(this.state),
146        ),
147        m(
148          Section,
149          {title: 'Related thread states'},
150          this.renderRelatedThreadStates(),
151        ),
152      ),
153    );
154  }
155
156  private renderLoadingText() {
157    if (!this.loaded) {
158      return 'Loading';
159    }
160    if (!this.state) {
161      return `Thread state ${this.config.id} does not exist`;
162    }
163    // TODO(stevegolton): Return something intelligent here.
164    return this.config.id;
165  }
166
167  private renderTree(state: ThreadState) {
168    const thread = state.thread;
169    const process = state.thread?.process;
170    return m(
171      Tree,
172      m(TreeNode, {
173        left: 'Start time',
174        right: m(Timestamp, {ts: state.ts}),
175      }),
176      m(TreeNode, {
177        left: 'Duration',
178        right: m(DurationWidget, {dur: state.dur}),
179      }),
180      m(TreeNode, {
181        left: 'State',
182        right: this.renderState(
183          state.state,
184          state.cpu,
185          state.schedSqlId,
186          state.ts,
187        ),
188      }),
189      state.blockedFunction &&
190        m(TreeNode, {
191          left: 'Blocked function',
192          right: state.blockedFunction,
193        }),
194      process &&
195        m(TreeNode, {
196          left: 'Process',
197          right: getProcessName(process),
198        }),
199      thread && m(TreeNode, {left: 'Thread', right: getThreadName(thread)}),
200      state.wakerThread && this.renderWakerThread(state.wakerThread),
201      m(TreeNode, {
202        left: 'SQL ID',
203        right: m(SqlRef, {table: 'thread_state', id: state.threadStateSqlId}),
204      }),
205    );
206  }
207
208  private renderState(
209    state: string,
210    cpu: number | undefined,
211    id: SchedSqlId | undefined,
212    ts: time,
213  ): m.Children {
214    if (!state) {
215      return null;
216    }
217    if (id === undefined || cpu === undefined) {
218      return state;
219    }
220    return m(
221      Anchor,
222      {
223        title: 'Go to CPU slice',
224        icon: 'call_made',
225        onclick: () => goToSchedSlice(cpu, id, ts),
226      },
227      `${state} on CPU ${cpu}`,
228    );
229  }
230
231  private renderWakerThread(wakerThread: ThreadInfo) {
232    return m(
233      TreeNode,
234      {left: 'Waker'},
235      m(TreeNode, {
236        left: 'Process',
237        right: getProcessName(wakerThread.process),
238      }),
239      m(TreeNode, {left: 'Thread', right: getThreadName(wakerThread)}),
240    );
241  }
242
243  private renderRelatedThreadStates(): m.Children {
244    if (this.state === undefined || this.relatedStates === undefined) {
245      return 'Loading';
246    }
247    const startTs = this.state.ts;
248    const renderRef = (state: ThreadState, name?: string) =>
249      m(ThreadStateRef, {
250        id: state.threadStateSqlId,
251        ts: state.ts,
252        dur: state.dur,
253        utid: state.thread!.utid,
254        name,
255      });
256
257    const sliceColumns = {ts: 'ts', dur: 'dur', name: 'name'};
258    const sliceColumnNames = ['id', 'utid', 'ts', 'dur', 'name', 'table_name'];
259
260    const sliceLiteColumns = {ts: 'ts', dur: 'dur', name: 'thread_name'};
261    const sliceLiteColumnNames = [
262      'id',
263      'utid',
264      'ts',
265      'dur',
266      'thread_name',
267      'process_name',
268      'table_name',
269    ];
270
271    const nameForNextOrPrev = (state: ThreadState) =>
272      `${state.state} for ${renderDuration(state.dur)}`;
273    return [
274      m(
275        Tree,
276        this.relatedStates.waker &&
277          m(TreeNode, {
278            left: 'Waker',
279            right: renderRef(
280              this.relatedStates.waker,
281              getFullThreadName(this.relatedStates.waker.thread),
282            ),
283          }),
284        this.relatedStates.prev &&
285          m(TreeNode, {
286            left: 'Previous state',
287            right: renderRef(
288              this.relatedStates.prev,
289              nameForNextOrPrev(this.relatedStates.prev),
290            ),
291          }),
292        this.relatedStates.next &&
293          m(TreeNode, {
294            left: 'Next state',
295            right: renderRef(
296              this.relatedStates.next,
297              nameForNextOrPrev(this.relatedStates.next),
298            ),
299          }),
300        this.relatedStates.wakee &&
301          this.relatedStates.wakee.length > 0 &&
302          m(
303            TreeNode,
304            {
305              left: 'Woken threads',
306            },
307            this.relatedStates.wakee.map((state) =>
308              m(TreeNode, {
309                left: m(Timestamp, {
310                  ts: state.ts,
311                  display: [
312                    'Start+',
313                    m(DurationWidget, {dur: Time.sub(state.ts, startTs)}),
314                  ],
315                }),
316                right: renderRef(state, getFullThreadName(state.thread)),
317              }),
318            ),
319          ),
320      ),
321      m(Button, {
322        label: 'Critical path lite',
323        intent: Intent.Primary,
324        onclick: () =>
325          this.engine
326            .query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
327            .then(() =>
328              addDebugSliceTrack(
329                // NOTE(stevegolton): This is a temporary patch, this menu
330                // should become part of a critical path plugin, at which point
331                // we can just use the plugin's context object.
332                {
333                  engine: this.engine,
334                  registerTrack: (x) => globals.trackManager.registerTrack(x),
335                },
336                {
337                  sqlSource: `
338                    SELECT
339                      cr.id,
340                      cr.utid,
341                      cr.ts,
342                      cr.dur,
343                      thread.name AS thread_name,
344                      process.name AS process_name,
345                      'thread_state' AS table_name
346                    FROM
347                      _thread_executing_span_critical_path(
348                        ${this.state?.thread?.utid},
349                        trace_bounds.start_ts,
350                        trace_bounds.end_ts - trace_bounds.start_ts) cr,
351                      trace_bounds
352                    JOIN thread USING(utid)
353                    JOIN process USING(upid)
354                  `,
355                  columns: sliceLiteColumnNames,
356                },
357                `${this.state?.thread?.name}`,
358                sliceLiteColumns,
359                sliceLiteColumnNames,
360              ),
361            ),
362      }),
363      m(Button, {
364        label: 'Critical path',
365        intent: Intent.Primary,
366        onclick: () =>
367          this.engine
368            .query(
369              `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
370            )
371            .then(() =>
372              addDebugSliceTrack(
373                // NOTE(stevegolton): This is a temporary patch, this menu
374                // should become part of a critical path plugin, at which point
375                // we can just use the plugin's context object.
376                {
377                  engine: this.engine,
378                  registerTrack: (x) => globals.trackManager.registerTrack(x),
379                },
380                {
381                  sqlSource: `
382                    SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
383                      FROM
384                        _thread_executing_span_critical_path_stack(
385                          ${this.state?.thread?.utid},
386                          trace_bounds.start_ts,
387                          trace_bounds.end_ts - trace_bounds.start_ts) cr,
388                        trace_bounds WHERE name IS NOT NULL
389                  `,
390                  columns: sliceColumnNames,
391                },
392                `${this.state?.thread?.name}`,
393                sliceColumns,
394                sliceColumnNames,
395              ),
396            ),
397      }),
398    ];
399  }
400
401  isLoading() {
402    return this.state === undefined || this.relatedStates === undefined;
403  }
404}
405