[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 <vegorov@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
This commit is contained in:
Vyacheslav Egorov 2020-06-22 21:38:16 +00:00 committed by commit-bot@chromium.org
parent 77db092507
commit 91c80bb798
8 changed files with 947 additions and 418 deletions

View file

@ -24,8 +24,10 @@ class CompareCommand extends Command<void> {
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.');
}
}

View file

@ -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() {

View file

@ -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<Object> loadJson(File input) async {
return await input
@ -21,8 +22,13 @@ Future<Object> loadJson(File input) async {
Future<ProgramInfo> 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,

View file

@ -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<int> _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<Node> 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<String, dynamic> && m.containsKey('snapshot');
/// Construct [Snapshot] object from the given JSON object.
factory Snapshot.fromJson(Map<String, dynamic> 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 = <int>[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<String> nodeTypes;
final List<String> 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<String, dynamic> m) {
final nodeFields = m['node_fields'];
final nodeTypes = m['node_types'].first.cast<String>();
final edgeFields = m['edge_fields'];
final edgeTypes = m['edge_types'].first.cast<String>();
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<Edge> 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<ProgramInfoNode> _infoNodes;
final Map<int, int> _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<ProgramInfoNode> infoNodes = [];
/// Mapping between snapshot [Node] index and id of [ProgramInfoNode] which
/// own this node.
final Map<int, int> 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<int, ProgramInfoNode> infoNodeByIndex = {};
// Mapping between package names and corresponding [ProgramInfoNode] objects
// representing those packages.
final Map<String, ProgramInfoNode> 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<int> nodesWithFrozenOwner = {};
/// Cache used to optimize common ancestor operation on [ProgramInfoNode] ids.
/// See [findCommonAncestor] method.
final Map<int, int> 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['<instructions>'];
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 != '<anonymous signature>') {
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<ProgramInfoNode> pathToRoot(ProgramInfoNode node) {
final path = <ProgramInfoNode>[];
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];
}
}

View file

@ -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<String> _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<String> _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<int> {
// Indexed by node offset.
final Map<int, _NodeInfo> _nodes = {};
// Indexed by start node offset.
final Map<int, List<_EdgeInfo>> _toEdges = {};
final Map<int, List<_EdgeInfo>> _fromEdges = {};
List<String> _nodeFields = [];
List<String> _edgeFields = [];
List<String> _nodeTypes = [];
List<String> _edgeTypes = [];
List<String> _strings = [];
// Only used to ensure IDs are unique.
Set<int> _ids = Set<int>();
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<int> roots = <int>{};
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<int> enqueued = {root};
final dfs = <int>[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<int, List<_EdgeInfo>> 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<int, _NodeInfo> entry in _nodes.entries) {
if (entry.value.type == unknownType) {
++count;
}
}
return count;
}
bool get isEmpty => _nodes.isEmpty;
int get nodeCount => _nodes.length;
Iterable<int> get nodes => _nodes.keys;
Iterable<int> targetsOf(int source) {
return _toEdges[source].map((_EdgeInfo i) => i.nodeOffset);
}
Iterable<int> 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<EdgeInfo> 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);
}
}
}

View file

@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String, dynamic> 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<String> args) {
'makeSomeClosures': {
'#type': 'function',
'#size': greaterThan(0), // We added code here.
'<anonymous closure @118>': {
'<anonymous closure @180>': {
'#type': 'function',
'#size': greaterThan(0),
},
@ -435,9 +443,220 @@ void main(List<String> 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.
'<anonymous closure @180>': {
'#type': 'function',
'#size': greaterThan(0),
'makeSomeClosures.<anonymous closure @180>': {
'#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),
'<anonymous closure>': {
'#type': 'function',
'#size': lessThan(0),
'makeSomeClosures.<anonymous closure>': {
'#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<String, String> source,
Future Function(String sizesJson) f) =>
withFlag(prefix, source, '--print_instructions_sizes_to', f);
Future withV8Profile(String prefix, Map<String, String> source,
Future Function(String sizesJson) f) =>
withFlag(prefix, source, '--write_v8_snapshot_profile_to', f);
Future withFlag(String prefix, Map<String, String> 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<String> 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<String, dynamic> diffToJson(ProgramInfo diff,
{bool keepOnlyInputPackage = false}) {
final diffJson = diff.toJson();
diffJson.removeWhere((key, _) =>
keepOnlyInputPackage ? key != 'package:input' : key.startsWith('file:'));
return diffJson;
}

View file

@ -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<int> 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<int> roots = <int>{};
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<int> reachable = {0};
final dfs = <int>[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";

View file

@ -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<int> 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<int> roots = <int>{};
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<int> reachable = {0};
final dfs = <int>[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";