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