• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright 2017, 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 {DiffType} from './utils/diff.js';
18
19// kind - a type used for categorization of different levels
20// name - name of the node
21// children - list of child entries. Each child entry is pair list
22//            [raw object, nested transform function].
23// bounds - used to calculate the full bounds of parents
24// stableId - unique id for an entry. Used to maintain selection across frames.
25function transform({
26  obj,
27  kind,
28  name,
29  shortName,
30  children,
31  timestamp,
32  rect,
33  bounds,
34  highlight,
35  rectsTransform,
36  chips,
37  visible,
38  flattened,
39  stableId,
40  freeze = true,
41}) {
42  function call(fn, arg) {
43    return (typeof fn == 'function') ? fn(arg) : fn;
44  }
45  function handleChildren(arg, transform) {
46    return [].concat(...arg.map((item) => {
47      const childrenFunc = item[0];
48      const transformFunc = item[1];
49      const childs = call(childrenFunc, obj);
50      if (childs) {
51        if (typeof childs.map != 'function') {
52          throw new Error(
53              'Childs should be an array, but is: ' + (typeof childs) + '.');
54        }
55        return transform ? childs.map(transformFunc) : childs;
56      } else {
57        return [];
58      }
59    }));
60  }
61  function concat(arg, args, argsmap) {
62    const validArg = arg !== undefined && arg !== null;
63
64    if (Array.isArray(args)) {
65      if (validArg) {
66        return [arg].concat(...args.map(argsmap));
67      } else {
68        return [].concat(...args.map(argsmap));
69      }
70    } else if (validArg) {
71      return [arg];
72    } else {
73      return undefined;
74    }
75  }
76
77  const transformedChildren = handleChildren(children, true /* transform */);
78  rectsTransform = (rectsTransform === undefined) ? (e) => e : rectsTransform;
79
80  const kindResolved = call(kind, obj);
81  const nameResolved = call(name, obj);
82  const shortNameResolved = call(shortName, obj);
83  const rectResolved = call(rect, obj);
84  // eslint-disable-next-line max-len
85  const stableIdResolved = (stableId === undefined) ? kindResolved + '|-|' + nameResolved : call(stableId, obj);
86
87  const result = {
88    kind: kindResolved,
89    name: nameResolved,
90    shortName: shortNameResolved,
91    collapsed: false,
92    children: transformedChildren,
93    obj: obj,
94    timestamp: call(timestamp, obj),
95    skip: handleChildren(children, false /* transform */),
96    bounds: call(bounds, obj) || transformedChildren.map(
97        (e) => e.bounds).find((e) => true) || undefined,
98    rect: rectResolved,
99    rects: rectsTransform(
100        concat(rectResolved, transformedChildren, (e) => e.rects)),
101    highlight: call(highlight, obj),
102    chips: call(chips, obj),
103    stableId: stableIdResolved,
104    visible: call(visible, obj),
105    childrenVisible: transformedChildren.some((c) => {
106      return c.childrenVisible || c.visible;
107    }),
108    flattened: call(flattened, obj),
109  };
110
111  if (rectResolved) {
112    rectResolved.ref = result;
113  }
114
115  return freeze ? Object.freeze(result) : result;
116}
117
118function getDiff(val, compareVal) {
119  if (val && isTerminal(compareVal)) {
120    return {type: DiffType.ADDED};
121  } else if (isTerminal(val) && compareVal) {
122    return {type: DiffType.DELETED};
123  } else if (compareVal != val) {
124    return {type: DiffType.MODIFIED};
125  } else {
126    return {type: DiffType.NONE};
127  }
128}
129
130// Represents termination of the object traversal,
131// differentiated with a null value in the object.
132class Terminal { }
133
134function isTerminal(obj) {
135  return obj instanceof Terminal;
136}
137
138class ObjectTransformer {
139  constructor(obj, rootName, stableId) {
140    this.obj = obj;
141    this.rootName = rootName;
142    this.stableId = stableId;
143    this.diff = false;
144  }
145
146  setOptions(options) {
147    this.options = options;
148    return this;
149  }
150
151  withDiff(obj, fieldOptions) {
152    this.diff = true;
153    this.compareWithObj = obj ?? new Terminal();
154    this.compareWithFieldOptions = fieldOptions;
155    return this;
156  }
157
158  /**
159   * Transform the raw JS Object into a TreeView compatible object
160   * @param {Object} transformOptions detailed below
161   * @param {bool} keepOriginal whether or not to store the original object in
162   *                            the obj property of a tree node for future
163   *                            reference
164   * @param {bool} freeze whether or not the returned objected should be frozen
165   *                      to prevent changing any of its properties
166   * @param {string} metadataKey the key that contains a node's metadata to be
167   *                             accessible after the transformation
168   * @return {Object} the transformed JS object compatible with treeviews.
169   */
170  transform(transformOptions = {
171    keepOriginal: false, freeze: true, metadataKey: null,
172  }) {
173    const {formatter} = this.options;
174    if (!formatter) {
175      throw new Error('Missing formatter, please set with setOptions()');
176    }
177
178    return this._transform(this.obj, this.rootName, null,
179        this.compareWithObj, this.rootName, null,
180        this.stableId, transformOptions);
181  }
182
183  /**
184   * @param {Object} obj the object to transform to a treeview compatible object
185   * @param {Object} fieldOptions options on how to transform fields
186   * @param {*} metadataKey if 'obj' contains this key, it is excluded from the
187   *                        transformation
188   * @return {Object} the transformed JS object compatible with treeviews.
189   */
190  _transformObject(obj, fieldOptions, metadataKey) {
191    const {skip, formatter} = this.options;
192    const transformedObj = {
193      obj: {},
194      fieldOptions: {},
195    };
196    let formatted = undefined;
197
198    if (skip && skip.includes(obj)) {
199      // skip
200    } else if ((formatted = formatter(obj))) {
201      // Obj has been formatted into a terminal node — has no children.
202      transformedObj.obj[formatted] = new Terminal();
203      transformedObj.fieldOptions[formatted] = fieldOptions;
204    } else if (Array.isArray(obj)) {
205      obj.forEach((e, i) => {
206        transformedObj.obj['' + i] = e;
207        transformedObj.fieldOptions['' + i] = fieldOptions;
208      });
209    } else if (typeof obj == 'string') {
210      // Object is a primitive type — has no children. Set to terminal
211      // to differentiate between null object and Terminal element.
212      transformedObj.obj[obj] = new Terminal();
213      transformedObj.fieldOptions[obj] = fieldOptions;
214    } else if (typeof obj == 'number' || typeof obj == 'boolean') {
215      // Similar to above — primitive type node has no children.
216      transformedObj.obj['' + obj] = new Terminal();
217      transformedObj.fieldOptions['' + obj] = fieldOptions;
218    } else if (obj && typeof obj == 'object') {
219      Object.keys(obj).forEach((key) => {
220        if (key === metadataKey) {
221          return;
222        }
223        transformedObj.obj[key] = obj[key];
224        transformedObj.fieldOptions[key] = obj.$type?.fields[key]?.options;
225      });
226    } else if (obj === null) {
227      // Null object is a has no children — set to be terminal node.
228      transformedObj.obj.null = new Terminal();
229      transformedObj.fieldOptions.null = undefined;
230    }
231
232    return transformedObj;
233  }
234
235  /**
236   * Extract the value of obj's property with key 'metadataKey'
237   * @param {Object} obj the obj we want to extract the metadata from
238   * @param {string} metadataKey the key that stores the metadata in the object
239   * @return {Object} the metadata value or null in no metadata is present
240   */
241  _getMetadata(obj, metadataKey) {
242    if (metadataKey && obj[metadataKey]) {
243      const metadata = obj[metadataKey];
244      obj[metadataKey] = undefined;
245      return metadata;
246    } else {
247      return null;
248    }
249  }
250
251  _transform(obj, name, fieldOptions,
252      compareWithObj, compareWithName, compareWithFieldOptions,
253      stableId, transformOptions) {
254    const originalObj = obj;
255    const metadata = this._getMetadata(obj, transformOptions.metadataKey);
256
257    const children = [];
258
259    if (!isTerminal(obj)) {
260      const transformedObj =
261          this._transformObject(
262              obj, fieldOptions, transformOptions.metadataKey);
263      obj = transformedObj.obj;
264      fieldOptions = transformedObj.fieldOptions;
265    }
266    if (!isTerminal(compareWithObj)) {
267      const transformedObj =
268          this._transformObject(
269              compareWithObj, compareWithFieldOptions,
270              transformOptions.metadataKey);
271      compareWithObj = transformedObj.obj;
272      compareWithFieldOptions = transformedObj.fieldOptions;
273    }
274
275    for (const key in obj) {
276      if (obj.hasOwnProperty(key)) {
277        let compareWithChild = new Terminal();
278        let compareWithChildName = new Terminal();
279        let compareWithChildFieldOptions = undefined;
280        if (compareWithObj.hasOwnProperty(key)) {
281          compareWithChild = compareWithObj[key];
282          compareWithChildName = key;
283          compareWithChildFieldOptions = compareWithFieldOptions[key];
284        }
285        children.push(this._transform(obj[key], key, fieldOptions[key],
286            compareWithChild, compareWithChildName,
287            compareWithChildFieldOptions,
288            `${stableId}.${key}`, transformOptions));
289      }
290    }
291
292    // Takes care of adding deleted items to final tree
293    for (const key in compareWithObj) {
294      if (!obj.hasOwnProperty(key) && compareWithObj.hasOwnProperty(key)) {
295        children.push(this._transform(new Terminal(), new Terminal(), undefined,
296            compareWithObj[key], key, compareWithFieldOptions[key],
297            `${stableId}.${key}`, transformOptions));
298      }
299    }
300
301    let transformedObj;
302    if (
303      children.length == 1 &&
304      children[0].children.length == 0 &&
305      !children[0].combined
306    ) {
307      // Merge leaf key value pairs.
308      const child = children[0];
309
310      transformedObj = {
311        kind: '',
312        name: name + ': ' + child.name,
313        stableId,
314        children: child.children,
315        combined: true,
316      };
317
318      if (this.diff) {
319        transformedObj.diff = child.diff;
320      }
321    } else {
322      transformedObj = {
323        kind: '',
324        name,
325        stableId,
326        children,
327      };
328
329      let fieldOptionsToUse = fieldOptions;
330
331      if (this.diff) {
332        const diff = getDiff(name, compareWithName);
333        transformedObj.diff = diff;
334
335        if (diff.type == DiffType.DELETED) {
336          transformedObj.name = compareWithName;
337          fieldOptionsToUse = compareWithFieldOptions;
338        }
339      }
340    }
341
342    if (transformOptions.keepOriginal) {
343      transformedObj.obj = originalObj;
344    }
345
346    if (metadata) {
347      transformedObj[transformOptions.metadataKey] = metadata;
348    }
349
350    return transformOptions.freeze ?
351      Object.freeze(transformedObj) : transformedObj;
352  }
353}
354
355// eslint-disable-next-line camelcase
356function nanos_to_string(elapsedRealtimeNanos) {
357  const units = [
358    [1000000, '(ns)'],
359    [1000, 'ms'],
360    [60, 's'],
361    [60, 'm'],
362    [24, 'h'],
363    [Infinity, 'd'],
364  ];
365
366  const parts = [];
367  units.some(([div, str], i) => {
368    const part = (elapsedRealtimeNanos % div).toFixed();
369    if (!str.startsWith('(')) {
370      parts.push(part + str);
371    }
372    elapsedRealtimeNanos = Math.floor(elapsedRealtimeNanos / div);
373    return elapsedRealtimeNanos == 0;
374  });
375
376  return parts.reverse().join('');
377}
378
379// Returns a UI element used highlight a visible entry.
380// eslint-disable-next-line camelcase
381function get_visible_chip() {
382  return {short: 'V', long: 'visible', class: 'default'};
383}
384
385// eslint-disable-next-line camelcase
386export {transform, ObjectTransformer, nanos_to_string, get_visible_chip};
387