census-tree-node.js (23559B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 "use strict"; 5 6 // CensusTreeNode is an intermediate representation of a census report that 7 // exists between after a report is generated by taking a census and before the 8 // report is rendered in the DOM. It must be dead simple to render, with no 9 // further data processing or massaging needed before rendering DOM nodes. Our 10 // goal is to do the census report to CensusTreeNode transformation in the 11 // HeapAnalysesWorker, and ensure that the **only** work that the main thread 12 // has to do is strictly DOM rendering work. 13 14 const { 15 Visitor, 16 walk, 17 basisTotalBytes, 18 basisTotalCount, 19 } = require("resource://devtools/shared/heapsnapshot/CensusUtils.js"); 20 21 // Monotonically increasing integer for CensusTreeNode `id`s. 22 let censusTreeNodeIdCounter = 0; 23 24 /** 25 * Return true if the given object is a SavedFrame stack object, false otherwise. 26 * 27 * @param {any} obj 28 * @returns {boolean} 29 */ 30 function isSavedFrame(obj) { 31 return Object.prototype.toString.call(obj) === "[object SavedFrame]"; 32 } 33 34 /** 35 * A CensusTreeNodeCache maps from SavedFrames to CensusTreeNodes. It is used when 36 * aggregating multiple SavedFrame allocation stack keys into a tree of many 37 * CensusTreeNodes. Each stack may share older frames, and we want to preserve 38 * this sharing when converting to CensusTreeNode, so before creating a new 39 * CensusTreeNode, we look for an existing one in one of our CensusTreeNodeCaches. 40 */ 41 class CensusTreeNodeCache { 42 /** 43 * Create a unique string for the given SavedFrame (ignoring the frame's parent 44 * chain) that can be used as a hash to key this frame within a CensusTreeNodeCache. 45 * 46 * NB: We manually hash rather than using an ES6 Map because we are purposely 47 * ignoring the parent chain and wish to consider frames with everything the 48 * same except their parents as the same. 49 * 50 * @param {SavedFrame} frame 51 * The SavedFrame object we would like to lookup in or insert into a 52 * CensusTreeNodeCache. 53 * 54 * @returns {string} 55 * The unique string that can be used as a key in a CensusTreeNodeCache. 56 */ 57 static hashFrame(frame) { 58 // eslint-disable-next-line max-len 59 return `FRAME,${frame.functionDisplayName},${frame.source},${frame.line},${frame.column},${frame.asyncCause}`; 60 } 61 62 /** 63 * Create a unique string for the given CensusTreeNode **with regards to 64 * siblings at the current depth of the tree, not within the whole tree.** It 65 * can be used as a hash to key this node within a CensusTreeNodeCache. 66 * 67 * @param {CensusTreeNode} node 68 * The node we would like to lookup in or insert into a cache. 69 * 70 * @returns {string} 71 * The unique string that can be used as a key in a CensusTreeNodeCache. 72 */ 73 static hashNode(node) { 74 return isSavedFrame(node.name) 75 ? this.hashFrame(node.name) 76 : `NODE,${node.name}`; 77 } 78 79 /** 80 * Insert the given CensusTreeNodeCacheValue whose node.name is a SavedFrame 81 * object in the given cache. 82 * 83 * @param {CensusTreeNodeCache} cache 84 * @param {CensusTreeNodeCacheValue} value 85 */ 86 static insertFrame(cache, value) { 87 cache[this.hashFrame(value.node.name)] = value; 88 } 89 90 /** 91 * Insert the given value in the cache. 92 * 93 * @param {CensusTreeNodeCache} cache 94 * @param {CensusTreeNodeCacheValue} value 95 */ 96 static insertNode(cache, value) { 97 if (isSavedFrame(value.node.name)) { 98 this.insertFrame(cache, value); 99 } else { 100 cache[this.hashNode(value.node)] = value; 101 } 102 } 103 104 /** 105 * Lookup `frame` in `cache` and return its value if it exists. 106 * 107 * @param {CensusTreeNodeCache} cache 108 * @param {SavedFrame} frame 109 * 110 * @returns {undefined|CensusTreeNodeCacheValue} 111 */ 112 static lookupFrame(cache, frame) { 113 return cache[this.hashFrame(frame)]; 114 } 115 116 /** 117 * Lookup `node` in `cache` and return its value if it exists. 118 * 119 * @param {CensusTreeNodeCache} cache 120 * @param {CensusTreeNode} node 121 * 122 * @returns {undefined|CensusTreeNodeCacheValue} 123 */ 124 static lookupNode(cache, node) { 125 return isSavedFrame(node.name) 126 ? this.lookupFrame(cache, node.name) 127 : cache[this.hashNode(node)]; 128 } 129 } 130 131 /** 132 * The value of a single entry stored in a CensusTreeNodeCache. It is a pair of 133 * the CensusTreeNode for this cache value, and the subsequent 134 * CensusTreeNodeCache for this node's children. 135 */ 136 class CensusTreeNodeCacheValue { 137 constructor() { 138 // The CensusTreeNode for this cache value. 139 this.node = undefined; 140 // The CensusTreeNodeCache for this frame's children. 141 this.children = undefined; 142 } 143 } 144 145 /** 146 * Add `child` to `parent`'s set of children and store the parent ID 147 * on the child. 148 * 149 * @param {CensusTreeNode} parent 150 * @param {CensusTreeNode} child 151 */ 152 function addChild(parent, child) { 153 if (!parent.children) { 154 parent.children = []; 155 } 156 child.parent = parent.id; 157 parent.children.push(child); 158 } 159 160 /** 161 * Get an array of each frame in the provided stack. 162 * 163 * @param {SavedFrame} stack 164 * @returns {Array<SavedFrame>} 165 */ 166 function getArrayOfFrames(stack) { 167 const frames = []; 168 let frame = stack; 169 while (frame) { 170 frames.push(frame); 171 frame = frame.parent; 172 } 173 frames.reverse(); 174 return frames; 175 } 176 177 /** 178 * Given an `edge` to a sub-`report` whose structure is described by 179 * `breakdown`, create a CensusTreeNode tree. 180 * 181 * @param {object} breakdown 182 * The breakdown specifying the structure of the given report. 183 * 184 * @param {object} report 185 * The census report. 186 * 187 * @param {null | string | SavedFrame} edge 188 * The edge leading to this report from the parent report. 189 * 190 * @param {CensusTreeNodeCache} cache 191 * The cache of CensusTreeNodes we have already made for the siblings of 192 * the node being created. The existing nodes are reused when possible. 193 * 194 * @param {object} outParams 195 * The return values are attached to this object after this function 196 * returns. Because we create a CensusTreeNode for each frame in a 197 * SavedFrame stack edge, there may multiple nodes per sub-report. 198 * 199 * - top: The deepest node in the CensusTreeNode subtree created. 200 * 201 * - bottom: The shallowest node in the CensusTreeNode subtree created. 202 * This is null if the shallowest node in the subtree was 203 * found in the `cache` and reused. 204 * 205 * Note that top and bottom are not necessarily different. In the case 206 * where there is a 1:1 correspondence between an edge in the report and 207 * a CensusTreeNode, top and bottom refer to the same node. 208 */ 209 function makeCensusTreeNodeSubTree(breakdown, report, edge, cache, outParams) { 210 if (!isSavedFrame(edge)) { 211 const node = new CensusTreeNode(edge); 212 outParams.top = outParams.bottom = node; 213 return; 214 } 215 216 const frames = getArrayOfFrames(edge); 217 let currentCache = cache; 218 let prevNode; 219 for (let i = 0, length = frames.length; i < length; i++) { 220 const frame = frames[i]; 221 222 // Get or create the CensusTreeNodeCacheValue for this frame. If we already 223 // have a CensusTreeNodeCacheValue (and hence a CensusTreeNode) for this 224 // frame, we don't need to add the node to the previous node's children as 225 // we have already done that. If we don't have a CensusTreeNodeCacheValue 226 // and CensusTreeNode for this frame, then create one and make sure to hook 227 // it up as a child of the previous node. 228 let isNewNode = false; 229 let val = CensusTreeNodeCache.lookupFrame(currentCache, frame); 230 if (!val) { 231 isNewNode = true; 232 val = new CensusTreeNodeCacheValue(); 233 val.node = new CensusTreeNode(frame); 234 235 CensusTreeNodeCache.insertFrame(currentCache, val); 236 if (prevNode) { 237 addChild(prevNode, val.node); 238 } 239 } 240 241 if (i === 0) { 242 outParams.bottom = isNewNode ? val.node : null; 243 } 244 if (i === length - 1) { 245 outParams.top = val.node; 246 } 247 248 prevNode = val.node; 249 250 if (i !== length - 1 && !val.children) { 251 // This is not the last frame and therefore this node will have 252 // children, which we must cache. 253 val.children = new CensusTreeNodeCache(); 254 } 255 256 currentCache = val.children; 257 } 258 } 259 260 /** 261 * A Visitor that walks a census report and creates the corresponding 262 * CensusTreeNode tree. 263 */ 264 class CensusTreeNodeVisitor extends Visitor { 265 constructor() { 266 super(); 267 // The root of the resulting CensusTreeNode tree. 268 this._root = null; 269 270 // The stack of CensusTreeNodes that we are in the process of building while 271 // walking the census report. 272 this._nodeStack = []; 273 274 // To avoid unnecessary allocations, we reuse the same out parameter object 275 // passed to `makeCensusTreeNodeSubTree` every time we call it. 276 this._outParams = { 277 top: null, 278 bottom: null, 279 }; 280 281 // The stack of `CensusTreeNodeCache`s that we use to aggregate many 282 // SavedFrame stacks into a single CensusTreeNode tree. 283 this._cacheStack = [new CensusTreeNodeCache()]; 284 285 // The current index in the DFS of the census report tree. 286 this._index = -1; 287 } 288 289 /** 290 * Create the CensusTreeNode subtree for this sub-report and link it to the 291 * parent CensusTreeNode. 292 * 293 * @override 294 */ 295 enter(breakdown, report, edge) { 296 this._index++; 297 298 const cache = this._cacheStack[this._cacheStack.length - 1]; 299 makeCensusTreeNodeSubTree(breakdown, report, edge, cache, this._outParams); 300 const { top, bottom } = this._outParams; 301 302 if (!this._root) { 303 this._root = bottom; 304 } else if (bottom) { 305 addChild(this._nodeStack[this._nodeStack.length - 1], bottom); 306 } 307 308 this._cacheStack.push(new CensusTreeNodeCache()); 309 this._nodeStack.push(top); 310 } 311 312 /** 313 * We have finished adding children to the CensusTreeNode subtree for the 314 * current sub-report. Make sure that the children are sorted for every node in 315 * the subtree. 316 * 317 * @override 318 */ 319 exit() { 320 // Ensure all children are sorted and have their counts/bytes aggregated. We 321 // only need to consider cache children here, because other children 322 // correspond to other sub-reports and we already fixed them up in an earlier 323 // invocation of `exit`. 324 325 function dfs(node, childrenCache) { 326 if (childrenCache) { 327 const childValues = values(childrenCache); 328 for (let i = 0, length = childValues.length; i < length; i++) { 329 dfs(childValues[i].node, childValues[i].children); 330 } 331 } 332 333 node.totalCount = node.count; 334 node.totalBytes = node.bytes; 335 336 if (node.children) { 337 // Prune empty leaves. 338 node.children = node.children.filter(isNonEmpty); 339 340 node.children.sort(compareByTotal); 341 342 for (let i = 0, length = node.children.length; i < length; i++) { 343 node.totalCount += node.children[i].totalCount; 344 node.totalBytes += node.children[i].totalBytes; 345 } 346 } 347 } 348 349 const top = this._nodeStack.pop(); 350 const cache = this._cacheStack.pop(); 351 dfs(top, cache); 352 } 353 354 /** 355 * @override 356 */ 357 count(breakdown, report) { 358 const node = this._nodeStack[this._nodeStack.length - 1]; 359 node.reportLeafIndex = this._index; 360 361 if (breakdown.count) { 362 node.count = report.count; 363 } 364 365 if (breakdown.bytes) { 366 node.bytes = report.bytes; 367 } 368 } 369 370 /** 371 * Get the root of the resulting CensusTreeNode tree. 372 * 373 * @returns {CensusTreeNode} 374 */ 375 root() { 376 if (!this._root) { 377 throw new Error( 378 "Attempt to get the root before walking the census report!" 379 ); 380 } 381 382 if (this._nodeStack.length) { 383 throw new Error( 384 "Attempt to get the root while walking the census report!" 385 ); 386 } 387 388 return this._root; 389 } 390 } 391 392 function values(cache) { 393 return Object.keys(cache).map(k => cache[k]); 394 } 395 396 function isNonEmpty(node) { 397 return ( 398 (node.children !== undefined && node.children.length) || 399 node.bytes !== 0 || 400 node.count !== 0 401 ); 402 } 403 404 /** 405 * Create a single, uninitialized CensusTreeNode. 406 */ 407 class CensusTreeNode { 408 /** 409 * 410 * @param {null | string | SavedFrame} name 411 */ 412 constructor(name) { 413 // Display name for this CensusTreeNode. Either null, a string, or a 414 // SavedFrame. 415 this.name = name; 416 417 // The number of bytes occupied by matching things in the heap snapshot. 418 this.bytes = 0; 419 420 // The sum of `this.bytes` and `child.totalBytes` for each child in 421 // `this.children`. 422 this.totalBytes = 0; 423 424 // The number of things in the heap snapshot that match this node in the 425 // census tree. 426 this.count = 0; 427 428 // The sum of `this.count` and `child.totalCount` for each child in 429 // `this.children`. 430 this.totalCount = 0; 431 432 // An array of this node's children, or undefined if it has no children. 433 this.children = undefined; 434 435 // The unique ID of this node. 436 this.id = ++censusTreeNodeIdCounter; 437 438 // If present, the unique ID of this node's parent. If this node does not have 439 // a parent, then undefined. 440 this.parent = undefined; 441 442 // The `reportLeafIndex` property allows mapping a CensusTreeNode node back to 443 // a leaf in the census report it was generated from. It is always one of the 444 // following variants: 445 // 446 // * A `Number` index pointing a leaf report in a pre-order DFS traversal of 447 // this CensusTreeNode's census report. 448 // 449 // * A `Set` object containing such indices, when this is part of an inverted 450 // CensusTreeNode tree and multiple leaves in the report map onto this node. 451 // 452 // * Finally, `undefined` when no leaves in the census report correspond with 453 // this node. 454 // 455 // The first and third cases are the common cases. The second case is rather 456 // uncommon, and to avoid doubling the number of allocations when creating 457 // CensusTreeNode trees, and objects that get structured cloned when sending 458 // such trees from the HeapAnalysesWorker to the main thread, we only allocate 459 // a Set object once a node actually does have multiple leaves it corresponds 460 // to. 461 this.reportLeafIndex = undefined; 462 } 463 } 464 465 /** 466 * Compare the given nodes by their `totalBytes` properties, and breaking ties 467 * with the `totalCount`, `bytes`, and `count` properties (in that order). 468 * 469 * @param {CensusTreeNode} node1 470 * @param {CensusTreeNode} node2 471 * 472 * @returns {number} 473 * A number suitable for using with Array.prototype.sort. 474 */ 475 function compareByTotal(node1, node2) { 476 return ( 477 Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes) || 478 Math.abs(node2.totalCount) - Math.abs(node1.totalCount) || 479 Math.abs(node2.bytes) - Math.abs(node1.bytes) || 480 Math.abs(node2.count) - Math.abs(node1.count) 481 ); 482 } 483 484 /** 485 * Compare the given nodes by their `bytes` properties, and breaking ties with 486 * the `count`, `totalBytes`, and `totalCount` properties (in that order). 487 * 488 * @param {CensusTreeNode} node1 489 * @param {CensusTreeNode} node2 490 * 491 * @returns {number} 492 * A number suitable for using with Array.prototype.sort. 493 */ 494 function compareBySelf(node1, node2) { 495 return ( 496 Math.abs(node2.bytes) - Math.abs(node1.bytes) || 497 Math.abs(node2.count) - Math.abs(node1.count) || 498 Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes) || 499 Math.abs(node2.totalCount) - Math.abs(node1.totalCount) 500 ); 501 } 502 503 /** 504 * Given a parent cache value from a tree we are building and a child node from 505 * a tree we are basing the new tree off of, if we already have a corresponding 506 * node in the parent's children cache, merge this node's counts with 507 * it. Otherwise, create the corresponding node, add it to the parent's children 508 * cache, and create the parent->child edge. 509 * 510 * @param {CensusTreeNodeCacheValue} parentCachevalue 511 * @param {CensusTreeNode} node 512 * 513 * @returns {CensusTreeNodeCacheValue} 514 * The new or extant child node's corresponding cache value. 515 */ 516 function insertOrMergeNode(parentCacheValue, node) { 517 if (!parentCacheValue.children) { 518 parentCacheValue.children = new CensusTreeNodeCache(); 519 } 520 521 let val = CensusTreeNodeCache.lookupNode(parentCacheValue.children, node); 522 523 if (val) { 524 // When inverting, it is possible that multiple leaves in the census report 525 // get merged into a single CensusTreeNode node. When this occurs, switch 526 // from a single index to a set of indices. 527 if ( 528 val.node.reportLeafIndex !== undefined && 529 val.node.reportLeafIndex !== node.reportLeafIndex 530 ) { 531 if (typeof val.node.reportLeafIndex === "number") { 532 const oldIndex = val.node.reportLeafIndex; 533 val.node.reportLeafIndex = new Set(); 534 val.node.reportLeafIndex.add(oldIndex); 535 val.node.reportLeafIndex.add(node.reportLeafIndex); 536 } else { 537 val.node.reportLeafIndex.add(node.reportLeafIndex); 538 } 539 } 540 541 val.node.count += node.count; 542 val.node.bytes += node.bytes; 543 } else { 544 val = new CensusTreeNodeCacheValue(); 545 546 val.node = new CensusTreeNode(node.name); 547 val.node.reportLeafIndex = node.reportLeafIndex; 548 val.node.count = node.count; 549 val.node.totalCount = node.totalCount; 550 val.node.bytes = node.bytes; 551 val.node.totalBytes = node.totalBytes; 552 553 addChild(parentCacheValue.node, val.node); 554 CensusTreeNodeCache.insertNode(parentCacheValue.children, val); 555 } 556 557 return val; 558 } 559 560 /** 561 * Given an un-inverted CensusTreeNode tree, return the corresponding inverted 562 * CensusTreeNode tree. The input tree is not modified. The resulting inverted 563 * tree is sorted by self bytes rather than by total bytes. 564 * 565 * @param {CensusTreeNode} tree 566 * The un-inverted tree. 567 * 568 * @returns {CensusTreeNode} 569 * The corresponding inverted tree. 570 */ 571 function invert(tree) { 572 const inverted = new CensusTreeNodeCacheValue(); 573 inverted.node = new CensusTreeNode(null); 574 575 // Do a depth-first search of the un-inverted tree. As we reach each leaf, 576 // take the path from the old root to the leaf, reverse that path, and add it 577 // to the new, inverted tree's root. 578 579 const path = []; 580 (function addInvertedPaths(node) { 581 path.push(node); 582 583 if (node.children) { 584 for (let i = 0, length = node.children.length; i < length; i++) { 585 addInvertedPaths(node.children[i]); 586 } 587 } else { 588 // We found a leaf node, add the reverse path to the inverted tree. 589 let currentCacheValue = inverted; 590 for (let i = path.length - 1; i >= 0; i--) { 591 currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]); 592 } 593 } 594 595 path.pop(); 596 })(tree); 597 598 // Ensure that the root node always has the totals. 599 inverted.node.totalBytes = tree.totalBytes; 600 inverted.node.totalCount = tree.totalCount; 601 602 return inverted.node; 603 } 604 605 /** 606 * Given a CensusTreeNode tree and predicate function, create the tree 607 * containing only the nodes in any path `(node_0, node_1, ..., node_n-1)` in 608 * the given tree where `predicate(node_j)` is true for `0 <= j < n`, `node_0` 609 * is the given tree's root, and `node_n-1` is a leaf in the given tree. The 610 * given tree is left unmodified. 611 * 612 * @param {CensusTreeNode} tree 613 * @param {Function} predicate 614 * 615 * @returns {CensusTreeNode} 616 */ 617 function filter(tree, predicate) { 618 const filtered = new CensusTreeNodeCacheValue(); 619 filtered.node = new CensusTreeNode(null); 620 621 // Do a DFS over the given tree. If the predicate returns true for any node, 622 // add that node and its whole subtree to the filtered tree. 623 624 const path = []; 625 let match = false; 626 627 function addMatchingNodes(node) { 628 path.push(node); 629 630 const oldMatch = match; 631 if (!match && predicate(node)) { 632 match = true; 633 } 634 635 if (node.children) { 636 for (let i = 0, length = node.children.length; i < length; i++) { 637 addMatchingNodes(node.children[i]); 638 } 639 } else if (match) { 640 // We found a matching leaf node, add it to the filtered tree. 641 let currentCacheValue = filtered; 642 for (let i = 0, length = path.length; i < length; i++) { 643 currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]); 644 } 645 } 646 647 match = oldMatch; 648 path.pop(); 649 } 650 651 if (tree.children) { 652 for (let i = 0, length = tree.children.length; i < length; i++) { 653 addMatchingNodes(tree.children[i]); 654 } 655 } 656 657 filtered.node.count = tree.count; 658 filtered.node.totalCount = tree.totalCount; 659 filtered.node.bytes = tree.bytes; 660 filtered.node.totalBytes = tree.totalBytes; 661 662 return filtered.node; 663 } 664 665 /** 666 * Given a filter string, return a predicate function that takes a node and 667 * returns true iff the node matches the filter. 668 * 669 * @param {string} filterString 670 * @returns {Function} 671 */ 672 function makeFilterPredicate(filterString) { 673 return function (node) { 674 if (!node.name) { 675 return false; 676 } 677 678 if (isSavedFrame(node.name)) { 679 return ( 680 node.name.source.includes(filterString) || 681 (node.name.functionDisplayName || "").includes(filterString) || 682 (node.name.asyncCause || "").includes(filterString) 683 ); 684 } 685 686 return String(node.name).includes(filterString); 687 }; 688 } 689 690 /** 691 * Takes a report from a census (`dbg.memory.takeCensus()`) and the breakdown 692 * used to generate the census and returns a structure used to render 693 * a tree to display the data. 694 * 695 * Returns a recursive "CensusTreeNode" object, looking like: 696 * 697 * CensusTreeNode = { 698 * // `children` if it exists, is sorted by `bytes`, if they are leaf nodes. 699 * children: ?[<CensusTreeNode...>], 700 * name: <?String> 701 * count: <?Number> 702 * bytes: <?Number> 703 * id: <?Number> 704 * parent: <?Number> 705 * } 706 * 707 * @param {object} breakdown 708 * The breakdown used to generate the census report. 709 * 710 * @param {object} report 711 * The census report generated with the specified breakdown. 712 * 713 * @param {object} options 714 * Configuration options. 715 * - invert: Whether to invert the resulting tree or not. Defaults to 716 * false, ie uninverted. 717 * 718 * @returns {CensusTreeNode} 719 */ 720 exports.censusReportToCensusTreeNode = function ( 721 breakdown, 722 report, 723 options = { 724 invert: false, 725 filter: null, 726 } 727 ) { 728 // Reset the counter so that turning the same census report into a 729 // CensusTreeNode tree repeatedly is idempotent. 730 censusTreeNodeIdCounter = 0; 731 732 const visitor = new CensusTreeNodeVisitor(); 733 walk(breakdown, report, visitor); 734 let result = visitor.root(); 735 736 if (options.invert) { 737 result = invert(result); 738 } 739 740 if (typeof options.filter === "string") { 741 result = filter(result, makeFilterPredicate(options.filter)); 742 } 743 744 // If the report is a delta report that was generated by diffing two other 745 // reports, make sure to use the basis totals rather than the totals of the 746 // difference. 747 if (typeof report[basisTotalBytes] === "number") { 748 result.totalBytes = report[basisTotalBytes]; 749 result.totalCount = report[basisTotalCount]; 750 } 751 752 // Inverting and filtering could have messed up the sort order, so do a 753 // depth-first search of the tree and ensure that siblings are sorted. 754 const comparator = options.invert ? compareBySelf : compareByTotal; 755 (function ensureSorted(node) { 756 if (node.children) { 757 node.children.sort(comparator); 758 for (let i = 0, length = node.children.length; i < length; i++) { 759 ensureSorted(node.children[i]); 760 } 761 } 762 })(result); 763 764 return result; 765 };