1/* 2 * Copyright (C) 2022 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 {FunctionUtils} from 'common/function_utils'; 20import { 21 TracePositionUpdate, 22 WinscopeEvent, 23 WinscopeEventType, 24} from 'messaging/winscope_event'; 25import { 26 EmitEvent, 27 WinscopeEventEmitter, 28} from 'messaging/winscope_event_emitter'; 29import {AbsoluteEntryIndex, Trace, TraceEntry} from 'trace/trace'; 30import {TraceEntryFinder} from 'trace/trace_entry_finder'; 31import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 32import {UiData, UiDataMessage} from './ui_data'; 33 34export class Presenter implements WinscopeEventEmitter { 35 private readonly trace: Trace<PropertyTreeNode>; 36 private readonly notifyUiDataCallback: (data: UiData) => void; 37 private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC; 38 private entry?: TraceEntry<PropertyTreeNode>; 39 private uiData = UiData.EMPTY; 40 private originalIndicesOfFilteredOutputMessages: number[] = []; 41 42 private isInitialized = false; 43 private allUiDataMessages: UiDataMessage[] = []; 44 private allTags: string[] = []; 45 private allSourceFiles: string[] = []; 46 private allLogLevels: string[] = []; 47 48 private tagsFilter: string[] = []; 49 private filesFilter: string[] = []; 50 private levelsFilter: string[] = []; 51 private searchString = ''; 52 53 constructor( 54 trace: Trace<PropertyTreeNode>, 55 notifyUiDataCallback: (data: UiData) => void, 56 ) { 57 this.trace = trace; 58 this.notifyUiDataCallback = notifyUiDataCallback; 59 this.notifyUiDataCallback(this.uiData); 60 } 61 62 setEmitEvent(callback: EmitEvent) { 63 this.emitAppEvent = callback; 64 } 65 66 async onAppEvent(event: WinscopeEvent) { 67 await event.visit( 68 WinscopeEventType.TRACE_POSITION_UPDATE, 69 async (event) => { 70 await this.initializeIfNeeded(); 71 this.entry = TraceEntryFinder.findCorrespondingEntry( 72 this.trace, 73 event.position, 74 ); 75 this.computeUiDataCurrentMessageIndex(); 76 this.notifyUiDataCallback(this.uiData); 77 }, 78 ); 79 } 80 81 onLogLevelsFilterChanged(levels: string[]) { 82 this.levelsFilter = levels; 83 this.computeUiData(); 84 this.computeUiDataCurrentMessageIndex(); 85 this.notifyUiDataCallback(this.uiData); 86 } 87 88 onTagsFilterChanged(tags: string[]) { 89 this.tagsFilter = tags; 90 this.computeUiData(); 91 this.computeUiDataCurrentMessageIndex(); 92 this.notifyUiDataCallback(this.uiData); 93 } 94 95 onSourceFilesFilterChanged(files: string[]) { 96 this.filesFilter = files; 97 this.computeUiData(); 98 this.computeUiDataCurrentMessageIndex(); 99 this.notifyUiDataCallback(this.uiData); 100 } 101 102 onSearchStringFilterChanged(searchString: string) { 103 this.searchString = searchString; 104 this.computeUiData(); 105 this.computeUiDataCurrentMessageIndex(); 106 this.notifyUiDataCallback(this.uiData); 107 } 108 109 onMessageClicked(index: number) { 110 if (this.uiData.selectedMessageIndex === index) { 111 this.uiData.selectedMessageIndex = undefined; 112 } else { 113 this.uiData.selectedMessageIndex = index; 114 } 115 this.notifyUiDataCallback(this.uiData); 116 } 117 118 async onLogTimestampClicked(traceIndex: AbsoluteEntryIndex) { 119 await this.emitAppEvent( 120 TracePositionUpdate.fromTraceEntry(this.trace.getEntry(traceIndex), true), 121 ); 122 } 123 124 private async initializeIfNeeded() { 125 if (this.isInitialized) { 126 return; 127 } 128 this.allUiDataMessages = await this.makeAllUiDataMessages(); 129 130 this.allLogLevels = this.getUniqueMessageValues( 131 this.allUiDataMessages, 132 (message: UiDataMessage) => message.level, 133 ); 134 this.allTags = this.getUniqueMessageValues( 135 this.allUiDataMessages, 136 (message: UiDataMessage) => message.tag, 137 ); 138 this.allSourceFiles = this.getUniqueMessageValues( 139 this.allUiDataMessages, 140 (message: UiDataMessage) => message.at, 141 ); 142 143 this.computeUiData(); 144 145 this.isInitialized = true; 146 } 147 148 private async makeAllUiDataMessages(): Promise<UiDataMessage[]> { 149 const messages: PropertyTreeNode[] = []; 150 151 for ( 152 let traceIndex = 0; 153 traceIndex < this.trace.lengthEntries; 154 ++traceIndex 155 ) { 156 const entry = assertDefined(this.trace.getEntry(traceIndex)); 157 const message = await entry.getValue(); 158 messages.push(message); 159 } 160 161 return messages.map((messageNode, index) => { 162 return { 163 traceIndex: index, 164 text: assertDefined( 165 messageNode.getChildByName('text'), 166 ).formattedValue(), 167 time: assertDefined(messageNode.getChildByName('timestamp')), 168 tag: assertDefined(messageNode.getChildByName('tag')).formattedValue(), 169 level: assertDefined( 170 messageNode.getChildByName('level'), 171 ).formattedValue(), 172 at: assertDefined(messageNode.getChildByName('at')).formattedValue(), 173 }; 174 }); 175 } 176 177 private computeUiData() { 178 let filteredMessages = this.allUiDataMessages; 179 180 if (this.levelsFilter.length > 0) { 181 filteredMessages = filteredMessages.filter((value) => 182 this.levelsFilter.includes(value.level), 183 ); 184 } 185 186 if (this.tagsFilter.length > 0) { 187 filteredMessages = filteredMessages.filter((value) => 188 this.tagsFilter.includes(value.tag), 189 ); 190 } 191 192 if (this.filesFilter.length > 0) { 193 filteredMessages = filteredMessages.filter((value) => 194 this.filesFilter.includes(value.at), 195 ); 196 } 197 198 filteredMessages = filteredMessages.filter((value) => 199 value.text.includes(this.searchString), 200 ); 201 202 this.originalIndicesOfFilteredOutputMessages = filteredMessages.map( 203 (message) => message.traceIndex, 204 ); 205 206 this.uiData = new UiData( 207 this.allLogLevels, 208 this.allTags, 209 this.allSourceFiles, 210 filteredMessages, 211 undefined, 212 undefined, 213 ); 214 } 215 216 private computeUiDataCurrentMessageIndex() { 217 if (!this.entry) { 218 this.uiData.currentMessageIndex = undefined; 219 return; 220 } 221 222 if (this.originalIndicesOfFilteredOutputMessages.length === 0) { 223 this.uiData.currentMessageIndex = undefined; 224 return; 225 } 226 227 this.uiData.currentMessageIndex = 228 ArrayUtils.binarySearchFirstGreaterOrEqual( 229 this.originalIndicesOfFilteredOutputMessages, 230 this.entry.getIndex(), 231 ) ?? this.originalIndicesOfFilteredOutputMessages.length - 1; 232 } 233 234 private getUniqueMessageValues( 235 allMessages: UiDataMessage[], 236 getValue: (message: UiDataMessage) => string, 237 ): string[] { 238 const uniqueValues = new Set<string>(); 239 allMessages.forEach((message) => { 240 uniqueValues.add(getValue(message)); 241 }); 242 const result = [...uniqueValues]; 243 result.sort(); 244 return result; 245 } 246} 247