• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {DetailsShell} from '../../widgets/details_shell';
18import {
19  MultiSelectDiff,
20  MultiSelectOption,
21  PopupMultiSelect,
22} from '../../widgets/multiselect';
23import {PopupPosition} from '../../widgets/popup';
24import {Timestamp} from '../../components/widgets/timestamp';
25import {FtraceFilter, FtraceStat} from './common';
26import {Engine} from '../../trace_processor/engine';
27import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
28import {AsyncLimiter} from '../../base/async_limiter';
29import {Monitor} from '../../base/monitor';
30import {Button} from '../../widgets/button';
31import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table';
32import {Store} from '../../base/store';
33import {Trace} from '../../public/trace';
34import {materialColorScheme} from '../../components/colorizer';
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    });
178  }
179
180  private renderData(): VirtualTableRow[] {
181    if (!this.data) {
182      return [];
183    }
184
185    return this.data.events.map((event) => {
186      const {ts, name, cpu, process, args, id} = event;
187      const timestamp = m(Timestamp, {ts});
188      const color = materialColorScheme(name).base.cssString;
189
190      return {
191        id,
192        cells: [
193          id,
194          timestamp,
195          m(
196            '.pf-ftrace-namebox',
197            m('.pf-ftrace-colorbox', {style: {background: color}}),
198            name,
199          ),
200          cpu,
201          process,
202          args,
203        ],
204      };
205    });
206  }
207
208  private renderTitle() {
209    if (this.data) {
210      const {numEvents} = this.data;
211      return `Ftrace Events (${numEvents})`;
212    } else {
213      return 'Ftrace Events';
214    }
215  }
216
217  private renderFilterPanel(attrs: FtraceExplorerAttrs) {
218    if (attrs.cache.state !== 'valid') {
219      return m(Button, {
220        label: 'Filter',
221        disabled: true,
222        loading: true,
223      });
224    }
225
226    const excludeList = attrs.filterStore.state.excludeList;
227    const options: MultiSelectOption[] = attrs.cache.counters.map(
228      ({name, count}) => {
229        return {
230          id: name,
231          name: `${name} (${count})`,
232          checked: !excludeList.some((excluded: string) => excluded === name),
233        };
234      },
235    );
236
237    return m(PopupMultiSelect, {
238      label: 'Filter',
239      icon: 'filter_list_alt',
240      popupPosition: PopupPosition.Top,
241      options,
242      onChange: (diffs: MultiSelectDiff[]) => {
243        const newList = new Set<string>(excludeList);
244        diffs.forEach(({checked, id}) => {
245          if (checked) {
246            newList.delete(id);
247          } else {
248            newList.add(id);
249          }
250        });
251        attrs.filterStore.edit((draft) => {
252          draft.excludeList = Array.from(newList);
253        });
254      },
255    });
256  }
257}
258
259async function lookupFtraceEvents(
260  trace: Trace,
261  offset: number,
262  count: number,
263  filter: FtraceFilter,
264): Promise<FtracePanelData> {
265  const {start, end} = trace.timeline.visibleWindow.toTimeSpan();
266
267  const excludeList = filter.excludeList;
268  const excludeListSql = excludeList.map((s) => `'${s}'`).join(',');
269
270  // TODO(stevegolton): This query can be slow when traces are huge.
271  // The number of events is only used for correctly sizing the panel's
272  // scroll container so that the scrollbar works as if the panel were fully
273  // populated.
274  // Perhaps we could work out some UX that doesn't need this.
275  let queryRes = await trace.engine.query(`
276    select count(id) as numEvents
277    from ftrace_event
278    where
279      ftrace_event.name not in (${excludeListSql}) and
280      ts >= ${start} and ts <= ${end}
281    `);
282  const {numEvents} = queryRes.firstRow({numEvents: NUM});
283
284  queryRes = await trace.engine.query(`
285    select
286      ftrace_event.id as id,
287      ftrace_event.ts as ts,
288      ftrace_event.name as name,
289      ftrace_event.cpu as cpu,
290      thread.name as thread,
291      process.name as process,
292      to_ftrace(ftrace_event.id) as args
293    from ftrace_event
294    join thread using (utid)
295    left join process on thread.upid = process.upid
296    where
297      ftrace_event.name not in (${excludeListSql}) and
298      ts >= ${start} and ts <= ${end}
299    order by id
300    limit ${count} offset ${offset};`);
301  const events: FtraceEvent[] = [];
302  const it = queryRes.iter({
303    id: NUM,
304    ts: LONG,
305    name: STR,
306    cpu: NUM,
307    thread: STR_NULL,
308    process: STR_NULL,
309    args: STR,
310  });
311  for (let row = 0; it.valid(); it.next(), row++) {
312    events.push({
313      id: it.id,
314      ts: Time.fromRaw(it.ts),
315      name: it.name,
316      cpu: it.cpu,
317      thread: it.thread,
318      process: it.process,
319      args: it.args,
320    });
321  }
322  return {events, offset, numEvents};
323}
324