blob: 9a0035ead8b0e8dc9fb2179881c96726ea4cf000 [file] [log] [blame]
// 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,
}