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 {assertDefined} from 'common/assert_utils'; 18import {FunctionUtils} from 'common/function_utils'; 19import {InMemoryStorage} from 'common/store/in_memory_storage'; 20import {parseMap, stringifyMap} from 'common/store/persistent_store_proxy'; 21import {Store} from 'common/store/store'; 22import {Analytics} from 'logging/analytics'; 23import { 24 TracePositionUpdate, 25 WinscopeEvent, 26 WinscopeEventType, 27} from 'messaging/winscope_event'; 28import {EmitEvent} from 'messaging/winscope_event_emitter'; 29import {Trace, TraceEntry} from 'trace/trace'; 30import {Traces} from 'trace/traces'; 31import {TraceEntryFinder} from 'trace/trace_entry_finder'; 32import {TRACE_INFO} from 'trace/trace_info'; 33import {TraceType} from 'trace/trace_type'; 34import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; 35import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 36import {PropertiesPresenter} from 'viewers/common/properties_presenter'; 37import {RectsPresenter} from 'viewers/common/rects_presenter'; 38import {TextFilter} from 'viewers/common/text_filter'; 39import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node'; 40import {UserOption, UserOptions} from 'viewers/common/user_options'; 41import {HierarchyPresenter, SelectedTree} from './hierarchy_presenter'; 42import {PresetHierarchy, TextFilterValues} from './preset_hierarchy'; 43import {RectShowState} from './rect_show_state'; 44import {UiDataHierarchy} from './ui_data_hierarchy'; 45import {ViewerEvents} from './viewer_events'; 46 47export type NotifyHierarchyViewCallbackType<UiData> = (uiData: UiData) => void; 48 49export abstract class AbstractHierarchyViewerPresenter< 50 UiData extends UiDataHierarchy, 51> { 52 protected emitWinscopeEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC; 53 protected overridePropertiesTree: PropertyTreeNode | undefined; 54 protected overridePropertiesTreeName: string | undefined; 55 protected rectsPresenter?: RectsPresenter; 56 protected abstract hierarchyPresenter: HierarchyPresenter; 57 protected abstract propertiesPresenter: PropertiesPresenter; 58 protected abstract readonly multiTraceType?: TraceType; 59 private highlightedItem = ''; 60 61 constructor( 62 private readonly trace: Trace<HierarchyTreeNode> | undefined, 63 protected readonly traces: Traces, 64 protected readonly storage: Readonly<Store>, 65 private readonly notifyViewCallback: NotifyHierarchyViewCallbackType<UiData>, 66 protected readonly uiData: UiData, 67 ) { 68 uiData.isDarkMode = storage.get('dark-mode') === 'true'; 69 this.copyUiDataAndNotifyView(); 70 } 71 72 setEmitEvent(callback: EmitEvent) { 73 this.emitWinscopeEvent = callback; 74 } 75 76 addEventListeners(htmlElement: HTMLElement) { 77 htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => 78 this.onPinnedItemChange((event as CustomEvent).detail.pinnedItem), 79 ); 80 htmlElement.addEventListener( 81 ViewerEvents.HighlightedIdChange, 82 async (event) => 83 await this.onHighlightedIdChange((event as CustomEvent).detail.id), 84 ); 85 htmlElement.addEventListener( 86 ViewerEvents.ArrowDownPress, 87 async (event) => 88 await this.onArrowPress((event as CustomEvent).detail, false), 89 ); 90 htmlElement.addEventListener( 91 ViewerEvents.ArrowUpPress, 92 async (event) => 93 await this.onArrowPress((event as CustomEvent).detail, true), 94 ); 95 htmlElement.addEventListener( 96 ViewerEvents.HighlightedPropertyChange, 97 (event) => 98 this.onHighlightedPropertyChange((event as CustomEvent).detail.id), 99 ); 100 htmlElement.addEventListener( 101 ViewerEvents.HierarchyUserOptionsChange, 102 async (event) => 103 await this.onHierarchyUserOptionsChange( 104 (event as CustomEvent).detail.userOptions, 105 ), 106 ); 107 htmlElement.addEventListener( 108 ViewerEvents.HierarchyFilterChange, 109 async (event) => { 110 const detail: TextFilter = (event as CustomEvent).detail; 111 await this.onHierarchyFilterChange(detail); 112 }, 113 ); 114 htmlElement.addEventListener( 115 ViewerEvents.PropertiesUserOptionsChange, 116 async (event) => 117 await this.onPropertiesUserOptionsChange( 118 (event as CustomEvent).detail.userOptions, 119 ), 120 ); 121 htmlElement.addEventListener( 122 ViewerEvents.PropertiesFilterChange, 123 async (event) => { 124 const detail: TextFilter = (event as CustomEvent).detail; 125 await this.onPropertiesFilterChange(detail); 126 }, 127 ); 128 htmlElement.addEventListener( 129 ViewerEvents.HighlightedNodeChange, 130 async (event) => 131 await this.onHighlightedNodeChange((event as CustomEvent).detail.node), 132 ); 133 htmlElement.addEventListener( 134 ViewerEvents.RectShowStateChange, 135 async (event) => { 136 await this.onRectShowStateChange( 137 (event as CustomEvent).detail.rectId, 138 (event as CustomEvent).detail.state, 139 ); 140 }, 141 ); 142 htmlElement.addEventListener( 143 ViewerEvents.RectsUserOptionsChange, 144 (event) => { 145 this.onRectsUserOptionsChange( 146 (event as CustomEvent).detail.userOptions, 147 ); 148 }, 149 ); 150 this.addViewerSpecificListeners(htmlElement); 151 } 152 153 onPinnedItemChange(pinnedItem: UiHierarchyTreeNode) { 154 this.hierarchyPresenter.applyPinnedItemChange(pinnedItem); 155 this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems(); 156 this.copyUiDataAndNotifyView(); 157 } 158 159 async onArrowPress(storage: InMemoryStorage, getPrevious: boolean) { 160 const newNode = this.hierarchyPresenter.getAdjacentVisibleNode( 161 storage, 162 getPrevious, 163 ); 164 if (newNode) { 165 await this.onHighlightedNodeChange(newNode); 166 } 167 } 168 169 onHighlightedPropertyChange(id: string) { 170 this.propertiesPresenter.applyHighlightedPropertyChange(id); 171 this.uiData.highlightedProperty = 172 this.propertiesPresenter.getHighlightedProperty(); 173 this.copyUiDataAndNotifyView(); 174 } 175 176 onRectsUserOptionsChange(userOptions: UserOptions) { 177 if (!this.rectsPresenter) { 178 return; 179 } 180 this.rectsPresenter.applyRectsUserOptionsChange(userOptions); 181 182 this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions(); 183 this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw(); 184 this.uiData.rectIdToShowState = this.rectsPresenter.getRectIdToShowState(); 185 186 this.copyUiDataAndNotifyView(); 187 } 188 189 async onHierarchyUserOptionsChange(userOptions: UserOptions) { 190 await this.hierarchyPresenter.applyHierarchyUserOptionsChange(userOptions); 191 this.uiData.hierarchyUserOptions = this.hierarchyPresenter.getUserOptions(); 192 this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees(); 193 this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems(); 194 this.copyUiDataAndNotifyView(); 195 } 196 197 async onHierarchyFilterChange(textFilter: TextFilter) { 198 await this.hierarchyPresenter.applyHierarchyFilterChange(textFilter); 199 this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees(); 200 this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems(); 201 this.copyUiDataAndNotifyView(); 202 } 203 204 async onPropertiesUserOptionsChange(userOptions: UserOptions) { 205 this.propertiesPresenter.applyPropertiesUserOptionsChange(userOptions); 206 await this.updatePropertiesTree(); 207 this.uiData.propertiesUserOptions = 208 this.propertiesPresenter.getUserOptions(); 209 this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree(); 210 this.copyUiDataAndNotifyView(); 211 } 212 213 async onPropertiesFilterChange(textFilter: TextFilter) { 214 this.propertiesPresenter.applyPropertiesFilterChange(textFilter); 215 await this.updatePropertiesTree(); 216 this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree(); 217 this.copyUiDataAndNotifyView(); 218 } 219 220 async onRectShowStateChange(id: string, newShowState: RectShowState) { 221 if (!this.rectsPresenter) { 222 return; 223 } 224 this.rectsPresenter.applyRectShowStateChange(id, newShowState); 225 226 this.uiData.rectsToDraw = this.rectsPresenter.getRectsToDraw(); 227 this.uiData.rectIdToShowState = this.rectsPresenter.getRectIdToShowState(); 228 this.copyUiDataAndNotifyView(); 229 } 230 231 async onAppEvent(event: WinscopeEvent) { 232 await event.visit( 233 WinscopeEventType.TRACE_POSITION_UPDATE, 234 async (event) => { 235 if (this.initializeIfNeeded) await this.initializeIfNeeded(event); 236 await this.applyTracePositionUpdate(event); 237 if (this.processDataAfterPositionUpdate) { 238 await this.processDataAfterPositionUpdate(event); 239 } 240 this.refreshUIData(); 241 }, 242 ); 243 await event.visit( 244 WinscopeEventType.FILTER_PRESET_SAVE_REQUEST, 245 async (event) => { 246 this.saveConfigAsPreset(event.name); 247 }, 248 ); 249 await event.visit(WinscopeEventType.DARK_MODE_TOGGLED, async (event) => { 250 this.uiData.isDarkMode = event.isDarkMode; 251 this.copyUiDataAndNotifyView(); 252 }); 253 await event.visit( 254 WinscopeEventType.FILTER_PRESET_APPLY_REQUEST, 255 async (event) => { 256 const filterPresetName = event.name; 257 await this.applyPresetConfig(filterPresetName); 258 this.refreshUIData(); 259 }, 260 ); 261 await this.onViewerSpecificWinscopeEvent(event); 262 } 263 264 protected async onViewerSpecificWinscopeEvent(event: WinscopeEvent) { 265 // do nothing 266 } 267 268 protected addViewerSpecificListeners(htmlElement: HTMLElement) { 269 // do nothing; 270 } 271 272 protected saveConfigAsPreset(storeKey: string) { 273 const preset: PresetHierarchy = { 274 hierarchyUserOptions: this.uiData.hierarchyUserOptions, 275 hierarchyFilter: TextFilterValues.fromTextFilter( 276 this.uiData.hierarchyFilter, 277 ), 278 propertiesUserOptions: this.uiData.propertiesUserOptions, 279 propertiesFilter: TextFilterValues.fromTextFilter( 280 this.uiData.propertiesFilter, 281 ), 282 rectsUserOptions: this.uiData.rectsUserOptions, 283 rectIdToShowState: this.uiData.rectIdToShowState, 284 }; 285 this.storage.add(storeKey, JSON.stringify(preset, stringifyMap)); 286 } 287 288 protected async applyPresetConfig(storeKey: string) { 289 const preset = this.storage.get(storeKey); 290 if (preset) { 291 const parsedPreset: PresetHierarchy = JSON.parse(preset, parseMap); 292 await this.hierarchyPresenter.applyHierarchyUserOptionsChange( 293 parsedPreset.hierarchyUserOptions, 294 ); 295 await this.hierarchyPresenter.applyHierarchyFilterChange( 296 new TextFilter( 297 parsedPreset.hierarchyFilter.filterString, 298 parsedPreset.hierarchyFilter.flags, 299 ), 300 ); 301 302 this.propertiesPresenter.applyPropertiesUserOptionsChange( 303 parsedPreset.propertiesUserOptions, 304 ); 305 this.propertiesPresenter.applyPropertiesFilterChange( 306 new TextFilter( 307 parsedPreset.propertiesFilter.filterString, 308 parsedPreset.propertiesFilter.flags, 309 ), 310 ); 311 await this.updatePropertiesTree(); 312 313 if (this.rectsPresenter) { 314 this.rectsPresenter?.applyRectsUserOptionsChange( 315 assertDefined(parsedPreset.rectsUserOptions), 316 ); 317 this.rectsPresenter?.updateRectShowStates( 318 parsedPreset.rectIdToShowState, 319 ); 320 } 321 this.refreshHierarchyViewerUiData(); 322 } 323 } 324 325 protected async applyTracePositionUpdate(event: TracePositionUpdate) { 326 const hierarchyStartTime = Date.now(); 327 328 let entries: Array<TraceEntry<HierarchyTreeNode>> = []; 329 if (this.multiTraceType !== undefined) { 330 entries = this.traces 331 .getTraces(this.multiTraceType) 332 .map((trace) => { 333 return TraceEntryFinder.findCorrespondingEntry( 334 trace, 335 event.position, 336 ) as TraceEntry<HierarchyTreeNode> | undefined; 337 }) 338 .filter((entry) => entry !== undefined) as Array< 339 TraceEntry<HierarchyTreeNode> 340 >; 341 } else { 342 const entry = TraceEntryFinder.findCorrespondingEntry( 343 assertDefined(this.trace), 344 event.position, 345 ); 346 if (entry) entries.push(entry); 347 } 348 349 try { 350 await this.hierarchyPresenter.applyTracePositionUpdate( 351 entries, 352 this.highlightedItem, 353 ); 354 const showDiff = this.hierarchyPresenter.getUserOptions()['showDiff']; 355 this.logFetchComponentData(hierarchyStartTime, 'hierarchy', showDiff); 356 } catch (e) { 357 this.hierarchyPresenter.clear(); 358 this.rectsPresenter?.clear(); 359 this.propertiesPresenter.clear(); 360 this.refreshHierarchyViewerUiData(); 361 throw e; 362 } 363 364 const propertiesOpts = this.propertiesPresenter.getUserOptions(); 365 const hasPreviousEntry = entries.some((e) => e.getIndex() > 0); 366 if (propertiesOpts['showDiff']?.isUnavailable !== undefined) { 367 propertiesOpts['showDiff'].isUnavailable = !hasPreviousEntry; 368 } 369 370 const currentHierarchyTrees = 371 this.hierarchyPresenter.getAllCurrentHierarchyTrees(); 372 if (currentHierarchyTrees) { 373 const rectStartTime = Date.now(); 374 this.rectsPresenter?.applyHierarchyTreesChange(currentHierarchyTrees); 375 this.logFetchComponentData(rectStartTime, 'rects'); 376 377 await this.updatePropertiesTree(); 378 } 379 } 380 381 protected async applyHighlightedNodeChange(node: UiHierarchyTreeNode) { 382 this.updateHighlightedItem(node.id); 383 this.hierarchyPresenter.applyHighlightedNodeChange(node); 384 await this.updatePropertiesTree(); 385 } 386 387 protected async applyHighlightedIdChange(newId: string) { 388 this.updateHighlightedItem(newId); 389 this.hierarchyPresenter.applyHighlightedIdChange(newId); 390 await this.updatePropertiesTree(); 391 } 392 393 protected async updatePropertiesTree() { 394 const showDiff = this.propertiesPresenter.getUserOptions()['showDiff']; 395 const propertiesStartTime = Date.now(); 396 397 if (this.overridePropertiesTree) { 398 this.propertiesPresenter.setPropertiesTree(this.overridePropertiesTree); 399 await this.propertiesPresenter.formatPropertiesTree( 400 undefined, 401 this.overridePropertiesTreeName, 402 false, 403 ); 404 this.logFetchComponentData(propertiesStartTime, 'properties', showDiff); 405 return; 406 } 407 const selected = this.hierarchyPresenter.getSelectedTree(); 408 if (selected) { 409 const {trace, tree: selectedTree} = selected; 410 const propertiesTree = await selectedTree.getAllProperties(); 411 if ( 412 showDiff?.enabled && 413 !this.hierarchyPresenter.getPreviousHierarchyTreeForTrace(trace) 414 ) { 415 await this.hierarchyPresenter.updatePreviousHierarchyTrees(); 416 } 417 const previousTree = 418 this.hierarchyPresenter.getPreviousHierarchyTreeForTrace(trace); 419 this.propertiesPresenter.setPropertiesTree(propertiesTree); 420 await this.propertiesPresenter.formatPropertiesTree( 421 previousTree, 422 this.getOverrideDisplayName(selected), 423 this.keepCalculated(selectedTree), 424 trace.type, 425 ); 426 this.logFetchComponentData(propertiesStartTime, 'properties', showDiff); 427 } else { 428 this.propertiesPresenter.clear(); 429 } 430 } 431 432 protected updateHighlightedItem(id: string) { 433 if (this.highlightedItem === id) { 434 this.highlightedItem = ''; 435 } else { 436 this.highlightedItem = id; 437 } 438 } 439 440 protected refreshHierarchyViewerUiData() { 441 this.uiData.highlightedItem = this.highlightedItem; 442 this.uiData.pinnedItems = this.hierarchyPresenter.getPinnedItems(); 443 this.uiData.hierarchyUserOptions = this.hierarchyPresenter.getUserOptions(); 444 this.uiData.hierarchyTrees = this.hierarchyPresenter.getAllFormattedTrees(); 445 this.uiData.hierarchyFilter = this.hierarchyPresenter.getTextFilter(); 446 447 this.uiData.propertiesUserOptions = 448 this.propertiesPresenter.getUserOptions(); 449 this.uiData.propertiesTree = this.propertiesPresenter.getFormattedTree(); 450 this.uiData.highlightedProperty = 451 this.propertiesPresenter.getHighlightedProperty(); 452 this.uiData.propertiesFilter = assertDefined( 453 this.propertiesPresenter.getTextFilter(), 454 ); 455 456 if (this.rectsPresenter) { 457 this.uiData.rectsToDraw = this.rectsPresenter?.getRectsToDraw(); 458 this.uiData.rectIdToShowState = 459 this.rectsPresenter.getRectIdToShowState(); 460 this.uiData.displays = this.rectsPresenter.getDisplays(); 461 this.uiData.rectsUserOptions = this.rectsPresenter.getUserOptions(); 462 } 463 464 this.copyUiDataAndNotifyView(); 465 } 466 467 protected getHighlightedItem(): string | undefined { 468 return this.highlightedItem; 469 } 470 471 protected getEntryFormattedTimestamp( 472 entry: TraceEntry<HierarchyTreeNode>, 473 ): string { 474 if (entry.getFullTrace().isDumpWithoutTimestamp()) { 475 return 'Dump'; 476 } 477 return entry.getTimestamp().format(); 478 } 479 480 private copyUiDataAndNotifyView() { 481 // Create a shallow copy of the data, otherwise the Angular OnPush change detection strategy 482 // won't detect the new input 483 const copy = Object.assign({}, this.uiData); 484 this.notifyViewCallback(copy); 485 } 486 487 private logFetchComponentData( 488 startTimeMs: number, 489 component: 'hierarchy' | 'properties' | 'rects', 490 showDiffs?: UserOption, 491 ) { 492 const traceName = 493 TRACE_INFO[this.trace?.type ?? assertDefined(this.multiTraceType)].name; 494 Analytics.Navigation.logFetchComponentDataTime( 495 component, 496 traceName, 497 showDiffs !== undefined && showDiffs.enabled && !showDiffs.isUnavailable, 498 Date.now() - startTimeMs, 499 ); 500 } 501 502 abstract onHighlightedNodeChange(node: UiHierarchyTreeNode): Promise<void>; 503 abstract onHighlightedIdChange(id: string): Promise<void>; 504 protected abstract keepCalculated(tree: HierarchyTreeNode): boolean; 505 protected abstract getOverrideDisplayName( 506 selected: SelectedTree, 507 ): string | undefined; 508 protected abstract refreshUIData(): void; 509 protected initializeIfNeeded?(event: TracePositionUpdate): Promise<void>; 510 protected processDataAfterPositionUpdate?( 511 event: TracePositionUpdate, 512 ): Promise<void>; 513} 514