diff --git a/pkg/vm/lib/snapshot/commands/compare.dart b/pkg/vm/lib/snapshot/commands/compare.dart index 9e1598d2bd6..7f43afd844b 100644 --- a/pkg/vm/lib/snapshot/commands/compare.dart +++ b/pkg/vm/lib/snapshot/commands/compare.dart @@ -24,8 +24,10 @@ class CompareCommand extends Command { Compare two instruction size outputs and report which symbols changed in size. This tool compares two JSON size reports produced by ---print-instructions-sizes-to and reports which symbols -changed in size. +--print-instructions-sizes-to or --write-v8-snapshot-profile-to +and reports which symbols changed in size. + +Both reports should be produced by the same flag! Use --narrow flag to limit column widths.'''; @@ -97,43 +99,48 @@ precisely based on their source position (which is included in their name). } return file; } -} - -void printComparison(File oldJson, File newJson, - {int maxWidth: 0, - bool collapseAnonymousClosures = false, - HistogramType granularity = HistogramType.bySymbol}) async { - final oldSizes = await loadProgramInfo(oldJson, - collapseAnonymousClosures: collapseAnonymousClosures); - final newSizes = await loadProgramInfo(newJson, - collapseAnonymousClosures: collapseAnonymousClosures); - final diff = computeDiff(oldSizes, newSizes); - - // Compute total sizes. - final totalOld = oldSizes.totalSize; - final totalNew = newSizes.totalSize; - final totalDiff = diff.totalSize; - - // Compute histogram. - final histogram = SizesHistogram.from(diff, granularity); - - // Now produce the report table. - const numLargerSymbolsToReport = 30; - const numSmallerSymbolsToReport = 10; - printHistogram(histogram, - sizeHeader: 'Diff (Bytes)', - prefix: histogram.bySize - .where((k) => histogram.buckets[k] > 0) - .take(numLargerSymbolsToReport), - suffix: histogram.bySize.reversed - .where((k) => histogram.buckets[k] < 0) - .take(numSmallerSymbolsToReport) - .toList() - .reversed, - maxWidth: maxWidth); - - print('Comparing ${oldJson.path} (old) to ${newJson.path} (new)'); - print('Old : ${totalOld} bytes.'); - print('New : ${totalNew} bytes.'); - print('Change: ${totalDiff > 0 ? '+' : ''}${totalDiff} bytes.'); + + void printComparison(File oldJson, File newJson, + {int maxWidth: 0, + bool collapseAnonymousClosures = false, + HistogramType granularity = HistogramType.bySymbol}) async { + final oldSizes = await loadProgramInfo(oldJson, + collapseAnonymousClosures: collapseAnonymousClosures); + final newSizes = await loadProgramInfo(newJson, + collapseAnonymousClosures: collapseAnonymousClosures); + + if ((oldSizes.snapshotInfo == null) != (newSizes.snapshotInfo == null)) { + usageException('Input files must be produced by the same flag.'); + } + + final diff = computeDiff(oldSizes, newSizes); + + // Compute total sizes. + final totalOld = oldSizes.totalSize; + final totalNew = newSizes.totalSize; + final totalDiff = diff.totalSize; + + // Compute histogram. + final histogram = SizesHistogram.from(diff, granularity); + + // Now produce the report table. + const numLargerSymbolsToReport = 30; + const numSmallerSymbolsToReport = 10; + printHistogram(histogram, + sizeHeader: 'Diff (Bytes)', + prefix: histogram.bySize + .where((k) => histogram.buckets[k] > 0) + .take(numLargerSymbolsToReport), + suffix: histogram.bySize.reversed + .where((k) => histogram.buckets[k] < 0) + .take(numSmallerSymbolsToReport) + .toList() + .reversed, + maxWidth: maxWidth); + + print('Comparing ${oldJson.path} (old) to ${newJson.path} (new)'); + print('Old : ${totalOld} bytes.'); + print('New : ${totalNew} bytes.'); + print('Change: ${totalDiff > 0 ? '+' : ''}${totalDiff} bytes.'); + } } diff --git a/pkg/vm/lib/snapshot/program_info.dart b/pkg/vm/lib/snapshot/program_info.dart index 09f233cf196..78f5fecb01c 100644 --- a/pkg/vm/lib/snapshot/program_info.dart +++ b/pkg/vm/lib/snapshot/program_info.dart @@ -7,6 +7,8 @@ library vm.snapshot.program_info; import 'package:meta/meta.dart'; +import 'package:vm/snapshot/v8_profile.dart'; + /// Represents information about compiled program. class ProgramInfo { static const int rootId = 0; @@ -18,6 +20,10 @@ class ProgramInfo { final ProgramInfoNode unknown; int _nextId = 3; + /// V8 snapshot profile if this [ProgramInfo] object was created from an + /// output of `--write-v8-snapshot-profile-to=...` flag. + SnapshotInfo snapshotInfo; + ProgramInfo._(this.root, this.stubs, this.unknown); factory ProgramInfo() { diff --git a/pkg/vm/lib/snapshot/utils.dart b/pkg/vm/lib/snapshot/utils.dart index e57a188a9c6..08caef8a2bb 100644 --- a/pkg/vm/lib/snapshot/utils.dart +++ b/pkg/vm/lib/snapshot/utils.dart @@ -9,6 +9,7 @@ import 'dart:convert'; import 'package:vm/snapshot/ascii_table.dart'; import 'package:vm/snapshot/program_info.dart'; import 'package:vm/snapshot/instruction_sizes.dart' as instruction_sizes; +import 'package:vm/snapshot/v8_profile.dart' as v8_profile; Future loadJson(File input) async { return await input @@ -21,8 +22,13 @@ Future loadJson(File input) async { Future loadProgramInfo(File input, {bool collapseAnonymousClosures = false}) async { final json = await loadJson(input); - return instruction_sizes.loadProgramInfo(json, - collapseAnonymousClosures: collapseAnonymousClosures); + if (v8_profile.Snapshot.isV8HeapSnapshot(json)) { + return v8_profile.toProgramInfo(v8_profile.Snapshot.fromJson(json), + collapseAnonymousClosures: collapseAnonymousClosures); + } else { + return instruction_sizes.loadProgramInfo(json, + collapseAnonymousClosures: collapseAnonymousClosures); + } } void printHistogram(SizesHistogram histogram, diff --git a/pkg/vm/lib/snapshot/v8_profile.dart b/pkg/vm/lib/snapshot/v8_profile.dart new file mode 100644 index 00000000000..96af1e1c592 --- /dev/null +++ b/pkg/vm/lib/snapshot/v8_profile.dart @@ -0,0 +1,539 @@ +// 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. + +/// This library contains utilities for reading and analyzing snapshot profiles +/// produced by `--write-v8-snapshot-profile-to` VM flag. +library vm.snapshot.v8_profile; + +import 'package:meta/meta.dart'; +import 'package:vm/snapshot/name.dart'; + +import 'package:vm/snapshot/program_info.dart'; + +/// This class represents snapshot graph. +/// +/// Note that we do not eagerly deserialize the graph, instead we provide helper +/// methods and wrapper objects to work with serialized representation. +class Snapshot { + final Meta meta; + final int nodeCount; + final int edgeCount; + + /// Serialized flat representation of nodes in the graph. Each node occupies + /// [meta.nodeFieldCount] consecutive elements of the list. + final List _nodes; + + /// Serialized flat representation of edges between nodes. Each edge occupies + /// [meta.edgeFieldCount] consecutive elements of the list. All outgoing edges + /// for a node are serialized consecutively, number of outgoing edges is given + /// by the value at index [meta.nodeEdgeCountIndex] inside the node. + final List _edges; + + /// Auxiliary array which gives starting index of edges (in the [_edges] list) + /// for the given node index. + final List _edgesStartIndexForNode; + + final List strings; + + Snapshot._(this.meta, this.nodeCount, this.edgeCount, this._nodes, + this._edges, this.strings, this._edgesStartIndexForNode); + + /// Return node with the given index. + Node nodeAt(int index) { + assert(index >= 0, 'Node index should be positive: $index'); + return Node._(snapshot: this, index: index); + } + + /// Return all nodes in the snapshot. + Iterable get nodes => Iterable.generate(nodeCount, nodeAt); + + /// Returns true if the given JSON object is likely to be a serialized + /// snapshot using V8 heap snapshot format. + static bool isV8HeapSnapshot(Object m) => + m is Map && m.containsKey('snapshot'); + + /// Construct [Snapshot] object from the given JSON object. + factory Snapshot.fromJson(Map m) { + // Extract meta information first. + final meta = Meta._fromJson(m['snapshot']['meta']); + + final nodes = m['nodes']; + + // Build an array of starting indexes of edges for each node. + final edgesStartIndexForNode = [0]; + int nextStartIndex = 0; + for (var i = meta.nodeEdgeCountIndex; + i < nodes.length; + i += meta.nodeFieldCount) { + nextStartIndex += nodes[i]; + edgesStartIndexForNode.add(nextStartIndex); + } + + return Snapshot._( + meta, + m['snapshot']['node_count'], + m['snapshot']['edge_count'], + m['nodes'], + m['edges'], + m['strings'], + edgesStartIndexForNode); + } +} + +/// Meta-information about the serialized snapshot. +/// +/// Describes the structure of serialized nodes and edges by giving indexes of +/// the various fields. +class Meta { + final int nodeTypeIndex; + final int nodeNameIndex; + final int nodeIdIndex; + final int nodeSelfSizeIndex; + final int nodeEdgeCountIndex; + final int nodeFieldCount; + + final int edgeTypeIndex; + final int edgeNameOrIndexIndex; + final int edgeToNodeIndex; + final int edgeFieldCount; + + final List nodeTypes; + final List edgeTypes; + + Meta._( + {this.nodeTypeIndex, + this.nodeNameIndex, + this.nodeIdIndex, + this.nodeSelfSizeIndex, + this.nodeEdgeCountIndex, + this.nodeFieldCount, + this.edgeTypeIndex, + this.edgeNameOrIndexIndex, + this.edgeToNodeIndex, + this.edgeFieldCount, + this.nodeTypes, + this.edgeTypes}); + + factory Meta._fromJson(Map m) { + final nodeFields = m['node_fields']; + final nodeTypes = m['node_types'].first.cast(); + final edgeFields = m['edge_fields']; + final edgeTypes = m['edge_types'].first.cast(); + return Meta._( + nodeTypeIndex: nodeFields.indexOf('type'), + nodeNameIndex: nodeFields.indexOf('name'), + nodeIdIndex: nodeFields.indexOf('id'), + nodeSelfSizeIndex: nodeFields.indexOf('self_size'), + nodeEdgeCountIndex: nodeFields.indexOf('edge_count'), + nodeFieldCount: nodeFields.length, + edgeTypeIndex: edgeFields.indexOf('type'), + edgeNameOrIndexIndex: edgeFields.indexOf('name_or_index'), + edgeToNodeIndex: edgeFields.indexOf('to_node'), + edgeFieldCount: edgeFields.length, + nodeTypes: nodeTypes, + edgeTypes: edgeTypes); + } +} + +/// Edge from [Node] to [Node] in the [Snapshot] graph. +class Edge { + final Snapshot snapshot; + + /// Index of this [Edge] within the [snapshot]. + final int index; + + Edge._({this.snapshot, this.index}); + + String get type => snapshot + .meta.edgeTypes[snapshot._edges[_offset + snapshot.meta.edgeTypeIndex]]; + + Node get target { + return Node._( + snapshot: snapshot, + index: snapshot._edges[_offset + snapshot.meta.edgeToNodeIndex] ~/ + snapshot.meta.nodeFieldCount); + } + + String get name { + final nameOrIndex = + snapshot._edges[_offset + snapshot.meta.edgeNameOrIndexIndex]; + return type == 'property' ? snapshot.strings[nameOrIndex] : '@$nameOrIndex'; + } + + @override + String toString() { + final nameOrIndex = + snapshot._edges[_offset + snapshot.meta.edgeNameOrIndexIndex]; + return { + 'type': type, + 'nameOrIndex': + type == 'property' ? snapshot.strings[nameOrIndex] : nameOrIndex, + 'toNode': target.index, + }.toString(); + } + + /// Offset into [Snapshot._edges] list at which this edge begins. + int get _offset => index * snapshot.meta.edgeFieldCount; +} + +/// Node in the [Snapshot] graph. +class Node { + final Snapshot snapshot; + + /// Index of this [Node] within the [snapshot]. + final int index; + + Node._({this.snapshot, this.index}); + + int get edgeCount => + snapshot._nodes[_offset + snapshot.meta.nodeEdgeCountIndex]; + + String get type => snapshot + .meta.nodeTypes[snapshot._nodes[_offset + snapshot.meta.nodeTypeIndex]]; + + String get name => + snapshot.strings[snapshot._nodes[_offset + snapshot.meta.nodeNameIndex]]; + + int get selfSize => + snapshot._nodes[_offset + snapshot.meta.nodeSelfSizeIndex]; + + int get id => snapshot._nodes[_offset + snapshot.meta.nodeIdIndex]; + + /// Returns all outgoing edges for this node. + Iterable get edges sync* { + var firstEdgeIndex = snapshot._edgesStartIndexForNode[index]; + for (var i = 0, n = edgeCount; i < n; i++) { + yield Edge._(snapshot: snapshot, index: firstEdgeIndex + i); + } + } + + @override + String toString() { + return { + 'type': type, + 'name': name, + 'id': id, + 'selfSize': selfSize, + 'edges': edges.toList(), + }.toString(); + } + + /// Returns the target of an outgoing edge with the given name (if any). + Node operator [](String edgeName) => this + .edges + .firstWhere((e) => e.name == edgeName, orElse: () => null) + ?.target; + + @override + bool operator ==(Object other) { + return other is Node && other.index == index; + } + + @override + int get hashCode => this.index.hashCode; + + /// Offset into [Snapshot._nodes] list at which this node begins. + int get _offset => index * snapshot.meta.nodeFieldCount; +} + +/// Class representing information about V8 snapshot profile in relation +/// to a [ProgramInfo] structure that was derived from it. +class SnapshotInfo { + final Snapshot snapshot; + + final List _infoNodes; + final Map _ownerOf; + + SnapshotInfo._(this.snapshot, this._infoNodes, this._ownerOf); + + ProgramInfoNode ownerOf(Node node) => + _infoNodes[_ownerOf[node.index] ?? ProgramInfo.unknownId]; +} + +ProgramInfo toProgramInfo(Snapshot snap, + {bool collapseAnonymousClosures = false}) { + return _ProgramInfoBuilder( + collapseAnonymousClosures: collapseAnonymousClosures) + .build(snap); +} + +class _ProgramInfoBuilder { + final bool collapseAnonymousClosures; + + final program = ProgramInfo(); + + final List infoNodes = []; + + /// Mapping between snapshot [Node] index and id of [ProgramInfoNode] which + /// own this node. + final Map ownerOf = {}; + + /// Mapping between snapshot [Node] indices and corresponding + /// [ProgramInfoNode] objects. Note that multiple snapshot nodes might be + /// mapped to a single [ProgramInfoNode] (e.g. when anonymous closures are + /// collapsed). + final Map infoNodeByIndex = {}; + + // Mapping between package names and corresponding [ProgramInfoNode] objects + // representing those packages. + final Map infoNodeForPackage = {}; + + /// Owners of some [Node] are determined by the program structure and not + /// by their reachability through the graph. For example, an owner of a + /// function is a class that contains it, even though the function can + /// also be reachable from another function through object pool. + final Set nodesWithFrozenOwner = {}; + + /// Cache used to optimize common ancestor operation on [ProgramInfoNode] ids. + /// See [findCommonAncestor] method. + final Map commonAncestorCache = {}; + + _ProgramInfoBuilder({this.collapseAnonymousClosures}); + + /// Recover [ProgramInfo] structure from the snapshot profile. + /// + /// This is done via a simple graph traversal: first all nodes representing + /// objects with clear ownership (like libraries, classes, functions) are + /// discovered and corresponding [ProgramInfoNode] objects are created for + /// them. Then the rest of the snapshot is attributed to one of these nodes + /// based on reachability (ignoring reachability from normal snapshot roots): + /// let `R(n)` be a set of [ProgramInfoNode] objects from which a given + /// snapshot node `n` is reachable. Then we define an owner of `n` to be + /// a lowest common ancestor of all nodes in `R(n)`. + /// + /// Nodes which are not reachable from any normal [ProgramInfoNode] are + /// attributed to special `@unknown` [ProgramInfoNode]. + ProgramInfo build(Snapshot snap) { + infoNodes.add(program.root); + infoNodes.add(program.stubs); + infoNodes.add(program.unknown); + + // Create ProgramInfoNode for every snapshot node representing an element + // of the program structure (e.g. a library, a class, a function). + snap.nodes.forEach(getInfoNodeFor); + + // Propagate the ownership information across the edges. + final worklist = ownerOf.keys.toList(); + while (worklist.isNotEmpty) { + final node = snap.nodeAt(worklist.removeLast()); + final sourceOwner = ownerOf[node.index]; + for (var e in node.edges) { + final target = e.target; + if (!nodesWithFrozenOwner.contains(target.index)) { + final targetOwner = ownerOf[target.index]; + final updatedOwner = findCommonAncestor(sourceOwner, targetOwner); + if (updatedOwner != targetOwner) { + ownerOf[target.index] = updatedOwner; + worklist.add(target.index); + } + } + } + } + + // Now attribute sizes from the snapshot to nodes that own them. + for (var node in snap.nodes) { + if (node.selfSize > 0) { + final owner = infoNodes[ownerOf[node.index] ?? ProgramInfo.unknownId]; + owner.size = (owner.size ?? 0) + node.selfSize; + } + } + + program.snapshotInfo = SnapshotInfo._(snap, infoNodes, ownerOf); + + return program; + } + + ProgramInfoNode getInfoNodeFor(Node node) { + var info = infoNodeByIndex[node.index]; + if (info == null) { + info = createInfoNodeFor(node); + if (info != null) { + // Snapshot nodes which represent the program structure can't change + // their owner during iteration - their owner is frozen and is given + // by the program structure. + nodesWithFrozenOwner.add(node.index); + ownerOf[node.index] = info.parent?.id ?? info.id; + + // Handle some nodes specially. + switch (node.type) { + case 'Code': + // Freeze ownership of the Instructions object. + final instructions = node['']; + nodesWithFrozenOwner.add(instructions.index); + ownerOf[instructions.index] = + findCommonAncestor(ownerOf[instructions.index], info.id); + break; + case 'Library': + // Freeze ownership of the Script objects owned by this library. + final scripts = node['owned_scripts_']; + if (scripts != null) { + for (var e in scripts.edges) { + if (e.target.type == 'Script') { + nodesWithFrozenOwner.add(e.target.index); + ownerOf[e.target.index] = + findCommonAncestor(ownerOf[e.target.index], info.id); + } + } + } + break; + } + } + } + return info; + } + + ProgramInfoNode createInfoNodeFor(Node node) { + switch (node.type) { + case 'Code': + var owner = node['owner_']; + if (owner.type != 'Type') { + if (owner.type == 'WeakSerializationReference') { + owner = node[':owner_']; + } + final ownerNode = + owner.type == 'Null' ? program.stubs : getInfoNodeFor(owner); + return makeInfoNode(node.index, + name: node.name, parent: ownerNode, type: NodeType.other); + } + break; + + case 'Function': + if (node.name != '') { + var owner = node['owner_']; + if (node['data_'].type == 'ClosureData') { + owner = node['data_']['parent_function_']; + } + return makeInfoNode(node.index, + name: node.name, + parent: getInfoNodeFor(owner), + type: NodeType.functionNode); + } + break; + + case 'PatchClass': + return getInfoNodeFor(node['patched_class_']); + + case 'Class': + if (node['library_'] != null) { + return makeInfoNode(node.index, + name: node.name, + parent: getInfoNodeFor(node['library_']) ?? program.root, + type: NodeType.classNode); + } + break; + + case 'Library': + // Create fake owner node for the package which contains this library. + final packageName = packageOf(node.name); + return makeInfoNode(node.index, + name: node.name, + parent: packageName != node.name + ? packageOwner(packageName) + : program.root, + type: NodeType.libraryNode); + + case 'Field': + return makeInfoNode(node.index, + name: node.name, + parent: getInfoNodeFor(node['owner_']), + type: NodeType.other); + } + return null; + } + + ProgramInfoNode makeInfoNode(int index, + {@required ProgramInfoNode parent, + @required String name, + @required NodeType type}) { + assert(parent != null, + 'Trying to create node of type ${type} with ${name} and no parent.'); + assert(name != null); + + name = Name(name).scrubbed; + if (collapseAnonymousClosures) { + name = Name.collapse(name); + } + + final node = program.makeNode(name: name, parent: parent, type: type); + if (node.id == infoNodes.length) { + infoNodes.add(node); + } + if (index != null) { + assert(!infoNodeByIndex.containsKey(index)); + infoNodeByIndex[index] = node; + } + return node; + } + + ProgramInfoNode packageOwner(String packageName) => + infoNodeForPackage.putIfAbsent( + packageName, + () => makeInfoNode(null, + name: packageName, + type: NodeType.packageNode, + parent: program.root)); + + /// Create a single key from two node ids. + /// Note that this operation is commutative, because common ancestor of A and + /// B is the same as common ancestor of B and A. + static int ancestorCacheKey(int a, int b) { + if (a > b) { + return b << 32 | a; + } else { + return a << 32 | b; + } + } + + /// Returns id of a common ancestor between [ProgramInfoNode] with [idA] and + /// [idB]. + int findCommonAncestor(int idA, int idB) { + if (idA == null) { + return idB; + } + if (idB == null) { + return idA; + } + if (idA == idB) { + return idA; + } + + // If either are shared - then result is shared. + if (idA == ProgramInfo.rootId || idB == ProgramInfo.rootId) { + return ProgramInfo.rootId; + } + + final infoA = infoNodes[idA]; + final infoB = infoNodes[idB]; + + final key = ancestorCacheKey(idA, idB); + var ancestor = commonAncestorCache[key]; + if (ancestor == null) { + commonAncestorCache[key] = + ancestor = findCommonAncestorImpl(infoA, infoB).id; + } + return ancestor; + } + + static List pathToRoot(ProgramInfoNode node) { + final path = []; + while (node != null) { + path.add(node); + node = node.parent; + } + return path; + } + + static ProgramInfoNode findCommonAncestorImpl( + ProgramInfoNode a, ProgramInfoNode b) { + final pathA = pathToRoot(a); + final pathB = pathToRoot(b); + var i = pathA.length - 1, j = pathB.length - 1; + while (i > 0 && j > 0 && (pathA[i - 1] == pathB[j - 1])) { + i--; + j--; + } + assert(pathA[i] == pathB[j]); + return pathA[i]; + } +} diff --git a/pkg/vm/lib/v8_snapshot_profile.dart b/pkg/vm/lib/v8_snapshot_profile.dart deleted file mode 100644 index 847836dab6e..00000000000 --- a/pkg/vm/lib/v8_snapshot_profile.dart +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright (c) 2018, 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. - -import "package:expect/expect.dart"; -import "package:dart2js_info/src/graph.dart"; - -class _NodeInfo { - int type; - int name; - int id; - int selfSize; - int edgeCount; - _NodeInfo( - this.type, - this.name, - this.id, - this.selfSize, - this.edgeCount, - ); -} - -const List _kRequiredNodeFields = [ - "type", - "name", - "id", - "self_size", - "edge_count", -]; - -class _EdgeInfo { - int type; - int nameOrIndex; - int nodeOffset; - _EdgeInfo( - this.type, - this.nameOrIndex, - this.nodeOffset, - ); -} - -const List _kRequiredEdgeFields = [ - "type", - "name_or_index", - "to_node", -]; - -class NodeInfo { - final String type; - final String name; - final int id; - final int selfSize; - NodeInfo( - this.type, - this.name, - this.id, - this.selfSize, - ); -} - -class EdgeInfo { - final int target; - final String type; - - // Either a string for property names or an int for array/context elements. - final dynamic nameOrIndex; - - EdgeInfo(this.target, this.type, this.nameOrIndex); -} - -class V8SnapshotProfile extends Graph { - // Indexed by node offset. - final Map _nodes = {}; - - // Indexed by start node offset. - final Map> _toEdges = {}; - final Map> _fromEdges = {}; - - List _nodeFields = []; - List _edgeFields = []; - - List _nodeTypes = []; - List _edgeTypes = []; - - List _strings = []; - - // Only used to ensure IDs are unique. - Set _ids = Set(); - - V8SnapshotProfile.fromJson(Map top) { - final Map snapshot = top["snapshot"]; - _parseMetadata(snapshot["meta"]); - - _parseStrings(top["strings"]); - Expect.equals(snapshot["node_count"], _parseNodes(top["nodes"])); - Expect.equals(snapshot["edge_count"], _parseEdges(top["edges"])); - - _verifyRoot(); - - _calculateFromEdges(); - } - - void _verifyRoot() { - // HeapSnapshotWorker.HeapSnapshot.calculateDistances (from HeapSnapshot.js) - // assumes that the root does not have more than one edge to any other node - // (most likely an oversight). - final Set roots = {}; - for (final edge in _toEdges[root]) { - final int to = edge.nodeOffset; - Expect.isTrue(!roots.contains(to), "multiple root edges to node ${to}"); - roots.add(to); - } - - // Check that all nodes are reachable from the root (offset 0). - final Set enqueued = {root}; - final dfs = [root]; - while (!dfs.isEmpty) { - final next = dfs.removeLast(); - for (final edge in _toEdges[next]) { - if (!enqueued.contains(edge.nodeOffset)) { - enqueued.add(edge.nodeOffset); - dfs.add(edge.nodeOffset); - } - } - } - Expect.equals(enqueued.length, nodeCount); - } - - void _parseMetadata(Map meta) { - final List nodeFields = meta["node_fields"]; - nodeFields.forEach(_nodeFields.add); - for (final field in _kRequiredNodeFields) { - Expect.isTrue(nodeFields.contains(field), "missing node field ${field}"); - } - - final List edgeFields = meta["edge_fields"]; - edgeFields.forEach(_edgeFields.add); - for (final field in _kRequiredEdgeFields) { - Expect.isTrue(edgeFields.contains(field), "missing edge field ${field}"); - } - - // First entry of "node_types" is an array with the actual node types. IDK - // what the other entries are for. - List nodeTypes = meta["node_types"]; - nodeTypes = nodeTypes[0]; - nodeTypes.forEach(_nodeTypes.add); - - // Same for edges. - List edgeTypes = meta["edge_types"]; - edgeTypes = edgeTypes[0]; - edgeTypes.forEach(_edgeTypes.add); - } - - int _parseNodes(List nodes) { - final int typeIndex = _nodeFields.indexOf("type"); - final int nameIndex = _nodeFields.indexOf("name"); - final int idIndex = _nodeFields.indexOf("id"); - final int selfSizeIndex = _nodeFields.indexOf("self_size"); - final int edgeCountIndex = _nodeFields.indexOf("edge_count"); - - int offset = 0; - for (; offset < nodes.length; offset += _nodeFields.length) { - final int type = nodes[offset + typeIndex]; - Expect.isTrue(0 <= type && type < _nodeTypes.length, - "node type ${type} outside range [0, ${_nodeTypes.length})"); - - final int name = nodes[offset + nameIndex]; - Expect.isTrue(0 <= name && name < _strings.length, - "node name ${name} outside range [0, ${_strings.length})"); - - final int id = nodes[offset + idIndex]; - Expect.isTrue(id >= 0, "negative node ID ${id}"); - Expect.isFalse(_ids.contains(id), "node ID ${id} already added"); - _ids.add(id); - - final int selfSize = nodes[offset + selfSizeIndex]; - Expect.isTrue(selfSize >= 0, "negative node selfSize ${selfSize}"); - - final int edgeCount = nodes[offset + edgeCountIndex]; - Expect.isTrue(edgeCount >= 0, "negative node edgeCount ${edgeCount}"); - - _nodes[offset] = _NodeInfo(type, name, id, selfSize, edgeCount); - } - - Expect.equals(offset, nodes.length); - return offset ~/ _nodeFields.length; - } - - int _parseEdges(List edges) { - final int typeIndex = _edgeFields.indexOf("type"); - final int nameOrIndexIndex = _edgeFields.indexOf("name_or_index"); - final int toNodeIndex = _edgeFields.indexOf("to_node"); - - int edgeOffset = 0; - for (int nodeOffset = 0; - nodeOffset < _nodes.length * _nodeFields.length; - nodeOffset += _nodeFields.length) { - final int edgeCount = _nodes[nodeOffset].edgeCount; - final List<_EdgeInfo> nodeEdges = List<_EdgeInfo>(edgeCount); - for (int i = 0; i < edgeCount; ++i, edgeOffset += _edgeFields.length) { - final int type = edges[edgeOffset + typeIndex]; - Expect.isTrue(0 <= type && type < _edgeTypes.length, - "edge type ${type} outside range [0, ${_edgeTypes.length}"); - - final int nameOrIndex = edges[edgeOffset + nameOrIndexIndex]; - if (_edgeTypes[type] == "property") { - Expect.isTrue(0 <= nameOrIndex && nameOrIndex < _strings.length, - "edge name ${nameOrIndex} outside range [0, ${_strings.length}"); - } else if (_edgeTypes[type] == "element" || - _edgeTypes[type] == "context") { - Expect.isTrue(nameOrIndex >= 0, "negative edge index ${nameOrIndex}"); - } - - final int toNode = edges[edgeOffset + toNodeIndex]; - checkNode(toNode); - nodeEdges[i] = _EdgeInfo(type, nameOrIndex, toNode); - } - _toEdges[nodeOffset] = nodeEdges; - } - - Expect.equals(edgeOffset, edges.length); - return edgeOffset ~/ _edgeFields.length; - } - - void checkNode(int offset) { - Expect.isTrue(offset >= 0, "negative offset ${offset}"); - Expect.isTrue(offset % _nodeFields.length == 0, - "offset ${offset} not a multiple of ${_nodeFields.length}"); - Expect.isTrue( - offset ~/ _nodeFields.length < _nodes.length, - "offset ${offset} divided by ${_nodeFields.length} is greater than or " - "equal to node count ${_nodes.length}"); - } - - void _calculateFromEdges() { - for (final MapEntry> entry in _toEdges.entries) { - final int fromNode = entry.key; - for (final _EdgeInfo edge in entry.value) { - final List<_EdgeInfo> backEdges = - _fromEdges.putIfAbsent(edge.nodeOffset, () => <_EdgeInfo>[]); - backEdges.add(_EdgeInfo(edge.type, edge.nameOrIndex, fromNode)); - } - } - } - - void _parseStrings(List strings) => strings.forEach(_strings.add); - - int get accountedBytes { - int sum = 0; - for (final _NodeInfo info in _nodes.values) { - sum += info.selfSize; - } - return sum; - } - - int get unknownCount { - final int unknownType = _nodeTypes.indexOf("Unknown"); - Expect.isTrue( - unknownType >= 0, 'negative type index for "Unknown": ${unknownType}'); - - int count = 0; - for (final MapEntry entry in _nodes.entries) { - if (entry.value.type == unknownType) { - ++count; - } - } - return count; - } - - bool get isEmpty => _nodes.isEmpty; - int get nodeCount => _nodes.length; - - Iterable get nodes => _nodes.keys; - - Iterable targetsOf(int source) { - return _toEdges[source].map((_EdgeInfo i) => i.nodeOffset); - } - - Iterable sourcesOf(int source) { - return _fromEdges[source].map((_EdgeInfo i) => i.nodeOffset); - } - - int get root => 0; - - NodeInfo operator [](int node) { - _NodeInfo info = _nodes[node]; - final type = info.type != null ? _nodeTypes[info.type] : null; - final name = info.name != null ? _strings[info.name] : null; - return NodeInfo(type, name, info.id, info.selfSize); - } - - Iterable targets(int node) sync* { - for (final _EdgeInfo info in _toEdges[node]) { - final String type = _edgeTypes[info.type]; - yield EdgeInfo(info.nodeOffset, type, - type == "property" ? _strings[info.nameOrIndex] : info.nameOrIndex); - } - } -} diff --git a/pkg/vm/test/snapshot/instruction_sizes_test.dart b/pkg/vm/test/snapshot/instruction_sizes_test.dart index a6ce2ebc219..dfec6516bcd 100644 --- a/pkg/vm/test/snapshot/instruction_sizes_test.dart +++ b/pkg/vm/test/snapshot/instruction_sizes_test.dart @@ -2,6 +2,7 @@ // 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. +import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; @@ -23,20 +24,18 @@ final dart2native = () { return path.canonicalize(dart2native); }(); -void main() async { - if (!Platform.executable.contains('dart-sdk')) { - // If we are not running from the prebuilt SDK then this test does nothing. - return; - } +final testSource = { + 'input.dart': """ +class K { + final value; + const K(this.value); +} - group('instruction-sizes', () { - final testSource = { - 'input.dart': """ @pragma('vm:never-inline') dynamic makeSomeClosures() { return [ - () => true, - () => false, + () => const K(0), + () => const K(1), () => 11, ]; } @@ -44,20 +43,20 @@ dynamic makeSomeClosures() { class A { @pragma('vm:never-inline') dynamic tornOff() { - return true; + return const K(2); } } class B { @pragma('vm:never-inline') dynamic tornOff() { - return false; + return const K(3); } } class C { static dynamic tornOff() async { - return true; + return const K(4); } } @@ -74,17 +73,22 @@ void main(List args) { print(C.tornOff); } """ - }; +}; + +// Almost exactly the same source as above, but with few modifications +// marked with a 'modified' comment. +final testSourceModified = { + 'input.dart': """ +class K { + final value; + const K(this.value); +} - // Almost exactly the same source as above, but with few modifications - // marked with a 'modified' comment. - final testSourceModified = { - 'input.dart': """ @pragma('vm:never-inline') dynamic makeSomeClosures() { return [ - () => true, - () => false, + () => const K(0), + () => const K(1), () => 11, () => {}, // modified ]; @@ -96,20 +100,20 @@ class A { for (var cl in makeSomeClosures()) { // modified print(cl()); // modified } // modified - return true; + return const K(2); } } class B { @pragma('vm:never-inline') dynamic tornOff() { - return false; + return const K(3); } } class C { static dynamic tornOff() async { - return true; + return const K(4); } } @@ -124,34 +128,39 @@ void main(List args) { print(C.tornOff); } """ - }; +}; + +final testSourceModified2 = { + 'input.dart': """ +class K { + final value; + const K(this.value); +} - final testSourceModified2 = { - 'input.dart': """ @pragma('vm:never-inline') dynamic makeSomeClosures() { return [ - () => 0, + () => const K(0), ]; } class A { @pragma('vm:never-inline') dynamic tornOff() { - return true; + return const K(2); } } class B { @pragma('vm:never-inline') dynamic tornOff() { - return false; + return const K(3); } } class C { static dynamic tornOff() async { - return true; + return const K(4); } } @@ -168,8 +177,15 @@ void main(List args) { print(C.tornOff); } """ - }; +}; +void main() async { + if (!Platform.executable.contains('dart-sdk')) { + // If we are not running from the prebuilt SDK then this test does nothing. + return; + } + + group('instruction-sizes', () { test('basic-parsing', () async { await withSymbolSizes('basic-parsing', testSource, (sizesJson) async { final symbols = @@ -243,8 +259,9 @@ void main(List args) { }); }); - test('program-info', () async { - await withSymbolSizes('program-info', testSource, (sizesJson) async { + test('program-info-from-sizes', () async { + await withSymbolSizes('program-info-from-sizes', testSource, + (sizesJson) async { final info = await loadProgramInfo(File(sizesJson)); expect(info.root.children, contains('dart:core')); expect(info.root.children, contains('dart:typed_data')); @@ -341,15 +358,6 @@ void main(List args) { }); }); - // On Windows there is some issue with interpreting entry point URI as a package URI - // it instead gets interpreted as a file URI - which breaks comparison. So we - // simply ignore entry point library (main.dart). - Map diffToJson(ProgramInfo diff) { - final diffJson = diff.toJson(); - diffJson.removeWhere((key, _) => key.startsWith('file:')); - return diffJson; - } - test('diff', () async { await withSymbolSizes('diff-1', testSource, (sizesJson) async { await withSymbolSizes('diff-2', testSourceModified, @@ -373,7 +381,7 @@ void main(List args) { 'makeSomeClosures': { '#type': 'function', '#size': greaterThan(0), // We added code here. - '': { + '': { '#type': 'function', '#size': greaterThan(0), }, @@ -435,9 +443,220 @@ void main(List args) { }); }); }); + + group('v8-profile', () { + test('program-info-from-profile', () async { + await withV8Profile('program-info-from-profile', testSource, + (profileJson) async { + final info = await loadProgramInfo(File(profileJson)); + expect(info.root.children, contains('dart:core')); + expect(info.root.children, contains('dart:typed_data')); + expect(info.root.children, contains('package:input')); + + final inputLib = info.root.children['package:input'] + .children['package:input/input.dart']; + expect(inputLib, isNotNull); + expect(inputLib.children, contains('::')); // Top-level class. + expect(inputLib.children, contains('A')); + expect(inputLib.children, contains('B')); + expect(inputLib.children, contains('C')); + + final topLevel = inputLib.children['::']; + expect(topLevel.children, contains('makeSomeClosures')); + expect( + topLevel.children['makeSomeClosures'].children.values + .where((child) => child.type == NodeType.functionNode) + .length, + equals(3)); + + for (var name in [ + 'tornOff', + 'Allocate A', + '[tear-off-extractor] get:tornOff' + ]) { + expect(inputLib.children['A'].children, contains(name)); + } + expect(inputLib.children['A'].children['tornOff'].children, + contains('[tear-off] tornOff')); + + for (var name in [ + 'tornOff', + 'Allocate B', + '[tear-off-extractor] get:tornOff' + ]) { + expect(inputLib.children['B'].children, contains(name)); + } + expect(inputLib.children['B'].children['tornOff'].children, + contains('[tear-off] tornOff')); + + expect(inputLib.children['C'].children, contains('tornOff')); + for (var name in ['tornOff{body}', '[tear-off] tornOff']) { + expect(inputLib.children['C'].children['tornOff'].children, + contains(name)); + } + }); + }); + + test('histograms', () async { + await withV8Profile('histograms', testSource, (sizesJson) async { + final info = await loadProgramInfo(File(sizesJson)); + final bySymbol = SizesHistogram.from(info, HistogramType.bySymbol); + expect( + bySymbol.buckets, + contains(bySymbol.bucketing.bucketFor( + 'package:input', 'package:input/input.dart', 'A', 'tornOff'))); + expect( + bySymbol.buckets, + contains(bySymbol.bucketing.bucketFor( + 'package:input', 'package:input/input.dart', 'B', 'tornOff'))); + expect( + bySymbol.buckets, + contains(bySymbol.bucketing.bucketFor( + 'package:input', 'package:input/input.dart', 'C', 'tornOff'))); + + final byClass = SizesHistogram.from(info, HistogramType.byClass); + expect( + byClass.buckets, + contains(byClass.bucketing.bucketFor('package:input', + 'package:input/input.dart', 'A', 'does-not-matter'))); + expect( + byClass.buckets, + contains(byClass.bucketing.bucketFor('package:input', + 'package:input/input.dart', 'B', 'does-not-matter'))); + expect( + byClass.buckets, + contains(byClass.bucketing.bucketFor('package:input', + 'package:input/input.dart', 'C', 'does-not-matter'))); + + final byLibrary = SizesHistogram.from(info, HistogramType.byLibrary); + expect( + byLibrary.buckets, + contains(byLibrary.bucketing.bucketFor( + 'package:input', + 'package:input/input.dart', + 'does-not-matter', + 'does-not-matter'))); + + final byPackage = SizesHistogram.from(info, HistogramType.byPackage); + expect( + byPackage.buckets, + contains(byPackage.bucketing.bucketFor( + 'package:input', + 'package:input/does-not-matter.dart', + 'does-not-matter', + 'does-not-matter'))); + }); + }); + + test('diff', () async { + await withV8Profile('diff-1', testSource, (profileJson) async { + await withV8Profile('diff-2', testSourceModified, + (modifiedProfileJson) async { + final info = await loadProgramInfo(File(profileJson)); + final modifiedInfo = await loadProgramInfo(File(modifiedProfileJson)); + final diff = computeDiff(info, modifiedInfo); + + expect( + diffToJson(diff, keepOnlyInputPackage: true), + equals({ + 'package:input': { + '#type': 'package', + 'package:input/input.dart': { + '#type': 'library', + '::': { + '#type': 'class', + 'makeSomeClosures': { + '#type': 'function', + '#size': greaterThan(0), // We added code here. + '': { + '#type': 'function', + '#size': greaterThan(0), + 'makeSomeClosures.': { + '#size': greaterThan(0) + }, + }, + 'makeSomeClosures': {'#size': greaterThan(0)}, + }, + 'main': { + '#type': 'function', + '#size': lessThan(0), // We removed code from main. + 'main': {'#size': lessThan(0)}, + }, + }, + 'A': { + '#type': 'class', + 'tornOff': { + '#type': 'function', + '#size': greaterThan(0), + 'tornOff': {'#size': greaterThan(0)}, + }, + } + } + } + })); + }); + }); + }); + + test('diff-collapsed', () async { + await withV8Profile('diff-collapsed-1', testSource, (profileJson) async { + await withV8Profile('diff-collapsed-2', testSourceModified2, + (modifiedProfileJson) async { + final info = await loadProgramInfo(File(profileJson), + collapseAnonymousClosures: true); + final modifiedInfo = await loadProgramInfo(File(modifiedProfileJson), + collapseAnonymousClosures: true); + final diff = computeDiff(info, modifiedInfo); + + expect( + diffToJson(diff, keepOnlyInputPackage: true), + equals({ + 'package:input': { + '#type': 'package', + 'package:input/input.dart': { + '#type': 'library', + '#size': lessThan(0), + '::': { + '#type': 'class', + 'makeSomeClosures': { + '#type': 'function', + '#size': lessThan(0), + '': { + '#type': 'function', + '#size': lessThan(0), + 'makeSomeClosures.': { + '#size': lessThan(0) + }, + }, + 'makeSomeClosures': {'#size': lessThan(0)}, + }, + }, + 'B': { + // There are some cascading changes to CodeSourceMap + '#type': 'class', + 'tornOff': { + '#type': 'function', + '#size': lessThan(0), + }, + } + } + } + })); + }); + }); + }); + }); } Future withSymbolSizes(String prefix, Map source, + Future Function(String sizesJson) f) => + withFlag(prefix, source, '--print_instructions_sizes_to', f); + +Future withV8Profile(String prefix, Map source, + Future Function(String sizesJson) f) => + withFlag(prefix, source, '--write_v8_snapshot_profile_to', f); + +Future withFlag(String prefix, Map source, String flag, Future Function(String sizesJson) f) { return withTempDir(prefix, (dir) async { final outputBinary = path.join(dir, 'output.exe'); @@ -463,7 +682,7 @@ void main(List args) => input.main(args); '-o', outputBinary, '--packages=$packages', - '--extra-gen-snapshot-options=--print_instructions_sizes_to=$sizesJson', + '--extra-gen-snapshot-options=$flag=$sizesJson', mainDart, ]); @@ -491,3 +710,14 @@ Future withTempDir(String prefix, Future Function(String dir) f) async { tempDir.deleteSync(recursive: true); } } + +// On Windows there is some issue with interpreting entry point URI as a package URI +// it instead gets interpreted as a file URI - which breaks comparison. So we +// simply ignore entry point library (main.dart). +Map diffToJson(ProgramInfo diff, + {bool keepOnlyInputPackage = false}) { + final diffJson = diff.toJson(); + diffJson.removeWhere((key, _) => + keepOnlyInputPackage ? key != 'package:input' : key.startsWith('file:')); + return diffJson; +} diff --git a/runtime/tests/vm/dart/v8_snapshot_profile_writer_test.dart b/runtime/tests/vm/dart/v8_snapshot_profile_writer_test.dart index ea385afb2a5..92ab923aee5 100644 --- a/runtime/tests/vm/dart/v8_snapshot_profile_writer_test.dart +++ b/runtime/tests/vm/dart/v8_snapshot_profile_writer_test.dart @@ -2,12 +2,12 @@ // 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. -import "dart:convert"; -import "dart:io"; +import 'dart:convert'; +import 'dart:io'; -import "package:expect/expect.dart"; +import 'package:expect/expect.dart'; import 'package:path/path.dart' as path; -import "package:vm/v8_snapshot_profile.dart"; +import 'package:vm/snapshot/v8_profile.dart'; import 'use_flag_test_helper.dart'; @@ -77,32 +77,52 @@ test( strippedPath = snapshotPath; } - final V8SnapshotProfile profile = V8SnapshotProfile.fromJson( - JsonDecoder().convert(File(profilePath).readAsStringSync())); + final profile = + Snapshot.fromJson(jsonDecode(File(profilePath).readAsStringSync())); // Verify that there are no "unknown" nodes. These are emitted when we see a // reference to an some object but no other metadata about the object was // recorded. We should at least record the type for every object in the // graph (in some cases the shallow size can legitimately be 0, e.g. for // "base objects"). - for (final int node in profile.nodes) { - Expect.notEquals("Unknown", profile[node].type, - "unknown node at ID ${profile[node].id}"); + for (final node in profile.nodes) { + Expect.notEquals("Unknown", node.type, "unknown node at ID ${node.id}"); } - // Verify that all nodes are reachable from the declared roots. - int unreachableNodes = 0; - Set nodesReachableFromRoots = profile.preOrder(profile.root).toSet(); - for (final int node in profile.nodes) { - Expect.isTrue(nodesReachableFromRoots.contains(node), - "unreachable node at ID ${profile[node].id}"); + // HeapSnapshotWorker.HeapSnapshot.calculateDistances (from HeapSnapshot.js) + // assumes that the root does not have more than one edge to any other node + // (most likely an oversight). + final Set roots = {}; + for (final edge in profile.nodeAt(0).edges) { + Expect.isTrue(roots.add(edge.target.index)); + } + + // Check that all nodes are reachable from the root (index 0). + final Set reachable = {0}; + final dfs = [0]; + while (!dfs.isEmpty) { + final next = dfs.removeLast(); + for (final edge in profile.nodeAt(next).edges) { + final target = edge.target; + if (!reachable.contains(target.index)) { + reachable.add(target.index); + dfs.add(target.index); + } + } + } + + if (reachable.length != profile.nodeCount) { + for (final node in profile.nodes) { + Expect.isTrue(reachable.contains(node.index), + "unreachable node at ID ${node.id}"); + } } // Verify that the actual size of the snapshot is close to the sum of the // shallow sizes of all objects in the profile. They will not be exactly // equal because of global headers and padding. final actual = await File(strippedPath).length(); - final expected = profile.accountedBytes; + final expected = profile.nodes.fold(0, (size, n) => size + n.selfSize); final bareUsed = useBare ? "bare" : "non-bare"; final fileType = useAsm ? "assembly" : "ELF"; diff --git a/runtime/tests/vm/dart_2/v8_snapshot_profile_writer_test.dart b/runtime/tests/vm/dart_2/v8_snapshot_profile_writer_test.dart index de5facfaf44..e41230fc371 100644 --- a/runtime/tests/vm/dart_2/v8_snapshot_profile_writer_test.dart +++ b/runtime/tests/vm/dart_2/v8_snapshot_profile_writer_test.dart @@ -2,12 +2,12 @@ // 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. -import "dart:convert"; -import "dart:io"; +import 'dart:convert'; +import 'dart:io'; -import "package:expect/expect.dart"; +import 'package:expect/expect.dart'; import 'package:path/path.dart' as path; -import "package:vm/v8_snapshot_profile.dart"; +import 'package:vm/snapshot/v8_profile.dart'; import 'use_flag_test_helper.dart'; @@ -77,32 +77,52 @@ test( strippedPath = snapshotPath; } - final V8SnapshotProfile profile = V8SnapshotProfile.fromJson( - JsonDecoder().convert(File(profilePath).readAsStringSync())); + final profile = + Snapshot.fromJson(jsonDecode(File(profilePath).readAsStringSync())); // Verify that there are no "unknown" nodes. These are emitted when we see a // reference to an some object but no other metadata about the object was // recorded. We should at least record the type for every object in the // graph (in some cases the shallow size can legitimately be 0, e.g. for // "base objects"). - for (final int node in profile.nodes) { - Expect.notEquals("Unknown", profile[node].type, - "unknown node at ID ${profile[node].id}"); + for (final node in profile.nodes) { + Expect.notEquals("Unknown", node.type, "unknown node at ID ${node.id}"); } - // Verify that all nodes are reachable from the declared roots. - int unreachableNodes = 0; - Set nodesReachableFromRoots = profile.preOrder(profile.root).toSet(); - for (final int node in profile.nodes) { - Expect.isTrue(nodesReachableFromRoots.contains(node), - "unreachable node at ID ${profile[node].id}"); + // HeapSnapshotWorker.HeapSnapshot.calculateDistances (from HeapSnapshot.js) + // assumes that the root does not have more than one edge to any other node + // (most likely an oversight). + final Set roots = {}; + for (final edge in profile.nodeAt(0).edges) { + Expect.isTrue(roots.add(edge.target.index)); + } + + // Check that all nodes are reachable from the root (index 0). + final Set reachable = {0}; + final dfs = [0]; + while (!dfs.isEmpty) { + final next = dfs.removeLast(); + for (final edge in profile.nodeAt(next).edges) { + final target = edge.target; + if (!reachable.contains(target.index)) { + reachable.add(target.index); + dfs.add(target.index); + } + } + } + + if (reachable.length != profile.nodeCount) { + for (final node in profile.nodes) { + Expect.isTrue(reachable.contains(node.index), + "unreachable node at ID ${node.id}"); + } } // Verify that the actual size of the snapshot is close to the sum of the // shallow sizes of all objects in the profile. They will not be exactly // equal because of global headers and padding. final actual = await File(strippedPath).length(); - final expected = profile.accountedBytes; + final expected = profile.nodes.fold(0, (size, n) => size + n.selfSize); final bareUsed = useBare ? "bare" : "non-bare"; final fileType = useAsm ? "assembly" : "ELF";