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