[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:
Vyacheslav Egorov 2021-04-15 12:03:47 +00:00 committed by commit-bot@chromium.org
parent 1bab406d8e
commit 458be23fa7
19 changed files with 394 additions and 304 deletions

View file

@ -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",

View file

@ -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);

View file

@ -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;

View file

@ -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

View file

@ -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);

View file

@ -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']);

View file

@ -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),

View file

@ -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.

View file

@ -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();
}
}

View file

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

View file

@ -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)!;
}

View file

@ -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});
}

View file

@ -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;

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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

View file

@ -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).

View file

@ -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

View file

@ -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)!;
}