From 91c80bb79864b4b27fa15faaf428a4f6821fa912 Mon Sep 17 00:00:00 2001 From: Vyacheslav Egorov Date: Mon, 22 Jun 2020 21:38:16 +0000 Subject: [PATCH] [vm/tool] Support reading V8 profiles in snapshot_analysis tool. We reconstruct ProgramInfo tree structure from the snapshot by applying a simple graph algorithm, which takes parts of the snapshot with clear ownership (e.g. library, class, function objects) and then attempts to attribute the rest of the snapshot to these clearly owned objects based on reachability. Issue https://github.com/dart-lang/sdk/issues/41249 Cq-Include-Trybots: luci.dart.try:pkg-linux-debug-try,pkg-linux-release-try,pkg-win-release-try,pkg-mac-release-try Change-Id: I17c0f8323ee9092a2214b18bd948ff51fa2ccc49 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/151384 Commit-Queue: Vyacheslav Egorov Reviewed-by: Alexander Markov --- pkg/vm/lib/snapshot/commands/compare.dart | 89 +-- pkg/vm/lib/snapshot/program_info.dart | 6 + pkg/vm/lib/snapshot/utils.dart | 10 +- pkg/vm/lib/snapshot/v8_profile.dart | 539 ++++++++++++++++++ pkg/vm/lib/v8_snapshot_profile.dart | 299 ---------- .../test/snapshot/instruction_sizes_test.dart | 318 +++++++++-- .../dart/v8_snapshot_profile_writer_test.dart | 52 +- .../v8_snapshot_profile_writer_test.dart | 52 +- 8 files changed, 947 insertions(+), 418 deletions(-) create mode 100644 pkg/vm/lib/snapshot/v8_profile.dart delete mode 100644 pkg/vm/lib/v8_snapshot_profile.dart 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";