xref: /aosp_15_r20/development/tools/winscope/src/viewers/common/abstract_log_viewer_presenter.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {DOMUtils} from 'common/dom_utils';
18import {FunctionUtils} from 'common/function_utils';
19import {Timestamp} from 'common/time';
20import {
21  TracePositionUpdate,
22  WinscopeEvent,
23  WinscopeEventType,
24} from 'messaging/winscope_event';
25import {
26  EmitEvent,
27  WinscopeEventEmitter,
28} from 'messaging/winscope_event_emitter';
29import {Trace, TraceEntry} from 'trace/trace';
30import {TraceEntryFinder} from 'trace/trace_entry_finder';
31import {TracePosition} from 'trace/trace_position';
32import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
33import {PropertiesPresenter} from 'viewers/common/properties_presenter';
34import {TextFilter} from 'viewers/common/text_filter';
35import {UserOptions} from 'viewers/common/user_options';
36import {LogPresenter} from './log_presenter';
37import {LogEntry, LogHeader, UiDataLog} from './ui_data_log';
38import {
39  LogFilterChangeDetail,
40  LogTextFilterChangeDetail,
41  TimestampClickDetail,
42  ViewerEvents,
43} from './viewer_events';
44
45export type NotifyLogViewCallbackType<UiData> = (uiData: UiData) => void;
46
47export abstract class AbstractLogViewerPresenter<
48  UiData extends UiDataLog,
49  TraceEntryType extends object,
50> implements WinscopeEventEmitter
51{
52  protected static readonly VALUE_NA = 'N/A';
53  protected emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
54  protected abstract logPresenter: LogPresenter<LogEntry>;
55  protected propertiesPresenter?: PropertiesPresenter;
56  protected keepCalculated?: boolean;
57  private activeTrace?: Trace<object>;
58  private isInitialized = false;
59
60  protected constructor(
61    protected readonly trace: Trace<TraceEntryType>,
62    private readonly notifyViewCallback: NotifyLogViewCallbackType<UiData>,
63    protected readonly uiData: UiData,
64  ) {
65    this.notifyViewChanged();
66  }
67
68  setEmitEvent(callback: EmitEvent) {
69    this.emitAppEvent = callback;
70  }
71
72  addEventListeners(htmlElement: HTMLElement) {
73    htmlElement.addEventListener(
74      ViewerEvents.LogFilterChange,
75      async (event) => {
76        const detail: LogFilterChangeDetail = (event as CustomEvent).detail;
77        await this.onSelectFilterChange(detail.header, detail.value);
78      },
79    );
80    htmlElement.addEventListener(
81      ViewerEvents.LogTextFilterChange,
82      async (event) => {
83        const detail: LogTextFilterChangeDetail = (event as CustomEvent).detail;
84        await this.onTextFilterChange(detail.header, detail.filter);
85      },
86    );
87    htmlElement.addEventListener(ViewerEvents.LogEntryClick, async (event) => {
88      await this.onLogEntryClick((event as CustomEvent).detail);
89    });
90    htmlElement.addEventListener(
91      ViewerEvents.ArrowDownPress,
92      async (event) => await this.onArrowDownPress(),
93    );
94    htmlElement.addEventListener(
95      ViewerEvents.ArrowUpPress,
96      async (event) => await this.onArrowUpPress(),
97    );
98    htmlElement.addEventListener(ViewerEvents.TimestampClick, async (event) => {
99      const detail: TimestampClickDetail = (event as CustomEvent).detail;
100      if (detail.entry !== undefined) {
101        await this.onLogTimestampClick(detail.entry);
102      } else if (detail.timestamp !== undefined) {
103        await this.onRawTimestampClick(detail.timestamp);
104      }
105    });
106    htmlElement.addEventListener(
107      ViewerEvents.PropertiesUserOptionsChange,
108      (event) =>
109        this.onPropertiesUserOptionsChange(
110          (event as CustomEvent).detail.userOptions,
111        ),
112    );
113    htmlElement.addEventListener(
114      ViewerEvents.PropertiesFilterChange,
115      async (event) => {
116        const detail: TextFilter = (event as CustomEvent).detail;
117        await this.onPropertiesFilterChange(detail);
118      },
119    );
120
121    document.addEventListener('keydown', async (event: KeyboardEvent) => {
122      const isViewerVisible = DOMUtils.isElementVisible(htmlElement);
123      const isPositionChange =
124        event.key === 'ArrowRight' || event.key === 'ArrowLeft';
125      if (!isViewerVisible || !isPositionChange) {
126        return;
127      }
128      await this.onPositionChangeByKeyPress(event);
129    });
130  }
131
132  async onAppEvent(event: WinscopeEvent) {
133    await event.visit(
134      WinscopeEventType.TRACE_POSITION_UPDATE,
135      async (event) => {
136        await this.applyTracePositionUpdate(event);
137      },
138    );
139    await event.visit(WinscopeEventType.DARK_MODE_TOGGLED, async (event) => {
140      this.uiData.isDarkMode = event.isDarkMode;
141      this.notifyViewChanged();
142    });
143    await event.visit(WinscopeEventType.ACTIVE_TRACE_CHANGED, async (event) => {
144      this.activeTrace = event.trace;
145    });
146  }
147
148  async onSelectFilterChange(header: LogHeader, value: string[]) {
149    this.logPresenter.applySelectFilterChange(header, value);
150    await this.updatePropertiesTree();
151    this.uiData.currentIndex = this.logPresenter.getCurrentIndex();
152    this.uiData.selectedIndex = this.logPresenter.getSelectedIndex();
153    this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex();
154    this.uiData.entries = this.logPresenter.getFilteredEntries();
155    this.notifyViewChanged();
156  }
157
158  async onTextFilterChange(header: LogHeader, value: TextFilter) {
159    this.logPresenter.applyTextFilterChange(header, value);
160    await this.updatePropertiesTree();
161    this.uiData.currentIndex = this.logPresenter.getCurrentIndex();
162    this.uiData.selectedIndex = this.logPresenter.getSelectedIndex();
163    this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex();
164    this.uiData.entries = this.logPresenter.getFilteredEntries();
165    this.notifyViewChanged();
166  }
167
168  async onPropertiesUserOptionsChange(userOptions: UserOptions) {
169    if (!this.propertiesPresenter) {
170      return;
171    }
172    this.propertiesPresenter.applyPropertiesUserOptionsChange(userOptions);
173    this.uiData.propertiesUserOptions =
174      this.propertiesPresenter.getUserOptions();
175    await this.updatePropertiesTree(false);
176    this.notifyViewChanged();
177  }
178
179  async onPropertiesFilterChange(textFilter: TextFilter) {
180    if (!this.propertiesPresenter) {
181      return;
182    }
183    this.propertiesPresenter.applyPropertiesFilterChange(textFilter);
184    await this.updatePropertiesTree(false);
185    this.uiData.propertiesFilter = textFilter;
186    this.notifyViewChanged();
187  }
188
189  async onLogTimestampClick(traceEntry: TraceEntry<object>) {
190    await this.emitAppEvent(
191      TracePositionUpdate.fromTraceEntry(traceEntry, true),
192    );
193  }
194
195  async onRawTimestampClick(timestamp: Timestamp) {
196    await this.emitAppEvent(TracePositionUpdate.fromTimestamp(timestamp, true));
197  }
198
199  async onLogEntryClick(index: number) {
200    this.logPresenter.applyLogEntryClick(index);
201    this.updateIndicesUiData();
202    await this.updatePropertiesTree();
203    this.notifyViewChanged();
204  }
205
206  async onArrowDownPress() {
207    this.logPresenter.applyArrowDownPress();
208    this.updateIndicesUiData();
209    await this.updatePropertiesTree();
210    this.notifyViewChanged();
211  }
212
213  async onArrowUpPress() {
214    this.logPresenter.applyArrowUpPress();
215    this.updateIndicesUiData();
216    await this.updatePropertiesTree();
217    this.notifyViewChanged();
218  }
219
220  async onPositionChangeByKeyPress(event: KeyboardEvent) {
221    const currIndex = this.uiData.currentIndex;
222    if (this.activeTrace === this.trace && currIndex !== undefined) {
223      if (event.key === 'ArrowRight') {
224        event.stopImmediatePropagation();
225        if (currIndex < this.uiData.entries.length - 1) {
226          const currTimestamp =
227            this.uiData.entries[currIndex].traceEntry.getTimestamp();
228          const nextEntry = this.uiData.entries
229            .slice(currIndex + 1)
230            .find((entry) => entry.traceEntry.getTimestamp() > currTimestamp);
231          if (nextEntry) {
232            return this.emitAppEvent(
233              new TracePositionUpdate(
234                TracePosition.fromTraceEntry(nextEntry.traceEntry),
235                true,
236              ),
237            );
238          }
239        }
240      } else {
241        event.stopImmediatePropagation();
242        if (currIndex > 0) {
243          return this.emitAppEvent(
244            new TracePositionUpdate(
245              TracePosition.fromTraceEntry(
246                this.uiData.entries[currIndex - 1].traceEntry,
247              ),
248              true,
249            ),
250          );
251        }
252      }
253    }
254  }
255
256  protected refreshUiData() {
257    this.uiData.headers = this.logPresenter.getHeaders();
258    this.uiData.entries = this.logPresenter.getFilteredEntries();
259    this.uiData.selectedIndex = this.logPresenter.getSelectedIndex();
260    this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex();
261    this.uiData.currentIndex = this.logPresenter.getCurrentIndex();
262    if (this.propertiesPresenter) {
263      this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
264      this.uiData.propertiesUserOptions =
265        this.propertiesPresenter.getUserOptions();
266      this.uiData.propertiesFilter = this.propertiesPresenter.getTextFilter();
267    }
268  }
269
270  protected async applyTracePositionUpdate(event: TracePositionUpdate) {
271    await this.initializeIfNeeded();
272    let entry: TraceEntry<TraceEntryType> | undefined;
273    if (event.position.entry?.getFullTrace() === this.trace) {
274      entry = event.position.entry as TraceEntry<TraceEntryType>;
275    } else {
276      entry = TraceEntryFinder.findCorrespondingEntry(
277        this.trace,
278        event.position,
279      );
280    }
281    this.logPresenter.applyTracePositionUpdate(entry);
282
283    this.uiData.selectedIndex = this.logPresenter.getSelectedIndex();
284    this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex();
285    this.uiData.currentIndex = this.logPresenter.getCurrentIndex();
286
287    if (this.propertiesPresenter) {
288      await this.updatePropertiesTree();
289      this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
290    }
291
292    this.notifyViewChanged();
293  }
294
295  protected async updatePropertiesTree(updateDefaultAllowlist = true) {
296    if (this.propertiesPresenter) {
297      const tree = this.getPropertiesTree();
298      this.propertiesPresenter.setPropertiesTree(tree);
299      if (updateDefaultAllowlist && this.updateDefaultAllowlist) {
300        this.updateDefaultAllowlist(tree);
301      }
302      await this.propertiesPresenter.formatPropertiesTree(
303        undefined,
304        undefined,
305        this.keepCalculated ?? false,
306      );
307      this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree();
308    }
309  }
310
311  private async initializeIfNeeded() {
312    if (this.isInitialized) {
313      return;
314    }
315
316    if (this.initializeTraceSpecificData) {
317      await this.initializeTraceSpecificData();
318    }
319
320    const headers = this.makeHeaders();
321    const allEntries = await this.makeUiDataEntries(headers);
322    if (this.updateFiltersInHeaders) {
323      this.updateFiltersInHeaders(headers, allEntries);
324    }
325
326    this.logPresenter.setAllEntries(allEntries);
327    this.logPresenter.setHeaders(headers);
328    this.refreshUiData();
329    this.isInitialized = true;
330  }
331
332  private updateIndicesUiData() {
333    this.uiData.selectedIndex = this.logPresenter.getSelectedIndex();
334    this.uiData.currentIndex = this.logPresenter.getCurrentIndex();
335    this.uiData.scrollToIndex = this.logPresenter.getScrollToIndex();
336  }
337
338  private getPropertiesTree(): PropertyTreeNode | undefined {
339    const entries = this.logPresenter.getFilteredEntries();
340    const selectedIndex = this.logPresenter.getSelectedIndex();
341    const currentIndex = this.logPresenter.getCurrentIndex();
342    if (selectedIndex !== undefined) {
343      return entries.at(selectedIndex)?.propertiesTree;
344    }
345    if (currentIndex !== undefined) {
346      return entries.at(currentIndex)?.propertiesTree;
347    }
348    return undefined;
349  }
350
351  protected notifyViewChanged() {
352    this.notifyViewCallback(this.uiData);
353  }
354
355  protected abstract makeHeaders(): LogHeader[];
356  protected abstract makeUiDataEntries(
357    headers: LogHeader[],
358  ): Promise<LogEntry[]>;
359  protected initializeTraceSpecificData?(): Promise<void>;
360  protected updateFiltersInHeaders?(
361    headers: LogHeader[],
362    allEntries: LogEntry[],
363  ): void;
364  protected updateDefaultAllowlist?(tree: PropertyTreeNode | undefined): void;
365}
366