| // Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file | 
 | // for details. All rights reserved. Use of this source code is governed by a | 
 | // BSD-style license that can be found in the LICENSE file. | 
 |  | 
 | // A way to test heap snapshot loading and analysis outside of Observatory, and | 
 | // to handle snapshots that require more memory to analyze than is available in | 
 | // a web browser. | 
 |  | 
 | import 'dart:async'; | 
 | import 'dart:convert'; | 
 | import 'dart:io'; | 
 | import 'dart:typed_data'; | 
 |  | 
 | import 'package:observatory/object_graph.dart'; | 
 |  | 
 | Future<SnapshotGraph> load(String uri) async { | 
 |   final ws = await WebSocket.connect(uri, | 
 |       compression: CompressionOptions.compressionOff); | 
 |  | 
 |   final getVM = new Completer<String>(); | 
 |   final reader = new SnapshotReader(); | 
 |  | 
 |   reader.onProgress.listen(print); | 
 |  | 
 |   ws.listen((dynamic dynResponse) { | 
 |     if (dynResponse is String) { | 
 |       final response = json.decode(dynResponse); | 
 |       if (response['id'] == 1) { | 
 |         getVM.complete(response['result']['isolates'][0]['id']); | 
 |       } | 
 |     } else if (dynResponse is List<int>) { | 
 |       final response = new Uint8List.fromList(dynResponse); | 
 |       final dataOffset = | 
 |           new ByteData.view(response.buffer).getUint32(0, Endian.little); | 
 |       dynamic metadata = new Uint8List.view(response.buffer, 4, dataOffset - 4); | 
 |       final data = new Uint8List.view( | 
 |           response.buffer, dataOffset, response.length - dataOffset); | 
 |       metadata = utf8.decode(metadata); | 
 |       metadata = json.decode(metadata); | 
 |       var event = metadata['params']['event']; | 
 |       if (event['kind'] == 'HeapSnapshot') { | 
 |         bool last = event['last'] == true; | 
 |         reader.add(data); | 
 |         if (last) { | 
 |           reader.close(); | 
 |           ws.close(); | 
 |         } | 
 |       } | 
 |     } | 
 |   }); | 
 |  | 
 |   ws.add(json.encode({ | 
 |     'jsonrpc': '2.0', | 
 |     'method': 'getVM', | 
 |     'params': {}, | 
 |     'id': 1, | 
 |   })); | 
 |  | 
 |   final String isolateId = await getVM.future; | 
 |  | 
 |   ws.add(json.encode({ | 
 |     'jsonrpc': '2.0', | 
 |     'method': 'streamListen', | 
 |     'params': {'streamId': 'HeapSnapshot'}, | 
 |     'id': 2, | 
 |   })); | 
 |   ws.add(json.encode({ | 
 |     'jsonrpc': '2.0', | 
 |     'method': 'requestHeapSnapshot', | 
 |     'params': {'isolateId': isolateId}, | 
 |     'id': 3, | 
 |   })); | 
 |  | 
 |   return reader.done; | 
 | } | 
 |  | 
 | String makeData(dynamic root) { | 
 |   // 'root' can be arbitrarily deep, so we can't directly represent it as a | 
 |   // JSON tree, which cause a stack overflow here encoding it and in the JS | 
 |   // engine decoding it. Instead we flatten the tree into a list of tuples with | 
 |   // a parent pointer and re-inflate it in JS. | 
 |   final indices = <dynamic, int>{}; | 
 |   final preorder = <dynamic>[]; | 
 |   preorder.add(root); | 
 |  | 
 |   for (var index = 0; index < preorder.length; index++) { | 
 |     final object = preorder[index]; | 
 |     preorder.addAll(object.children); | 
 |   } | 
 |  | 
 |   final flattened = <dynamic>[]; | 
 |   for (var index = 0; index < preorder.length; index++) { | 
 |     final object = preorder[index]; | 
 |     indices[object] = index; | 
 |  | 
 |     flattened.add(object.description); | 
 |     flattened.add(object.klass.name); | 
 |     flattened.add(object.retainedSize); | 
 |     if (index == 0) { | 
 |       flattened.add(null); | 
 |     } else { | 
 |       flattened.add(indices[object.parent] as int); | 
 |     } | 
 |   } | 
 |  | 
 |   return json.encode(flattened); | 
 | } | 
 |  | 
 | var css = ''' | 
 | .treemapTile { | 
 |     position: absolute; | 
 |     box-sizing: border-box; | 
 |     border: solid 1px; | 
 |     font-size: 10; | 
 |     text-align: center; | 
 |     overflow: hidden; | 
 |     white-space: nowrap; | 
 |     cursor: default; | 
 | } | 
 | '''; | 
 |  | 
 | var js = ''' | 
 | function hash(string) { | 
 |   // Jenkin's one_at_a_time. | 
 |   let h = string.length; | 
 |   for (let i = 0; i < string.length; i++) { | 
 |     h += string.charCodeAt(i); | 
 |     h += h << 10; | 
 |     h ^= h >> 6; | 
 |   } | 
 |   h += h << 3; | 
 |   h ^= h >> 11; | 
 |   h += h << 15; | 
 |   return h; | 
 | } | 
 |  | 
 | function color(string) { | 
 |   let hue = hash(string) % 360; | 
 |   return "hsl(" + hue + ",60%,60%)"; | 
 | } | 
 |  | 
 | function prettySize(size) { | 
 |   if (size < 1024) return size + "B"; | 
 |   size /= 1024; | 
 |   if (size < 1024) return size.toFixed(1) + "KiB"; | 
 |   size /= 1024; | 
 |   if (size < 1024) return size.toFixed(1) + "MiB"; | 
 |   size /= 1024; | 
 |   return size.toFixed(1) + "GiB"; | 
 | } | 
 |  | 
 | function prettyPercent(fraction) { | 
 |   return (fraction * 100).toFixed(1); | 
 | } | 
 |  | 
 | function createTreemapTile(v, width, height, depth) { | 
 |   let div = document.createElement("div"); | 
 |   div.className = "treemapTile"; | 
 |   div.style["background-color"] = color(v.type); | 
 |   div.ondblclick = function(event) { | 
 |     event.stopPropagation(); | 
 |     if (depth == 0) { | 
 |       let dom = v.parent; | 
 |       if (dom == undefined) { | 
 |         // Already at root. | 
 |       } else { | 
 |         showDominatorTree(dom);  // Zoom out. | 
 |       } | 
 |     } else { | 
 |       showDominatorTree(v);  // Zoom in. | 
 |     } | 
 |   }; | 
 |  | 
 |   let left = 0; | 
 |   let top = 0; | 
 |  | 
 |   const kPadding = 5; | 
 |   const kBorder = 1; | 
 |   left += kPadding - kBorder; | 
 |   top += kPadding - kBorder; | 
 |   width -= 2 * kPadding; | 
 |   height -= 2 * kPadding; | 
 |  | 
 |   let label = v.name + " [" + prettySize(v.size) + "]"; | 
 |   div.title = label; | 
 |  | 
 |   if (width < 10 || height < 10) { | 
 |     // Too small: don't render label or children. | 
 |     return div; | 
 |   } | 
 |  | 
 |   div.appendChild(document.createTextNode(label)); | 
 |   const kLabelHeight = 9; | 
 |   top += kLabelHeight; | 
 |   height -= kLabelHeight; | 
 |  | 
 |   if (depth > 2) { | 
 |     // Too deep: don't render children. | 
 |     return div; | 
 |   } | 
 |   if (width < 4 || height < 4) { | 
 |     // Too small: don't render children. | 
 |     return div; | 
 |   } | 
 |  | 
 |   let children = new Array(); | 
 |   v.children.forEach(function(c) { | 
 |     // Size 0 children seem to confuse the layout algorithm (accumulating | 
 |     // rounding errors?). | 
 |     if (c.size > 0) { | 
 |       children.push(c); | 
 |     } | 
 |   }); | 
 |   children.sort(function (a, b) { | 
 |     return b.size - a.size; | 
 |   }); | 
 |  | 
 |   const scale = width * height / v.size; | 
 |  | 
 |   // Bruls M., Huizing K., van Wijk J.J. (2000) Squarified Treemaps. In: de | 
 |   // Leeuw W.C., van Liere R. (eds) Data Visualization 2000. Eurographics. | 
 |   // Springer, Vienna. | 
 |   for (let rowStart = 0;  // Index of first child in the next row. | 
 |        rowStart < children.length;) { | 
 |     // Prefer wider rectangles, the better to fit text labels. | 
 |     const GOLDEN_RATIO = 1.61803398875; | 
 |     let verticalSplit = (width / height) > GOLDEN_RATIO; | 
 |  | 
 |     let space; | 
 |     if (verticalSplit) { | 
 |       space = height; | 
 |     } else { | 
 |       space = width; | 
 |     } | 
 |  | 
 |     let rowMin = children[rowStart].size * scale; | 
 |     let rowMax = rowMin; | 
 |     let rowSum = 0; | 
 |     let lastRatio = 0; | 
 |  | 
 |     let rowEnd;  // One after index of last child in the next row. | 
 |     for (rowEnd = rowStart; rowEnd < children.length; rowEnd++) { | 
 |       let size = children[rowEnd].size * scale; | 
 |       if (size < rowMin) rowMin = size; | 
 |       if (size > rowMax) rowMax = size; | 
 |       rowSum += size; | 
 |  | 
 |       let ratio = Math.max((space * space * rowMax) / (rowSum * rowSum), | 
 |                            (rowSum * rowSum) / (space * space * rowMin)); | 
 |       if ((lastRatio != 0) && (ratio > lastRatio)) { | 
 |         // Adding the next child makes the aspect ratios worse: remove it and | 
 |         // add the row. | 
 |         rowSum -= size; | 
 |         break; | 
 |       } | 
 |       lastRatio = ratio; | 
 |     } | 
 |  | 
 |     let rowLeft = left; | 
 |     let rowTop = top; | 
 |     let rowSpace = rowSum / space; | 
 |  | 
 |     for (let i = rowStart; i < rowEnd; i++) { | 
 |       let child = children[i]; | 
 |       let size = child.size * scale; | 
 |  | 
 |       let childWidth; | 
 |       let childHeight; | 
 |       if (verticalSplit) { | 
 |         childWidth = rowSpace; | 
 |         childHeight = size / childWidth; | 
 |       } else { | 
 |         childHeight = rowSpace; | 
 |         childWidth = size / childHeight; | 
 |       } | 
 |  | 
 |       let childDiv = createTreemapTile(child, childWidth, childHeight, depth + 1); | 
 |       childDiv.style.left = rowLeft + "px"; | 
 |       childDiv.style.top = rowTop + "px"; | 
 |       // Oversize the final div by kBorder to make the borders overlap. | 
 |       childDiv.style.width = (childWidth + kBorder) + "px"; | 
 |       childDiv.style.height = (childHeight + kBorder) + "px"; | 
 |       div.appendChild(childDiv); | 
 |  | 
 |       if (verticalSplit) | 
 |         rowTop += childHeight; | 
 |       else | 
 |         rowLeft += childWidth; | 
 |     } | 
 |  | 
 |     if (verticalSplit) { | 
 |       left += rowSpace; | 
 |       width -= rowSpace; | 
 |     } else { | 
 |       top += rowSpace; | 
 |       height -= rowSpace; | 
 |     } | 
 |  | 
 |     rowStart = rowEnd; | 
 |   } | 
 |  | 
 |   return div; | 
 | } | 
 |  | 
 | function setBody(div) { | 
 |   let body = document.body; | 
 |   while (body.firstChild) { | 
 |     body.removeChild(body.firstChild); | 
 |   } | 
 |   body.appendChild(div); | 
 | } | 
 |  | 
 | function showDominatorTree(v) { | 
 |   let header = document.createElement("div"); | 
 |   header.textContent = "Dominator Tree"; | 
 |   header.title = | 
 |     "Double click a box to zoom in.\\n" + | 
 |     "Double click the outermost box to zoom out."; | 
 |   header.className = "headerRow"; | 
 |   header.style["flex-grow"] = 0; | 
 |   header.style["padding"] = "5px"; | 
 |   header.style["border-bottom"] = "solid 1px"; | 
 |  | 
 |   let content = document.createElement("div"); | 
 |   content.style["flex-basis"] = 0; | 
 |   content.style["flex-grow"] = 1; | 
 |  | 
 |   let column = document.createElement("div"); | 
 |   column.style["width"] = "100%"; | 
 |   column.style["height"] = "100%"; | 
 |   column.style["border"] = "solid 2px"; | 
 |   column.style["display"] = "flex"; | 
 |   column.style["flex-direction"] = "column"; | 
 |   column.appendChild(header); | 
 |   column.appendChild(content); | 
 |  | 
 |   setBody(column); | 
 |  | 
 |   // Add the content div to the document first so the browser will calculate | 
 |   // the available width and height. | 
 |   let w = content.offsetWidth; | 
 |   let h = content.offsetHeight; | 
 |  | 
 |   let topTile = createTreemapTile(v, w, h, 0); | 
 |   topTile.style.width = w; | 
 |   topTile.style.height = h; | 
 |   topTile.style.border = "none"; | 
 |   content.appendChild(topTile); | 
 | } | 
 |  | 
 | function inflateData(flattened) { | 
 |   // 'root' can be arbitrarily deep, so we need to use an explicit stack | 
 |   // instead of the call stack. | 
 |   let nodes = new Array(); | 
 |   let i = 0; | 
 |   while (i < flattened.length) { | 
 |     let node = { | 
 |       "name": flattened[i++], | 
 |       "type": flattened[i++], | 
 |       "size": flattened[i++], | 
 |       "children": [], | 
 |       "parent": null | 
 |     }; | 
 |     nodes.push(node); | 
 |  | 
 |     let parentIndex = flattened[i++]; | 
 |     if (parentIndex != null) { | 
 |       let parent = nodes[parentIndex]; | 
 |       parent.children.push(node); | 
 |       node.parent = parent; | 
 |     } | 
 |   } | 
 |  | 
 |   return nodes[0]; | 
 | } | 
 |  | 
 | var root = __DATA__; | 
 | root = inflateData(root); | 
 |  | 
 | showDominatorTree(root); | 
 | '''; | 
 |  | 
 | var html = ''' | 
 | <html> | 
 |   <head> | 
 |     <title>Dart Heap Snapshot</title> | 
 |     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | 
 |     <style>$css</style> | 
 |   </head> | 
 |   <body> | 
 |     <script>$js</script> | 
 |   </body> | 
 | </html> | 
 | '''; | 
 |  | 
 | main(List<String> args) async { | 
 |   if (args.length < 1) { | 
 |     print('Usage: heap_snapshot.dart <vm-service-uri>'); | 
 |     exitCode = 1; | 
 |     return; | 
 |   } | 
 |  | 
 |   var uri = Uri.parse(args[0]); | 
 |   if (uri.isScheme('http')) { | 
 |     uri = uri.replace(scheme: 'ws'); | 
 |   } else if (uri.isScheme('https')) { | 
 |     uri = uri.replace(scheme: 'wss'); | 
 |   } | 
 |   if (!uri.path.endsWith('/ws')) { | 
 |     uri = uri.resolve('ws'); | 
 |   } | 
 |  | 
 |   final snapshot = await load(uri.toString()); | 
 |  | 
 |   final dir = await Directory.systemTemp.createTemp('heap-snapshot'); | 
 |   final path = dir.path + '/merged-dominator.html'; | 
 |   final file = await File(path).create(); | 
 |   final tree = makeData(snapshot.mergedRoot); | 
 |   await file.writeAsString(html.replaceAll('__DATA__', tree)); | 
 |   print('Wrote file://' + path); | 
 | } |