• 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 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 {StringListPatch} from 'src/common/state';
17
18import {assertExists} from '../base/logging';
19import {Actions} from '../common/actions';
20import {colorForString} from '../common/colorizer';
21import {formatTPTime, TPTime} from '../common/time';
22
23import {globals} from './globals';
24import {Panel} from './panel';
25import {
26  MultiSelect,
27  MultiSelectDiff,
28  Option as MultiSelectOption,
29} from './widgets/multiselect';
30import {PopupPosition} from './widgets/popup';
31
32const ROW_H = 20;
33const PAGE_SIZE = 250;
34
35// This class is quite a weird one. The state looks something like this:
36//
37// view() -> renders the panel from the data, for now we have no idea what size
38// the scroll window is going to be so we don't know how many rows to ask for,
39// and the number of rendered rows in our state is likely going to be 0 or wrong
40//
41// oncreate() -> we now know how many rows we need to display and our scroll
42// offset. This is where we as our controller to update the rows, which could
43// take some time. Record the first and last row we can see. Attach scroll
44// handler to the scrolly window here.
45//
46// onScroll() -> we know the window has been scrolled, we need to see if things
47// have changed enough to constitute a redraw.
48
49// Another call to view() can come at any time, as a reusult of the controller
50// giving us some data.
51//
52export class FtracePanel extends Panel<{}> {
53  private page: number = 0;
54  private pageCount: number = 0;
55
56  view(_: m.CVnode<{}>) {
57    return m(
58        '.ftrace-panel',
59        m(
60            '.sticky',
61            [
62              this.renderRowsLabel(),
63              this.renderFilterPanel(),
64            ],
65            ),
66        this.renderRows(),
67    );
68  }
69
70  private scrollContainer(dom: Element): HTMLElement {
71    const el = dom.parentElement!.parentElement!.parentElement;
72    return assertExists(el);
73  }
74
75  oncreate({dom}: m.CVnodeDOM) {
76    const sc = this.scrollContainer(dom);
77    sc.addEventListener('scroll', this.onScroll);
78    this.recomputeVisibleRowsAndUpdate(sc);
79  }
80
81  onupdate({dom}: m.CVnodeDOM) {
82    const sc = this.scrollContainer(dom);
83    this.recomputeVisibleRowsAndUpdate(sc);
84  }
85
86  recomputeVisibleRowsAndUpdate(scrollContainer: HTMLElement) {
87    const prevPage = this.page;
88    const prevPageCount = this.pageCount;
89
90    const visibleRowOffset = Math.floor(scrollContainer.scrollTop / ROW_H);
91    const visibleRowCount = Math.ceil(scrollContainer.clientHeight / ROW_H);
92
93    // Work out which "page" we're on
94    this.page = Math.floor(visibleRowOffset / PAGE_SIZE) - 1;
95    this.pageCount = Math.ceil(visibleRowCount / PAGE_SIZE) + 2;
96
97    if (this.page !== prevPage || this.pageCount !== prevPageCount) {
98      globals.dispatch(Actions.updateFtracePagination({
99        offset: Math.max(0, this.page) * PAGE_SIZE,
100        count: this.pageCount * PAGE_SIZE,
101      }));
102    }
103  }
104
105  onremove({dom}: m.CVnodeDOM) {
106    const sc = this.scrollContainer(dom);
107    sc.removeEventListener('scroll', this.onScroll);
108
109    globals.dispatch(Actions.updateFtracePagination({
110      offset: 0,
111      count: 0,
112    }));
113  }
114
115  onScroll = (e: Event) => {
116    const scrollContainer = e.target as HTMLElement;
117    this.recomputeVisibleRowsAndUpdate(scrollContainer);
118  };
119
120  onRowOver(ts: TPTime) {
121    globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
122  }
123
124  onRowOut() {
125    globals.dispatch(Actions.setHoverCursorTimestamp({ts: -1n}));
126  }
127
128  private renderRowsLabel() {
129    if (globals.ftracePanelData) {
130      const {numEvents} = globals.ftracePanelData;
131      return m('.ftrace-rows-label', `Ftrace Events (${numEvents})`);
132    } else {
133      return m('.ftrace-rows-label', 'Ftrace Rows');
134    }
135  }
136
137  private renderFilterPanel() {
138    if (!globals.ftraceCounters) {
139      return null;
140    }
141
142    const options: MultiSelectOption[] =
143        globals.ftraceCounters.map(({name, count}) => {
144          return {
145            id: name,
146            name: `${name} (${count})`,
147            checked: !globals.state.ftraceFilter.excludedNames.some(
148                (excluded: string) => excluded === name),
149          };
150        });
151
152    return m(
153        MultiSelect,
154        {
155          label: 'Filter by name',
156          icon: 'filter_list_alt',
157          popupPosition: PopupPosition.Top,
158          options,
159          onChange: (diffs: MultiSelectDiff[]) => {
160            const excludedNames: StringListPatch[] = diffs.map(
161                ({id, checked}) => [checked ? 'remove' : 'add', id],
162            );
163            globals.dispatchMultiple([
164              Actions.updateFtraceFilter({excludedNames}),
165              Actions.requestTrackReload({}),
166            ]);
167          },
168        },
169    );
170  }
171
172  // Render all the rows including the first title row
173  private renderRows() {
174    const data = globals.ftracePanelData;
175    const rows: m.Children = [];
176
177    rows.push(m(
178        `.row`,
179        m('.cell.row-header', 'Timestamp'),
180        m('.cell.row-header', 'Name'),
181        m('.cell.row-header', 'CPU'),
182        m('.cell.row-header', 'Process'),
183        m('.cell.row-header', 'Args'),
184        ));
185
186    if (data) {
187      const {events, offset, numEvents} = data;
188      for (let i = 0; i < events.length; i++) {
189        const {ts, name, cpu, process, args} = events[i];
190
191        const timestamp = formatTPTime(ts - globals.state.traceTime.start);
192
193        const rank = i + offset;
194
195        const color = colorForString(name);
196        const hsl = `hsl(
197          ${color.h},
198          ${color.s - 20}%,
199          ${Math.min(color.l + 10, 60)}%
200        )`;
201
202        rows.push(m(
203            `.row`,
204            {
205              style: {top: `${(rank + 1.0) * ROW_H}px`},
206              onmouseover: this.onRowOver.bind(this, ts),
207              onmouseout: this.onRowOut.bind(this),
208            },
209            m('.cell', timestamp),
210            m('.cell', m('span.colour', {style: {background: hsl}}), name),
211            m('.cell', cpu),
212            m('.cell', process),
213            m('.cell', args),
214            ));
215      }
216      return m('.rows', {style: {height: `${numEvents * ROW_H}px`}}, rows);
217    } else {
218      return m('.rows', rows);
219    }
220  }
221
222  renderCanvas() {}
223}
224