xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.ThreadState/thread_state_details_panel.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 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';
16import {Anchor} from '../../widgets/anchor';
17import {Button} from '../../widgets/button';
18import {DetailsShell} from '../../widgets/details_shell';
19import {GridLayout} from '../../widgets/grid_layout';
20import {Section} from '../../widgets/section';
21import {SqlRef} from '../../widgets/sql_ref';
22import {Tree, TreeNode} from '../../widgets/tree';
23import {Intent} from '../../widgets/common';
24import {SchedSqlId} from '../../components/sql_utils/core_types';
25import {
26  getThreadState,
27  getThreadStateFromConstraints,
28  ThreadState,
29} from '../../components/sql_utils/thread_state';
30import {DurationWidget} from '../../components/widgets/duration';
31import {Timestamp} from '../../components/widgets/timestamp';
32import {getProcessName} from '../../components/sql_utils/process';
33import {
34  getFullThreadName,
35  getThreadName,
36} from '../../components/sql_utils/thread';
37import {ThreadStateRef} from '../../components/widgets/thread_state';
38import {
39  CRITICAL_PATH_CMD,
40  CRITICAL_PATH_LITE_CMD,
41} from '../../public/exposed_commands';
42import {goToSchedSlice} from '../../components/widgets/sched';
43import {TrackEventDetailsPanel} from '../../public/details_panel';
44import {Trace} from '../../public/trace';
45import {formatDuration} from '../../components/time_utils';
46
47interface RelatedThreadStates {
48  prev?: ThreadState;
49  next?: ThreadState;
50  waker?: ThreadState;
51  wakerInterruptCtx?: boolean;
52  wakee?: ThreadState[];
53}
54
55export class ThreadStateDetailsPanel implements TrackEventDetailsPanel {
56  private threadState?: ThreadState;
57  private relatedStates?: RelatedThreadStates;
58
59  constructor(
60    private readonly trace: Trace,
61    private readonly id: number,
62  ) {}
63
64  async load() {
65    const id = this.id;
66    this.threadState = await getThreadState(this.trace.engine, id);
67
68    if (!this.threadState) {
69      return;
70    }
71
72    const relatedStates: RelatedThreadStates = {};
73    relatedStates.prev = (
74      await getThreadStateFromConstraints(this.trace.engine, {
75        filters: [
76          `ts + dur = ${this.threadState.ts}`,
77          `utid = ${this.threadState.thread?.utid}`,
78        ],
79        limit: 1,
80      })
81    )[0];
82    relatedStates.next = (
83      await getThreadStateFromConstraints(this.trace.engine, {
84        filters: [
85          `ts = ${this.threadState.ts + this.threadState.dur}`,
86          `utid = ${this.threadState.thread?.utid}`,
87        ],
88        limit: 1,
89      })
90    )[0];
91
92    // note: this might be valid even if there is no |waker| slice, in the case
93    // of an interrupt wakeup while in the idle process (which is omitted from
94    // the thread_state table).
95    relatedStates.wakerInterruptCtx = this.threadState.wakerInterruptCtx;
96
97    if (this.threadState.wakerId !== undefined) {
98      relatedStates.waker = await getThreadState(
99        this.trace.engine,
100        this.threadState.wakerId,
101      );
102    } else if (
103      this.threadState.state == 'Running' &&
104      relatedStates.prev.wakerId != undefined
105    ) {
106      // For running slices, extract waker info from the preceding runnable.
107      relatedStates.waker = await getThreadState(
108        this.trace.engine,
109        relatedStates.prev.wakerId,
110      );
111      relatedStates.wakerInterruptCtx = relatedStates.prev.wakerInterruptCtx;
112    }
113
114    relatedStates.wakee = await getThreadStateFromConstraints(
115      this.trace.engine,
116      {
117        filters: [
118          `waker_id = ${id}`,
119          `(irq_context is null or irq_context = 0)`,
120        ],
121      },
122    );
123    this.relatedStates = relatedStates;
124  }
125
126  render() {
127    // TODO(altimin/stevegolton): Differentiate between "Current Selection" and
128    // "Pinned" views in DetailsShell.
129    return m(
130      DetailsShell,
131      {title: 'Thread State', description: this.renderLoadingText()},
132      m(
133        GridLayout,
134        m(
135          Section,
136          {title: 'Details'},
137          this.threadState && this.renderTree(this.threadState),
138        ),
139        m(
140          Section,
141          {title: 'Related thread states'},
142          this.renderRelatedThreadStates(),
143        ),
144      ),
145    );
146  }
147
148  private renderLoadingText() {
149    if (!this.threadState) {
150      return 'Loading';
151    }
152    return this.id;
153  }
154
155  private renderTree(threadState: ThreadState) {
156    const thread = threadState.thread;
157    const process = threadState.thread?.process;
158    return m(
159      Tree,
160      m(TreeNode, {
161        left: 'Start time',
162        right: m(Timestamp, {ts: threadState.ts}),
163      }),
164      m(TreeNode, {
165        left: 'Duration',
166        right: m(DurationWidget, {dur: threadState.dur}),
167      }),
168      m(TreeNode, {
169        left: 'State',
170        right: this.renderState(
171          threadState.state,
172          threadState.cpu,
173          threadState.schedSqlId,
174        ),
175      }),
176      threadState.blockedFunction &&
177        m(TreeNode, {
178          left: 'Blocked function',
179          right: threadState.blockedFunction,
180        }),
181      process &&
182        m(TreeNode, {
183          left: 'Process',
184          right: getProcessName(process),
185        }),
186      thread && m(TreeNode, {left: 'Thread', right: getThreadName(thread)}),
187      threadState.priority !== undefined &&
188        m(TreeNode, {
189          left: 'Priority',
190          right: threadState.priority,
191        }),
192      m(TreeNode, {
193        left: 'SQL ID',
194        right: m(SqlRef, {table: 'thread_state', id: threadState.id}),
195      }),
196    );
197  }
198
199  private renderState(
200    state: string,
201    cpu: number | undefined,
202    id: SchedSqlId | undefined,
203  ): m.Children {
204    if (!state) {
205      return null;
206    }
207    if (id === undefined || cpu === undefined) {
208      return state;
209    }
210    return m(
211      Anchor,
212      {
213        title: 'Go to CPU slice',
214        icon: 'call_made',
215        onclick: () => goToSchedSlice(id),
216      },
217      `${state} on CPU ${cpu}`,
218    );
219  }
220
221  private renderRelatedThreadStates(): m.Children {
222    if (this.threadState === undefined || this.relatedStates === undefined) {
223      return 'Loading';
224    }
225    const startTs = this.threadState.ts;
226    const renderRef = (state: ThreadState, name?: string) =>
227      m(ThreadStateRef, {
228        id: state.id,
229        name,
230      });
231
232    const nameForNextOrPrev = (threadState: ThreadState) =>
233      `${threadState.state} for ${formatDuration(this.trace, threadState.dur)}`;
234
235    const renderWaker = (related: RelatedThreadStates) => {
236      // Could be absent if:
237      // * this thread state wasn't woken up (e.g. it is a running slice).
238      // * the wakeup is from an interrupt during the idle process (which
239      //   isn't populated in thread_state).
240      // * at the start of the trace, before all per-cpu scheduling is known.
241      const hasWakerId = related.waker !== undefined;
242      // Interrupt context for the wakeups is absent from older traces.
243      const hasInterruptCtx = related.wakerInterruptCtx !== undefined;
244
245      if (!hasWakerId && !hasInterruptCtx) {
246        return null;
247      }
248      if (related.wakerInterruptCtx) {
249        return m(TreeNode, {
250          left: 'Woken by',
251          right: `Interrupt`,
252        });
253      }
254      return (
255        related.waker &&
256        m(TreeNode, {
257          left: hasInterruptCtx ? 'Woken by' : 'Woken by (maybe interrupt)',
258          right: renderRef(
259            related.waker,
260            getFullThreadName(related.waker.thread),
261          ),
262        })
263      );
264    };
265
266    const renderWakees = (related: RelatedThreadStates) => {
267      if (related.wakee === undefined || related.wakee.length == 0) {
268        return null;
269      }
270      const hasInterruptCtx = related.wakee[0].wakerInterruptCtx !== undefined;
271      return m(
272        TreeNode,
273        {
274          left: hasInterruptCtx
275            ? 'Woken threads'
276            : 'Woken threads (maybe interrupt)',
277        },
278        related.wakee.map((state) =>
279          m(TreeNode, {
280            left: m(Timestamp, {
281              ts: state.ts,
282              display: `+${formatDuration(this.trace, state.ts - startTs)}`,
283            }),
284            right: renderRef(state, getFullThreadName(state.thread)),
285          }),
286        ),
287      );
288    };
289
290    return [
291      m(
292        Tree,
293        this.relatedStates.prev &&
294          m(TreeNode, {
295            left: 'Previous state',
296            right: renderRef(
297              this.relatedStates.prev,
298              nameForNextOrPrev(this.relatedStates.prev),
299            ),
300          }),
301        this.relatedStates.next &&
302          m(TreeNode, {
303            left: 'Next state',
304            right: renderRef(
305              this.relatedStates.next,
306              nameForNextOrPrev(this.relatedStates.next),
307            ),
308          }),
309        renderWaker(this.relatedStates),
310        renderWakees(this.relatedStates),
311      ),
312      this.trace.commands.hasCommand(CRITICAL_PATH_LITE_CMD) &&
313        m(Button, {
314          label: 'Critical path lite',
315          intent: Intent.Primary,
316          onclick: () => {
317            this.trace.commands.runCommand(
318              CRITICAL_PATH_LITE_CMD,
319              this.threadState?.thread?.utid,
320            );
321          },
322        }),
323      this.trace.commands.hasCommand(CRITICAL_PATH_CMD) &&
324        m(Button, {
325          label: 'Critical path',
326          intent: Intent.Primary,
327          onclick: () => {
328            this.trace.commands.runCommand(
329              CRITICAL_PATH_CMD,
330              this.threadState?.thread?.utid,
331            );
332          },
333        }),
334    ];
335  }
336
337  isLoading() {
338    return this.threadState === undefined || this.relatedStates === undefined;
339  }
340}
341