// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. /// Classes for representing information about the program structure. library; import 'package:vm_snapshot_analysis/v8_profile.dart'; /// Represents information about compiled program. class ProgramInfo { static const int rootId = 0; static const int stubsId = 1; static const int unknownId = 2; final ProgramInfoNode root; final ProgramInfoNode stubs; final ProgramInfoNode unknown; int _nextId = 3; /// V8 snapshot profile if this [ProgramInfo] object was created from an /// output of `--write-v8-snapshot-profile-to=...` flag. SnapshotInfo? snapshotInfo; ProgramInfo._(this.root, this.stubs, this.unknown); factory ProgramInfo() { final ProgramInfoNode root = ProgramInfoNode._( id: rootId, name: '@shared', type: NodeType.libraryNode, parent: null); final ProgramInfoNode stubs = ProgramInfoNode._( id: stubsId, name: '@stubs', type: NodeType.libraryNode, parent: root); root.children[stubs.name] = stubs; final ProgramInfoNode unknown = ProgramInfoNode._( id: unknownId, name: '@unknown', type: NodeType.libraryNode, parent: root); root.children[unknown.name] = unknown; return ProgramInfo._(root, stubs, unknown); } ProgramInfoNode makeNode( {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) callback) { final context = List.filled(NodeType.values.length, null); void recurse(ProgramInfoNode node) { final prevContext = context[node._type]; if (prevContext != null && node._type == NodeType.functionNode.index) { context[node._type] = '$prevContext.${node.name}'; } else { context[node._type] = node.name; } 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); } context[node._type] = prevContext; } recurse(root); } /// Total size of all the nodes in the program. int get totalSize => root.totalSize; /// Convert this program info to a JSON map using [infoToJson] to convert /// data attached to nodes into its JSON representation. Map toJson() => root.toJson(); /// Lookup a node in the program given a path to it. ProgramInfoNode? lookup(List path) { var n = root; for (var p in path) { final next = n.children[p]; if (next == null) { return null; } n = next; } return n; } } enum NodeType { packageNode, libraryNode, classNode, functionNode, other, } String _typeToJson(NodeType type) => const { NodeType.packageNode: 'package', NodeType.libraryNode: 'library', NodeType.classNode: 'class', NodeType.functionNode: 'function', NodeType.other: 'other', }[type]!; class ProgramInfoNode { final int id; final String name; final ProgramInfoNode? parent; final Map children = {}; final int _type; /// Number of bytes in the snapshot which can be attributed to this node. /// /// Note that this is neither a shallow size of the object that represents /// this node in the AOT snapshot, nor a cumulative size of its children. /// Instead this size is similar to _retained size_ used by heap profiling /// tools. Snapshot nodes corresponding to the info nodes are viewed as /// a dominator tree and all nodes in the snapshot are partitioned based /// on which node of this tree dominates them. /// /// Consider for example the following Dart code: /// /// ```dart /// class C { /// void f() { use("something"); } /// void g() { use("something"); } /// } /// ``` /// /// 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; ProgramInfoNode._( {required this.id, required this.name, required this.parent, required NodeType type}) : _type = type.index; NodeType get type => NodeType.values[_type]; Map toJson() => { if (size != null) '#size': size, if (_type != NodeType.other.index) '#type': _typeToJson(type), if (children.isNotEmpty) for (var clo in children.entries) clo.key: clo.value.toJson() }; /// Returns the name of this node prefixed by the [qualifiedName] of its /// [parent]. String get qualifiedName { var prefix = ''; // Do not include root name or package name (library uri already contains // package name). final p = parent; if (p != null && p.parent != null && p.type != NodeType.packageNode) { prefix = p.qualifiedName; if (p.type != NodeType.libraryNode) { prefix += '.'; } else { prefix += '::'; } } return '$prefix$name'; } @override String toString() { return '${_typeToJson(type)} $qualifiedName'; } /// Returns path to this node such that [ProgramInfo.lookup] would return /// this node given its [path]. List get path { final result = []; var n = this; while (n.parent != null) { result.add(n.name); n = n.parent!; } return result.reversed.toList(); } /// Cumulative size of this node and all of its children. int get totalSize { return (size ?? 0) + children.values.fold(0, (s, n) => s + n.totalSize); } } /// Computes the size difference between two [ProgramInfo]. ProgramInfo computeDiff(ProgramInfo oldInfo, ProgramInfo newInfo) { final programDiff = ProgramInfo(); var path = []; 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] as String; final type = path[i + 1] as NodeType; diffNode = programDiff.makeNode(name: name, parent: diffNode, type: type); } 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)!); recurse(oldChildNode, newChildNode); path.removeLast(); path.removeLast(); } } recurse(oldInfo.root, newInfo.root); return programDiff; } Iterable _allKeys(Map? a, Map? b) { return {...?a?.keys, ...?b?.keys}; } class Histogram { /// Rule used to produce this histogram. Specifies how bucket names /// are constructed given (library-uri,class-name,function-name) tuples and /// how these bucket names can be deconstructed back into human readable form. final BucketInfo bucketInfo; /// Histogram buckets. final Map buckets; /// Bucket names sorted by the size of the corresponding bucket in descending /// order. final List bySize; final int totalSize; int get length => bySize.length; Histogram._(this.bucketInfo, this.buckets) : bySize = buckets.keys.toList(growable: false) ..sort((a, b) => buckets[b]! - buckets[a]!), totalSize = buckets.values.fold(0, (sum, size) => sum + size); static Histogram fromIterable( Iterable entries, { required int Function(T) sizeOf, required String Function(T) bucketFor, required BucketInfo bucketInfo, }) { final buckets = {}; for (var e in entries) { final bucket = bucketFor(e); final size = sizeOf(e); buckets[bucket] = (buckets[bucket] ?? 0) + size; } return Histogram._(bucketInfo, buckets); } /// Rebuckets the histogram given the new bucketing rule. Histogram map(String Function(String) bucketFor) { return Histogram.fromIterable(buckets.keys, 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; if (filter != null) { final re = RegExp(filter.replaceAll('*', '.*'), caseSensitive: false); matchesFilter = (lib, cls, fun) => re.hasMatch("$lib::${cls ?? ''}.${fun ?? ''}"); } else { matchesFilter = (_, __, ___) => true; } if (type == HistogramType.byNodeType) { final Set filteredNodes = {}; if (filter != null) { info.visit((pkg, lib, cls, fun, node) { if (matchesFilter(lib, cls, fun)) { filteredNodes.add(node.id); } }); } final snapshotInfo = info.snapshotInfo!; return Histogram.fromIterable( snapshotInfo.snapshot.nodes.where((n) => filter == null || filteredNodes.contains(snapshotInfo.ownerOf(n).id)), sizeOf: (n) { return n.selfSize; }, bucketFor: (n) => n.type, bucketInfo: const BucketInfo(nameComponents: ['Type'])); } else { final buckets = {}; final bucketing = Bucketing._forType[type]!; info.visit((pkg, lib, cls, fun, node) { 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) + sz; }); return Histogram._(bucketing, buckets); } } enum HistogramType { bySymbol, byClass, byLibrary, byPackage, byNodeType, } class BucketInfo { /// Specifies which human readable name components can be extracted from /// the bucket name. final List nameComponents; /// Deconstructs bucket name into human readable components (the order matches /// one returned by [nameComponents]). List namesFromBucket(String bucket) => [bucket]; 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); const Bucketing({required super.nameComponents}); static const _forType = { HistogramType.bySymbol: _BucketBySymbol(), HistogramType.byClass: _BucketByClass(), HistogramType.byLibrary: _BucketByLibrary(), HistogramType.byPackage: _BucketByPackage(), }; } /// A combination of characters that is unlikely to occur in the symbol name. const String _nameSeparator = ';;;'; class _BucketBySymbol extends Bucketing { @override String bucketFor(String? pkg, String lib, String? cls, String? fun) { if (fun == null) { return '@other$_nameSeparator'; } return '$lib$_nameSeparator${cls ?? ''}${cls != '' && cls != null ? '.' : ''}$fun'; } @override List namesFromBucket(String bucket) => bucket.split(_nameSeparator); const _BucketBySymbol() : super(nameComponents: const ['Library', 'Symbol']); } class _BucketByClass extends Bucketing { @override String bucketFor(String? pkg, String lib, String? cls, String? fun) { if (cls == null) { return '@other$_nameSeparator'; } return '$lib$_nameSeparator$cls'; } @override List namesFromBucket(String bucket) => bucket.split(_nameSeparator); const _BucketByClass() : super(nameComponents: const ['Library', 'Class']); } class _BucketByLibrary extends Bucketing { @override 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) => pkg ?? lib; const _BucketByPackage() : super(nameComponents: const ['Package']); } String packageOf(String lib) { if (lib.startsWith('package:')) { final separatorPos = lib.indexOf('/'); return lib.substring(0, separatorPos); } else { return lib; } }