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 */ 16import {FilterType, TreeNode} from 'common/tree_utils'; 17import {ObjectFormatter} from 'trace/flickerlib/ObjectFormatter'; 18import {TraceTreeNode} from 'trace/trace_tree_node'; 19 20import { 21 DiffType, 22 HierarchyTreeNode, 23 PropertiesDump, 24 PropertiesTreeNode, 25 Terminal, 26} from './ui_tree_utils'; 27 28interface TransformOptions { 29 freeze: boolean; 30 keepOriginal: boolean; 31 metadataKey: string | null; 32} 33interface TreeTransformerOptions { 34 skip?: any; 35 formatter?: any; 36} 37 38export class TreeTransformer { 39 private stableId: string; 40 private rootName: string; 41 private isShowDefaults = false; 42 private isShowDiff = false; 43 private filter: FilterType; 44 private properties: PropertiesDump | Terminal | null = null; 45 private compareWithProperties: PropertiesDump | Terminal | null = null; 46 private options?: TreeTransformerOptions; 47 private onlyProtoDump = false; 48 private transformOptions: TransformOptions = { 49 keepOriginal: false, 50 freeze: true, 51 metadataKey: null, 52 }; 53 54 constructor(selectedTree: HierarchyTreeNode, filter: FilterType) { 55 this.stableId = this.compatibleStableId(selectedTree); 56 this.rootName = selectedTree.name; 57 this.filter = filter; 58 this.setTransformerOptions({}); 59 } 60 61 setOnlyProtoDump(onlyProto: boolean): TreeTransformer { 62 this.onlyProtoDump = onlyProto; 63 return this; 64 } 65 66 setIsShowDefaults(enabled: boolean): TreeTransformer { 67 this.isShowDefaults = enabled; 68 return this; 69 } 70 71 setIsShowDiff(enabled: boolean): TreeTransformer { 72 this.isShowDiff = enabled; 73 return this; 74 } 75 76 setTransformerOptions(options: TreeTransformerOptions): TreeTransformer { 77 this.options = options; 78 if (!this.options.formatter) { 79 this.options.formatter = this.formatProto; 80 } 81 return this; 82 } 83 84 setProperties(currentEntry: TraceTreeNode | null): TreeTransformer { 85 const currFlickerItem = this.getOriginalFlickerItem(currentEntry, this.stableId); 86 const target = currFlickerItem ? currFlickerItem.obj ?? currFlickerItem : null; 87 ObjectFormatter.displayDefaults = this.isShowDefaults; 88 this.properties = this.onlyProtoDump 89 ? this.getProtoDumpPropertiesForDisplay(target) 90 : this.getPropertiesForDisplay(target); 91 return this; 92 } 93 94 setDiffProperties(previousEntry: TraceTreeNode | null): TreeTransformer { 95 if (this.isShowDiff) { 96 const prevFlickerItem = this.findFlickerItem(previousEntry, this.stableId); 97 const target = prevFlickerItem ? prevFlickerItem.obj ?? prevFlickerItem : null; 98 this.compareWithProperties = this.onlyProtoDump 99 ? this.getProtoDumpPropertiesForDisplay(target) 100 : this.getPropertiesForDisplay(target); 101 } 102 return this; 103 } 104 105 getOriginalFlickerItem(entry: TraceTreeNode | null, stableId: string): TraceTreeNode | null { 106 return this.findFlickerItem(entry, stableId); 107 } 108 109 private getProtoDumpPropertiesForDisplay(entry: TraceTreeNode): PropertiesDump | null { 110 if (!entry) { 111 return null; 112 } 113 114 return ObjectFormatter.format(entry.proto); 115 } 116 117 private getPropertiesForDisplay(entry: TraceTreeNode): PropertiesDump | null { 118 if (!entry) { 119 return null; 120 } 121 122 return ObjectFormatter.format(entry); 123 } 124 125 private findFlickerItem( 126 entryFlickerItem: TraceTreeNode | null, 127 stableId: string 128 ): TraceTreeNode | null { 129 if (!entryFlickerItem) { 130 return null; 131 } 132 133 if (entryFlickerItem.stableId && entryFlickerItem.stableId === stableId) { 134 return entryFlickerItem; 135 } 136 137 if (!entryFlickerItem.children) { 138 return null; 139 } 140 141 for (const child of entryFlickerItem.children) { 142 const foundEntry: any = this.findFlickerItem(child, stableId); 143 if (foundEntry) { 144 return foundEntry; 145 } 146 } 147 148 return null; 149 } 150 151 transform(): PropertiesTreeNode { 152 const {formatter} = this.options!; 153 if (!formatter) { 154 throw new Error('Missing formatter, please set with setOptions()'); 155 } 156 157 const transformedTree = this.transformTree( 158 this.properties, 159 this.rootName, 160 this.compareWithProperties, 161 this.rootName, 162 this.stableId, 163 this.transformOptions 164 ); 165 return transformedTree; 166 } 167 168 private transformTree( 169 properties: PropertiesDump | null | Terminal, 170 name: string | Terminal, 171 compareWithProperties: PropertiesDump | null | Terminal, 172 compareWithName: string | Terminal, 173 stableId: string, 174 transformOptions: TransformOptions 175 ): PropertiesTreeNode { 176 const originalProperties = properties; 177 const metadata = this.getMetadata(originalProperties, transformOptions.metadataKey); 178 179 const children: any[] = []; 180 181 if (properties === null) { 182 properties = 'null'; 183 } 184 185 if (!this.isTerminal(properties)) { 186 const transformedProperties = this.transformProperties( 187 properties, 188 transformOptions.metadataKey 189 ); 190 properties = transformedProperties.properties; 191 } 192 193 if (compareWithProperties && !this.isTerminal(compareWithProperties)) { 194 const transformedProperties = this.transformProperties( 195 compareWithProperties, 196 transformOptions.metadataKey 197 ); 198 compareWithProperties = transformedProperties.properties; 199 } 200 201 for (const key in properties) { 202 if (!(properties instanceof Terminal) /* && properties[key]*/) { 203 let compareWithChild = new Terminal(); 204 let compareWithChildName = new Terminal(); 205 if ( 206 compareWithProperties && 207 !(compareWithProperties instanceof Terminal) && 208 compareWithProperties[key] 209 ) { 210 compareWithChild = compareWithProperties[key]; 211 compareWithChildName = key; 212 } 213 const child = this.transformTree( 214 properties[key], 215 key, 216 compareWithChild, 217 compareWithChildName, 218 `${stableId}.${key}`, 219 transformOptions 220 ); 221 222 children.push(child); 223 } 224 } 225 226 // Takes care of adding deleted items to final tree 227 for (const key in compareWithProperties) { 228 if ( 229 properties && 230 !(properties instanceof Terminal) && 231 !properties[key] && 232 !(compareWithProperties instanceof Terminal) && 233 compareWithProperties[key] 234 ) { 235 const child = this.transformTree( 236 new Terminal(), 237 new Terminal(), 238 compareWithProperties[key], 239 key, 240 `${stableId}.${key}`, 241 transformOptions 242 ); 243 244 children.push(child); 245 } 246 } 247 248 let transformedProperties: any; 249 if (children.length === 1 && children[0].children?.length === 0 && !children[0].combined) { 250 // Merge leaf key value pairs. 251 const child = children[0]; 252 253 transformedProperties = { 254 kind: '', 255 name: (this.isTerminal(name) ? compareWithName : name) + ': ' + child.name, 256 stableId, 257 children: child.children, 258 combined: true, 259 }; 260 261 if (this.isShowDiff) { 262 transformedProperties.diffType = child.diffType; 263 } 264 } else { 265 transformedProperties = { 266 kind: '', 267 name, 268 stableId, 269 children, 270 }; 271 272 if (this.isShowDiff) { 273 const diffType = this.getDiff(name, compareWithName); 274 transformedProperties.diffType = diffType; 275 276 if (diffType === DiffType.DELETED) { 277 transformedProperties.name = compareWithName; 278 } 279 } 280 } 281 282 if (transformOptions.keepOriginal) { 283 transformedProperties.properties = originalProperties; 284 } 285 286 if (metadata && transformOptions.metadataKey) { 287 transformedProperties[transformOptions.metadataKey] = metadata; 288 } 289 290 if (!this.isTerminal(transformedProperties.name)) { 291 transformedProperties.propertyKey = this.getPropertyKey(transformedProperties); 292 transformedProperties.propertyValue = this.getPropertyValue(transformedProperties); 293 } 294 295 if ( 296 !this.filterMatches(transformedProperties) && 297 !this.hasChildMatchingFilter(transformedProperties?.children) 298 ) { 299 transformedProperties.propertyKey = new Terminal(); 300 } 301 return transformOptions.freeze ? Object.freeze(transformedProperties) : transformedProperties; 302 } 303 304 private hasChildMatchingFilter(children: PropertiesTreeNode[] | null | undefined): boolean { 305 if (!children || children.length === 0) return false; 306 307 let match = false; 308 for (let i = 0; i < children.length; i++) { 309 if (this.filterMatches(children[i]) || this.hasChildMatchingFilter(children[i].children)) { 310 match = true; 311 } 312 } 313 314 return match; 315 } 316 317 private getMetadata(obj: PropertiesDump | null | Terminal, metadataKey: string | null): any { 318 if (obj == null) { 319 return null; 320 } 321 if (metadataKey && !(obj instanceof Terminal) && obj[metadataKey]) { 322 const metadata = obj[metadataKey]; 323 obj[metadataKey] = undefined; 324 return metadata; 325 } else { 326 return null; 327 } 328 } 329 330 private getPropertyKey(item: PropertiesDump): string { 331 if (item['name'] && (!item['children'] || item['children'].length === 0)) { 332 return item['name'].split(': ')[0]; 333 } 334 return item['name']; 335 } 336 337 private getPropertyValue(item: PropertiesDump): string | null { 338 if (item['name'] && (!item['children'] || item['children'].length === 0)) { 339 return item['name'].split(': ').slice(1).join(': '); 340 } 341 return null; 342 } 343 344 private filterMatches(item: PropertiesDump | null): boolean { 345 //TODO: fix PropertiesDump type. What is it? Why does it declare only a "key" property and yet it is used as a TreeNode? 346 return this.filter(item as TreeNode) ?? false; 347 } 348 349 private transformProperties( 350 properties: PropertiesDump, 351 metadataKey: string | null 352 ): PropertiesTreeNode { 353 const {skip, formatter} = this.options!; 354 const transformedProperties: PropertiesTreeNode = { 355 properties: {}, 356 }; 357 358 if (skip && skip.includes(properties)) { 359 return transformedProperties; // skip 360 } 361 362 const formatted = formatter(properties); 363 if (formatted) { 364 // Obj has been formatted into a terminal node — has no children. 365 transformedProperties.properties[formatted] = new Terminal(); 366 } else if (Array.isArray(properties)) { 367 properties.forEach((e, i) => { 368 transformedProperties.properties['' + i] = e; 369 }); 370 } else if (typeof properties === 'string') { 371 // Object is a primitive type — has no children. Set to terminal 372 // to differentiate between null object and Terminal element. 373 transformedProperties.properties[properties] = new Terminal(); 374 } else if (typeof properties === 'number' || typeof properties === 'boolean') { 375 // Similar to above — primitive type node has no children. 376 transformedProperties.properties['' + properties] = new Terminal(); 377 } else if (properties && typeof properties === 'object') { 378 // Empty objects 379 if (Object.keys(properties).length === 0) { 380 transformedProperties.properties['[empty]'] = new Terminal(); 381 } else { 382 // Non empty objects 383 Object.keys(properties).forEach((key) => { 384 if (key === metadataKey) { 385 return; 386 } 387 transformedProperties.properties[key] = properties[key]; 388 }); 389 } 390 } else if (properties === null) { 391 // Null object has no children — set to be terminal node. 392 transformedProperties.properties.null = new Terminal(); 393 } 394 return transformedProperties; 395 } 396 397 private getDiff(val: string | Terminal, compareVal: string | Terminal): string { 398 if (val && this.isTerminal(compareVal)) { 399 return DiffType.ADDED; 400 } else if (this.isTerminal(val) && compareVal) { 401 return DiffType.DELETED; 402 } else if (compareVal !== val) { 403 return DiffType.MODIFIED; 404 } else { 405 return DiffType.NONE; 406 } 407 } 408 409 private compatibleStableId(item: HierarchyTreeNode): string { 410 // For backwards compatibility 411 // (the only item that doesn't have a unique stable ID in the tree) 412 if (item.stableId === 'winToken|-|') { 413 return item.stableId + item.children[0].stableId; 414 } 415 return item.stableId; 416 } 417 418 private formatProto(item: any) { 419 if (item?.prettyPrint) { 420 return item.prettyPrint(); 421 } 422 } 423 424 private isTerminal(item: any): boolean { 425 return item instanceof Terminal; 426 } 427} 428