xref: /aosp_15_r20/development/tools/winscope/src/viewers/common/log_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 {ArrayUtils} from 'common/array_utils';
18import {assertDefined} from 'common/assert_utils';
19import {TraceEntry} from 'trace/trace';
20import {StringFilterPredicate} from 'viewers/common/string_filter_predicate';
21import {TextFilter} from 'viewers/common/text_filter';
22import {ColumnSpec, LogEntry, LogHeader} from './ui_data_log';
23
24export class LogPresenter<Entry extends LogEntry> {
25  private allEntries: Entry[] = [];
26  private filteredEntries: Entry[] = [];
27  private headers: LogHeader[] = [];
28  private filterPredicates = new Map<ColumnSpec, StringFilterPredicate>();
29  private currentEntry: TraceEntry<object> | undefined;
30  private selectedIndex: number | undefined;
31  private scrollToIndex: number | undefined;
32  private currentIndex: number | undefined;
33  private originalIndicesOfAllEntries: number[] = [];
34
35  constructor(private timeOrderedEntries = true) {}
36
37  setAllEntries(value: Entry[]) {
38    this.allEntries = value;
39    this.updateFilteredEntries();
40  }
41
42  setHeaders(headers: LogHeader[]) {
43    this.headers = headers;
44
45    this.filterPredicates = new Map<ColumnSpec, StringFilterPredicate>();
46    this.headers.forEach((header) => {
47      if (!header.filter) return;
48      this.filterPredicates.set(
49        header.spec,
50        header.filter.getFilterPredicate(),
51      );
52    });
53    this.updateFilteredEntries();
54    this.resetIndices();
55  }
56
57  getHeaders(): LogHeader[] {
58    return this.headers;
59  }
60
61  getFilteredEntries(): Entry[] {
62    return this.filteredEntries;
63  }
64
65  getSelectedIndex(): number | undefined {
66    return this.selectedIndex;
67  }
68
69  getScrollToIndex(): number | undefined {
70    return this.scrollToIndex;
71  }
72
73  getCurrentIndex(): number | undefined {
74    return this.currentIndex;
75  }
76
77  applyLogEntryClick(index: number) {
78    if (this.selectedIndex === index) {
79      this.scrollToIndex = undefined;
80      return;
81    }
82    this.selectedIndex = index;
83    this.scrollToIndex = undefined; // no scrolling
84  }
85
86  applyArrowDownPress() {
87    const index = this.selectedIndex ?? this.currentIndex;
88    if (index === undefined) {
89      this.changeLogByKeyPress(0);
90      return;
91    }
92    if (index < this.filteredEntries.length - 1) {
93      this.changeLogByKeyPress(index + 1);
94    }
95  }
96
97  applyArrowUpPress() {
98    const index = this.selectedIndex ?? this.currentIndex;
99    if (index === undefined) {
100      this.changeLogByKeyPress(0);
101      return;
102    }
103    if (index > 0) {
104      this.changeLogByKeyPress(index - 1);
105    }
106  }
107
108  applyTracePositionUpdate(entry: TraceEntry<object> | undefined) {
109    this.currentEntry = entry;
110    this.resetIndices();
111  }
112
113  applyTextFilterChange(header: LogHeader, value: TextFilter) {
114    const filter = assertDefined(header.filter);
115    const filterString = value.filterString;
116    filter.updateFilterValue([filterString]);
117    if (filterString.length > 0) {
118      this.filterPredicates.set(header.spec, filter.getFilterPredicate());
119    } else {
120      this.filterPredicates.delete(header.spec);
121    }
122    this.updateEntriesAfterFilterChange();
123  }
124
125  applySelectFilterChange(header: LogHeader, value: string[]) {
126    const filter = assertDefined(header.filter);
127    filter.updateFilterValue(value);
128    if (value.length > 0) {
129      this.filterPredicates.set(
130        header.spec,
131        assertDefined(header.filter).getFilterPredicate(),
132      );
133    } else {
134      this.filterPredicates.delete(header.spec);
135    }
136    this.updateEntriesAfterFilterChange();
137  }
138
139  private updateEntriesAfterFilterChange() {
140    this.updateFilteredEntries();
141    this.currentIndex = this.getCurrentTracePositionIndex();
142    if (
143      this.selectedIndex !== undefined &&
144      this.selectedIndex > this.filteredEntries.length - 1
145    ) {
146      this.selectedIndex = this.currentIndex;
147    }
148    this.scrollToIndex = this.selectedIndex ?? this.currentIndex;
149  }
150
151  private changeLogByKeyPress(index: number) {
152    if (this.selectedIndex === index || this.filteredEntries.length === 0) {
153      return;
154    }
155    this.selectedIndex = index;
156    this.scrollToIndex = index;
157  }
158
159  private resetIndices() {
160    this.currentIndex = this.getCurrentTracePositionIndex();
161    this.selectedIndex = undefined;
162    this.scrollToIndex = this.currentIndex;
163  }
164
165  private updateFilteredEntries() {
166    this.filteredEntries = this.allEntries.filter((entry) => {
167      for (const [spec, predicate] of this.filterPredicates) {
168        const entryValue = entry.fields.find((f) => f.spec === spec)?.value;
169
170        if (entryValue === undefined) {
171          continue;
172        }
173
174        const entryValueStr = entryValue.toString();
175
176        if (!predicate(entryValueStr)) return false;
177      }
178      return true;
179    });
180    if (this.filteredEntries.length === 0) {
181      this.currentIndex = undefined;
182      this.selectedIndex = undefined;
183      this.scrollToIndex = undefined;
184    }
185    this.originalIndicesOfAllEntries = this.filteredEntries.map((entry) =>
186      entry.traceEntry.getIndex(),
187    );
188  }
189
190  private getCurrentTracePositionIndex(): number | undefined {
191    if (!this.currentEntry) {
192      this.currentIndex = undefined;
193      return;
194    }
195    if (this.originalIndicesOfAllEntries.length === 0) {
196      this.currentIndex = undefined;
197      return;
198    }
199    const target = this.currentEntry.getIndex();
200
201    if (this.timeOrderedEntries) {
202      return (
203        ArrayUtils.binarySearchFirstGreaterOrEqual(
204          this.originalIndicesOfAllEntries,
205          this.currentEntry.getIndex(),
206        ) ?? this.originalIndicesOfAllEntries.length - 1
207      );
208    }
209
210    const currentIndex = this.originalIndicesOfAllEntries.findIndex(
211      (i) => i === target,
212    );
213    return currentIndex !== -1
214      ? currentIndex
215      : this.originalIndicesOfAllEntries.length - 1;
216  }
217}
218