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