// Copyright (c) 2014, 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. library object_graph; import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; /// Decodes and analyzes heap snapshots produced by the Dart VM. abstract class SnapshotReader { factory SnapshotReader() => _SnapshotReader._new(); void add(Uint8List chunk); Future close(); Future get done; Stream get onProgress; } class _SnapshotReader implements SnapshotReader { bool _closed = false; List? _chunks = []; final _onProgress = new StreamController.broadcast(); final _done = new Completer(); _SnapshotReader._new(); void add(Uint8List chunk) { if (_closed) { throw new StateError("Stream is closed"); } _chunks!.add(chunk); _onProgress.add("Receiving snapshot chunk ${_chunks!.length}..."); // TODO(rmacnak): Incremental loading. } Future close() { if (_closed) { throw new StateError("Stream is closed"); } _closed = true; var graph = new _SnapshotGraph._new(); var chunks = _chunks!; _chunks = null; // Let the binary chunks be GCable. _done.complete(graph._load(chunks, _onProgress)); return _done.future; } Future get done => _done.future; Stream get onProgress => _onProgress.stream; } Uint8List _newUint8Array(int size) { try { return new Uint8List(size); } on ArgumentError { // JS throws a misleading invalid argument error. Convert to a more user-friendly message. throw new Exception( "OutOfMemoryError: Not enough memory available to analyze the snapshot.", ); } } Uint16List _newUint16Array(int size) { try { return new Uint16List(size); } on ArgumentError { // JS throws a misleading invalid argument error. Convert to a more user-friendly message. throw new Exception( "OutOfMemoryError: Not enough memory available to analyze the snapshot.", ); } } Uint32List _newUint32Array(int size) { try { return new Uint32List(size); } on ArgumentError { // JS throws a misleading invalid argument error. Convert to a more user-friendly message. throw new Exception( "OutOfMemoryError: Not enough memory available to analyze the snapshot.", ); } } class _ReadStream { final List _buffers; Uint8List _currentBuffer = Uint8List(0); int _bufferIndex = 0; int _byteIndex = 0; _ReadStream._new(this._buffers); bool atEnd() { return _bufferIndex >= _buffers.length && _byteIndex >= _currentBuffer.length; } int readByte() { int i = _byteIndex; Uint8List b = _currentBuffer; if (i < b.length) { int r = b[i]; _byteIndex = i + 1; return r; } return _readByteSlowPath(); } int _readByteSlowPath() { int i = _byteIndex; Uint8List b = _currentBuffer; while (i >= b.length) { if (_bufferIndex >= _buffers.length) { throw new StateError("Attempt to read past the end of a stream"); } b = _currentBuffer = _buffers[_bufferIndex++]; i = 0; } int r = b[i]; _byteIndex = i + 1; return r; } /// Read one ULEB128 number. int readUnsigned() { int result = 0; int shift = 0; for (;;) { int part = readByte(); result |= (part & 0x7F) << shift; if ((part & 0x80) == 0) { break; } shift += 7; } return result; } /// Read one SLEB128 number. int readSigned() { int result = 0; int shift = 0; for (;;) { int part = readByte(); result |= (part & 0x7F) << shift; shift += 7; if ((part & 0x80) == 0) { if ((part & 0x40) != 0) { result |= (-1 << shift); } break; } } return result; } double readFloat64() { final bytes = _newUint8Array(8); for (var i = 0; i < 8; i++) { bytes[i] = readByte(); } return new Float64List.view(bytes.buffer)[0]; } String readUtf8() { final len = readUnsigned(); final bytes = _newUint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = readByte(); } return new Utf8Codec(allowMalformed: true).decode(bytes); } String readLatin1() { final len = readUnsigned(); final codeUnits = _newUint8Array(len); for (var i = 0; i < len; i++) { codeUnits[i] = readByte(); } return new String.fromCharCodes(codeUnits); } String readUtf16() { final len = readUnsigned(); final codeUnits = _newUint16Array(len); for (var i = 0; i < len; i++) { codeUnits[i] = readByte() | (readByte() << 8); } return new String.fromCharCodes(codeUnits); } } // Node indices for the root and sentinel nodes. Note that using 0 as the // sentinel means a newly allocated typed array comes initialized with all // elements as the sentinel. const _ROOT = 1; const _SENTINEL = 0; /// An object in a heap snapshot. abstract class SnapshotObject { /// The identity hash code of this object, used to compare objects across /// snapshots. If [identityHashCode] is 0, this object cannot be compared to /// other objects. int get identityHashCode; /// If this object has been obtained from [successors] or [predecessors], the /// name of slot. Otherwise, the empty string. String get label; /// The value for primitives. Otherwise, the class name. String get description; /// [internalSize] + [externalSize]. int get shallowSize; /// The number of bytes in the Dart heap occupied by this object. May be 0 /// for objects that are in another heap but referenced from the heap of /// interest. May also be 0 for synthetic objects such as the root. int get internalSize; /// The sum of all external allocations associated with this object. /// See Dart_NewFinalizableHandle and Dart_NewWeakPersistentHandle. int get externalSize; /// The [shallowSize] of this object, plus the retainedSize of all its /// children in the dominator tree. This is the amount of memory that would /// be freed if the last reference to this object was erased. int get retainedSize; SnapshotClass get klass; /// The objects directly referenced by this object. The [SnapshotObject]s /// returned by this iterable have their [label] set to name of the slot /// if it is available. Iterable get successors; /// The objects directly referencing this object. The [SnapshotObject]s /// returned by this iterable have their [label] set to name of the slot /// if it is available. Iterable get predecessors; /// The immediate dominator of this object. For the root object, returns self. /// /// See https://en.wikipedia.org/wiki/Dominator_(graph_theory). SnapshotObject get parent; /// The objects for which this object is the immediate dominator. /// /// See https://en.wikipedia.org/wiki/Dominator_(graph_theory). Iterable get children; /// An iterable containing only this object. For polymorphism with /// SnapshotMergedDominators. Iterable get objects; } class _SnapshotObject implements SnapshotObject { final int _id; final _SnapshotGraph _graph; final String label; _SnapshotObject._new(this._id, this._graph, this.label); bool operator ==(Object other) { if (other is _SnapshotObject) { return _id == other._id && _graph == other._graph; } return false; } int get hashCode => _id ^ _graph.hashCode; int get identityHashCode => _graph._identityHashes![_id]; int get shallowSize => internalSize + externalSize; int get internalSize => _graph._internalSizes![_id]; int get externalSize => _graph._externalSizes![_id]; int get retainedSize => _graph._retainedSizes![_id]; String get description => _graph._describeObject(_id); SnapshotClass get klass => _graph._classes![_graph._cids![_id]]!; Iterable get successors sync* { final id = _id; final cid = _graph._cids![id]; final startSuccIndex = _graph._firstSuccs![id]; final limitSuccIndex = _graph._firstSuccs![id + 1]; for ( var nextSuccIndex = startSuccIndex; nextSuccIndex < limitSuccIndex; nextSuccIndex++ ) { final index = nextSuccIndex - startSuccIndex; final succId = _graph._succs![nextSuccIndex]; final name = _graph._edgeName(cid, index); yield _SnapshotObject._new(succId, _graph, name); } } Iterable get predecessors sync* { var firstSuccs = _graph._firstSuccs!; var succs = _graph._succs!; var id = _id; var N = _graph._N!; for (var predId = 1; predId <= N; predId++) { var base = firstSuccs[predId]; var limit = firstSuccs[predId + 1]; for (var i = base; i < limit; i++) { if (succs[i] == id) { var cid = _graph._cids![predId]; var name = _graph._edgeName(cid, i - base); yield _SnapshotObject._new(predId, _graph, name); } } } } SnapshotObject get parent { if (_id == _ROOT) { return this; } return _SnapshotObject._new(_graph._doms![_id], _graph, ""); } Iterable get children sync* { var N = _graph._N!; var doms = _graph._doms!; var parentId = _id; for (var childId = _ROOT; childId <= N; childId++) { if (doms[childId] == parentId) { yield _SnapshotObject._new(childId, _graph, ""); } } } Iterable get objects sync* { yield this; } } class _SyntheticSnapshotObject implements SnapshotObject { late String _description; late SnapshotClass _klass; late int _internalSize; late int _externalSize; late int _retainedSize; late List _successors; late List _predecessors; late SnapshotObject _parent; late List _children; int get identityHashCode => 0; String get label => ""; String get description => _description; SnapshotClass get klass => _klass; int get shallowSize => internalSize + externalSize; int get internalSize => _internalSize; int get externalSize => _externalSize; int get retainedSize => _retainedSize; Iterable get successors => _successors; Iterable get predecessors => _predecessors; SnapshotObject get parent => _parent; Iterable get children => _children; Iterable get objects sync* { yield this; } } /// A set of sibling objects in the graph's dominator tree that have the same /// class. abstract class SnapshotMergedDominator { SnapshotClass get klass; /// "n instances of Class". String get description; /// [internalSize] + [externalSize]. int get shallowSize; /// The sum of [internalSize] for all objects in this set. int get internalSize; /// The sum of [externalSize] for all objects in this set. int get externalSize; /// The sum of [externalSize] for all objects in this set. /// This is the amount of memory that would be freed if all references to /// objects in this set were erased. int get retainedSize; /// The number of objects in this set. Polymorphic with /// [SnapshotClass.instanceCount]. int get instanceCount; SnapshotMergedDominator get parent; Iterable get children; Iterable get objects; } // A node in the dominator tree where siblings with the same class are merged. // That is, a set of objects with the same cid whose parent chains in the // dominator tree have the same cids at each level. [id_] is the representative // object of this set. The other members of the set are found by walking the // mergedDomNext links until finding the sentinel node or a node with a // different class. class _SnapshotMergedDominator implements SnapshotMergedDominator { final int _id; final _SnapshotGraph _graph; final _SnapshotMergedDominator? _parent; _SnapshotMergedDominator._new(this._id, this._graph, this._parent); bool operator ==(Object other) { if (other is _SnapshotMergedDominator) { return _id == other._id && _graph == other._graph; } return false; } int get hashCode => _id ^ _graph.hashCode; String get description { return _id == _ROOT ? "Live Objects + External" : "$instanceCount instances of ${klass.name}"; } SnapshotClass get klass => _graph._classes![_graph._cids![_id]]!; int get shallowSize => internalSize + externalSize; int get internalSize { var cids = _graph._cids!; var internalSizes = _graph._internalSizes!; var mergedDomNext = _graph._mergedDomNext!; var size = 0; var sibling = _id; while (sibling != _SENTINEL && cids[sibling] == cids[_id]) { size += internalSizes[sibling]; sibling = mergedDomNext[sibling]; } return size; } int get externalSize { var cids = _graph._cids!; var externalSizes = _graph._externalSizes!; var mergedDomNext = _graph._mergedDomNext!; var size = 0; var sibling = _id; while (sibling != _SENTINEL && cids[sibling] == cids[_id]) { size += externalSizes[sibling]; sibling = mergedDomNext[sibling]; } return size; } int get retainedSize { var cids = _graph._cids!; var retainedSizes = _graph._retainedSizes!; var mergedDomNext = _graph._mergedDomNext!; var size = 0; var sibling = _id; while (sibling != _SENTINEL && cids[sibling] == cids[_id]) { size += retainedSizes[sibling]; sibling = mergedDomNext[sibling]; } return size; } int get instanceCount { var cids = _graph._cids!; var mergedDomNext = _graph._mergedDomNext!; var count = 0; var sibling = _id; while (sibling != _SENTINEL && cids[sibling] == cids[_id]) { count++; sibling = mergedDomNext[sibling]; } return count; } Iterable get objects sync* { var cids = _graph._cids!; var mergedDomNext = _graph._mergedDomNext!; var sibling = _id; while (sibling != _SENTINEL && cids[sibling] == cids[_id]) { yield _SnapshotObject._new(sibling, _graph, ""); sibling = mergedDomNext[sibling]; } } SnapshotMergedDominator get parent => _parent ?? this; Iterable get children sync* { var next = _graph._mergedDomNext!; var cids = _graph._cids!; var prev = _SENTINEL; var child = _graph._mergedDomHead![_id]; // Walk the list of children and look for the representative objects, i.e. // the first sibling of each cid. while (child != _SENTINEL) { if (prev == _SENTINEL || cids[prev] != cids[child]) { yield _SnapshotMergedDominator._new(child, _graph, this); } prev = child; child = next[child]; } } } class _SyntheticSnapshotMergedDominator implements SnapshotMergedDominator { late String _description; late SnapshotClass _klass; late int _internalSize; late int _externalSize; late int _retainedSize; late List _objects; late SnapshotMergedDominator _parent; late List _children; SnapshotClass get klass => _klass; String get description => _description; int get shallowSize => internalSize + externalSize; int get internalSize => _internalSize; int get externalSize => _externalSize; int get retainedSize => _retainedSize; int get instanceCount => _objects.length; SnapshotMergedDominator get parent => _parent; Iterable get children => _children; Iterable get objects => _objects; } /// A class in a heap snapshot. abstract class SnapshotClass { String get name; String get qualifiedName; int get shallowSize; int get externalSize; int get internalSize; int get ownedSize; int get instanceCount; Iterable get instances; } class _SnapshotClass implements SnapshotClass { final _SnapshotGraph _graph; final int _cid; final String name; String get qualifiedName => "$libUri $name"; final String libName; final String libUri; final Map fields = new Map(); int totalExternalSize = 0; int totalInternalSize = 0; int totalInstanceCount = 0; int ownedSize = 0; int liveExternalSize = 0; int liveInternalSize = 0; int liveInstanceCount = 0; int get shallowSize => internalSize + externalSize; int get internalSize => liveInternalSize; int get externalSize => liveExternalSize; int get instanceCount => liveInstanceCount; Iterable get instances sync* { final N = _graph._N!; final cids = _graph._cids!; final retainedSizes = _graph._retainedSizes!; for (var id = 1; id <= N; id++) { if (cids[id] == _cid && retainedSizes[id] > 0) { yield _SnapshotObject._new(id, _graph, ""); } } } _SnapshotClass._new( this._graph, this._cid, this.name, this.libName, this.libUri, ); } class _SyntheticSnapshotClass implements SnapshotClass { final _SyntheticSnapshotObject _node; _SyntheticSnapshotClass(this._node); String get name => _node.description; String get qualifiedName => _node.description; int get shallowSize => _node.shallowSize; int get externalSize => _node.externalSize; int get internalSize => _node.internalSize; int get ownedSize => 0; int get instanceCount => 1; Iterable get instances => [_node]; } /// The analyzed graph from a heap snapshot. abstract class SnapshotGraph { String get description; int get internalSize; int get externalSize; // [internalSize] + [externalSize] int get size; // The amount of memory reserved for the heap. [internalSize] will always be // less than or equal to [capacity]. int get capacity; Iterable get classes; Iterable get objects; SnapshotObject get root; SnapshotObject get extendedRoot; SnapshotMergedDominator get mergedRoot; SnapshotMergedDominator get extendedMergedRoot; // TODO: Insist that the client remember the chunks if needed? Always keeping // this increasing the peak memory usage during analysis. List get chunks; } const _tagNone = 0; const _tagNull = 1; const _tagBool = 2; const _tagInt = 3; const _tagDouble = 4; const _tagLatin1 = 5; const _tagUtf16 = 6; const _tagLength = 7; const _tagName = 8; const _kSentinelName = ""; const _kRootName = "Live Objects + External"; const _kUnknownFieldName = ""; class _SnapshotGraph implements SnapshotGraph { List? _chunks; List get chunks => _chunks!; _SnapshotGraph._new(); String get description => _description!; int get size => _liveInternalSize! + _liveExternalSize!; int get internalSize => _liveInternalSize!; int get externalSize => _liveExternalSize!; int get capacity => _capacity!; SnapshotObject get root => _SnapshotObject._new(_ROOT, this, "Root"); SnapshotMergedDominator get mergedRoot => _SnapshotMergedDominator._new(_ROOT, this, null); SnapshotObject? _extendedRoot; SnapshotObject get extendedRoot { if (_extendedRoot == null) { _createExtended(); } return _extendedRoot!; } SnapshotMergedDominator? _extendedMergedRoot; SnapshotMergedDominator get extendedMergedRoot { if (_extendedMergedRoot == null) { _createExtended(); } return _extendedMergedRoot!; } void _createExtended() { var capacity = new _SyntheticSnapshotObject(); var uncollected = new _SyntheticSnapshotObject(); var fragmentation = new _SyntheticSnapshotObject(); var live = root; var mcapacity = new _SyntheticSnapshotMergedDominator(); var muncollected = new _SyntheticSnapshotMergedDominator(); var mfragmentation = new _SyntheticSnapshotMergedDominator(); var mlive = mergedRoot; capacity._description = "Capacity + External"; capacity._klass = new _SyntheticSnapshotClass(capacity); capacity._internalSize = 0; // No shallow size. capacity._externalSize = 0; // No shallow size. capacity._retainedSize = _capacity! + _totalExternalSize!; capacity._successors = [live, uncollected, fragmentation]; capacity._predecessors = []; capacity._children = [live, uncollected, fragmentation]; mcapacity._description = "Capacity + External"; mcapacity._klass = capacity._klass; mcapacity._internalSize = 0; // No shallow size. mcapacity._externalSize = 0; // No shallow size. mcapacity._retainedSize = _capacity! + _totalExternalSize!; mcapacity._children = [ mlive, muncollected, mfragmentation, ]; mcapacity._objects = [capacity]; uncollected._description = "Uncollected Garbage"; uncollected._klass = new _SyntheticSnapshotClass(uncollected); uncollected._internalSize = _totalInternalSize! - _liveInternalSize!; uncollected._externalSize = _totalExternalSize! - _liveExternalSize!; uncollected._retainedSize = uncollected._internalSize + uncollected._externalSize; uncollected._successors = []; uncollected._predecessors = [capacity]; uncollected._parent = capacity; uncollected._children = []; muncollected._description = "Uncollected Garbage"; muncollected._klass = uncollected._klass; muncollected._internalSize = _totalInternalSize! - _liveInternalSize!; muncollected._externalSize = _totalExternalSize! - _liveExternalSize!; muncollected._retainedSize = muncollected._internalSize + muncollected._externalSize; muncollected._parent = mcapacity; muncollected._children = []; muncollected._objects = [uncollected]; fragmentation._description = "Free"; fragmentation._klass = new _SyntheticSnapshotClass(fragmentation); fragmentation._internalSize = _capacity! - _totalInternalSize!; fragmentation._externalSize = 0; fragmentation._retainedSize = fragmentation._internalSize; fragmentation._successors = []; fragmentation._predecessors = [capacity]; fragmentation._parent = capacity; fragmentation._children = []; mfragmentation._description = "Free"; mfragmentation._klass = fragmentation._klass; mfragmentation._internalSize = _capacity! - _totalInternalSize!; mfragmentation._externalSize = 0; mfragmentation._retainedSize = mfragmentation._internalSize; mfragmentation._parent = mcapacity; mfragmentation._children = []; mfragmentation._objects = [fragmentation]; _extendedRoot = capacity; _extendedMergedRoot = mcapacity; } Iterable get objects sync* { final N = _N!; final retainedSizes = _retainedSizes!; for (var id = 1; id <= N; id++) { if (retainedSizes[id] > 0) { yield _SnapshotObject._new(id, this, ""); } } } String _describeObject(int oid) { if (oid == _SENTINEL) { return _kSentinelName; } if (oid == _ROOT) { return _kRootName; } var cls = _className(oid); var data = _nonReferenceData![oid]; if (data == null) { return cls; } else { return "$cls($data)"; } } String _className(int oid) { var cid = _cids![oid]; var cls = _classes![cid]; if (cls == null) { return "Class$cid"; } return cls.name; } String _edgeName(int cid, int index) { var c = _classes![cid]; if (c == null) { return _kUnknownFieldName; } var n = c.fields[index]; if (n == null) { return _kUnknownFieldName; } return n; } Iterable get classes sync* { for (final c in _classes!) { // Not all CIDs are occupied. if (c != null) { yield c; } } } Future _load( List? chunks, StreamController onProgress, ) async { _chunks = chunks; _ReadStream? stream = _ReadStream._new(chunks!); chunks = null; // The phases of loading are placed in explicit `new Future(computation)` so // they will be deferred to the message loop. Ordinary async-await will only // defer to the microtask loop. onProgress.add("Loading classes..."); await new Future(() => _readClasses(stream!)); onProgress.add("Loading objects..."); await new Future(() => _readObjects(stream!)); onProgress.add("Loading external properties..."); await new Future(() => _readExternalProperties(stream!)); onProgress.add("Loading object identity hash codes..."); await new Future(() => _readObjectIdentityHashes(stream!)); stream = null; onProgress.add("Compute class table..."); await new Future(() => _computeClassTable()); onProgress.add("Finding depth-first order..."); await new Future(() => _dfs()); onProgress.add("Finding predecessors..."); await new Future(() => _buildPredecessors()); onProgress.add("Finding dominators..."); await new Future(() => _buildDominators()); _semi = null; _parent = null; onProgress.add("Finding in-degree(1) groups..."); await new Future(() => _buildOwnedSizes()); _firstPreds = null; _preds = null; onProgress.add("Finding retained sizes..."); await new Future(() => _calculateRetainedSizes()); _vertex = null; onProgress.add("Linking dominator tree children..."); await new Future(() => _linkDominatorChildren()); onProgress.add("Sorting dominator tree children..."); await new Future(() => _sortDominatorChildren()); onProgress.add("Merging dominator tree siblings..."); await new Future(() => _mergeDominatorSiblings()); onProgress.add("Loaded"); // We await here so SnapshotReader clients see all progress events before // seeing the done future as completed. await onProgress.close(); return this; } String? _description; int? _kStackCid; int? _kFieldCid; int? _N; // Objects in the snapshot. int? _Nconnected; // Objects reachable from root. int? _E; // References in the snapshot. int? _capacity; int? _liveInternalSize; int? _liveExternalSize; int? _totalInternalSize; int? _totalExternalSize; List<_SnapshotClass?>? _classes; // Indexed by node id, with id 0 representing invalid/uninitialized. // From snapshot. List? _nonReferenceData; Uint16List? _cids; Uint32List? _internalSizes; Uint32List? _externalSizes; Uint32List? _firstSuccs; Uint32List? _succs; Uint32List? _identityHashes; // Intermediates. Uint32List? _vertex; Uint32List? _parent; Uint32List? _semi; Uint32List? _firstPreds; // Offset into preds. Uint32List? _preds; // Outputs. Uint32List? _doms; Uint32List? _retainedSizes; Uint32List? _mergedDomHead; Uint32List? _mergedDomNext; void _readClasses(_ReadStream stream) { for (var i = 0; i < 8; i++) { stream.readByte(); // Magic value. } stream.readUnsigned(); // Flags _description = stream.readUtf8(); _totalInternalSize = stream.readUnsigned(); _capacity = stream.readUnsigned(); _totalExternalSize = stream.readUnsigned(); var K = stream.readUnsigned(); var classes = new List<_SnapshotClass?>.filled(K + 1, null); classes[0] = _SnapshotClass._new(this, 0, "Root", "", ""); for (var cid = 1; cid <= K; cid++) { // ignore: unused_local_variable int flags = stream.readUnsigned(); String name = stream.readUtf8(); String libName = stream.readUtf8(); String libUri = stream.readUtf8(); // ignore: unused_local_variable String reserved = stream.readUtf8(); final cls = _SnapshotClass._new(this, cid, name, libName, libUri); int edgeCount = stream.readUnsigned(); for (int i = 0; i < edgeCount; i++) { // ignore: unused_local_variable int flags = stream.readUnsigned(); int index = stream.readUnsigned(); String fieldName = stream.readUtf8(); // ignore: unused_local_variable String reserved = stream.readUtf8(); cls.fields[index] = fieldName; } classes[cid] = cls; } _classes = classes; } void _readObjects(_ReadStream stream) { final E = stream.readUnsigned(); final N = stream.readUnsigned(); // The negative check accounts for int64 overflow in readUnsigned. const maxUint32 = 0xFFFFFFFF; if (N < 0 || N + 2 >= maxUint32) { throw new Exception("Snapshot contains too many objects: $N"); } if (E < 0 || E + 2 >= maxUint32) { throw new Exception("Snapshot contains too many references: $E"); } _N = N; _E = E; var internalSizes = _newUint32Array(N + 1); var cids = _newUint16Array(N + 1); var nonReferenceData = new List.filled(N + 1, null); var firstSuccs = _newUint32Array(N + 2); var succs = _newUint32Array(E); var eid = 0; for (var oid = 1; oid <= N; oid++) { var cid = stream.readUnsigned(); cids[oid] = cid; var internalSize = stream.readUnsigned(); internalSizes[oid] = internalSize; var nonReferenceDataTag = stream.readUnsigned(); switch (nonReferenceDataTag) { case _tagNone: break; case _tagNull: nonReferenceData[oid] = "null"; break; case _tagBool: nonReferenceData[oid] = stream.readByte() != 0; break; case _tagInt: nonReferenceData[oid] = stream.readSigned(); break; case _tagDouble: nonReferenceData[oid] = stream.readFloat64(); break; case _tagLatin1: var len = stream.readUnsigned(); var str = stream.readLatin1(); if (str.length < len) { nonReferenceData[oid] = '$str...'; } else { nonReferenceData[oid] = str; } break; case _tagUtf16: int len = stream.readUnsigned(); var str = stream.readUtf16(); if (str.length < len) { nonReferenceData[oid] = '$str...'; } else { nonReferenceData[oid] = str; } break; case _tagLength: nonReferenceData[oid] = stream.readUnsigned(); // Length break; case _tagName: nonReferenceData[oid] = stream.readUtf8(); // Name break; default: throw "Unknown tag $nonReferenceDataTag"; } firstSuccs[oid] = eid; var referenceCount = stream.readUnsigned(); while (referenceCount > 0) { var childOid = stream.readUnsigned(); succs[eid] = childOid; eid++; referenceCount--; } } firstSuccs[N + 1] = eid; assert(eid <= E); _E = eid; _internalSizes = internalSizes; _cids = cids; _nonReferenceData = nonReferenceData; _firstSuccs = firstSuccs; _succs = succs; } void _readExternalProperties(_ReadStream stream) { final N = _N!; final externalPropertyCount = stream.readUnsigned(); final externalSizes = _newUint32Array(N + 1); for (var i = 0; i < externalPropertyCount; i++) { final oid = stream.readUnsigned(); final externalSize = stream.readUnsigned(); // ignore: unused_local_variable final name = stream.readUtf8(); externalSizes[oid] += externalSize; } _externalSizes = externalSizes; } void _readObjectIdentityHashes(_ReadStream stream) { final N = _N!; final identityHashes = _newUint32Array(N + 1); for (int oid = 1; oid <= N; ++oid) { identityHashes[oid] = stream.readUnsigned(); } _identityHashes = identityHashes; } void _computeClassTable() { final N = _N!; final classes = _classes!; final cids = _cids!; final internalSizes = _internalSizes!; final externalSizes = _externalSizes!; var totalInternalSize = 0; var totalExternalSize = 0; for (var oid = 1; oid <= N; oid++) { var internalSize = internalSizes[oid]; totalInternalSize += internalSize; var externalSize = externalSizes[oid]; totalExternalSize += externalSize; var cls = classes[cids[oid]]!; cls.totalInternalSize += internalSize; cls.totalExternalSize += externalSize; cls.totalInstanceCount++; } _totalInternalSize = totalInternalSize; _totalExternalSize = totalExternalSize; } void _dfs() { final N = _N!; final firstSuccs = _firstSuccs!; final succs = _succs!; final stackNodes = _newUint32Array(N); final stackCurrentEdgePos = _newUint32Array(N); final vertex = _newUint32Array(N + 1); final semi = _newUint32Array(N + 1); final parent = _newUint32Array(N + 1); var dfsNumber = 0; var stackTop = 0; // Push root. stackNodes[0] = _ROOT; stackCurrentEdgePos[0] = firstSuccs[_ROOT]; while (stackTop >= 0) { var v = stackNodes[stackTop]; var edgePos = stackCurrentEdgePos[stackTop]; if (semi[v] == 0) { // First visit. dfsNumber++; semi[v] = dfsNumber; vertex[dfsNumber] = v; } if (edgePos < firstSuccs[v + 1]) { var childId = succs[edgePos]; edgePos++; stackCurrentEdgePos[stackTop] = edgePos; if (childId == _SENTINEL) { // Omitted target. } else if (semi[childId] == 0) { parent[childId] = v; // Push child. stackTop++; stackNodes[stackTop] = childId; stackCurrentEdgePos[stackTop] = firstSuccs[childId]; } } else { // Done with all children. stackTop--; } } if (dfsNumber != N) { // This may happen in filtered snapshots. print('Heap snapshot contains ${N - dfsNumber} unreachable nodes.'); } assert(() { for (var i = 1; i <= dfsNumber; i++) { var v = vertex[i]; assert(semi[v] != _SENTINEL); } assert(parent[1] == _SENTINEL); for (var i = 2; i <= dfsNumber; i++) { var v = vertex[i]; assert(parent[v] != _SENTINEL); } return true; }()); if (dfsNumber != N) { // Remove successors of unconnected nodes for (var i = _ROOT + 1; i <= N; i++) { if (parent[i] == _SENTINEL) { var startSuccIndex = firstSuccs[i]; var limitSuccIndex = firstSuccs[i + 1]; for ( var succIndex = startSuccIndex; succIndex < limitSuccIndex; succIndex++ ) { succs[succIndex] = _SENTINEL; } } } } _Nconnected = dfsNumber; _vertex = vertex; _semi = semi; _parent = parent; } void _buildPredecessors() { final N = _N!; final Nconnected = _Nconnected!; final E = _E!; final firstSuccs = _firstSuccs!; final succs = _succs!; // This is first filled with the predecessor counts, then reused to hold the // offset to the first predecessor (see alias below). // + 1 because 0 is a sentinel // + 1 so the number of predecessors can be found from the difference with // the next node's offset. final numPreds = _newUint32Array(N + 2); final preds = _newUint32Array(E); // Count predecessors of each node. for (var succIndex = 0; succIndex < E; succIndex++) { final succId = succs[succIndex]; if (succId != _SENTINEL) { numPreds[succId]++; } } // Assign indices into predecessors array. final firstPreds = numPreds; // Alias. final nextPreds = _newUint32Array(N + 1); var predIndex = 0; for (var i = 1; i <= N; i++) { var thisPredIndex = predIndex; predIndex += numPreds[i]; firstPreds[i] = thisPredIndex; nextPreds[i] = thisPredIndex; } if (N == Nconnected) { assert(predIndex == E); } firstPreds[N + 1] = predIndex; // Extra entry for cheap boundary detection. // Fill predecessors array. for (var i = 1; i <= N; i++) { var startSuccIndex = firstSuccs[i]; var limitSuccIndex = firstSuccs[i + 1]; for ( var succIndex = startSuccIndex; succIndex < limitSuccIndex; succIndex++ ) { var succId = succs[succIndex]; if (succId != _SENTINEL) { var predIndex = nextPreds[succId]++; preds[predIndex] = i; } } } _firstPreds = firstPreds; _preds = preds; } // Fold the size of any object with in-degree(1) into its parent. // Requires the DFS numbering and predecessor lists. void _buildOwnedSizes() { final N = _N!; final Nconnected = _Nconnected!; final kStackCid = _kStackCid; final kFieldCid = _kFieldCid; final cids = _cids!; final internalSizes = _internalSizes!; final externalSizes = _externalSizes!; final vertex = _vertex!; final firstPreds = _firstPreds!; final preds = _preds!; final ownedSizes = _newUint32Array(N + 1); for (var i = 1; i <= Nconnected; i++) { final v = vertex[i]; ownedSizes[v] = internalSizes[v] + externalSizes[v]; } for (var i = Nconnected; i > 1; i--) { var w = vertex[i]; assert(w != _ROOT); var onlyPred = _SENTINEL; var startPred = firstPreds[w]; var limitPred = firstPreds[w + 1]; for (var predIndex = startPred; predIndex < limitPred; predIndex++) { var v = preds[predIndex]; if (v == w) { // Ignore self-predecessor. } else if (onlyPred == _SENTINEL) { onlyPred = v; } else if (onlyPred == v) { // Repeated predecessor. } else { // Multiple-predecessors. onlyPred = _SENTINEL; break; } } // If this object has a single precessor which is not a Field, Stack or // the root, blame its size against the precessor. if ((onlyPred != _SENTINEL) && (onlyPred != _ROOT) && (cids[onlyPred] != kStackCid) && (cids[onlyPred] != kFieldCid)) { assert(onlyPred != w); ownedSizes[onlyPred] += ownedSizes[w]; ownedSizes[w] = 0; } } // TODO(rmacnak): Maybe keep the per-objects sizes to be able to provide // examples of large owners for each class. final classes = _classes!; for (var i = 1; i <= Nconnected; i++) { final v = vertex[i]; final cid = cids[v]; final cls = classes[cid]!; cls.ownedSize += ownedSizes[v]; } } static int _eval( int v, Uint32List ancestor, Uint32List semi, Uint32List label, Uint32List stackNode, Uint8List stackState, ) { if (ancestor[v] == _SENTINEL) { return label[v]; } else { { // Inlined 'compress' with an explicit stack to prevent JS stack // overflow. var top = 0; stackNode[top] = v; stackState[top] = 0; while (top >= 0) { var v = stackNode[top]; var state = stackState[top]; if (state == 0) { assert(ancestor[v] != 0); if (ancestor[ancestor[v]] != 0) { stackState[top] = 1; // Recurse with ancestor[v] top++; stackNode[top] = ancestor[v]; stackState[top] = 0; } else { top--; } } else { assert(state == 1); if (semi[label[ancestor[v]]] < semi[label[v]]) { label[v] = label[ancestor[v]]; } ancestor[v] = ancestor[ancestor[v]]; top--; } } } if (semi[label[ancestor[v]]] >= semi[label[v]]) { return label[v]; } else { return label[ancestor[v]]; } } } // Note the version in the main text of Lengauer & Tarjan incorrectly // uses parent instead of ancestor. The correct version is in Appendix B. static void _link( int v, int w, Uint32List size, Uint32List label, Uint32List semi, Uint32List child, Uint32List ancestor, ) { assert(size[0] == 0); assert(label[0] == 0); assert(semi[0] == 0); var s = w; while (semi[label[w]] < semi[label[child[s]]]) { if (size[s] + size[child[child[s]]] >= 2 * size[child[s]]) { ancestor[child[s]] = s; child[s] = child[child[s]]; } else { size[child[s]] = size[s]; s = ancestor[s] = child[s]; } } label[s] = label[w]; size[v] = size[v] + size[w]; if (size[v] < 2 * size[w]) { var tmp = s; s = child[v]; child[v] = tmp; } while (s != 0) { ancestor[s] = v; s = child[s]; } } // T. Lengauer and R. E. Tarjan. "A Fast Algorithm for Finding Dominators // in a Flowgraph." void _buildDominators() { final N = _N!; final Nconnected = _Nconnected!; final vertex = _vertex!; final semi = _semi!; final parent = _parent!; final firstPreds = _firstPreds!; final preds = _preds!; final dom = _newUint32Array(N + 1); final ancestor = _newUint32Array(N + 1); final label = _newUint32Array(N + 1); for (var i = 1; i <= N; i++) { label[i] = i; } final buckets = new List.filled(N + 1, null); final child = _newUint32Array(N + 1); final size = _newUint32Array(N + 1); for (var i = 1; i <= N; i++) { size[i] = 1; } final stackNode = _newUint32Array(N + 1); final stackState = _newUint8Array(N + 1); for (var i = Nconnected; i > 1; i--) { var w = vertex[i]; assert(w != _ROOT); // Lengauer & Tarjan Step 2. var startPred = firstPreds[w]; var limitPred = firstPreds[w + 1]; for (var predIndex = startPred; predIndex < limitPred; predIndex++) { var v = preds[predIndex]; var u = _eval(v, ancestor, semi, label, stackNode, stackState); if (semi[u] < semi[w]) { semi[w] = semi[u]; } } // w.semi.bucket.add(w); var tmp = vertex[semi[w]]; if (buckets[tmp] == null) { buckets[tmp] = []; } buckets[tmp].add(w); _link(parent[w], w, size, label, semi, child, ancestor); // Lengauer & Tarjan Step 3. tmp = parent[w]; var bucket = buckets[tmp]; buckets[tmp] = null; if (bucket != null) { for (var v in bucket) { var u = _eval(v, ancestor, semi, label, stackNode, stackState); dom[v] = semi[u] < semi[v] ? u : parent[w]; } } } for (var i = _ROOT; i <= N; i++) { assert(buckets[i] == null); } // Lengauer & Tarjan Step 4. for (var i = 2; i <= Nconnected; i++) { var w = vertex[i]; if (dom[w] != vertex[semi[w]]) { dom[w] = dom[dom[w]]; } } _doms = dom; } void _calculateRetainedSizes() { final N = _N!; final Nconnected = _Nconnected!; var liveInternalSize = 0; var liveExternalSize = 0; final classes = _classes!; final cids = _cids!; final internalSizes = _internalSizes!; final externalSizes = _externalSizes!; final vertex = _vertex!; final doms = _doms!; // Sum internal and external sizes. for (var i = 1; i <= Nconnected; i++) { var v = vertex[i]; var internalSize = internalSizes[v]; var externalSize = externalSizes[v]; liveInternalSize += internalSize; liveExternalSize += externalSize; var cls = classes[cids[v]]!; cls.liveInternalSize += internalSize; cls.liveExternalSize += externalSize; cls.liveInstanceCount++; } // Start with retained size as shallow size + external size. For reachable // objects only; leave unreachable objects with a retained size of 0 so // they can be filtered during graph iterations. var retainedSizes = new Uint32List(N + 1); assert(Nconnected <= N); for (var i = 0; i <= Nconnected; i++) { var v = vertex[i]; retainedSizes[v] = internalSizes[v] + externalSizes[v]; } // In post order (bottom up), add retained size to dominator's retained // size, skipping root. for (var i = Nconnected; i > 1; i--) { var v = vertex[i]; assert(v != _ROOT); retainedSizes[doms[v]] += retainedSizes[v]; } // Root retains everything. assert(retainedSizes[_ROOT] == (liveInternalSize + liveExternalSize)); _retainedSizes = retainedSizes; _liveInternalSize = liveInternalSize; _liveExternalSize = liveExternalSize; print("internal-garbage: ${_totalInternalSize! - _liveInternalSize!}"); print("external-garbage: ${_totalExternalSize! - _liveExternalSize!}"); print("fragmentation: ${_capacity! - _totalInternalSize!}"); assert(_liveInternalSize! <= _totalInternalSize!); assert(_liveExternalSize! <= _totalExternalSize!); assert(_totalInternalSize! <= _capacity!); } // Build linked lists of the children for each node in the dominator tree. void _linkDominatorChildren() { final N = _N!; final doms = _doms!; final head = _newUint32Array(N + 1); final next = _newUint32Array(N + 1); for (var child = _ROOT; child <= N; child++) { var parent = doms[child]; next[child] = head[parent]; head[parent] = child; } _mergedDomHead = head; _mergedDomNext = next; } // Merge the given lists according to the given key in ascending order. // Returns the head of the merged list. static int _mergeSorted( int head1, int head2, Uint32List next, Uint16List key, ) { var head = head1; var beforeInsert = _SENTINEL; var afterInsert = head1; var startInsert = head2; while (startInsert != _SENTINEL) { while ((afterInsert != _SENTINEL) && (key[afterInsert] <= key[startInsert])) { beforeInsert = afterInsert; afterInsert = next[beforeInsert]; } var endInsert = startInsert; var peek = next[endInsert]; while ((peek != _SENTINEL) && (key[peek] < key[afterInsert])) { endInsert = peek; peek = next[endInsert]; } assert(endInsert != _SENTINEL); if (beforeInsert == _SENTINEL) { head = startInsert; } else { next[beforeInsert] = startInsert; } next[endInsert] = afterInsert; startInsert = peek; beforeInsert = endInsert; } return head; } void _sortDominatorChildren() { final N = _N!; final cids = _cids!; final head = _mergedDomHead!; final next = _mergedDomNext!; // Returns the new head of the sorted list. int sort(int head) { if (head == _SENTINEL) return _SENTINEL; if (next[head] == _SENTINEL) return head; // Find the middle of the list. int head1 = head; int slow = head; int fast = head; while (next[fast] != _SENTINEL && next[next[fast]] != _SENTINEL) { slow = next[slow]; fast = next[next[fast]]; } // Split the list in half. int head2 = next[slow]; next[slow] = _SENTINEL; // Recursively sort the sublists and merge. assert(head1 != head2); int newHead1 = sort(head1); int newHead2 = sort(head2); return _mergeSorted(newHead1, newHead2, next, cids); } // Sort all list of dominator tree children by cid. for (var parent = _ROOT; parent <= N; parent++) { head[parent] = sort(head[parent]); } } void _mergeDominatorSiblings() { var N = _N!; var cids = _cids!; var head = _mergedDomHead!; var next = _mergedDomNext!; var workStack = _newUint32Array(N); var workStackTop = 0; mergeChildrenAndSort(parent1, end) { assert(parent1 != _SENTINEL); if (next[parent1] == end) return; // Find the middle of the list. int slow = parent1; int fast = parent1; while (next[fast] != end && next[next[fast]] != end) { slow = next[slow]; fast = next[next[fast]]; } int parent2 = next[slow]; assert(parent2 != _SENTINEL); assert(parent1 != parent2); assert(cids[parent1] == cids[parent2]); // Recursively sort the sublists. mergeChildrenAndSort(parent1, parent2); mergeChildrenAndSort(parent2, end); // Merge sorted sublists. head[parent1] = _mergeSorted(head[parent1], head[parent2], next, cids); // Children moved to parent1. head[parent2] = _SENTINEL; } // Push root. workStack[workStackTop++] = _ROOT; while (workStackTop > 0) { var parent = workStack[--workStackTop]; var child = head[parent]; while (child != _SENTINEL) { // Push child. workStack[workStackTop++] = child; // Find next sibling with a different cid. var after = child; while (after != _SENTINEL && cids[after] == cids[child]) { after = next[after]; } // From all the siblings between child and after, take their children, // merge them and given to child. mergeChildrenAndSort(child, after); child = after; } } } }