• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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