mirror of
https://github.com/dart-lang/sdk
synced 2024-09-04 16:03:44 +00:00
[vm_snapshot_analysis] Migrate to null-safety
Fixes https://github.com/dart-lang/sdk/issues/45683 TEST=pkg-*-try bots Cq-Include-Trybots: luci.dart.try:pkg-linux-release-try,pkg-mac-release-try,pkg-win-release-try Change-Id: Ie10f313da9778d001f9c4fb618997e3b3c781dd0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/195263 Commit-Queue: Vyacheslav Egorov <vegorov@google.com> Reviewed-by: Tess Strickland <sstrickl@google.com>
This commit is contained in:
parent
1bab406d8e
commit
458be23fa7
|
@ -11,7 +11,7 @@
|
|||
"constraint, update this by running tools/generate_package_config.dart."
|
||||
],
|
||||
"configVersion": 2,
|
||||
"generated": "2021-04-13T13:32:11.977579",
|
||||
"generated": "2021-04-14T10:32:11.514470",
|
||||
"generator": "tools/generate_package_config.dart",
|
||||
"packages": [
|
||||
{
|
||||
|
@ -749,7 +749,7 @@
|
|||
"name": "vm_snapshot_analysis",
|
||||
"rootUri": "../pkg/vm_snapshot_analysis",
|
||||
"packageUri": "lib/",
|
||||
"languageVersion": "2.8"
|
||||
"languageVersion": "2.12"
|
||||
},
|
||||
{
|
||||
"name": "wasm",
|
||||
|
|
|
@ -16,7 +16,7 @@ abstract class Row {
|
|||
/// Note: there is a border on the left and right of each column
|
||||
/// plus whitespace around it.
|
||||
static int totalWidth(List<int> widths) =>
|
||||
widths.fold(0, (sum, width) => sum + width + 3) + 1;
|
||||
widths.fold<int>(0, (sum, width) => sum + width + 3) + 1;
|
||||
}
|
||||
|
||||
enum Separator {
|
||||
|
@ -91,7 +91,7 @@ class Text {
|
|||
final String value;
|
||||
final AlignmentDirection direction;
|
||||
|
||||
Text({this.value, this.direction});
|
||||
Text({required this.value, required this.direction});
|
||||
Text.left(String value)
|
||||
: this(value: value, direction: AlignmentDirection.Left);
|
||||
Text.right(String value)
|
||||
|
@ -113,7 +113,6 @@ class Text {
|
|||
final diff = width - value.length;
|
||||
return ' ' * (diff ~/ 2) + value + (' ' * (diff - diff ~/ 2));
|
||||
}
|
||||
return null; // Make analyzer happy.
|
||||
}
|
||||
|
||||
int get length => value.length;
|
||||
|
@ -126,7 +125,7 @@ class AsciiTable {
|
|||
|
||||
final List<Row> rows = <Row>[];
|
||||
|
||||
AsciiTable({List<dynamic> header, this.maxWidth = unlimitedWidth}) {
|
||||
AsciiTable({List<dynamic>? header, this.maxWidth = unlimitedWidth}) {
|
||||
if (header != null) {
|
||||
addSeparator();
|
||||
addRow(header);
|
||||
|
|
|
@ -40,16 +40,20 @@ class SymbolInfo {
|
|||
|
||||
/// If this instructions object originated from a function then [libraryUri]
|
||||
/// will contain uri of the library of that function.
|
||||
final String libraryUri;
|
||||
final String? libraryUri;
|
||||
|
||||
/// If this instructions object originated from a function then [className]
|
||||
/// would contain name of the class owning that function.
|
||||
final String className;
|
||||
final String? className;
|
||||
|
||||
/// Size of the instructions object in bytes.
|
||||
final int size;
|
||||
|
||||
SymbolInfo({String name, this.libraryUri, this.className, this.size})
|
||||
SymbolInfo(
|
||||
{required String name,
|
||||
this.libraryUri,
|
||||
this.className,
|
||||
required this.size})
|
||||
: name = Name(name);
|
||||
|
||||
static SymbolInfo _fromJson(Map<String, dynamic> map) {
|
||||
|
@ -74,37 +78,41 @@ ProgramInfo toProgramInfo(List<SymbolInfo> symbols,
|
|||
final program = ProgramInfo();
|
||||
for (var sym in symbols) {
|
||||
final scrubbed = sym.name.scrubbed;
|
||||
if (sym.libraryUri == null) {
|
||||
final libraryUri = sym.libraryUri;
|
||||
|
||||
// Handle stubs specially.
|
||||
if (libraryUri == null) {
|
||||
assert(sym.name.isStub);
|
||||
final node = program.makeNode(
|
||||
name: scrubbed, parent: program.stubs, type: NodeType.functionNode);
|
||||
assert(node.size == null || sym.name.isTypeTestingStub);
|
||||
node.size = (node.size ?? 0) + sym.size;
|
||||
} else {
|
||||
// Split the name into components (names of individual functions).
|
||||
final path = sym.name.components;
|
||||
|
||||
var node = program.root;
|
||||
final package = packageOf(sym.libraryUri);
|
||||
if (package != sym.libraryUri) {
|
||||
node = program.makeNode(
|
||||
name: package, parent: node, type: NodeType.packageNode);
|
||||
}
|
||||
node = program.makeNode(
|
||||
name: sym.libraryUri, parent: node, type: NodeType.libraryNode);
|
||||
node = program.makeNode(
|
||||
name: sym.className, parent: node, type: NodeType.classNode);
|
||||
node = program.makeNode(
|
||||
name: path.first, parent: node, type: NodeType.functionNode);
|
||||
for (var name in path.skip(1)) {
|
||||
if (collapseAnonymousClosures) {
|
||||
name = Name.collapse(name);
|
||||
}
|
||||
node = program.makeNode(
|
||||
name: name, parent: node, type: NodeType.functionNode);
|
||||
}
|
||||
node.size = (node.size ?? 0) + sym.size;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split the name into components (names of individual functions).
|
||||
final path = sym.name.components;
|
||||
|
||||
var node = program.root;
|
||||
final package = packageOf(libraryUri);
|
||||
if (package != libraryUri) {
|
||||
node = program.makeNode(
|
||||
name: package, parent: node, type: NodeType.packageNode);
|
||||
}
|
||||
node = program.makeNode(
|
||||
name: libraryUri, parent: node, type: NodeType.libraryNode);
|
||||
node = program.makeNode(
|
||||
name: sym.className!, parent: node, type: NodeType.classNode);
|
||||
node = program.makeNode(
|
||||
name: path.first, parent: node, type: NodeType.functionNode);
|
||||
for (var name in path.skip(1)) {
|
||||
if (collapseAnonymousClosures) {
|
||||
name = Name.collapse(name);
|
||||
}
|
||||
node = program.makeNode(
|
||||
name: name, parent: node, type: NodeType.functionNode);
|
||||
}
|
||||
node.size = (node.size ?? 0) + sym.size;
|
||||
}
|
||||
|
||||
return program;
|
||||
|
|
|
@ -15,9 +15,6 @@ class Name {
|
|||
/// Raw textual representation of the name as it occurred in the output
|
||||
/// of the AOT compiler.
|
||||
final String raw;
|
||||
String _scrubbed;
|
||||
|
||||
Name(this.raw);
|
||||
|
||||
/// Pretty version of the name, with some of the irrelevant information
|
||||
/// removed from it.
|
||||
|
@ -26,16 +23,18 @@ class Name {
|
|||
/// so we are not removing any details that are used for disambiguation.
|
||||
/// The only exception are type testing stubs, these refer to type names
|
||||
/// and types names are not bound to be unique between compilations.
|
||||
String get scrubbed => _scrubbed ??=
|
||||
late final String scrubbed =
|
||||
raw.replaceAll(isStub ? _stubScrubbingRe : _scrubbingRe, '');
|
||||
|
||||
Name(this.raw);
|
||||
|
||||
/// Returns true if this name refers to a stub.
|
||||
bool get isStub => raw.startsWith('[Stub] ');
|
||||
|
||||
/// Returns true if this name refers to an allocation stub.
|
||||
bool get isAllocationStub => raw.startsWith('[Stub] Allocate ');
|
||||
|
||||
/// Returns ture if this name refers to a type testing stub.
|
||||
/// Returns true if this name refers to a type testing stub.
|
||||
bool get isTypeTestingStub => raw.startsWith('[Stub] Type Test ');
|
||||
|
||||
/// Split this name into individual '.' separated components (e.g. names of
|
||||
|
|
|
@ -11,7 +11,8 @@ import 'package:vm_snapshot_analysis/program_info.dart';
|
|||
|
||||
/// Build [CallGraph] based on the trace written by `--trace-precompiler-to`
|
||||
/// flag.
|
||||
CallGraph loadTrace(Object inputJson) => _TraceReader(inputJson).readTrace();
|
||||
CallGraph loadTrace(Object inputJson) =>
|
||||
_TraceReader(inputJson as Map<String, dynamic>).readTrace();
|
||||
|
||||
/// [CallGraphNode] represents a node of the call-graph. It can either be:
|
||||
///
|
||||
|
@ -39,7 +40,7 @@ class CallGraphNode {
|
|||
/// Dominator of this node.
|
||||
///
|
||||
/// Computed by [CallGraph.computeDominators].
|
||||
CallGraphNode dominator;
|
||||
late CallGraphNode dominator;
|
||||
|
||||
/// Nodes dominated by this node.
|
||||
///
|
||||
|
@ -99,13 +100,13 @@ class CallGraph {
|
|||
|
||||
// Mapping from [ProgramInfoNode] to a corresponding [CallGraphNode] (if any)
|
||||
// via [ProgramInfoNode.id].
|
||||
final List<CallGraphNode> _graphNodeByEntityId;
|
||||
final List<CallGraphNode?> _graphNodeByEntityId;
|
||||
|
||||
CallGraph._(this.program, this.nodes, this._graphNodeByEntityId);
|
||||
|
||||
CallGraphNode get root => nodes.first;
|
||||
|
||||
CallGraphNode lookup(ProgramInfoNode node) => _graphNodeByEntityId[node.id];
|
||||
CallGraphNode lookup(ProgramInfoNode node) => _graphNodeByEntityId[node.id]!;
|
||||
|
||||
Iterable<CallGraphNode> get dynamicCalls =>
|
||||
nodes.where((n) => n.isDynamicCallNode);
|
||||
|
@ -113,7 +114,7 @@ class CallGraph {
|
|||
/// Compute a collapsed version of the call-graph, where
|
||||
CallGraph collapse(NodeType type, {bool dropCallNodes = false}) {
|
||||
final graphNodesByData = <Object, CallGraphNode>{};
|
||||
final graphNodeByEntityId = <CallGraphNode>[];
|
||||
final graphNodeByEntityId = <CallGraphNode?>[];
|
||||
|
||||
ProgramInfoNode collapsed(ProgramInfoNode nn) {
|
||||
// Root always collapses onto itself.
|
||||
|
@ -127,7 +128,7 @@ class CallGraph {
|
|||
// hitting the root node.
|
||||
var n = nn;
|
||||
while (n.parent != program.root && n.type != type) {
|
||||
n = n.parent;
|
||||
n = n.parent!;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
@ -186,14 +187,14 @@ class CallGraph {
|
|||
///
|
||||
/// See README.md for description of the format.
|
||||
class _TraceReader {
|
||||
final List<Object> trace;
|
||||
final List<Object> strings;
|
||||
final List<Object> entities;
|
||||
final List<dynamic> trace;
|
||||
final List<String> strings;
|
||||
final List<dynamic> entities;
|
||||
|
||||
final program = ProgramInfo();
|
||||
|
||||
/// Mapping between entity ids and corresponding [ProgramInfoNode] nodes.
|
||||
final entityById = List<ProgramInfoNode>.filled(1024, null, growable: true);
|
||||
final entityById = List<ProgramInfoNode?>.filled(1024, null, growable: true);
|
||||
|
||||
/// Mapping between functions (represented as [ProgramInfoNode]s) and
|
||||
/// their selector ids.
|
||||
|
@ -203,21 +204,22 @@ class _TraceReader {
|
|||
final dynamicFunctions = Set<ProgramInfoNode>();
|
||||
|
||||
_TraceReader(Map<String, dynamic> data)
|
||||
: strings = data['strings'],
|
||||
: strings = (data['strings'] as List<dynamic>).cast<String>(),
|
||||
entities = data['entities'],
|
||||
trace = data['trace'];
|
||||
|
||||
/// Read all trace events and construct the call graph based on them.
|
||||
CallGraph readTrace() {
|
||||
var pos = 0; // Position in the [trace] array.
|
||||
CallGraphNode currentNode;
|
||||
late CallGraphNode currentNode;
|
||||
int maxId = 0;
|
||||
|
||||
final nodes = <CallGraphNode>[];
|
||||
final nodeByEntityId = <CallGraphNode>[];
|
||||
final nodeByEntityId = <CallGraphNode?>[];
|
||||
final callNodesBySelector = <dynamic, CallGraphNode>{};
|
||||
final allocated = Set<ProgramInfoNode>();
|
||||
|
||||
Object next() => trace[pos++];
|
||||
T next<T>() => trace[pos++] as T;
|
||||
|
||||
CallGraphNode makeNode({dynamic data}) {
|
||||
final n = CallGraphNode(nodes.length, data: data);
|
||||
|
@ -232,6 +234,9 @@ class _TraceReader {
|
|||
if (nodeByEntityId.length <= n.id) {
|
||||
nodeByEntityId.length = n.id * 2 + 1;
|
||||
}
|
||||
if (n.id > maxId) {
|
||||
maxId = n.id;
|
||||
}
|
||||
return nodeByEntityId[n.id] ??= makeNode(data: n);
|
||||
}
|
||||
|
||||
|
@ -363,10 +368,13 @@ class _TraceReader {
|
|||
/// Read the entity at the given [index] in [entities].
|
||||
ProgramInfoNode readEntityAt(int index) {
|
||||
final type = entities[index];
|
||||
final idx0 = entities[index + 1] as int;
|
||||
final idx1 = entities[index + 2] as int;
|
||||
final idx2 = entities[index + 3] as int;
|
||||
switch (type) {
|
||||
case 'C': // Class: 'C', <library-uri-idx>, <name-idx>, 0
|
||||
final libraryUri = strings[entities[index + 1]];
|
||||
final className = strings[entities[index + 2]];
|
||||
final libraryUri = strings[idx0];
|
||||
final className = strings[idx1];
|
||||
|
||||
return program.makeNode(
|
||||
name: className,
|
||||
|
@ -375,9 +383,9 @@ class _TraceReader {
|
|||
|
||||
case 'S':
|
||||
case 'F': // Function: 'F'|'S', <class-idx>, <name-idx>, <selector-id>
|
||||
final classNode = getEntityAt(entities[index + 1]);
|
||||
final functionName = strings[entities[index + 2]];
|
||||
final int selectorId = entities[index + 3];
|
||||
final classNode = getEntityAt(idx0);
|
||||
final functionName = strings[idx1];
|
||||
final int selectorId = idx2;
|
||||
|
||||
final path = Name(functionName).rawComponents;
|
||||
if (path.last == 'FfiTrampoline') {
|
||||
|
@ -398,8 +406,8 @@ class _TraceReader {
|
|||
return node;
|
||||
|
||||
case 'V': // Field: 'V', <class-idx>, <name-idx>, 0
|
||||
final classNode = getEntityAt(entities[index + 1]);
|
||||
final fieldName = strings[entities[index + 2]];
|
||||
final classNode = getEntityAt(idx0);
|
||||
final fieldName = strings[idx1];
|
||||
|
||||
return program.makeNode(
|
||||
name: fieldName, parent: classNode, type: NodeType.other);
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
/// Classes for representing information about the program structure.
|
||||
library vm_snapshot_analysis.program_info;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'package:vm_snapshot_analysis/v8_profile.dart';
|
||||
|
||||
/// Represents information about compiled program.
|
||||
|
@ -22,7 +20,7 @@ class ProgramInfo {
|
|||
|
||||
/// V8 snapshot profile if this [ProgramInfo] object was created from an
|
||||
/// output of `--write-v8-snapshot-profile-to=...` flag.
|
||||
SnapshotInfo snapshotInfo;
|
||||
SnapshotInfo? snapshotInfo;
|
||||
|
||||
ProgramInfo._(this.root, this.stubs, this.unknown);
|
||||
|
||||
|
@ -45,24 +43,22 @@ class ProgramInfo {
|
|||
}
|
||||
|
||||
ProgramInfoNode makeNode(
|
||||
{@required String name,
|
||||
@required ProgramInfoNode parent,
|
||||
@required NodeType type}) {
|
||||
return parent.children.putIfAbsent(name, () {
|
||||
final node = ProgramInfoNode._(
|
||||
id: _nextId++, name: name, parent: parent ?? root, type: type);
|
||||
node.parent.children[name] = node;
|
||||
return node;
|
||||
});
|
||||
{required String name,
|
||||
required ProgramInfoNode parent,
|
||||
required NodeType type}) {
|
||||
return parent.children.putIfAbsent(
|
||||
name,
|
||||
() => ProgramInfoNode._(
|
||||
id: _nextId++, name: name, parent: parent, type: type));
|
||||
}
|
||||
|
||||
/// Recursively visit all function nodes, which have [FunctionInfo.info]
|
||||
/// populated.
|
||||
void visit(
|
||||
void Function(
|
||||
String pkg, String lib, String cls, String fun, ProgramInfoNode n)
|
||||
void Function(String? pkg, String lib, String? cls, String? fun,
|
||||
ProgramInfoNode n)
|
||||
callback) {
|
||||
final context = List<String>.filled(NodeType.values.length, null);
|
||||
final context = List<String?>.filled(NodeType.values.length, null);
|
||||
|
||||
void recurse(ProgramInfoNode node) {
|
||||
final prevContext = context[node._type];
|
||||
|
@ -72,11 +68,11 @@ class ProgramInfo {
|
|||
context[node._type] = node.name;
|
||||
}
|
||||
|
||||
final String pkg = context[NodeType.packageNode.index];
|
||||
final String lib = context[NodeType.libraryNode.index];
|
||||
final String cls = context[NodeType.classNode.index];
|
||||
final String mem = context[NodeType.functionNode.index];
|
||||
callback(pkg, lib, cls, mem, node);
|
||||
final pkg = context[NodeType.packageNode.index];
|
||||
final lib = context[NodeType.libraryNode.index];
|
||||
final cls = context[NodeType.classNode.index];
|
||||
final mem = context[NodeType.functionNode.index];
|
||||
callback(pkg, lib!, cls, mem, node);
|
||||
|
||||
for (var child in node.children.values) {
|
||||
recurse(child);
|
||||
|
@ -96,12 +92,14 @@ class ProgramInfo {
|
|||
Map<String, dynamic> toJson() => root.toJson();
|
||||
|
||||
/// Lookup a node in the program given a path to it.
|
||||
ProgramInfoNode lookup(List<String> path) {
|
||||
ProgramInfoNode? lookup(List<String> path) {
|
||||
var n = root;
|
||||
for (var p in path) {
|
||||
if ((n = n.children[p]) == null) {
|
||||
break;
|
||||
final next = n.children[p];
|
||||
if (next == null) {
|
||||
return null;
|
||||
}
|
||||
n = next;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
@ -121,12 +119,12 @@ String _typeToJson(NodeType type) => const {
|
|||
NodeType.classNode: 'class',
|
||||
NodeType.functionNode: 'function',
|
||||
NodeType.other: 'other',
|
||||
}[type];
|
||||
}[type]!;
|
||||
|
||||
class ProgramInfoNode {
|
||||
final int id;
|
||||
final String name;
|
||||
final ProgramInfoNode parent;
|
||||
final ProgramInfoNode? parent;
|
||||
final Map<String, ProgramInfoNode> children = {};
|
||||
final int _type;
|
||||
|
||||
|
@ -151,13 +149,13 @@ class ProgramInfoNode {
|
|||
/// Assuming that both `C.f` and `C.g` are included into AOT snapshot
|
||||
/// and string `"something"` does not occur anywhere else in the program
|
||||
/// then the size of `"something"` is going to be attributed to `C`.
|
||||
int size;
|
||||
int? size;
|
||||
|
||||
ProgramInfoNode._(
|
||||
{@required this.id,
|
||||
@required this.name,
|
||||
@required this.parent,
|
||||
@required NodeType type})
|
||||
{required this.id,
|
||||
required this.name,
|
||||
required this.parent,
|
||||
required NodeType type})
|
||||
: _type = type.index;
|
||||
|
||||
NodeType get type => NodeType.values[_type];
|
||||
|
@ -175,9 +173,10 @@ class ProgramInfoNode {
|
|||
var prefix = '';
|
||||
// Do not include root name or package name (library uri already contains
|
||||
// package name).
|
||||
if (parent?.parent != null && parent?.type != NodeType.packageNode) {
|
||||
prefix = parent.qualifiedName;
|
||||
if (parent.type != NodeType.libraryNode) {
|
||||
final p = parent;
|
||||
if (p != null && p.parent != null && p.type != NodeType.packageNode) {
|
||||
prefix = p.qualifiedName;
|
||||
if (p.type != NodeType.libraryNode) {
|
||||
prefix += '.';
|
||||
} else {
|
||||
prefix += '::';
|
||||
|
@ -198,7 +197,7 @@ class ProgramInfoNode {
|
|||
var n = this;
|
||||
while (n.parent != null) {
|
||||
result.add(n.name);
|
||||
n = n.parent;
|
||||
n = n.parent!;
|
||||
}
|
||||
return result.reversed.toList();
|
||||
}
|
||||
|
@ -215,24 +214,24 @@ ProgramInfo computeDiff(ProgramInfo oldInfo, ProgramInfo newInfo) {
|
|||
final programDiff = ProgramInfo();
|
||||
|
||||
var path = <Object>[];
|
||||
void recurse(ProgramInfoNode oldNode, ProgramInfoNode newNode) {
|
||||
void recurse(ProgramInfoNode? oldNode, ProgramInfoNode? newNode) {
|
||||
if (oldNode?.size != newNode?.size) {
|
||||
var diffNode = programDiff.root;
|
||||
for (var i = 0; i < path.length; i += 2) {
|
||||
final name = path[i];
|
||||
final type = path[i + 1];
|
||||
final name = path[i] as String;
|
||||
final type = path[i + 1] as NodeType;
|
||||
diffNode =
|
||||
programDiff.makeNode(name: name, parent: diffNode, type: type);
|
||||
}
|
||||
diffNode.size ??= 0;
|
||||
diffNode.size += (newNode?.size ?? 0) - (oldNode?.size ?? 0);
|
||||
diffNode.size =
|
||||
(diffNode.size ?? 0) + (newNode?.size ?? 0) - (oldNode?.size ?? 0);
|
||||
}
|
||||
|
||||
for (var key in _allKeys(newNode?.children, oldNode?.children)) {
|
||||
final newChildNode = newNode != null ? newNode.children[key] : null;
|
||||
final oldChildNode = oldNode != null ? oldNode.children[key] : null;
|
||||
path.add(key);
|
||||
path.add(oldChildNode?.type ?? newChildNode?.type);
|
||||
path.add((oldChildNode?.type ?? newChildNode?.type)!);
|
||||
recurse(oldChildNode, newChildNode);
|
||||
path.removeLast();
|
||||
path.removeLast();
|
||||
|
@ -244,7 +243,7 @@ ProgramInfo computeDiff(ProgramInfo oldInfo, ProgramInfo newInfo) {
|
|||
return programDiff;
|
||||
}
|
||||
|
||||
Iterable<T> _allKeys<T>(Map<T, dynamic> a, Map<T, dynamic> b) {
|
||||
Iterable<T> _allKeys<T>(Map<T, dynamic>? a, Map<T, dynamic>? b) {
|
||||
return <T>{...?a?.keys, ...?b?.keys};
|
||||
}
|
||||
|
||||
|
@ -267,14 +266,14 @@ class Histogram {
|
|||
|
||||
Histogram._(this.bucketInfo, this.buckets)
|
||||
: bySize = buckets.keys.toList(growable: false)
|
||||
..sort((a, b) => buckets[b] - buckets[a]),
|
||||
..sort((a, b) => buckets[b]! - buckets[a]!),
|
||||
totalSize = buckets.values.fold(0, (sum, size) => sum + size);
|
||||
|
||||
static Histogram fromIterable<T>(
|
||||
Iterable<T> entries, {
|
||||
@required int Function(T) sizeOf,
|
||||
@required String Function(T) bucketFor,
|
||||
@required BucketInfo bucketInfo,
|
||||
required int Function(T) sizeOf,
|
||||
required String Function(T) bucketFor,
|
||||
required BucketInfo bucketInfo,
|
||||
}) {
|
||||
final buckets = <String, int>{};
|
||||
|
||||
|
@ -290,20 +289,26 @@ class Histogram {
|
|||
/// Rebuckets the histogram given the new bucketing rule.
|
||||
Histogram map(String Function(String) bucketFor) {
|
||||
return Histogram.fromIterable(buckets.keys,
|
||||
sizeOf: (key) => buckets[key],
|
||||
sizeOf: (key) => buckets[key]!,
|
||||
bucketFor: bucketFor,
|
||||
bucketInfo: bucketInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct the histogram of specific [type] given a [ProgramInfo].
|
||||
///
|
||||
/// [filter] glob can be provided to skip some of the nodes in the [info]:
|
||||
/// a string is created which contains library name, class name and function
|
||||
/// name for the given node and if this string does not match the [filter]
|
||||
/// glob then this node is ignored.
|
||||
Histogram computeHistogram(ProgramInfo info, HistogramType type,
|
||||
{String filter}) {
|
||||
bool Function(String, String, String) matchesFilter;
|
||||
{String? filter}) {
|
||||
bool Function(String, String?, String?) matchesFilter;
|
||||
|
||||
if (filter != null) {
|
||||
final re = RegExp(filter.replaceAll('*', '.*'), caseSensitive: false);
|
||||
matchesFilter = (lib, cls, fun) => re.hasMatch("${lib}::${cls}.${fun}");
|
||||
matchesFilter =
|
||||
(lib, cls, fun) => re.hasMatch("${lib}::${cls ?? ''}.${fun ?? ''}");
|
||||
} else {
|
||||
matchesFilter = (_, __, ___) => true;
|
||||
}
|
||||
|
@ -318,10 +323,12 @@ Histogram computeHistogram(ProgramInfo info, HistogramType type,
|
|||
});
|
||||
}
|
||||
|
||||
final snapshotInfo = info.snapshotInfo!;
|
||||
|
||||
return Histogram.fromIterable<Node>(
|
||||
info.snapshotInfo.snapshot.nodes.where((n) =>
|
||||
snapshotInfo.snapshot.nodes.where((n) =>
|
||||
filter == null ||
|
||||
filteredNodes.contains(info.snapshotInfo.ownerOf(n).id)),
|
||||
filteredNodes.contains(snapshotInfo.ownerOf(n).id)),
|
||||
sizeOf: (n) {
|
||||
return n.selfSize;
|
||||
},
|
||||
|
@ -329,17 +336,18 @@ Histogram computeHistogram(ProgramInfo info, HistogramType type,
|
|||
bucketInfo: const BucketInfo(nameComponents: ['Type']));
|
||||
} else {
|
||||
final buckets = <String, int>{};
|
||||
final bucketing = Bucketing._forType[type];
|
||||
final bucketing = Bucketing._forType[type]!;
|
||||
|
||||
info.visit((pkg, lib, cls, fun, node) {
|
||||
if (node.size == null || node.size == 0) {
|
||||
final sz = node.size;
|
||||
if (sz == null || sz == 0) {
|
||||
return;
|
||||
}
|
||||
if (!matchesFilter(lib, cls, fun)) {
|
||||
return;
|
||||
}
|
||||
final bucket = bucketing.bucketFor(pkg, lib, cls, fun);
|
||||
buckets[bucket] = (buckets[bucket] ?? 0) + node.size;
|
||||
buckets[bucket] = (buckets[bucket] ?? 0) + sz;
|
||||
});
|
||||
|
||||
return Histogram._(bucketing, buckets);
|
||||
|
@ -363,15 +371,15 @@ class BucketInfo {
|
|||
/// one returned by [nameComponents]).
|
||||
List<String> namesFromBucket(String bucket) => [bucket];
|
||||
|
||||
const BucketInfo({@required this.nameComponents});
|
||||
const BucketInfo({required this.nameComponents});
|
||||
}
|
||||
|
||||
abstract class Bucketing extends BucketInfo {
|
||||
/// Constructs the bucket name from the given library name [lib], class name
|
||||
/// [cls] and function name [fun].
|
||||
String bucketFor(String pkg, String lib, String cls, String fun);
|
||||
String bucketFor(String? pkg, String lib, String? cls, String? fun);
|
||||
|
||||
const Bucketing({@required List<String> nameComponents})
|
||||
const Bucketing({required List<String> nameComponents})
|
||||
: super(nameComponents: nameComponents);
|
||||
|
||||
static const _forType = {
|
||||
|
@ -387,11 +395,11 @@ const String _nameSeparator = ';;;';
|
|||
|
||||
class _BucketBySymbol extends Bucketing {
|
||||
@override
|
||||
String bucketFor(String pkg, String lib, String cls, String fun) {
|
||||
String bucketFor(String? pkg, String lib, String? cls, String? fun) {
|
||||
if (fun == null) {
|
||||
return '@other${_nameSeparator}';
|
||||
}
|
||||
return '$lib${_nameSeparator}${cls}${cls != '' ? '.' : ''}${fun}';
|
||||
return '$lib${_nameSeparator}${cls ?? ''}${cls != '' && cls != null ? '.' : ''}${fun}';
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -402,7 +410,7 @@ class _BucketBySymbol extends Bucketing {
|
|||
|
||||
class _BucketByClass extends Bucketing {
|
||||
@override
|
||||
String bucketFor(String pkg, String lib, String cls, String fun) {
|
||||
String bucketFor(String? pkg, String lib, String? cls, String? fun) {
|
||||
if (cls == null) {
|
||||
return '@other${_nameSeparator}';
|
||||
}
|
||||
|
@ -417,14 +425,14 @@ class _BucketByClass extends Bucketing {
|
|||
|
||||
class _BucketByLibrary extends Bucketing {
|
||||
@override
|
||||
String bucketFor(String pkg, String lib, String cls, String fun) => '$lib';
|
||||
String bucketFor(String? pkg, String lib, String? cls, String? fun) => '$lib';
|
||||
|
||||
const _BucketByLibrary() : super(nameComponents: const ['Library']);
|
||||
}
|
||||
|
||||
class _BucketByPackage extends Bucketing {
|
||||
@override
|
||||
String bucketFor(String pkg, String lib, String cls, String fun) =>
|
||||
String bucketFor(String? pkg, String lib, String? cls, String? fun) =>
|
||||
pkg ?? lib;
|
||||
|
||||
const _BucketByPackage() : super(nameComponents: const ['Package']);
|
||||
|
|
|
@ -64,23 +64,25 @@ precisely based on their source position (which is included in their name).
|
|||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
if (argResults.rest.length != 2) {
|
||||
final args = argResults!;
|
||||
|
||||
if (args.rest.length != 2) {
|
||||
usageException('Need to provide path to old.json and new.json reports.');
|
||||
}
|
||||
|
||||
final columnWidth = argResults['column-width'];
|
||||
final columnWidth = args['column-width'];
|
||||
final maxWidth = int.tryParse(columnWidth);
|
||||
if (maxWidth == null) {
|
||||
usageException(
|
||||
'Specified column width (${columnWidth}) is not an integer');
|
||||
}
|
||||
|
||||
final oldJsonPath = _checkExists(argResults.rest[0]);
|
||||
final newJsonPath = _checkExists(argResults.rest[1]);
|
||||
final oldJsonPath = _checkExists(args.rest[0]);
|
||||
final newJsonPath = _checkExists(args.rest[1]);
|
||||
printComparison(oldJsonPath, newJsonPath,
|
||||
maxWidth: maxWidth,
|
||||
granularity: _parseHistogramType(argResults['by']),
|
||||
collapseAnonymousClosures: argResults['collapse-anonymous-closures']);
|
||||
granularity: _parseHistogramType(args['by']),
|
||||
collapseAnonymousClosures: args['collapse-anonymous-closures']);
|
||||
}
|
||||
|
||||
HistogramType _parseHistogramType(String value) {
|
||||
|
@ -93,8 +95,9 @@ precisely based on their source position (which is included in their name).
|
|||
return HistogramType.byLibrary;
|
||||
case 'package':
|
||||
return HistogramType.byPackage;
|
||||
default:
|
||||
usageException('Unrecognized histogram type $value');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
File _checkExists(String path) {
|
||||
|
@ -136,10 +139,10 @@ precisely based on their source position (which is included in their name).
|
|||
printHistogram(diff, histogram,
|
||||
sizeHeader: 'Diff (Bytes)',
|
||||
prefix: histogram.bySize
|
||||
.where((k) => histogram.buckets[k] > 0)
|
||||
.where((k) => histogram.buckets[k]! > 0)
|
||||
.take(numLargerSymbolsToReport),
|
||||
suffix: histogram.bySize.reversed
|
||||
.where((k) => histogram.buckets[k] < 0)
|
||||
.where((k) => histogram.buckets[k]! < 0)
|
||||
.take(numSmallerSymbolsToReport)
|
||||
.toList()
|
||||
.reversed,
|
||||
|
@ -159,7 +162,7 @@ precisely based on their source position (which is included in their name).
|
|||
final newTypeHistogram =
|
||||
computeHistogram(newSizes, HistogramType.byNodeType);
|
||||
|
||||
final diffTypeHistogram = Histogram.fromIterable(
|
||||
final diffTypeHistogram = Histogram.fromIterable<String>(
|
||||
Set<String>()
|
||||
..addAll(oldTypeHistogram.buckets.keys)
|
||||
..addAll(newTypeHistogram.buckets.keys),
|
||||
|
|
|
@ -53,13 +53,15 @@ precompiler trace (an output of --trace-precompiler-to flag).
|
|||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final sizesJson = File(argResults.rest[0]);
|
||||
final args = argResults!;
|
||||
|
||||
final sizesJson = File(args.rest[0]);
|
||||
if (!sizesJson.existsSync()) {
|
||||
usageException('Size profile ${sizesJson.path} does not exist!');
|
||||
}
|
||||
final sizesJsonRaw = await loadJsonFromFile(sizesJson);
|
||||
|
||||
final traceJson = File(argResults.rest[1]);
|
||||
final traceJson = File(args.rest[1]);
|
||||
if (!traceJson.existsSync()) {
|
||||
usageException('Size profile ${traceJson.path} does not exist!');
|
||||
}
|
||||
|
@ -103,7 +105,7 @@ precompiler trace (an output of --trace-precompiler-to flag).
|
|||
}, bucketInfo: BucketInfo(nameComponents: ['Selector']));
|
||||
|
||||
printHistogram(programInfo, histogram,
|
||||
prefix: histogram.bySize.where((key) => histogram.buckets[key] > 0));
|
||||
prefix: histogram.bySize.where((key) => histogram.buckets[key]! > 0));
|
||||
|
||||
// For top 10 dynamic selectors print the functions which contain these
|
||||
// dynamic calls.
|
||||
|
|
|
@ -12,7 +12,6 @@ import 'dart:io';
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'package:vm_snapshot_analysis/ascii_table.dart';
|
||||
import 'package:vm_snapshot_analysis/precompiler_trace.dart';
|
||||
|
@ -95,18 +94,20 @@ precisely based on their source position (which is included in their name).
|
|||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
if (argResults.rest.length != 1) {
|
||||
final args = argResults!;
|
||||
|
||||
if (args.rest.length != 1) {
|
||||
usageException('Need to specify input JSON.');
|
||||
}
|
||||
|
||||
final input = File(argResults.rest[0]);
|
||||
final input = File(args.rest[0]);
|
||||
if (!input.existsSync()) {
|
||||
usageException('Input file ${input.path} does not exist!');
|
||||
}
|
||||
|
||||
final granularity = _parseHistogramType(argResults['by']);
|
||||
final granularity = _parseHistogramType(args['by']);
|
||||
|
||||
final traceJson = argResults['precompiler-trace'];
|
||||
final traceJson = args['precompiler-trace'];
|
||||
if (traceJson != null) {
|
||||
if (!File(traceJson).existsSync()) {
|
||||
usageException('Trace ${traceJson} does not exist!');
|
||||
|
@ -119,21 +120,21 @@ precisely based on their source position (which is included in their name).
|
|||
}
|
||||
}
|
||||
|
||||
final columnWidth = argResults['column-width'];
|
||||
final columnWidth = args['column-width'];
|
||||
final maxWidth = int.tryParse(columnWidth);
|
||||
if (maxWidth == null) {
|
||||
usageException(
|
||||
'Specified column width (${columnWidth}) is not an integer');
|
||||
}
|
||||
|
||||
final depsStartDepthStr = argResults['deps-start-depth'];
|
||||
final depsStartDepthStr = args['deps-start-depth'];
|
||||
final depsStartDepth = int.tryParse(depsStartDepthStr);
|
||||
if (depsStartDepth == null) {
|
||||
usageException('Specified depsStartDepth (${depsStartDepthStr})'
|
||||
' is not an integer');
|
||||
}
|
||||
|
||||
final depsDisplayDepthStr = argResults['deps-display-depth'];
|
||||
final depsDisplayDepthStr = args['deps-display-depth'];
|
||||
final depsDisplayDepth = int.tryParse(depsDisplayDepthStr);
|
||||
if (depsDisplayDepth == null) {
|
||||
usageException('Specified depsDisplayDepth (${depsStartDepthStr})'
|
||||
|
@ -143,14 +144,14 @@ precisely based on their source position (which is included in their name).
|
|||
await outputSummary(input,
|
||||
maxWidth: maxWidth,
|
||||
granularity: granularity,
|
||||
collapseAnonymousClosures: argResults['collapse-anonymous-closures'],
|
||||
filter: argResults['where'],
|
||||
collapseAnonymousClosures: args['collapse-anonymous-closures'],
|
||||
filter: args['where'],
|
||||
traceJson: traceJson != null ? File(traceJson) : null,
|
||||
depsStartDepth: depsStartDepth,
|
||||
depsDisplayDepth: depsDisplayDepth);
|
||||
}
|
||||
|
||||
static HistogramType _parseHistogramType(String value) {
|
||||
HistogramType _parseHistogramType(String value) {
|
||||
switch (value) {
|
||||
case 'method':
|
||||
return HistogramType.bySymbol;
|
||||
|
@ -160,17 +161,18 @@ precisely based on their source position (which is included in their name).
|
|||
return HistogramType.byLibrary;
|
||||
case 'package':
|
||||
return HistogramType.byPackage;
|
||||
default:
|
||||
usageException('Unrecognized histogram type $value');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void outputSummary(File input,
|
||||
Future<void> outputSummary(File input,
|
||||
{int maxWidth = 0,
|
||||
bool collapseAnonymousClosures = false,
|
||||
HistogramType granularity = HistogramType.bySymbol,
|
||||
String filter,
|
||||
File traceJson,
|
||||
String? filter,
|
||||
File? traceJson,
|
||||
int depsStartDepth = 2,
|
||||
int depsDisplayDepth = 4,
|
||||
int topToReport = 30}) async {
|
||||
|
@ -182,7 +184,7 @@ void outputSummary(File input,
|
|||
|
||||
// If precompiler trace is provided, collapse entries based on the dependency
|
||||
// graph (dominator tree) extracted from the trace.
|
||||
void Function() printDependencyTrees;
|
||||
void Function()? printDependencyTrees;
|
||||
if (traceJson != null &&
|
||||
(granularity == HistogramType.byLibrary ||
|
||||
granularity == HistogramType.byPackage)) {
|
||||
|
@ -235,8 +237,8 @@ void outputSummary(File input,
|
|||
var totalCount = 1;
|
||||
for (var n in node.dominated) {
|
||||
computeTotalsRecursively(n);
|
||||
totalSize += totalSizes[n.data.name];
|
||||
totalCount += totalCounts[n.data.name];
|
||||
totalSize += totalSizes[n.data.name]!;
|
||||
totalCount += totalCounts[n.data.name]!;
|
||||
}
|
||||
totalSizes[node.data.name] = totalSize;
|
||||
totalCounts[node.data.name] = totalCount;
|
||||
|
@ -255,7 +257,7 @@ void outputSummary(File input,
|
|||
final collapsedEntries = histogram.bySize
|
||||
.take(topToReport)
|
||||
.map((k) => collapsed[k])
|
||||
.where((n) => n != null);
|
||||
.whereType<CallGraphNode>();
|
||||
if (collapsedEntries.isNotEmpty) {
|
||||
print('\bDependency trees:');
|
||||
for (var n in collapsedEntries) {
|
||||
|
@ -302,9 +304,9 @@ void outputSummary(File input,
|
|||
void _printDominatedNodes(CallGraphNode node,
|
||||
{int displayDepth = 4,
|
||||
int maxChildrenToPrint = 10,
|
||||
List<bool> isLastPerLevel,
|
||||
@required Map<String, int> totalSizes,
|
||||
@required Map<String, int> totalCounts}) {
|
||||
List<bool>? isLastPerLevel,
|
||||
required Map<String, int> totalSizes,
|
||||
required Map<String, int> totalCounts}) {
|
||||
isLastPerLevel ??= [];
|
||||
|
||||
// Subtract one to account for the parent node that is printed before the
|
||||
|
@ -315,8 +317,8 @@ void _printDominatedNodes(CallGraphNode node,
|
|||
|
||||
final sizes = node.dominated.map((n) => totalSizes[n.data.name]).toList();
|
||||
final order = List.generate(node.dominated.length, (i) => i)
|
||||
..sort((a, b) => sizes[b] - sizes[a]);
|
||||
final lastIndex = order.lastIndexWhere((i) => sizes[i] > 0);
|
||||
..sort((a, b) => sizes[b]! - sizes[a]!);
|
||||
final lastIndex = order.lastIndexWhere((i) => sizes[i]! > 0);
|
||||
|
||||
for (var j = 0, n = math.min(maxChildrenToPrint - 1, lastIndex);
|
||||
j <= n;
|
||||
|
@ -339,7 +341,7 @@ void _printDominatedNodes(CallGraphNode node,
|
|||
if (maxChildrenToPrint < lastIndex) {
|
||||
isLastPerLevel.add(true);
|
||||
print(
|
||||
'${_treeLines(isLastPerLevel)} ... (+${totalCounts[node.data.name] - 1} deps)');
|
||||
'${_treeLines(isLastPerLevel)} ... (+${totalCounts[node.data.name]! - 1} deps)');
|
||||
isLastPerLevel.removeLast();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,24 +56,27 @@ viewed in a browser:
|
|||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
if (argResults.rest.length != 2) {
|
||||
final args = argResults!;
|
||||
|
||||
if (args.rest.length != 2) {
|
||||
usageException('Need to specify input JSON and output directory.');
|
||||
}
|
||||
|
||||
final input = File(argResults.rest[0]);
|
||||
final outputDir = Directory(argResults.rest[1]);
|
||||
final input = File(args.rest[0]);
|
||||
final outputDir = Directory(args.rest[1]);
|
||||
|
||||
if (!input.existsSync()) {
|
||||
usageException('Input file ${input.path} does not exist!');
|
||||
}
|
||||
|
||||
if (outputDir.existsSync() && !argResults['force']) {
|
||||
if (outputDir.existsSync() && !args['force']) {
|
||||
usageException(
|
||||
'Output directory ${outputDir.path} already exists, specify --force to ignore.');
|
||||
}
|
||||
|
||||
await generateTreeMap(input, outputDir,
|
||||
format: _formatFromString[argResults['format']]);
|
||||
format: _formatFromString[args['format']] ??
|
||||
(throw 'Unrecognized format ${args['format']}'));
|
||||
}
|
||||
|
||||
// Note: the first key in this map is the default format.
|
||||
|
@ -95,8 +98,8 @@ Future<void> generateTreeMap(File input, Directory outputDir,
|
|||
// Create output directory and copy all auxiliary files from binary_size tool.
|
||||
await outputDir.create(recursive: true);
|
||||
|
||||
final assetsUri = await Isolate.resolvePackageUri(
|
||||
Uri.parse('package:vm_snapshot_analysis/src/assets'));
|
||||
final assetsUri = (await Isolate.resolvePackageUri(
|
||||
Uri.parse('package:vm_snapshot_analysis/src/assets')))!;
|
||||
final assetsDir = assetsUri.toFilePath();
|
||||
final d3SrcDir = p.join(assetsDir, 'd3', 'src');
|
||||
|
||||
|
@ -114,7 +117,7 @@ Future<void> generateTreeMap(File input, Directory outputDir,
|
|||
final dataJsPath = p.join(outputDir.path, 'data.js');
|
||||
final sink = File(dataJsPath).openWrite();
|
||||
sink.write('var tree_data=');
|
||||
await sink.addStream(Stream<Object>.fromIterable([tree])
|
||||
await sink.addStream(Stream<Object?>.fromIterable([tree])
|
||||
.transform(json.encoder.fuse(utf8.encoder)));
|
||||
await sink.close();
|
||||
|
||||
|
|
|
@ -6,9 +6,9 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
|
||||
Future<Object> loadJsonFromFile(File input) async {
|
||||
return await input
|
||||
return (await input
|
||||
.openRead()
|
||||
.transform(utf8.decoder)
|
||||
.transform(json.decoder)
|
||||
.first;
|
||||
.first)!;
|
||||
}
|
||||
|
|
|
@ -13,16 +13,16 @@ import 'dart:math' as math;
|
|||
/// native compiler (see runtime/vm/compiler/backend/flow_graph.cc).
|
||||
@pragma('vm:prefer-inline')
|
||||
List<int> computeDominators({
|
||||
int size,
|
||||
int root,
|
||||
Iterable<int> succ(int n),
|
||||
Iterable<int> predOf(int n),
|
||||
void handleEdge(int from, int to),
|
||||
required int size,
|
||||
required int root,
|
||||
required Iterable<int> Function(int) succ,
|
||||
required Iterable<int> Function(int) predOf,
|
||||
required void handleEdge(int from, int to),
|
||||
}) {
|
||||
// Compute preorder numbering for the graph using DFS.
|
||||
final parent = List<int>.filled(size, -1);
|
||||
final preorder = List<int>.filled(size, null);
|
||||
final preorderNumber = List<int>.filled(size, null);
|
||||
final preorder = List<int>.filled(size, -1);
|
||||
final preorderNumber = List<int>.filled(size, -1);
|
||||
|
||||
var N = 0;
|
||||
void dfs() {
|
||||
|
@ -32,7 +32,7 @@ List<int> computeDominators({
|
|||
final p = s.p;
|
||||
final n = s.n;
|
||||
handleEdge(s.n, s.p);
|
||||
if (preorderNumber[n] == null) {
|
||||
if (preorderNumber[n] == -1) {
|
||||
preorderNumber[n] = N;
|
||||
preorder[preorderNumber[n]] = n;
|
||||
parent[preorderNumber[n]] = p;
|
||||
|
@ -122,5 +122,5 @@ List<int> computeDominators({
|
|||
class _DfsState {
|
||||
final int p;
|
||||
final int n;
|
||||
_DfsState({this.p, this.n});
|
||||
_DfsState({required this.p, required this.n});
|
||||
}
|
||||
|
|
|
@ -82,10 +82,11 @@ Map<String, dynamic> treemapFromJson(Object inputJson,
|
|||
final root = {'n': '', 'children': {}, 'k': kindPath, 'maxDepth': 0};
|
||||
|
||||
if (v8_profile.Snapshot.isV8HeapSnapshot(inputJson)) {
|
||||
_treemapFromSnapshot(root, v8_profile.Snapshot.fromJson(inputJson),
|
||||
_treemapFromSnapshot(
|
||||
root, v8_profile.Snapshot.fromJson(inputJson as Map<String, dynamic>),
|
||||
format: format);
|
||||
} else {
|
||||
final symbols = instruction_sizes.fromJson(inputJson);
|
||||
final symbols = instruction_sizes.fromJson(inputJson as List<dynamic>);
|
||||
for (var symbol in symbols) {
|
||||
_addSymbol(root, _treePath(symbol), symbol.name.scrubbed, symbol.size);
|
||||
}
|
||||
|
@ -163,20 +164,22 @@ void _treemapFromSnapshot(Map<String, dynamic> root, v8_profile.Snapshot snap,
|
|||
return;
|
||||
}
|
||||
|
||||
final snapshotInfo = info.snapshotInfo!;
|
||||
|
||||
final ownerPathCache =
|
||||
List<String>.filled(info.snapshotInfo.infoNodes.length, null);
|
||||
List<String?>.filled(snapshotInfo.infoNodes.length, null);
|
||||
ownerPathCache[info.root.id] = info.root.name;
|
||||
|
||||
String ownerPath(ProgramInfoNode n) {
|
||||
return ownerPathCache[n.id] ??=
|
||||
((n.parent != info.root) ? '${ownerPath(n.parent)}/${n.name}' : n.name);
|
||||
return ownerPathCache[n.id] ??= ((n.parent != info.root)
|
||||
? '${ownerPath(n.parent!)}/${n.name}'
|
||||
: n.name);
|
||||
}
|
||||
|
||||
final nameFormatter = _nameFormatters[format];
|
||||
|
||||
final nameFormatter = _nameFormatters[format]!;
|
||||
for (var node in snap.nodes) {
|
||||
if (node.selfSize > 0) {
|
||||
final owner = info.snapshotInfo.ownerOf(node);
|
||||
final owner = snapshotInfo.ownerOf(node);
|
||||
|
||||
final name = nameFormatter(node);
|
||||
final path = ownerPath(owner);
|
||||
|
@ -227,7 +230,7 @@ Map<String, dynamic> _addChild(
|
|||
}
|
||||
|
||||
/// Add the given symbol to the tree.
|
||||
void _addSymbol(Map<String, dynamic> root, String path, String name, int size,
|
||||
void _addSymbol(Map<String, dynamic> root, String path, String name, int? size,
|
||||
{String symbolType = symbolTypeGlobalText}) {
|
||||
if (size == null || size == 0) {
|
||||
return;
|
||||
|
|
|
@ -13,10 +13,11 @@ import 'package:vm_snapshot_analysis/v8_profile.dart' as v8_profile;
|
|||
ProgramInfo loadProgramInfoFromJson(Object json,
|
||||
{bool collapseAnonymousClosures = false}) {
|
||||
if (v8_profile.Snapshot.isV8HeapSnapshot(json)) {
|
||||
return v8_profile.toProgramInfo(v8_profile.Snapshot.fromJson(json),
|
||||
return v8_profile.toProgramInfo(
|
||||
v8_profile.Snapshot.fromJson(json as Map<String, dynamic>),
|
||||
collapseAnonymousClosures: collapseAnonymousClosures);
|
||||
} else {
|
||||
return instruction_sizes.loadProgramInfo(json,
|
||||
return instruction_sizes.loadProgramInfo(json as List<dynamic>,
|
||||
collapseAnonymousClosures: collapseAnonymousClosures);
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +63,7 @@ void printHistogram(ProgramInfo info, Histogram histogram,
|
|||
|
||||
final visibleRows = [prefix, suffix].expand((l) => l).toList();
|
||||
final visibleSize =
|
||||
visibleRows.fold(0, (sum, key) => sum + histogram.buckets[key]);
|
||||
visibleRows.fold<int>(0, (sum, key) => sum + histogram.buckets[key]!);
|
||||
final numRestRows = histogram.length - (suffix.length + prefix.length);
|
||||
final hiddenRows = Set<String>.from(histogram.bySize)
|
||||
.difference(Set<String>.from(visibleRows));
|
||||
|
@ -71,7 +72,7 @@ void printHistogram(ProgramInfo info, Histogram histogram,
|
|||
|
||||
if (prefix.isNotEmpty) {
|
||||
for (var key in prefix) {
|
||||
final size = histogram.buckets[key];
|
||||
final size = histogram.buckets[key]!;
|
||||
table.addRow([
|
||||
...histogram.bucketInfo.namesFromBucket(key),
|
||||
size.toString(),
|
||||
|
@ -99,7 +100,7 @@ void printHistogram(ProgramInfo info, Histogram histogram,
|
|||
table.addRow([
|
||||
...histogram.bucketInfo.namesFromBucket(key),
|
||||
histogram.buckets[key].toString(),
|
||||
formatPercent(histogram.buckets[key], histogram.totalSize),
|
||||
formatPercent(histogram.buckets[key]!, histogram.totalSize),
|
||||
]);
|
||||
}
|
||||
table.addSeparator(Separator.Line);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
/// produced by `--write-v8-snapshot-profile-to` VM flag.
|
||||
library vm_snapshot_analysis.v8_profile;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:vm_snapshot_analysis/src/dominators.dart' as dominators;
|
||||
import 'package:vm_snapshot_analysis/name.dart';
|
||||
|
@ -35,7 +35,7 @@ class Snapshot {
|
|||
/// for the given node index.
|
||||
final List<int> _edgesStartIndexForNode;
|
||||
|
||||
List<int> _dominators;
|
||||
late final List<int> _dominators = _computeDominators(this);
|
||||
|
||||
final List strings;
|
||||
|
||||
|
@ -53,7 +53,6 @@ class Snapshot {
|
|||
|
||||
/// Return dominator node for the given node [n].
|
||||
Node dominatorOf(Node n) {
|
||||
_dominators ??= _computeDominators(this);
|
||||
return nodeAt(_dominators[n.index]);
|
||||
}
|
||||
|
||||
|
@ -67,7 +66,7 @@ class Snapshot {
|
|||
// Extract meta information first.
|
||||
final meta = Meta._fromJson(m['snapshot']['meta']);
|
||||
|
||||
final nodes = m['nodes'];
|
||||
final nodes = (m['nodes'] as List<dynamic>).cast<int>();
|
||||
|
||||
// Build an array of starting indexes of edges for each node.
|
||||
final edgesStartIndexForNode = <int>[0];
|
||||
|
@ -130,18 +129,18 @@ class Meta {
|
|||
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});
|
||||
{required this.nodeTypeIndex,
|
||||
required this.nodeNameIndex,
|
||||
required this.nodeIdIndex,
|
||||
required this.nodeSelfSizeIndex,
|
||||
required this.nodeEdgeCountIndex,
|
||||
required this.nodeFieldCount,
|
||||
required this.edgeTypeIndex,
|
||||
required this.edgeNameOrIndexIndex,
|
||||
required this.edgeToNodeIndex,
|
||||
required this.edgeFieldCount,
|
||||
required this.nodeTypes,
|
||||
required this.edgeTypes});
|
||||
|
||||
factory Meta._fromJson(Map<String, dynamic> m) {
|
||||
final nodeFields = m['node_fields'];
|
||||
|
@ -171,7 +170,7 @@ class Edge {
|
|||
/// Index of this [Edge] within the [snapshot].
|
||||
final int index;
|
||||
|
||||
Edge._({this.snapshot, this.index});
|
||||
Edge._({required this.snapshot, required this.index});
|
||||
|
||||
String get type => snapshot
|
||||
.meta.edgeTypes[snapshot._edges[_offset + snapshot.meta.edgeTypeIndex]];
|
||||
|
@ -212,7 +211,7 @@ class Node {
|
|||
/// Index of this [Node] within the [snapshot].
|
||||
final int index;
|
||||
|
||||
Node._({this.snapshot, this.index});
|
||||
Node._({required this.snapshot, required this.index});
|
||||
|
||||
int get edgeCount =>
|
||||
snapshot._nodes[_offset + snapshot.meta.nodeEdgeCountIndex];
|
||||
|
@ -248,10 +247,8 @@ class Node {
|
|||
}
|
||||
|
||||
/// 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;
|
||||
Node? operator [](String edgeName) =>
|
||||
this.edges.firstWhereOrNull((e) => e.name == edgeName)?.target;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
|
@ -317,7 +314,7 @@ class _ProgramInfoBuilder {
|
|||
/// See [findCommonAncestor] method.
|
||||
final Map<int, int> commonAncestorCache = {};
|
||||
|
||||
_ProgramInfoBuilder({this.collapseAnonymousClosures});
|
||||
_ProgramInfoBuilder({required this.collapseAnonymousClosures});
|
||||
|
||||
/// Recover [ProgramInfo] structure from the snapshot profile.
|
||||
///
|
||||
|
@ -372,7 +369,7 @@ class _ProgramInfoBuilder {
|
|||
return program;
|
||||
}
|
||||
|
||||
ProgramInfoNode getInfoNodeFor(Node node) {
|
||||
ProgramInfoNode? getInfoNodeFor(Node node) {
|
||||
var info = infoNodeByIndex[node.index];
|
||||
if (info == null) {
|
||||
info = createInfoNodeFor(node);
|
||||
|
@ -390,7 +387,7 @@ class _ProgramInfoBuilder {
|
|||
switch (node.type) {
|
||||
case 'Code':
|
||||
// Freeze ownership of the Instructions object.
|
||||
final instructions = node['<instructions>'];
|
||||
final instructions = node['<instructions>']!;
|
||||
nodesWithFrozenOwner.add(instructions.index);
|
||||
ownerOf[instructions.index] =
|
||||
findCommonAncestor(ownerOf[instructions.index], info.id);
|
||||
|
@ -403,7 +400,7 @@ class _ProgramInfoBuilder {
|
|||
if (e.target.type == 'Script') {
|
||||
nodesWithFrozenOwner.add(e.target.index);
|
||||
ownerOf[e.target.index] =
|
||||
findCommonAncestor(ownerOf[e.target.index], info.id);
|
||||
findCommonAncestor(ownerOf[e.target.index]!, info.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -414,13 +411,13 @@ class _ProgramInfoBuilder {
|
|||
return info;
|
||||
}
|
||||
|
||||
ProgramInfoNode createInfoNodeFor(Node node) {
|
||||
ProgramInfoNode? createInfoNodeFor(Node node) {
|
||||
switch (node.type) {
|
||||
case 'Code':
|
||||
var owner = node['owner_'];
|
||||
final owner = node['owner_']!;
|
||||
if (owner.type != 'Type') {
|
||||
final ownerNode =
|
||||
owner.type == 'Null' ? program.stubs : getInfoNodeFor(owner);
|
||||
owner.type == 'Null' ? program.stubs : getInfoNodeFor(owner)!;
|
||||
if (owner.type == 'Function') {
|
||||
// For normal functions we just attribute Code object and all
|
||||
// objects dominated by it to the function itself.
|
||||
|
@ -436,29 +433,29 @@ class _ProgramInfoBuilder {
|
|||
|
||||
case 'Function':
|
||||
if (node.name != '<anonymous signature>') {
|
||||
var owner = node['owner_'];
|
||||
var owner = node['owner_']!;
|
||||
|
||||
// Artificial nodes may not have a data_ field.
|
||||
var data = node['data_'];
|
||||
if (data?.type == 'ClosureData') {
|
||||
owner = data['parent_function_'];
|
||||
if (data != null && data.type == 'ClosureData') {
|
||||
owner = data['parent_function_']!;
|
||||
}
|
||||
return makeInfoNode(node.index,
|
||||
name: node.name,
|
||||
parent: getInfoNodeFor(owner),
|
||||
parent: getInfoNodeFor(owner)!,
|
||||
type: NodeType.functionNode);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PatchClass':
|
||||
return getInfoNodeFor(node['patched_class_']);
|
||||
return getInfoNodeFor(node['patched_class_']!);
|
||||
|
||||
case 'Class':
|
||||
// Default to root node. Some builtin classes (void, dynamic) don't have
|
||||
// any information about their library written out.
|
||||
var ownerNode = program.root;
|
||||
if (node['library_'] != null) {
|
||||
ownerNode = getInfoNodeFor(node['library_']) ?? ownerNode;
|
||||
ownerNode = getInfoNodeFor(node['library_']!) ?? ownerNode;
|
||||
}
|
||||
|
||||
return makeInfoNode(node.index,
|
||||
|
@ -477,20 +474,16 @@ class _ProgramInfoBuilder {
|
|||
case 'Field':
|
||||
return makeInfoNode(node.index,
|
||||
name: node.name,
|
||||
parent: getInfoNodeFor(node['owner_']),
|
||||
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);
|
||||
|
||||
ProgramInfoNode makeInfoNode(int? index,
|
||||
{required ProgramInfoNode parent,
|
||||
required String name,
|
||||
required NodeType type}) {
|
||||
name = Name(name).scrubbed;
|
||||
if (collapseAnonymousClosures) {
|
||||
name = Name.collapse(name);
|
||||
|
@ -527,10 +520,10 @@ class _ProgramInfoBuilder {
|
|||
}
|
||||
|
||||
/// Returns id of a common ancestor between [ProgramInfoNode] with [idA] and
|
||||
/// [idB].
|
||||
int findCommonAncestor(int idA, int idB) {
|
||||
/// [idB]. At least either [idA] or [idB] are expected to be not null.
|
||||
int findCommonAncestor(int? idA, int? idB) {
|
||||
if (idA == null) {
|
||||
return idB;
|
||||
return idB!;
|
||||
}
|
||||
if (idB == null) {
|
||||
return idA;
|
||||
|
@ -558,9 +551,8 @@ class _ProgramInfoBuilder {
|
|||
|
||||
static List<ProgramInfoNode> pathToRoot(ProgramInfoNode node) {
|
||||
final path = <ProgramInfoNode>[];
|
||||
while (node != null) {
|
||||
path.add(node);
|
||||
node = node.parent;
|
||||
for (ProgramInfoNode? n = node; n != null; n = n.parent) {
|
||||
path.add(n);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
@ -609,14 +601,15 @@ structure nodes (usually VM internal objects).
|
|||
/// The code for dominator tree computation is taken verbatim from the
|
||||
/// native compiler (see runtime/vm/compiler/backend/flow_graph.cc).
|
||||
List<int> _computeDominators(Snapshot snap) {
|
||||
final predecessors = List<Object>.filled(snap.nodeCount, null);
|
||||
final predecessors = List<Object?>.filled(snap.nodeCount, null);
|
||||
void addPred(int n, int p) {
|
||||
if (predecessors[n] == null) {
|
||||
final pred = predecessors[n];
|
||||
if (pred == null) {
|
||||
predecessors[n] = p;
|
||||
} else if (predecessors[n] is int) {
|
||||
predecessors[n] = <int>[predecessors[n], p];
|
||||
} else if (pred is int) {
|
||||
predecessors[n] = <int>[pred, p];
|
||||
} else {
|
||||
(predecessors[n] as List<int>).add(p);
|
||||
(pred as List<int>).add(p);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ version: 0.6.0
|
|||
homepage: https://github.com/dart-lang/sdk/tree/master/pkg/vm_snapshot_analysis
|
||||
|
||||
environment:
|
||||
sdk: '>=2.8.0 <3.0.0'
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
|
||||
executables:
|
||||
snapshot_analysis: analyse
|
||||
|
@ -13,7 +13,7 @@ executables:
|
|||
dependencies:
|
||||
args: ^2.0.0
|
||||
path: ^1.8.0
|
||||
meta: ^1.3.0
|
||||
collection: ^1.15.0
|
||||
|
||||
dev_dependencies:
|
||||
pedantic: ^1.11.0
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:vm_snapshot_analysis/instruction_sizes.dart'
|
||||
as instruction_sizes;
|
||||
import 'package:vm_snapshot_analysis/program_info.dart';
|
||||
|
@ -242,7 +244,7 @@ void main() async {
|
|||
test('basic-parsing', () async {
|
||||
await withSymbolSizes(testSource, (sizesJson) async {
|
||||
final json = await loadJson(File(sizesJson));
|
||||
final symbols = instruction_sizes.fromJson(json);
|
||||
final symbols = instruction_sizes.fromJson(json as List<dynamic>);
|
||||
expect(symbols, isNotNull,
|
||||
reason: 'Sizes file was successfully parsed');
|
||||
expect(symbols.length, greaterThan(0),
|
||||
|
@ -254,7 +256,7 @@ void main() async {
|
|||
final symbolScrubbedNames = <String, Map<String, Set<String>>>{};
|
||||
|
||||
Set<String> getSetOfNames(Map<String, Map<String, Set<String>>> map,
|
||||
String libraryUri, String className) {
|
||||
String? libraryUri, String? className) {
|
||||
return map
|
||||
.putIfAbsent(libraryUri ?? '', () => {})
|
||||
.putIfAbsent(className ?? '', () => {});
|
||||
|
@ -280,10 +282,12 @@ void main() async {
|
|||
expect(inputDartSymbolNames, isNotNull,
|
||||
reason: 'Symbols from input.dart are included into sizes output');
|
||||
|
||||
inputDartSymbolNames!; // Checked above. Promote the variable type.
|
||||
|
||||
expect(inputDartSymbolNames[''], isNotNull,
|
||||
reason: 'Should include top-level members from input.dart');
|
||||
expect(inputDartSymbolNames[''], contains('makeSomeClosures'));
|
||||
final closures = inputDartSymbolNames[''].where(
|
||||
final closures = inputDartSymbolNames['']!.where(
|
||||
(name) => name.startsWith('makeSomeClosures.<anonymous closure'));
|
||||
expect(closures.length, 3,
|
||||
reason: 'There are three closures inside makeSomeClosure');
|
||||
|
@ -311,7 +315,7 @@ void main() async {
|
|||
expect(inputDartSymbolNames['D'], contains('[tear-off] tornOff'));
|
||||
|
||||
// Check that output does not contain '[unknown stub]'
|
||||
expect(symbolRawNames[''][''], isNot(contains('[unknown stub]')),
|
||||
expect(symbolRawNames['']![''], isNot(contains('[unknown stub]')),
|
||||
reason: 'All stubs must be named');
|
||||
});
|
||||
});
|
||||
|
@ -324,19 +328,18 @@ void main() async {
|
|||
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);
|
||||
final inputLib = info.root.children['package:input']!
|
||||
.children['package:input/input.dart']!;
|
||||
expect(inputLib.children, contains('')); // Top-level class.
|
||||
expect(inputLib.children, contains('A'));
|
||||
expect(inputLib.children, contains('B'));
|
||||
expect(inputLib.children, contains('C'));
|
||||
expect(inputLib.children, contains('D'));
|
||||
|
||||
final topLevel = inputLib.children[''];
|
||||
final topLevel = inputLib.children['']!;
|
||||
expect(topLevel.children, contains('makeSomeClosures'));
|
||||
expect(
|
||||
topLevel.children['makeSomeClosures'].children.length, equals(3));
|
||||
topLevel.children['makeSomeClosures']!.children.length, equals(3));
|
||||
|
||||
for (var name in [
|
||||
'[tear-off] tornOff',
|
||||
|
@ -344,8 +347,8 @@ void main() async {
|
|||
'Allocate A',
|
||||
'[tear-off-extractor] get:tornOff'
|
||||
]) {
|
||||
expect(inputLib.children['A'].children, contains(name));
|
||||
expect(inputLib.children['A'].children[name].children, isEmpty);
|
||||
expect(inputLib.children['A']!.children, contains(name));
|
||||
expect(inputLib.children['A']!.children[name]!.children, isEmpty);
|
||||
}
|
||||
|
||||
for (var name in [
|
||||
|
@ -354,13 +357,13 @@ void main() async {
|
|||
'Allocate B',
|
||||
'[tear-off-extractor] get:tornOff'
|
||||
]) {
|
||||
expect(inputLib.children['B'].children, contains(name));
|
||||
expect(inputLib.children['B'].children[name].children, isEmpty);
|
||||
expect(inputLib.children['B']!.children, contains(name));
|
||||
expect(inputLib.children['B']!.children[name]!.children, isEmpty);
|
||||
}
|
||||
|
||||
for (var name in ['tornOff{body}', 'tornOff', '[tear-off] tornOff']) {
|
||||
expect(inputLib.children['C'].children, contains(name));
|
||||
expect(inputLib.children['C'].children[name].children, isEmpty);
|
||||
expect(inputLib.children['C']!.children, contains(name));
|
||||
expect(inputLib.children['C']!.children[name]!.children, isEmpty);
|
||||
}
|
||||
|
||||
for (var name in [
|
||||
|
@ -369,8 +372,8 @@ void main() async {
|
|||
'tornOff',
|
||||
'[tear-off] tornOff'
|
||||
]) {
|
||||
expect(inputLib.children['D'].children, contains(name));
|
||||
expect(inputLib.children['D'].children[name].children, isEmpty);
|
||||
expect(inputLib.children['D']!.children, contains(name));
|
||||
expect(inputLib.children['D']!.children[name]!.children, isEmpty);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -435,6 +438,64 @@ void main() async {
|
|||
});
|
||||
});
|
||||
|
||||
test('histograms-filter', () async {
|
||||
await withSymbolSizes(testSource, (sizesJson) async {
|
||||
final json = await loadJson(File(sizesJson));
|
||||
final info = loadProgramInfoFromJson(json);
|
||||
|
||||
{
|
||||
final h = computeHistogram(info, HistogramType.bySymbol,
|
||||
filter: 'A.tornOff');
|
||||
expect(
|
||||
h.buckets,
|
||||
contains(h.bucketFor('package:input', 'package:input/input.dart',
|
||||
'A', 'tornOff')));
|
||||
expect(
|
||||
h.buckets,
|
||||
isNot(contains(h.bucketFor('package:input',
|
||||
'package:input/input.dart', 'B', 'tornOff'))));
|
||||
}
|
||||
|
||||
{
|
||||
final h = computeHistogram(info, HistogramType.bySymbol,
|
||||
filter: '::A*tornOff');
|
||||
expect(
|
||||
h.buckets,
|
||||
contains(h.bucketFor('package:input', 'package:input/input.dart',
|
||||
'A', 'tornOff')));
|
||||
expect(
|
||||
h.buckets,
|
||||
isNot(contains(h.bucketFor('package:input',
|
||||
'package:input/input.dart', 'B', 'tornOff'))));
|
||||
}
|
||||
|
||||
{
|
||||
final h = computeHistogram(info, HistogramType.bySymbol,
|
||||
filter: 'input.dart*tornOff');
|
||||
expect(
|
||||
h.buckets,
|
||||
contains(h.bucketFor('package:input', 'package:input/input.dart',
|
||||
'A', 'tornOff')));
|
||||
expect(
|
||||
h.buckets,
|
||||
contains(h.bucketFor('package:input', 'package:input/input.dart',
|
||||
'B', 'tornOff')));
|
||||
}
|
||||
|
||||
{
|
||||
final h =
|
||||
computeHistogram(info, HistogramType.byPackage, filter: 'A');
|
||||
expect(
|
||||
h.buckets,
|
||||
contains(h.bucketFor(
|
||||
'package:input',
|
||||
'package:input/does-not-matter.dart',
|
||||
'does-not-matter',
|
||||
'does-not-matter')));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('diff', () async {
|
||||
await withSymbolSizes(testSource, (sizesJson) async {
|
||||
await withSymbolSizes(testSourceModified, (modifiedSizesJson) async {
|
||||
|
@ -534,18 +595,17 @@ void main() async {
|
|||
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);
|
||||
final inputLib = info.root.children['package:input']!
|
||||
.children['package:input/input.dart']!;
|
||||
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['::'];
|
||||
final topLevel = inputLib.children['::']!;
|
||||
expect(topLevel.children, contains('makeSomeClosures'));
|
||||
expect(
|
||||
topLevel.children['makeSomeClosures'].children.values
|
||||
topLevel.children['makeSomeClosures']!.children.values
|
||||
.where((child) => child.type == NodeType.functionNode)
|
||||
.length,
|
||||
equals(3));
|
||||
|
@ -555,9 +615,9 @@ void main() async {
|
|||
'Allocate A',
|
||||
'[tear-off-extractor] get:tornOff'
|
||||
]) {
|
||||
expect(inputLib.children['A'].children, contains(name));
|
||||
expect(inputLib.children['A']!.children, contains(name));
|
||||
}
|
||||
expect(inputLib.children['A'].children['tornOff'].children,
|
||||
expect(inputLib.children['A']!.children['tornOff']!.children,
|
||||
contains('[tear-off] tornOff'));
|
||||
|
||||
for (var name in [
|
||||
|
@ -565,35 +625,35 @@ void main() async {
|
|||
'Allocate B',
|
||||
'[tear-off-extractor] get:tornOff'
|
||||
]) {
|
||||
expect(inputLib.children['B'].children, contains(name));
|
||||
expect(inputLib.children['B']!.children, contains(name));
|
||||
}
|
||||
expect(inputLib.children['B'].children['tornOff'].children,
|
||||
expect(inputLib.children['B']!.children['tornOff']!.children,
|
||||
contains('[tear-off] tornOff'));
|
||||
|
||||
final classC = inputLib.children['C'];
|
||||
final classC = inputLib.children['C']!;
|
||||
expect(classC.children, contains('tornOff'));
|
||||
for (var name in ['tornOff{body}', '[tear-off] tornOff']) {
|
||||
expect(classC.children['tornOff'].children, contains(name));
|
||||
expect(classC.children['tornOff']!.children, contains(name));
|
||||
}
|
||||
|
||||
// Verify that [ProgramInfoNode] owns its corresponding snapshot [Node].
|
||||
final classesOwnedByC = info.snapshotInfo.snapshot.nodes
|
||||
.where((n) => info.snapshotInfo.ownerOf(n) == classC)
|
||||
final classesOwnedByC = info.snapshotInfo!.snapshot.nodes
|
||||
.where((n) => info.snapshotInfo!.ownerOf(n) == classC)
|
||||
.where((n) => n.type == 'Class')
|
||||
.map((n) => n.name);
|
||||
expect(classesOwnedByC, equals(['C']));
|
||||
|
||||
final classD = inputLib.children['D'];
|
||||
final classD = inputLib.children['D']!;
|
||||
expect(classD.children, contains('tornOff'));
|
||||
for (var name in ['tornOff{body}', '[tear-off] tornOff']) {
|
||||
expect(classD.children['tornOff'].children, contains(name));
|
||||
expect(classD.children['tornOff']!.children, contains(name));
|
||||
}
|
||||
expect(classD.children['tornOff'].children['tornOff{body}'].children,
|
||||
expect(classD.children['tornOff']!.children['tornOff{body}']!.children,
|
||||
contains('tornOff{body depth 2}'));
|
||||
|
||||
// Verify that [ProgramInfoNode] owns its corresponding snapshot [Node].
|
||||
final classesOwnedByD = info.snapshotInfo.snapshot.nodes
|
||||
.where((n) => info.snapshotInfo.ownerOf(n) == classD)
|
||||
final classesOwnedByD = info.snapshotInfo!.snapshot.nodes
|
||||
.where((n) => info.snapshotInfo!.ownerOf(n) == classD)
|
||||
.where((n) => n.type == 'Class')
|
||||
.map((n) => n.name);
|
||||
expect(classesOwnedByD, equals(['D']));
|
||||
|
@ -616,20 +676,20 @@ void main() async {
|
|||
'::',
|
||||
'makeSomeClosures'
|
||||
]);
|
||||
final codeNode = fromProfile.snapshotInfo.snapshot.nodes.firstWhere(
|
||||
final codeNode = fromProfile.snapshotInfo!.snapshot.nodes.firstWhere(
|
||||
(n) =>
|
||||
n.type == 'Code' &&
|
||||
fromProfile.snapshotInfo.ownerOf(n) == functionNode &&
|
||||
fromProfile.snapshotInfo!.ownerOf(n) == functionNode &&
|
||||
n.name.contains('makeSomeClosures'));
|
||||
expect(codeNode['<instructions>'], isNotNull);
|
||||
final instructionsSize = codeNode['<instructions>'].selfSize;
|
||||
final instructionsSize = codeNode['<instructions>']!.selfSize;
|
||||
final symbolSize = fromSymbolSizes.lookup([
|
||||
'package:input',
|
||||
'package:input/input.dart',
|
||||
'',
|
||||
'makeSomeClosures'
|
||||
]).size;
|
||||
expect(instructionsSize - symbolSize, equals(0));
|
||||
])!.size;
|
||||
expect(instructionsSize - symbolSize!, equals(0));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -786,9 +846,10 @@ void main() async {
|
|||
|
||||
String nameOf(Map<String, dynamic> node) => node['n'];
|
||||
|
||||
Map<String, dynamic> findChild(Map<String, dynamic> node, String name) {
|
||||
Map<String, dynamic>? findChild(
|
||||
Map<String, dynamic> node, String name) {
|
||||
return childrenOf(node)
|
||||
.firstWhere((child) => nameOf(child) == name, orElse: () => null);
|
||||
.firstWhereOrNull((child) => nameOf(child) == name);
|
||||
}
|
||||
|
||||
Set<String> childrenNames(Map<String, dynamic> node) {
|
||||
|
@ -802,7 +863,7 @@ void main() async {
|
|||
// for some reason.
|
||||
expect(findChild(treemap, 'package:input/input.dart'), isNotNull);
|
||||
} else {
|
||||
expect(childrenNames(findChild(treemap, 'package:input')),
|
||||
expect(childrenNames(findChild(treemap, 'package:input')!),
|
||||
equals({'main.dart', 'input.dart'}));
|
||||
}
|
||||
});
|
||||
|
@ -813,7 +874,7 @@ void main() async {
|
|||
// Note: computing dominators also verifies that we don't have
|
||||
// unreachable nodes in the snapshot.
|
||||
final infoJson = await loadJson(File(profileJson));
|
||||
final snapshot = Snapshot.fromJson(infoJson);
|
||||
final snapshot = Snapshot.fromJson(infoJson as Map<String, dynamic>);
|
||||
for (var n in snapshot.nodes.skip(1)) {
|
||||
expect(snapshot.dominatorOf(n), isNotNull);
|
||||
}
|
||||
|
@ -839,7 +900,7 @@ Future withV8Profile(
|
|||
// simply ignore entry point library (main.dart).
|
||||
// Additionally this function removes all nodes with the size below
|
||||
// the given threshold.
|
||||
Map<String, dynamic> diffToJson(ProgramInfo diff,
|
||||
Map<String, dynamic>? diffToJson(ProgramInfo diff,
|
||||
{bool keepOnlyInputPackage = false}) {
|
||||
final diffJson = diff.toJson();
|
||||
diffJson.removeWhere((key, _) =>
|
||||
|
@ -847,7 +908,7 @@ Map<String, dynamic> diffToJson(ProgramInfo diff,
|
|||
|
||||
// Rebuild the diff JSON discarding all nodes with size below threshold.
|
||||
const smallChangeThreshold = 16;
|
||||
Map<String, dynamic> discardSmallChanges(Map<String, dynamic> map) {
|
||||
Map<String, dynamic>? discardSmallChanges(Map<String, dynamic> map) {
|
||||
final result = <String, dynamic>{};
|
||||
|
||||
// First recursively process all children (skipping #type and #size keys).
|
||||
|
|
|
@ -75,7 +75,7 @@ void main() async {
|
|||
callGraph.computeDominators();
|
||||
|
||||
final main = callGraph.program
|
||||
.lookup(['package:input', 'package:input/input.dart', '', 'main']);
|
||||
.lookup(['package:input', 'package:input/input.dart', '', 'main'])!;
|
||||
final mainNode = callGraph.lookup(main);
|
||||
|
||||
final retainedClasses = mainNode.dominated
|
||||
|
|
|
@ -24,7 +24,7 @@ class AotSnapshot {
|
|||
final String outputBinary;
|
||||
final String sizesJson;
|
||||
|
||||
AotSnapshot({this.outputBinary, this.sizesJson});
|
||||
AotSnapshot({required this.outputBinary, required this.sizesJson});
|
||||
}
|
||||
|
||||
Future withFlag(
|
||||
|
@ -33,7 +33,7 @@ Future withFlag(
|
|||
}
|
||||
|
||||
Future withFlagImpl(
|
||||
Map<String, String> source, String flag, Future Function(AotSnapshot) f) {
|
||||
Map<String, String> source, String? flag, Future Function(AotSnapshot) f) {
|
||||
return withTempDir((dir) async {
|
||||
final snapshot = AotSnapshot(
|
||||
outputBinary: path.join(dir, 'output.exe'),
|
||||
|
@ -90,7 +90,8 @@ stderr: ${result.stderr}
|
|||
});
|
||||
}
|
||||
|
||||
const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES';
|
||||
late final shouldKeepTemporaryDirectories =
|
||||
Platform.environment['KEEP_TEMPORARY_DIRECTORIES']?.isNotEmpty == true;
|
||||
|
||||
Future withTempDir(Future Function(String dir) f) async {
|
||||
final tempDir =
|
||||
|
@ -98,17 +99,16 @@ Future withTempDir(Future Function(String dir) f) async {
|
|||
try {
|
||||
await f(tempDir.path);
|
||||
} finally {
|
||||
if (!Platform.environment.containsKey(keepTempKey) ||
|
||||
Platform.environment[keepTempKey].isEmpty) {
|
||||
if (shouldKeepTemporaryDirectories) {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Object> loadJson(File input) async {
|
||||
return await input
|
||||
return (await input
|
||||
.openRead()
|
||||
.transform(utf8.decoder)
|
||||
.transform(json.decoder)
|
||||
.first;
|
||||
.first)!;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue