| // Copyright (c) 2022, 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. |
| |
| // ignore_for_file: annotate_overrides |
| // ignore_for_file: avoid_init_to_null |
| // ignore_for_file: empty_constructor_bodies |
| // ignore_for_file: prefer_is_not_empty |
| // ignore_for_file: unnecessary_brace_in_string_interps |
| |
| import 'dart:typed_data'; |
| |
| import 'package:vm_service/vm_service.dart'; |
| |
| import 'format.dart'; |
| import 'intset.dart'; |
| export 'intset.dart'; |
| |
| const int _invalidIdx = 0; |
| const int _rootObjectIdx = 1; |
| |
| class Analysis { |
| final HeapSnapshotGraph graph; |
| |
| late final reachableObjects = transitiveGraph(roots); |
| |
| late final Uint32List _retainers = _calculateRetainers(); |
| |
| late final _oneByteStringCid = _findClassId('_OneByteString'); |
| late final _twoByteStringCid = _findClassId('_TwoByteString'); |
| late final _nonGrowableListCid = _findClassId('_List'); |
| late final _immutableListCid = _findClassId('_ImmutableList'); |
| late final _weakPropertyCid = _findClassId('_WeakProperty'); |
| late final _weakReferenceCid = _findClassId('_WeakReference'); |
| late final _patchClassCid = _findClassId('PatchClass'); |
| late final _finalizerEntryCid = _findClassId('FinalizerEntry'); |
| |
| late final _weakPropertyKeyIdx = _findFieldIndex(_weakPropertyCid, 'key_'); |
| late final _weakPropertyValueIdx = |
| _findFieldIndex(_weakPropertyCid, 'value_'); |
| |
| late final _finalizerEntryDetachIdx = |
| _findFieldIndex(_finalizerEntryCid, 'detach_'); |
| late final _finalizerEntryValueIdx = |
| _findFieldIndex(_finalizerEntryCid, 'value_'); |
| |
| late final _Arch _arch = (() { |
| // We want to figure out the architecture this heapsnapshot was made from |
| // without it being directly included in the snapshot. |
| // In order to distinguish 32-bit/64-bit/64-bit-compressed we need |
| // - an object whose shallowSize will be different for all 3 architectures |
| // - have an actual object in the heap snapshot |
| // -> PatchClass seems to satisfy this. |
| final size = graph.objects |
| .firstWhere( |
| (obj) => obj.classId == _patchClassCid && obj.shallowSize != 0) |
| .shallowSize; |
| |
| switch (size) { |
| case 24: |
| return _Arch.arch32; |
| case 32: |
| return _Arch.arch64c; |
| case 48: |
| return _Arch.arch64; |
| default: |
| throw 'Unexpected size of patch class: $size.'; |
| } |
| })(); |
| |
| late final int _headerSize = _arch != _Arch.arch32 ? 8 : 4; |
| late final int _wordSize = _arch == _Arch.arch64 ? 8 : 4; |
| |
| late final elementSizeForExternalTypedDataCid = (() { |
| final cidToSize = <int, int>{}; |
| for (final k in graph.classes) { |
| final cid = k.classId; |
| final name = k.name; |
| if (name.startsWith('_External') && name.endsWith('Array')) { |
| if (name.contains('64x2') || name.contains('32x4')) { |
| cidToSize[cid] = 16; |
| } else if (name.contains('64')) { |
| cidToSize[cid] = 8; |
| } else if (name.contains('32')) { |
| cidToSize[cid] = 4; |
| } else if (name.contains('16')) { |
| cidToSize[cid] = 2; |
| } else if (name.contains('8')) { |
| cidToSize[cid] = 1; |
| } else { |
| throw 'dont know elementsize of $name'; |
| } |
| } |
| } |
| return cidToSize; |
| })(); |
| |
| Analysis(this.graph) {} |
| |
| /// The roots from which alive data can be discovered. |
| final IntSet roots = IntSet()..add(_rootObjectIdx); |
| |
| /// Calculates retaining paths for all objects in [objs]. |
| /// |
| /// All retaining paths will have the object itself plus at most [depth] |
| /// retainers in it. |
| List<DedupedUint32List> retainingPathsOf(IntSet objs, int depth) { |
| final paths = <DedupedUint32List, int>{}; |
| for (var oId in objs) { |
| final rpath = _retainingPathOf(oId, depth); |
| final old = paths[rpath]; |
| paths[rpath] = (old == null) ? 1 : old + 1; |
| } |
| paths.forEach((path, count) { |
| path.count = count; |
| }); |
| return paths.keys.toList()..sort((a, b) => paths[b]! - paths[a]!); |
| } |
| |
| /// Returns information about a specific object. |
| ObjectInformation examine(int oId) { |
| String stringifyValue(int valueId) { |
| if (valueId == _invalidIdx) return 'int/double/simd'; |
| |
| final object = graph.objects[valueId]; |
| final cid = object.classId; |
| if (cid == _oneByteStringCid || cid == _twoByteStringCid) { |
| return '"${truncateString(object.data as String)}"'; |
| } |
| |
| final valueClass = graph.classes[cid]; |
| return '${valueClass.name}@${valueId} (${valueClass.libraryUri})'; |
| } |
| |
| final object = graph.objects[oId]; |
| final cid = object.classId; |
| final klass = graph.classes[cid]; |
| final fs = klass.fields.toList()..sort((a, b) => a.index - b.index); |
| final fieldValues = <String, String>{}; |
| if (cid == _oneByteStringCid || cid == _twoByteStringCid) { |
| fieldValues['data'] = stringifyValue(oId); |
| } else { |
| int maxFieldIndex = -1; |
| for (final field in fs) { |
| final valueId = object.references[field.index]; |
| fieldValues[field.name] = stringifyValue(valueId); |
| if (field.index > maxFieldIndex) { |
| maxFieldIndex = field.index; |
| } |
| } |
| |
| if (cid == _immutableListCid || cid == _nonGrowableListCid) { |
| final refs = object.references; |
| int len = refs.length - (maxFieldIndex + 1); |
| if (len < 10) { |
| for (int i = 0; i < len; ++i) { |
| fieldValues['[$i]'] = stringifyValue(refs[1 + maxFieldIndex + i]); |
| } |
| } else { |
| for (int i = 0; i < 4; ++i) { |
| fieldValues['[$i]'] = stringifyValue(refs[1 + maxFieldIndex + i]); |
| } |
| fieldValues['[...]'] = ''; |
| for (int i = len - 4; i < len; ++i) { |
| fieldValues['[$i]'] = stringifyValue(refs[1 + maxFieldIndex + i]); |
| } |
| } |
| } |
| } |
| return ObjectInformation( |
| klass.name, klass.libraryUri.toString(), fieldValues); |
| } |
| |
| /// Generates statistics about the given set of [objects]. |
| /// |
| /// The classes are sored by sum of shallow-size of objects of a class if |
| /// [sortBySize] is true and by number of objects per-class otherwise. |
| HeapStats generateObjectStats(IntSet objects, {bool sortBySize = true}) { |
| final graphObjects = graph.objects; |
| final numCids = graph.classes.length; |
| |
| final counts = Int32List(numCids); |
| final sizes = Int32List(numCids); |
| for (final objectId in objects) { |
| final obj = graphObjects[objectId]; |
| final cid = obj.classId; |
| counts[cid]++; |
| sizes[cid] += obj.shallowSize + externalSize(obj); |
| } |
| |
| final classes = graph.classes.where((c) => counts[c.classId] > 0).toList(); |
| if (sortBySize) { |
| classes.sort((a, b) { |
| var diff = sizes[b.classId] - sizes[a.classId]; |
| if (diff != 0) return diff; |
| diff = counts[b.classId] - counts[a.classId]; |
| if (diff != 0) return diff; |
| return graph.classes[b.classId].name |
| .compareTo(graph.classes[a.classId].name); |
| }); |
| } else { |
| classes.sort((a, b) { |
| var diff = counts[b.classId] - counts[a.classId]; |
| if (diff != 0) return diff; |
| diff = sizes[b.classId] - sizes[a.classId]; |
| if (diff != 0) return diff; |
| return graph.classes[b.classId].name |
| .compareTo(graph.classes[a.classId].name); |
| }); |
| } |
| |
| return HeapStats(classes, sizes, counts); |
| } |
| |
| /// Generate statistics about the variable-length data of [objects]. |
| /// |
| /// The returned [HeapData]s are sorted by cumulative size if |
| /// [sortBySize] is true and by number of objects otherwise. |
| HeapDataStats generateDataStats(IntSet objects, {bool sortBySize = true}) { |
| final graphObjects = graph.objects; |
| final klasses = graph.classes; |
| final counts = <HeapData, int>{}; |
| for (final objectId in objects) { |
| final obj = graphObjects[objectId]; |
| final klass = klasses[obj.classId].name; |
| // Should use length here instead! |
| final len = variableLengthOf(obj); |
| if (len == -1) continue; |
| final size = obj.shallowSize + externalSize(obj); |
| final data = HeapData(klass, obj.data, size, len); |
| counts[data] = (counts[data] ?? 0) + 1; |
| } |
| counts.forEach((HeapData data, int count) { |
| data.count = count; |
| }); |
| |
| final datas = counts.keys.toList(); |
| if (sortBySize) { |
| datas.sort((a, b) => b.totalSize - a.totalSize); |
| } else { |
| datas.sort((a, b) => b.count - a.count); |
| } |
| |
| return HeapDataStats(datas); |
| } |
| |
| /// Calculates the set of objects transitively reachable by [roots]. |
| IntSet transitiveGraph(IntSet roots, [TraverseFilter? tfilter = null]) { |
| final objects = graph.objects; |
| final reachable = SpecializedIntSet(objects.length); |
| final worklist = <int>[]; |
| |
| reachable.addAll(roots); |
| worklist.addAll(roots); |
| |
| final weakProperties = IntSet(); |
| |
| while (worklist.isNotEmpty) { |
| while (worklist.isNotEmpty) { |
| final objectIdToExpand = worklist.removeLast(); |
| final objectToExpand = objects[objectIdToExpand]; |
| final cid = objectToExpand.classId; |
| |
| // Weak references don't keep their value alive. |
| if (cid == _weakReferenceCid) continue; |
| |
| // Weak properties keep their value alive if the key is alive. |
| if (cid == _weakPropertyCid) { |
| if (tfilter == null || |
| tfilter._shouldTraverseEdge( |
| _weakPropertyCid, _weakPropertyValueIdx)) { |
| weakProperties.add(objectIdToExpand); |
| } |
| continue; |
| } |
| |
| // Normal object (or FinalizerEntry). |
| final references = objectToExpand.references; |
| final bool isFinalizerEntry = cid == _finalizerEntryCid; |
| for (int i = 0; i < references.length; ++i) { |
| // [FinalizerEntry] objects don't keep their "detach" and "value" |
| // fields alive. |
| if (isFinalizerEntry && |
| (i == _finalizerEntryDetachIdx || i == _finalizerEntryValueIdx)) { |
| continue; |
| } |
| |
| final successor = references[i]; |
| if (!reachable.contains(successor)) { |
| if (tfilter == null || |
| (tfilter._shouldTraverseEdge(objectToExpand.classId, i) && |
| tfilter._shouldIncludeObject(objects[successor].classId))) { |
| reachable.add(successor); |
| worklist.add(successor); |
| } |
| } |
| } |
| } |
| |
| // Enqueue values of weak properties if their key is alive. |
| weakProperties.removeWhere((int weakProperty) { |
| final wpReferences = objects[weakProperty].references; |
| final keyId = wpReferences[_weakPropertyKeyIdx]; |
| final valueId = wpReferences[_weakPropertyValueIdx]; |
| if (reachable.contains(keyId)) { |
| if (!reachable.contains(valueId)) { |
| if (tfilter == null || |
| tfilter._shouldIncludeObject(objects[valueId].classId)) { |
| reachable.add(valueId); |
| worklist.add(valueId); |
| } |
| } |
| return true; |
| } |
| return false; |
| }); |
| } |
| return reachable; |
| } |
| |
| /// Calculates the set of objects that transitively can reach [oids]. |
| IntSet reverseTransitiveGraph(IntSet oids, [TraverseFilter? tfilter = null]) { |
| final reachable = IntSet(); |
| final worklist = <int>[]; |
| |
| final objects = graph.objects; |
| |
| reachable.addAll(oids); |
| worklist.addAll(oids); |
| |
| while (worklist.isNotEmpty) { |
| final objectIdToExpand = worklist.removeLast(); |
| final objectToExpand = objects[objectIdToExpand]; |
| final referrers = objectToExpand.referrers; |
| for (int i = 0; i < referrers.length; ++i) { |
| final predecessorId = referrers[i]; |
| // This is a dead object in heap that refers to a live object. |
| if (!reachableObjects.contains(predecessorId)) continue; |
| if (!reachable.contains(predecessorId)) { |
| final predecessor = objects[predecessorId]; |
| final cid = predecessor.classId; |
| |
| // A WeakReference does not keep its object alive. |
| if (cid == _weakReferenceCid) continue; |
| |
| // A WeakProperty does not keep its key alive, but may keep it's value |
| // alive. |
| if (cid == _weakPropertyCid) { |
| final refs = predecessor.references; |
| bool hasRealRef = false; |
| for (int i = 0; i < refs.length; ++i) { |
| if (i == _weakPropertyKeyIdx) continue; |
| if (refs[i] == objectIdToExpand) hasRealRef = true; |
| } |
| if (!hasRealRef) continue; |
| } |
| |
| // A FinalizerEntry] does not keep its {detach_,value_} fields alive. |
| if (cid == _finalizerEntryCid) { |
| final refs = predecessor.references; |
| bool hasRealRef = false; |
| for (int i = 0; i < refs.length; ++i) { |
| if (i == _finalizerEntryDetachIdx) continue; |
| if (i == _finalizerEntryValueIdx) continue; |
| if (refs[i] == objectIdToExpand) hasRealRef = true; |
| } |
| if (!hasRealRef) continue; |
| } |
| |
| bool passedFilter = true; |
| if (tfilter != null) { |
| final index = predecessor.references.indexOf(objectIdToExpand); |
| passedFilter = |
| (tfilter._shouldTraverseEdge(predecessor.classId, index) && |
| tfilter._shouldIncludeObject(predecessor.classId)); |
| } |
| if (passedFilter) { |
| reachable.add(predecessorId); |
| worklist.add(predecessorId); |
| } |
| } |
| } |
| } |
| return reachable; |
| } |
| |
| // Only keep those in [toFilter] that have references from [from]. |
| IntSet filterObjectsReferencedBy(IntSet toFilter, IntSet from) { |
| final result = IntSet(); |
| final objects = graph.objects; |
| |
| for (final fromId in from) { |
| final from = objects[fromId]; |
| for (final refId in from.references) { |
| if (toFilter.contains(refId)) { |
| result.add(refId); |
| break; |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| /// Returns set of cids that are matching the provided [patterns]. |
| IntSet findClassIdsMatching(Iterable<String> patterns) { |
| final regexPatterns = patterns.map((p) => RegExp(p)).toList(); |
| |
| final classes = graph.classes; |
| final cids = IntSet(); |
| for (final klass in classes) { |
| if (regexPatterns.any((pattern) => |
| pattern.hasMatch(klass.name) || |
| pattern.hasMatch(klass.libraryUri.toString()))) { |
| cids.add(klass.classId); |
| } |
| } |
| return cids; |
| } |
| |
| /// Create filters that can be used in traversing object graphs. |
| TraverseFilter? parseTraverseFilter(List<String> patterns) { |
| if (patterns.isEmpty) return null; |
| |
| final aset = IntSet(); |
| final naset = IntSet(); |
| |
| int bits = 0; |
| |
| final fmap = <int, IntSet>{}; |
| final nfmap = <int, IntSet>{}; |
| for (String pattern in patterns) { |
| final bool isNegated = pattern.startsWith('^'); |
| if (isNegated) { |
| pattern = pattern.substring(1); |
| } |
| |
| // Edge filter. |
| final int sep = pattern.indexOf(':'); |
| if (sep != -1 && sep != (pattern.length - 1)) { |
| final klassPattern = pattern.substring(0, sep); |
| |
| final fieldNamePattern = pattern.substring(sep + 1); |
| final cids = findClassIdsMatching([klassPattern]); |
| |
| final fieldNameRegexp = RegExp(fieldNamePattern); |
| for (final cid in cids) { |
| final klass = graph.classes[cid]; |
| for (final field in klass.fields) { |
| if (fieldNameRegexp.hasMatch(field.name)) { |
| (isNegated ? nfmap : fmap) |
| .putIfAbsent(cid, IntSet.new) |
| .add(field.index); |
| } |
| } |
| } |
| |
| if (!isNegated) { |
| bits |= TraverseFilter._hasPositiveEdgePatternBit; |
| } |
| |
| continue; |
| } |
| |
| // Class filter. |
| final cids = findClassIdsMatching([pattern]); |
| (isNegated ? naset : aset).addAll(cids); |
| |
| if (!isNegated) { |
| bits |= TraverseFilter._hasPositiveClassPatternBit; |
| } |
| } |
| return TraverseFilter._(patterns, bits, aset, naset, fmap, nfmap); |
| } |
| |
| /// Returns set of objects from [objectIds] whose class id is in [cids]. |
| IntSet filterByClassId(IntSet objectIds, IntSet cids) { |
| return filter(objectIds, (object) => cids.contains(object.classId)); |
| } |
| |
| /// Returns set of objects from [objectIds] whose class id is in [cids]. |
| IntSet filterByClassPatterns(IntSet objectIds, List<String> patterns) { |
| final tfilter = parseTraverseFilter(patterns); |
| if (tfilter == null) return objectIds; |
| return filter(objectIds, tfilter._shouldFilterObject); |
| } |
| |
| /// Returns set of objects from [objectIds] whose class id is in [cids]. |
| IntSet filter(IntSet objectIds, bool Function(HeapSnapshotObject) filter) { |
| final result = IntSet(); |
| final objects = graph.objects; |
| for (final objId in objectIds) { |
| if (filter(objects[objId])) { |
| result.add(objId); |
| } |
| } |
| return result; |
| } |
| |
| /// Returns users of [objs]. |
| IntSet findUsers(IntSet objs, List<String> patterns) { |
| final tfilter = parseTraverseFilter(patterns); |
| |
| final objects = graph.objects; |
| final result = IntSet(); |
| for (final objId in objs) { |
| final object = objects[objId]; |
| final referrers = object.referrers; |
| for (int i = 0; i < referrers.length; ++i) { |
| final userId = referrers[i]; |
| // This is a dead object in heap that refers to a live object. |
| if (!reachableObjects.contains(userId)) continue; |
| bool passedFilter = true; |
| if (tfilter != null) { |
| final user = objects[userId]; |
| final idx = user.references.indexOf(objId); |
| passedFilter = tfilter._shouldTraverseEdge(user.classId, idx) && |
| tfilter._shouldIncludeObject(user.classId); |
| } |
| if (passedFilter) { |
| result.add(userId); |
| } |
| } |
| } |
| return result; |
| } |
| |
| /// Returns references of [objs]. |
| IntSet findReferences(IntSet objs, List<String> patterns) { |
| final tfilter = parseTraverseFilter(patterns); |
| |
| final objects = graph.objects; |
| final result = IntSet(); |
| for (final objId in objs) { |
| final object = objects[objId]; |
| final references = object.references; |
| for (int i = 0; i < references.length; ++i) { |
| final refId = references[i]; |
| bool passedFilter = true; |
| if (tfilter != null) { |
| final other = objects[refId]; |
| passedFilter = tfilter._shouldTraverseEdge(object.classId, i) && |
| tfilter._shouldIncludeObject(other.classId); |
| } |
| if (passedFilter) { |
| result.add(refId); |
| } |
| } |
| } |
| return result; |
| } |
| |
| /// Returns the size of the variable part of [object] |
| /// |
| /// For strings this is the length of the string (or approximation thereof). |
| /// For typed data this is the number of elements. |
| /// For fixed-length arrays this is the length of the array. |
| int variableLengthOf(HeapSnapshotObject object) { |
| final cid = object.classId; |
| |
| final isList = cid == _nonGrowableListCid || cid == _immutableListCid; |
| if (isList) { |
| // Return the length of the non-growable array. |
| final numFields = graph.classes[cid].fields.length; |
| return object.references.length - numFields; |
| } |
| |
| final isString = cid == _oneByteStringCid || cid == _twoByteStringCid; |
| if (isString) { |
| // Return the length of the string. |
| // |
| // - For lengths <128 the length of string is precise |
| // - For larger strings, the data is truncated, so we use the payload |
| // size. |
| // - TODO: The *heapsnapshot format contains actual length but it gets |
| // lost after reading. Can we preserve it somewhere on |
| // `HeapSnapshotGraph`? |
| // |
| // The approximation is based on knowing the header size of a string: |
| // - String has: header, length (hash - on 32-bit platforms) + payload |
| final fixedSize = |
| _headerSize + _wordSize * (_arch == _Arch.arch32 ? 2 : 1); |
| final len = |
| object.shallowSize == 0 ? 0 : (object.shallowSize - fixedSize); |
| if (len < 128) return (object.data as String).length; |
| return len; // Over-approximates to 2 * wordsize. |
| } |
| |
| final data = object.data; |
| if (data is HeapSnapshotObjectLengthData) { |
| // Most likely typed data object, return length in elements. |
| return data.length; |
| } |
| |
| final fixedSize = _headerSize + _wordSize * object.references.length; |
| final dataSize = object.shallowSize - fixedSize; |
| if (dataSize > _wordSize) { |
| final klass = graph.classes[cid]; |
| // User-visible, but VM-recognized objects with variable size. |
| if (!['_RegExp', '_SuspendState'].contains(klass.name)) { |
| // Non-user-visible, VM-recognized objects (empty library uri). |
| final uri = klass.libraryUri.toString().trim(); |
| if (uri != '') { |
| throw 'Object has fixed size: $fixedSize and total ' |
| 'size: ${object.shallowSize} but is not known to ' |
| 'be variable-length (class: ${graph.classes[cid].name})'; |
| } |
| } |
| } |
| |
| return -1; |
| } |
| |
| int externalSize(HeapSnapshotObject object) { |
| final tdElementSize = elementSizeForExternalTypedDataCid[object.classId]; |
| if (tdElementSize == null) return 0; |
| |
| return tdElementSize * (object.data as HeapSnapshotObjectLengthData).length; |
| } |
| |
| int _findClassId(String className) { |
| return graph.classes |
| .singleWhere((klass) => |
| klass.name == className && |
| (klass.libraryUri.scheme == 'dart' || |
| klass.libraryUri.toString() == '')) |
| .classId; |
| } |
| |
| int _findFieldIndex(int cid, String fieldName) { |
| return graph.classes[cid].fields |
| .singleWhere((f) => f.name == fieldName) |
| .index; |
| } |
| |
| DedupedUint32List _retainingPathOf(int oId, int depth) { |
| final objects = graph.objects; |
| final classes = graph.classes; |
| |
| @pragma('vm:prefer-inline') |
| int getFieldIndex(int oId, int childId) { |
| final object = objects[oId]; |
| final fields = classes[object.classId].fields; |
| final idx = object.references.indexOf(childId); |
| if (idx == -1) throw 'should not happen'; |
| |
| int fieldIndex = fields.any((f) => f.index == idx) |
| ? idx |
| : DedupedUint32List.noFieldIndex; |
| return fieldIndex; |
| } |
| |
| @pragma('vm:prefer-inline') |
| int retainingPathLength(int id) { |
| int length = 1; |
| int id = oId; |
| while (id != _rootObjectIdx && length <= depth) { |
| id = _retainers[id]; |
| length++; |
| } |
| return length; |
| } |
| |
| @pragma('vm:prefer-inline') |
| bool hasMoreThanOneAlive(IntSet reachableObjects, Uint32List list) { |
| int count = 0; |
| for (int i = 0; i < list.length; ++i) { |
| if (reachableObjects.contains(list[i])) { |
| count++; |
| if (count >= 2) return true; |
| } |
| } |
| return false; |
| } |
| |
| int lastId = oId; |
| var lastObject = objects[lastId]; |
| |
| final path = Uint32List(2 * retainingPathLength(oId) - 1); |
| path[0] = lastObject.classId; |
| for (int i = 1; i < path.length; i += 2) { |
| assert(lastId != _rootObjectIdx && ((i - 1) ~/ 2) < depth); |
| final users = lastObject.referrers; |
| final int userId = _retainers[lastId]; |
| |
| final user = objects[userId]; |
| int fieldIndex = getFieldIndex(userId, lastId); |
| final lastWasUniqueRef = !hasMoreThanOneAlive(reachableObjects, users); |
| |
| path[i] = (lastWasUniqueRef ? 1 : 0) << 0 | fieldIndex << 1; |
| path[i + 1] = user.classId; |
| |
| lastId = userId; |
| lastObject = user; |
| } |
| return DedupedUint32List(path); |
| } |
| |
| Uint32List _calculateRetainers() { |
| final retainers = Uint32List(graph.objects.length); |
| |
| var worklist = IntSet()..add(_rootObjectIdx); |
| while (!worklist.isEmpty) { |
| final next = IntSet(); |
| |
| for (final objId in worklist) { |
| final object = graph.objects[objId]; |
| final cid = object.classId; |
| |
| // Weak references don't keep their value alive. |
| if (cid == _weakReferenceCid) continue; |
| |
| // Weak properties keep their value alive if the key is alive. |
| if (cid == _weakPropertyCid) { |
| final valueId = object.references[_weakPropertyValueIdx]; |
| if (reachableObjects.contains(valueId)) { |
| if (retainers[valueId] == 0) { |
| retainers[valueId] = objId; |
| next.add(valueId); |
| } |
| } |
| continue; |
| } |
| |
| // Normal object (or FinalizerEntry). |
| final references = object.references; |
| final bool isFinalizerEntry = cid == _finalizerEntryCid; |
| for (int i = 0; i < references.length; ++i) { |
| // [FinalizerEntry] objects don't keep their "detach" and "value" |
| // fields alive. |
| if (isFinalizerEntry && |
| (i == _finalizerEntryDetachIdx || i == _finalizerEntryValueIdx)) { |
| continue; |
| } |
| |
| final refId = references[i]; |
| if (retainers[refId] == 0) { |
| retainers[refId] = objId; |
| next.add(refId); |
| } |
| } |
| } |
| worklist = next; |
| } |
| return retainers; |
| } |
| } |
| |
| class TraverseFilter { |
| static const int _hasPositiveClassPatternBit = (1 << 0); |
| static const int _hasPositiveEdgePatternBit = (1 << 1); |
| |
| final List<String> _patterns; |
| |
| final int _bits; |
| |
| final IntSet? _allowed; |
| final IntSet? _disallowed; |
| |
| final Map<int, IntSet>? _followMap; |
| final Map<int, IntSet>? _notFollowMap; |
| |
| const TraverseFilter._(this._patterns, this._bits, this._allowed, |
| this._disallowed, this._followMap, this._notFollowMap); |
| |
| bool get _hasPositiveClassPattern => |
| (_bits & _hasPositiveClassPatternBit) != 0; |
| bool get _hasPositiveEdgePattern => (_bits & _hasPositiveEdgePatternBit) != 0; |
| |
| String asString(HeapSnapshotGraph graph) { |
| final sb = StringBuffer(); |
| sb.writeln( |
| 'The traverse filter expression "${_patterns.join(' ')}" matches:\n'); |
| |
| final ca = _allowed ?? IntSet(); |
| final cna = _disallowed ?? IntSet(); |
| |
| final klasses = graph.classes.toList() |
| ..sort((a, b) => a.name.compareTo(b.name)); |
| |
| for (final klass in klasses) { |
| final cid = klass.classId; |
| |
| final posEdge = []; |
| final negEdge = []; |
| |
| final f = _followMap?[cid] ?? IntSet(); |
| final nf = _notFollowMap?[cid] ?? IntSet(); |
| for (final field in klass.fields) { |
| final fieldIndex = field.index; |
| if (f.contains(fieldIndex)) { |
| posEdge.add(field.name); |
| } |
| if (nf.contains(fieldIndex)) { |
| negEdge.add(field.name); |
| } |
| } |
| |
| bool printedClass = false; |
| final name = klass.name; |
| if (ca.contains(cid)) { |
| sb.writeln('[+] $name'); |
| printedClass = true; |
| } |
| if (cna.contains(cid)) { |
| sb.writeln('[-] $name'); |
| printedClass = true; |
| } |
| if (posEdge.isNotEmpty || negEdge.isNotEmpty) { |
| if (!printedClass) { |
| sb.writeln('[ ] $name'); |
| printedClass = true; |
| } |
| for (final field in posEdge) { |
| sb.writeln('[+] .$field'); |
| } |
| for (final field in negEdge) { |
| sb.writeln('[-] .$field'); |
| } |
| } |
| } |
| return sb.toString().trim(); |
| } |
| |
| // Should include the edge when building transitive graphs. |
| bool _shouldTraverseEdge(int cid, int fieldIndex) { |
| final nf = _notFollowMap?[cid]; |
| if (nf != null && nf.contains(fieldIndex)) return false; |
| |
| final f = _followMap?[cid]; |
| if (f != null && f.contains(fieldIndex)) return true; |
| |
| // If there's an allow list we only allow allowed ones, otherwise we allow |
| // all. |
| return !_hasPositiveEdgePattern; |
| } |
| |
| // Should include the object when building transitive graphs. |
| bool _shouldIncludeObject(int cid) { |
| if (_disallowed?.contains(cid) == true) return false; |
| if (_allowed?.contains(cid) == true) return true; |
| |
| // If there's an allow list we only allow allowed ones, otherwise we allow |
| // all. |
| return !_hasPositiveClassPattern; |
| } |
| |
| // Should include the object when filtering a set of objects. |
| bool _shouldFilterObject(HeapSnapshotObject object) { |
| final cid = object.classId; |
| final numReferences = object.references.length; |
| return __shouldFilterObject(cid, numReferences); |
| } |
| |
| bool __shouldFilterObject(int cid, int numReferences) { |
| if (!_shouldIncludeObject(cid)) return false; |
| |
| // Check if the object has an explicitly disallowed field. |
| final nf = _notFollowMap?[cid]; |
| if (nf != null) { |
| for (int fieldIndex = 0; fieldIndex < numReferences; ++fieldIndex) { |
| if (nf.contains(fieldIndex)) return false; |
| } |
| } |
| |
| // Check if the object has an explicitly allowed field. |
| final f = _followMap?[cid]; |
| if (f != null) { |
| for (int fieldIndex = 0; fieldIndex < numReferences; ++fieldIndex) { |
| if (f.contains(fieldIndex)) return true; |
| } |
| } |
| |
| // If there's an allow list we only allow allowed ones, otherwise we allow |
| // all. |
| return !_hasPositiveEdgePattern; |
| } |
| } |
| |
| /// Stringified representation of a heap object. |
| class ObjectInformation { |
| final String className; |
| final String libraryUri; |
| final Map<String, String> fieldValues; |
| |
| ObjectInformation(this.className, this.libraryUri, this.fieldValues); |
| } |
| |
| /// Heap usage statistics calculated for a set of heap objects. |
| class HeapStats { |
| final List<HeapSnapshotClass> classes; |
| final Int32List sizes; |
| final Int32List counts; |
| |
| HeapStats(this.classes, this.sizes, this.counts); |
| |
| int get totalSize => sizes.fold(0, (int a, int b) => a + b); |
| int get totalCount => counts.fold(0, (int a, int b) => a + b); |
| } |
| |
| /// Heap object data statistics calculated for a set of heap objects. |
| class HeapDataStats { |
| final List<HeapData> datas; |
| |
| HeapDataStats(this.datas); |
| |
| int get totalSizeUniqueDatas => |
| datas.fold(0, (int sum, HeapData d) => sum + d.size); |
| int get totalSize => |
| datas.fold(0, (int sum, HeapData d) => sum + d.totalSize); |
| int get totalCount => datas.fold(0, (int sum, HeapData d) => sum + d.count); |
| } |
| |
| /// Representing the data of one heap object. |
| /// |
| /// Since the data can be truncated, it has an extra size that allows to |
| /// distinguish datas with same truncated value with high probability. |
| class HeapData { |
| final String klass; |
| final dynamic value; |
| final int size; |
| final int len; |
| |
| late final int count; |
| |
| HeapData(this.klass, this.value, this.size, this.len); |
| |
| int? _hashCode; |
| int get hashCode { |
| if (_hashCode != null) return _hashCode!; |
| |
| var valueToHash = value; |
| if (valueToHash is! String && |
| valueToHash is! bool && |
| valueToHash is! double) { |
| if (valueToHash is HeapSnapshotObjectLengthData) { |
| valueToHash = valueToHash.length; |
| } else if (valueToHash is HeapSnapshotObjectNoData) { |
| valueToHash = 0; |
| } else if (valueToHash is HeapSnapshotObjectNullData) { |
| valueToHash = 0; |
| } else { |
| throw '${valueToHash.runtimeType}'; |
| } |
| } |
| |
| return _hashCode = Object.hash(klass, valueToHash, size, len); |
| } |
| |
| bool operator ==(other) { |
| if (identical(this, other)) return true; |
| if (other is! HeapData) return false; |
| if (size != other.size) return false; |
| if (len != other.len) return false; |
| if (klass != other.klass) return false; |
| |
| final ovalue = other.value; |
| if (value is String || value is bool || value is double) { |
| return value == ovalue; |
| } |
| // We don't have the typed data content, so we don't know whether they are |
| // equal / dedupable. |
| return false; |
| } |
| |
| String get valueAsString { |
| var d = value; |
| if (d is String) { |
| final newLine = d.indexOf('\n'); |
| if (newLine >= 0) { |
| d = d.substring(0, newLine); |
| } |
| if (d.length > 80) { |
| d = d.substring(0, 80); |
| } |
| return d; |
| } |
| return 'len:$len'; |
| } |
| |
| int get totalSize => size * count; |
| } |
| |
| /// Used to represent retaining paths. |
| /// |
| /// For retaining paths: `[cid0, fieldIdx1 << 1 | isUniqueOwner, cid1, ...]` |
| class DedupedUint32List { |
| static const int noFieldIndex = (1 << 29); |
| |
| final Uint32List path; |
| late final int count; |
| |
| DedupedUint32List(this.path); |
| |
| int? _hashCode; |
| int get hashCode => _hashCode ??= Object.hashAll(path); |
| |
| bool operator ==(other) { |
| if (identical(this, other)) return true; |
| if (other is! DedupedUint32List) return false; |
| if (path.length != other.path.length) return false; |
| for (int i = 0; i < path.length; ++i) { |
| if (path[i] != other.path[i]) return false; |
| } |
| return true; |
| } |
| } |
| |
| enum _Arch { |
| arch32, |
| arch64, |
| arch64c, |
| } |