xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 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 {time, Time} from '../../base/time';
17import {colorForFtrace} from '../../components/colorizer';
18import {DetailsShell} from '../../widgets/details_shell';
19import {
20  MultiSelectDiff,
21  Option as MultiSelectOption,
22  PopupMultiSelect,
23} from '../../widgets/multiselect';
24import {PopupPosition} from '../../widgets/popup';
25import {Timestamp} from '../../components/widgets/timestamp';
26import {FtraceFilter, FtraceStat} from './common';
27import {Engine} from '../../trace_processor/engine';
28import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
29import {AsyncLimiter} from '../../base/async_limiter';
30import {Monitor} from '../../base/monitor';
31import {Button} from '../../widgets/button';
32import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table';
33import {Store} from '../../base/store';
34import {Trace} from '../../public/trace';
35
36const ROW_H = 20;
37
38interface FtraceExplorerAttrs {
39  cache: FtraceExplorerCache;
40  filterStore: Store<FtraceFilter>;
41  trace: Trace;
42}
43
44interface FtraceEvent {
45  id: number;
46  ts: time;
47  name: string;
48  cpu: number;
49  thread: string | null;
50  process: string | null;
51  args: string;
52}
53
54interface FtracePanelData {
55  events: FtraceEvent[];
56  offset: number;
57  numEvents: number; // Number of events in the visible window
58}
59
60interface Pagination {
61  offset: number;
62  count: number;
63}
64
65export interface FtraceExplorerCache {
66  state: 'blank' | 'loading' | 'valid';
67  counters: FtraceStat[];
68}
69
70async function getFtraceCounters(engine: Engine): Promise<FtraceStat[]> {
71  // TODO(stevegolton): this is an extraordinarily slow query on large traces
72  // as it goes through every ftrace event which can be a lot on big traces.
73  // Consider if we can have some different UX which avoids needing these
74  // counts
75  // TODO(mayzner): the +name below is an awful hack to workaround
76  // extraordinarily slow sorting of strings. However, even with this hack,
77  // this is just a slow query. There are various ways we can improve this
78  // (e.g. with using the vtab_distinct APIs of SQLite).
79  const result = await engine.query(`
80    select
81      name,
82      count(1) as cnt
83    from ftrace_event
84    group by name
85    order by cnt desc
86  `);
87  const counters: FtraceStat[] = [];
88  const it = result.iter({name: STR, cnt: NUM});
89  for (let row = 0; it.valid(); it.next(), row++) {
90    counters.push({name: it.name, count: it.cnt});
91  }
92  return counters;
93}
94
95export class FtraceExplorer implements m.ClassComponent<FtraceExplorerAttrs> {
96  private pagination: Pagination = {
97    offset: 0,
98    count: 0,
99  };
100  private readonly monitor: Monitor;
101  private readonly queryLimiter = new AsyncLimiter();
102
103  // A cache of the data we have most recently loaded from our store
104  private data?: FtracePanelData;
105
106  constructor({attrs}: m.CVnode<FtraceExplorerAttrs>) {
107    this.monitor = new Monitor([
108      () => attrs.trace.timeline.visibleWindow.toTimeSpan().start,
109      () => attrs.trace.timeline.visibleWindow.toTimeSpan().end,
110      () => attrs.filterStore.state,
111    ]);
112
113    if (attrs.cache.state === 'blank') {
114      getFtraceCounters(attrs.trace.engine)
115        .then((counters) => {
116          attrs.cache.counters = counters;
117          attrs.cache.state = 'valid';
118        })
119        .catch(() => {
120          attrs.cache.state = 'blank';
121        });
122      attrs.cache.state = 'loading';
123    }
124  }
125
126  view({attrs}: m.CVnode<FtraceExplorerAttrs>) {
127    this.monitor.ifStateChanged(() => {
128      this.reloadData(attrs);
129    });
130
131    return m(
132      DetailsShell,
133      {
134        title: this.renderTitle(),
135        buttons: this.renderFilterPanel(attrs),
136        fillParent: true,
137      },
138      m(VirtualTable, {
139        className: 'pf-ftrace-explorer',
140        columns: [
141          {header: 'ID', width: '5em'},
142          {header: 'Timestamp', width: '13em'},
143          {header: 'Name', width: '24em'},
144          {header: 'CPU', width: '3em'},
145          {header: 'Process', width: '24em'},
146          {header: 'Args', width: '200em'},
147        ],
148        firstRowOffset: this.data?.offset ?? 0,
149        numRows: this.data?.numEvents ?? 0,
150        rowHeight: ROW_H,
151        rows: this.renderData(),
152        onReload: (offset, count) => {
153          this.pagination = {offset, count};
154          this.reloadData(attrs);
155        },
156        onRowHover: (id) => {
157          const event = this.data?.events.find((event) => event.id === id);
158          if (event) {
159            attrs.trace.timeline.hoverCursorTimestamp = event.ts;
160          }
161        },
162        onRowOut: () => {
163          attrs.trace.timeline.hoverCursorTimestamp = undefined;
164        },
165      }),
166    );
167  }
168
169  private reloadData(attrs: FtraceExplorerAttrs): void {
170    this.queryLimiter.schedule(async () => {
171      this.data = await lookupFtraceEvents(
172        attrs.trace,
173        this.pagination.offset,
174        this.pagination.count,
175        attrs.filterStore.state,
176      );
177      attrs.trace.scheduleFullRedraw();
178    });
179  }
180
181  private renderData(): VirtualTableRow[] {
182    if (!this.data) {
183      return [];
184    }
185
186    return this.data.events.map((event) => {
187      const {ts, name, cpu, process, args, id} = event;
188      const timestamp = m(Timestamp, {ts});
189      const color = colorForFtrace(name).base.cssString;
190
191      return {
192        id,
193        cells: [
194          id,
195          timestamp,
196          m(
197            '.pf-ftrace-namebox',
198            m('.pf-ftrace-colorbox', {style: {background: color}}),
199            name,
200          ),
201          cpu,
202          process,
203          args,
204        ],
205      };
206    });
207  }
208
209  private renderTitle() {
210    if (this.data) {
211      const {numEvents} = this.data;
212      return `Ftrace Events (${numEvents})`;
213    } else {
214      return 'Ftrace Events';
215    }
216  }
217
218  private renderFilterPanel(attrs: FtraceExplorerAttrs) {
219    if (attrs.cache.state !== 'valid') {
220      return m(Button, {
221        label: 'Filter',
222        disabled: true,
223        loading: true,
224      });
225    }
226
227    const excludeList = attrs.filterStore.state.excludeList;
228    const options: MultiSelectOption[] = attrs.cache.counters.map(
229      ({name, count}) => {
230        return {
231          id: name,
232          name: `${name} (${count})`,
233          checked: !excludeList.some((excluded: string) => excluded === name),
234        };
235      },
236    );
237
238    return m(PopupMultiSelect, {
239      label: 'Filter',
240      icon: 'filter_list_alt',
241      popupPosition: PopupPosition.Top,
242      options,
243      onChange: (diffs: MultiSelectDiff[]) => {
244        const newList = new Set<string>(excludeList);
245        diffs.forEach(({checked, id}) => {
246          if (checked) {
247            newList.delete(id);
248          } else {
249            newList.add(id);
250          }
251        });
252        attrs.filterStore.edit((draft) => {
253          draft.excludeList = Array.from(newList);
254        });
255      },
256    });
257  }
258}
259
260async function lookupFtraceEvents(
261  trace: Trace,
262  offset: number,
263  count: number,
264  filter: FtraceFilter,
265): Promise<FtracePanelData> {
266  const {start, end} = trace.timeline.visibleWindow.toTimeSpan();
267
268  const excludeList = filter.excludeList;
269  const excludeListSql = excludeList.map((s) => `'${s}'`).join(',');
270
271  // TODO(stevegolton): This query can be slow when traces are huge.
272  // The number of events is only used for correctly sizing the panel's
273  // scroll container so that the scrollbar works as if the panel were fully
274  // populated.
275  // Perhaps we could work out some UX that doesn't need this.
276  let queryRes = await trace.engine.query(`
277    select count(id) as numEvents
278    from ftrace_event
279    where
280      ftrace_event.name not in (${excludeListSql}) and
281      ts >= ${start} and ts <= ${end}
282    `);
283  const {numEvents} = queryRes.firstRow({numEvents: NUM});
284
285  queryRes = await trace.engine.query(`
286    select
287      ftrace_event.id as id,
288      ftrace_event.ts as ts,
289      ftrace_event.name as name,
290      ftrace_event.cpu as cpu,
291      thread.name as thread,
292      process.name as process,
293      to_ftrace(ftrace_event.id) as args
294    from ftrace_event
295    join thread using (utid)
296    left join process on thread.upid = process.upid
297    where
298      ftrace_event.name not in (${excludeListSql}) and
299      ts >= ${start} and ts <= ${end}
300    order by id
301    limit ${count} offset ${offset};`);
302  const events: FtraceEvent[] = [];
303  const it = queryRes.iter({
304    id: NUM,
305    ts: LONG,
306    name: STR,
307    cpu: NUM,
308    thread: STR_NULL,
309    process: STR_NULL,
310    args: STR,
311  });
312  for (let row = 0; it.valid(); it.next(), row++) {
313    events.push({
314      id: it.id,
315      ts: Time.fromRaw(it.ts),
316      name: it.name,
317      cpu: it.cpu,
318      thread: it.thread,
319      process: it.process,
320      args: it.args,
321    });
322  }
323  return {events, offset, numEvents};
324}
325