Reland "[ VM / Service ] Report identity hash codes of objects at end of heap snapshot"
This reverts commit 70836926240d0233cb67d40e00a60a3a9f0b31b3 in order to
reland commit 8d99d295da76c5ac381ea80623876cf04f23ee84.
TEST=N/A
Change-Id: I06c80a666a9307caf0313fef1fde212ec801203c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/182600
Reviewed-by: Nate Bosch <nbosch@google.com>
diff --git a/pkg/vm_service/CHANGELOG.md b/pkg/vm_service/CHANGELOG.md
index eb20917..81695ea 100644
--- a/pkg/vm_service/CHANGELOG.md
+++ b/pkg/vm_service/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## 6.1.0
+- Added `identityHashCode` property to `HeapSnapshotObject`, which can be used to compare
+ objects across heap snapshots.
+- Added `successors` iterable to `HeapSnapshotObject`, which provides a convenient way to
+ access children of a given object.
+- Added `klass` getter to `HeapSnapshotObject`.
+
## 6.0.1
- Stable null-safe release.
diff --git a/pkg/vm_service/lib/src/snapshot_graph.dart b/pkg/vm_service/lib/src/snapshot_graph.dart
index 7e93eed..4db040a 100644
--- a/pkg/vm_service/lib/src/snapshot_graph.dart
+++ b/pkg/vm_service/lib/src/snapshot_graph.dart
@@ -15,6 +15,9 @@
_ReadStream(this._chunks);
+ bool get atEnd => ((_byteIndex >= _chunks[_chunkIndex].lengthInBytes) &&
+ (_chunkIndex + 1 >= _chunks.length));
+
int readByte() {
while (_byteIndex >= _chunks[_chunkIndex].lengthInBytes) {
_chunkIndex++;
@@ -117,6 +120,9 @@
/// A representation of a class type captured in a memory snapshot.
class HeapSnapshotClass {
+ /// The class ID representing this type.
+ int get classId => _classId;
+
/// The simple (not qualified) name of the class.
String get name => _name;
@@ -129,12 +135,13 @@
/// The list of fields in the class.
List<HeapSnapshotField> get fields => _fields;
+ final int _classId;
String _name = '';
String _libraryName = '';
late final Uri _libraryUri;
final List<HeapSnapshotField> _fields = <HeapSnapshotField>[];
- HeapSnapshotClass._read(_ReadStream reader) {
+ HeapSnapshotClass._read(this._classId, _ReadStream reader) {
// flags (reserved).
reader.readUnsigned();
@@ -148,6 +155,18 @@
_populateFields(reader);
}
+ HeapSnapshotClass._root()
+ : _classId = 0,
+ _name = 'Root',
+ _libraryName = '',
+ _libraryUri = Uri();
+
+ HeapSnapshotClass._sentinel()
+ : _classId = 0,
+ _name = 'Sentinel',
+ _libraryName = '',
+ _libraryUri = Uri();
+
void _populateFields(_ReadStream reader) {
final fieldCount = reader.readUnsigned();
for (int i = 0; i < fieldCount; ++i) {
@@ -161,6 +180,14 @@
/// The class ID representing the type of this object.
int get classId => _classId;
+ /// The class representing the type of this object.
+ HeapSnapshotClass get klass {
+ if (_classId <= 0) {
+ return HeapSnapshotClass._sentinel();
+ }
+ return _graph._classes[_classId];
+ }
+
/// The space used by this object in bytes.
int get shallowSize => _shallowSize;
@@ -170,22 +197,54 @@
/// A list of 1-origin indicies into [HeapSnapshotGraph.objects].
List<int> get references => _references;
+ /// The identity hash code of this object.
+ ///
+ /// If `identityHashCode` is 0, either the snapshot did not contain the list
+ /// of identity hash codes or this object cannot be compared across
+ /// snapshots.
+ int get identityHashCode => _identityHashCode;
+
+ Iterable<HeapSnapshotObject> get successors sync* {
+ final startSuccessorIndex = _graph._firstSuccessors[_oid];
+ final limitSuccessorIndex = _graph._firstSuccessors[_oid + 1];
+
+ for (int nextSuccessorIndex = startSuccessorIndex;
+ nextSuccessorIndex < limitSuccessorIndex;
+ ++nextSuccessorIndex) {
+ final successorId = _graph._successors[nextSuccessorIndex];
+ yield _graph.objects[successorId];
+ }
+ }
+
+ final HeapSnapshotGraph _graph;
+ final int _oid;
int _classId = -1;
int _shallowSize = -1;
+ int _identityHashCode = 0;
late final dynamic _data;
final List<int> _references = <int>[];
- HeapSnapshotObject._read(_ReadStream reader) {
+ HeapSnapshotObject._sentinel(this._graph)
+ : _oid = 0,
+ _data = HeapSnapshotObjectNoData() {
+ _graph._firstSuccessors[_oid] = _graph._eid;
+ }
+
+ HeapSnapshotObject._read(this._graph, this._oid, _ReadStream reader) {
_classId = reader.readUnsigned();
_shallowSize = reader.readUnsigned();
_data = _getNonReferenceData(reader);
+ _graph._firstSuccessors[_oid] = _graph._eid;
_populateReferences(reader);
}
void _populateReferences(_ReadStream reader) {
final referencesCount = reader.readUnsigned();
for (int i = 0; i < referencesCount; ++i) {
- _references.add(reader.readUnsigned());
+ int childOid = reader.readUnsigned();
+ _references.add(childOid);
+ _graph._successors[_graph._eid] = childOid;
+ _graph._eid++;
}
}
}
@@ -249,6 +308,10 @@
final List<HeapSnapshotExternalProperty> _externalProperties =
<HeapSnapshotExternalProperty>[];
+ late Uint32List _firstSuccessors;
+ late Uint32List _successors;
+ int _eid = 0;
+
/// Requests a heap snapshot for a given isolate and builds a
/// [HeapSnapshotGraph].
///
@@ -290,23 +353,31 @@
_capacity = reader.readUnsigned();
_externalSize = reader.readUnsigned();
_populateClasses(reader);
- _referenceCount = reader.readUnsigned();
_populateObjects(reader);
_populateExternalProperties(reader);
+ _populateIdentityHashCodes(reader);
}
void _populateClasses(_ReadStream reader) {
final classCount = reader.readUnsigned();
- for (int i = 0; i < classCount; ++i) {
- _classes.add(HeapSnapshotClass._read(reader));
+ _classes.add(HeapSnapshotClass._root());
+ for (int i = 1; i <= classCount; ++i) {
+ final klass = HeapSnapshotClass._read(i, reader);
+ _classes.add(klass);
}
}
void _populateObjects(_ReadStream reader) {
+ _referenceCount = reader.readUnsigned();
final objectCount = reader.readUnsigned();
- for (int i = 0; i < objectCount; ++i) {
- _objects.add(HeapSnapshotObject._read(reader));
+ _firstSuccessors = _newUint32Array(objectCount + 2);
+ _successors = _newUint32Array(_referenceCount);
+
+ _objects.add(HeapSnapshotObject._sentinel(this));
+ for (int i = 1; i <= objectCount; ++i) {
+ _objects.add(HeapSnapshotObject._read(this, i, reader));
}
+ _firstSuccessors[objectCount + 1] = _eid;
}
void _populateExternalProperties(_ReadStream reader) {
@@ -315,6 +386,29 @@
_externalProperties.add(HeapSnapshotExternalProperty._read(reader));
}
}
+
+ void _populateIdentityHashCodes(_ReadStream reader) {
+ if (reader.atEnd) {
+ // Older VMs don't include identity hash codes.
+ return;
+ }
+ final objectCount = _objects.length;
+ for (int i = 1; i < objectCount; ++i) {
+ _objects[i]._identityHashCode = reader.readUnsigned();
+ }
+ }
+
+ Uint32List _newUint32Array(int size) {
+ try {
+ return Uint32List(size);
+ } on ArgumentError {
+ // JS throws a misleading invalid argument error. Convert to a more
+ // user-friendly message.
+ throw Exception(
+ 'OutOfMemoryError: Not enough memory available to analyze the snapshot.',
+ );
+ }
+ }
}
const _kNoData = 0;
diff --git a/pkg/vm_service/pubspec.yaml b/pkg/vm_service/pubspec.yaml
index fcf9783..7760e11 100644
--- a/pkg/vm_service/pubspec.yaml
+++ b/pkg/vm_service/pubspec.yaml
@@ -3,7 +3,7 @@
A library to communicate with a service implementing the Dart VM
service protocol.
-version: 6.0.1
+version: 6.1.0-dev
homepage: https://github.com/dart-lang/sdk/tree/master/pkg/vm_service
diff --git a/pkg/vm_service/test/common/test_helper.dart b/pkg/vm_service/test/common/test_helper.dart
index 634ec64..cd6a983 100644
--- a/pkg/vm_service/test/common/test_helper.dart
+++ b/pkg/vm_service/test/common/test_helper.dart
@@ -133,7 +133,7 @@
fullArgs.add('--pause-isolates-on-start');
}
if (pause_on_exit) {
- fullArgs.add('--pause-isolates-on-io.exit');
+ fullArgs.add('--pause-isolates-on-exit');
}
if (!useAuthToken) {
fullArgs.add('--disable-service-auth-codes');
diff --git a/pkg/vm_service/test/heap_snapshot_graph_test.dart b/pkg/vm_service/test/heap_snapshot_graph_test.dart
index b9fbe10..51490ec 100644
--- a/pkg/vm_service/test/heap_snapshot_graph_test.dart
+++ b/pkg/vm_service/test/heap_snapshot_graph_test.dart
@@ -44,7 +44,8 @@
int actualShallowSize = 0;
int actualRefCount = 0;
snapshotGraph.objects.forEach((HeapSnapshotObject o) {
- expect(o.classId >= 0, isTrue);
+ // -1 is the CID used by the sentinel.
+ expect(o.classId >= -1, isTrue);
expect(o.data, isNotNull);
expect(o.references, isNotNull);
actualShallowSize += o.shallowSize;
@@ -91,7 +92,7 @@
foosFound = 0;
snapshotGraph.objects.forEach((HeapSnapshotObject o) {
if (o.classId == 0) return;
- if (o.classId - 1 == fooClassId) {
+ if (o.classId == fooClassId) {
foosFound++;
}
});
diff --git a/pkg/vm_service/test/object_graph_identity_hash_test.dart b/pkg/vm_service/test/object_graph_identity_hash_test.dart
new file mode 100644
index 0000000..da61a9d
--- /dev/null
+++ b/pkg/vm_service/test/object_graph_identity_hash_test.dart
@@ -0,0 +1,159 @@
+// Copyright (c) 2021, 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.
+
+import 'dart:collection';
+
+import 'package:vm_service/vm_service.dart';
+import 'package:test/test.dart';
+import 'common/service_test_common.dart';
+import 'common/test_helper.dart';
+
+class Foo {}
+
+class Bar {}
+
+class Container1 {
+ @pragma("vm:entry-point")
+ Foo foo = Foo();
+ @pragma("vm:entry-point")
+ Bar bar = Bar();
+}
+
+class Container2 {
+ Container2(this.foo);
+
+ @pragma("vm:entry-point")
+ Foo foo;
+ @pragma("vm:entry-point")
+ Bar bar = Bar();
+}
+
+class Container3 {
+ @pragma("vm:entry-point")
+ int number = 42;
+ @pragma("vm:entry-point")
+ double doub = 3.14;
+ @pragma("vm:entry-point")
+ String foo = 'foobar';
+ @pragma("vm:entry-point")
+ bool bar = false;
+ @pragma("vm:entry-point")
+ late Map baz;
+ @pragma("vm:entry-point")
+ late LinkedHashMap linkedBaz;
+ @pragma("vm:entry-point")
+ late List list;
+ @pragma("vm:entry-point")
+ late List unmodifiableList;
+
+ Container3() {
+ baz = {
+ 'a': 'b',
+ };
+ linkedBaz = LinkedHashMap.from(baz);
+ list = [1, 2, 3];
+ unmodifiableList = List.empty();
+ }
+}
+
+late Container1 c1;
+late Container2 c2;
+late Container3 c3;
+
+void script() {
+ c1 = Container1();
+ c2 = Container2(c1.foo);
+ c3 = Container3();
+}
+
+late HeapSnapshotGraph snapshot1;
+late HeapSnapshotObject snapshot1Foo;
+late HeapSnapshotObject snapshot1Bar;
+
+late HeapSnapshotGraph snapshot2;
+late HeapSnapshotObject snapshot2Foo;
+late HeapSnapshotObject snapshot2Bar;
+
+late HeapSnapshotGraph snapshot3;
+
+final tests = <IsolateTest>[
+ (VmService service, IsolateRef isolate) async {
+ snapshot1 = await HeapSnapshotGraph.getSnapshot(service, isolate);
+
+ Iterable<HeapSnapshotObject> container1s = snapshot1.objects.where(
+ (HeapSnapshotObject obj) => obj.klass.name == 'Container1',
+ );
+ expect(container1s.length, 1);
+
+ final c1Obj = container1s.first;
+
+ snapshot1Foo = c1Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Foo',
+ );
+ expect(
+ snapshot1Foo.identityHashCode != 0,
+ true,
+ );
+
+ snapshot1Bar = c1Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Bar',
+ );
+ expect(
+ snapshot1Bar.identityHashCode != 0,
+ true,
+ );
+ },
+ (VmService service, IsolateRef isolate) async {
+ snapshot2 = await HeapSnapshotGraph.getSnapshot(service, isolate);
+ ;
+ Iterable<HeapSnapshotObject> container2s = snapshot2.objects.where(
+ (HeapSnapshotObject obj) => obj.klass.name == 'Container2',
+ );
+ expect(container2s.length, 1);
+
+ final c2Obj = container2s.first;
+
+ snapshot2Foo = c2Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Foo',
+ );
+ expect(
+ snapshot2Foo.identityHashCode != 0,
+ true,
+ );
+ expect(
+ snapshot1Foo.identityHashCode == snapshot2Foo.identityHashCode,
+ true,
+ );
+
+ snapshot2Bar = c2Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Bar',
+ );
+ expect(
+ snapshot2Bar.identityHashCode != 0,
+ true,
+ );
+ expect(
+ snapshot1Bar.identityHashCode != snapshot2Bar.identityHashCode,
+ true,
+ );
+ },
+ (VmService service, IsolateRef isolate) async {
+ snapshot3 = await HeapSnapshotGraph.getSnapshot(service, isolate);
+ Iterable<HeapSnapshotObject> container3s = snapshot3.objects.where(
+ (HeapSnapshotObject obj) => obj.klass.name == 'Container3',
+ );
+ expect(container3s.length, 1);
+ final c3Obj = container3s.first;
+ for (final successor in c3Obj.successors) {
+ expect(successor.identityHashCode, 0);
+ }
+ },
+];
+
+main(args) => runIsolateTests(
+ args,
+ tests,
+ testeeBefore: script,
+ pause_on_exit: true,
+ );
diff --git a/runtime/observatory/lib/object_graph.dart b/runtime/observatory/lib/object_graph.dart
index f0adee0..85f64df 100644
--- a/runtime/observatory/lib/object_graph.dart
+++ b/runtime/observatory/lib/object_graph.dart
@@ -202,11 +202,16 @@
/// An object in a heap snapshot.
abstract class SnapshotObject {
- // If this object has been obtained from [successors] or [predecessors], the
- // name of slot. Otherwise, the empty string.
+ /// 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.
+ /// The value for primitives. Otherwise, the class name.
String get description;
/// [internalSize] + [externalSize].
@@ -255,10 +260,12 @@
class _SnapshotObject implements SnapshotObject {
final int _id;
+ final int identityHashCode;
final _SnapshotGraph _graph;
final String label;
- _SnapshotObject._new(this._id, this._graph, this.label);
+ _SnapshotObject._new(this._id, this._graph, this.label)
+ : identityHashCode = _graph._identityHashes![_id];
bool operator ==(Object other) {
if (other is _SnapshotObject) {
@@ -344,6 +351,7 @@
late SnapshotObject _parent;
late List<SnapshotObject> _children;
+ int get identityHashCode => 0;
String get label => "";
String get description => _description;
SnapshotClass get klass => _klass;
@@ -805,6 +813,9 @@
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...");
@@ -877,6 +888,7 @@
Uint32List? _externalSizes;
Uint32List? _firstSuccs;
Uint32List? _succs;
+ Uint32List? _identityHashes;
// Intermediates.
Uint32List? _vertex;
@@ -1036,6 +1048,15 @@
_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!;
diff --git a/runtime/observatory/tests/service/object_graph_identity_hash_test.dart b/runtime/observatory/tests/service/object_graph_identity_hash_test.dart
new file mode 100644
index 0000000..eb7c109
--- /dev/null
+++ b/runtime/observatory/tests/service/object_graph_identity_hash_test.dart
@@ -0,0 +1,160 @@
+// Copyright (c) 2021, 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.
+
+import 'package:observatory/object_graph.dart';
+import 'package:observatory/service_io.dart';
+import 'package:test/test.dart';
+import 'service_test_common.dart';
+import 'test_helper.dart';
+
+class Foo {}
+
+class Bar {}
+
+class Container1 {
+ @pragma("vm:entry-point")
+ Foo foo = Foo();
+ @pragma("vm:entry-point")
+ Bar bar = Bar();
+}
+
+class Container2 {
+ Container2(this.foo);
+
+ @pragma("vm:entry-point")
+ Foo foo;
+ @pragma("vm:entry-point")
+ Bar bar = Bar();
+}
+
+class Container3 {
+ @pragma("vm:entry-point")
+ int number = 42;
+ @pragma("vm:entry-point")
+ double doub = 3.14;
+ @pragma("vm:entry-point")
+ String foo = 'foobar';
+ @pragma("vm:entry-point")
+ bool bar = false;
+ @pragma("vm:entry-point")
+ late Map baz;
+ @pragma("vm:entry-point")
+ late List list;
+ @pragma("vm:entry-point")
+ late List unmodifiableList;
+
+ Container3() {
+ baz = {
+ 'a': 'b',
+ };
+ list = [1, 2, 3];
+ unmodifiableList = List.empty();
+ }
+}
+
+@pragma("vm:entry-point")
+late Container1 c1;
+@pragma("vm:entry-point")
+late Container2 c2;
+@pragma("vm:entry-point")
+late Container3 c3;
+
+void script() {
+ c1 = Container1();
+ c2 = Container2(c1.foo);
+ c3 = Container3();
+}
+
+late SnapshotGraph snapshot1;
+late SnapshotObject snapshot1Foo;
+late SnapshotObject snapshot1Bar;
+
+late SnapshotGraph snapshot2;
+late SnapshotObject snapshot2Foo;
+late SnapshotObject snapshot2Bar;
+
+late SnapshotGraph snapshot3;
+
+final tests = <IsolateTest>[
+ (Isolate isolate) async {
+ snapshot1 = await isolate.fetchHeapSnapshot().done;
+
+ Iterable<SnapshotObject> container1s = snapshot1.objects.where(
+ (SnapshotObject obj) => obj.klass.name == 'Container1',
+ );
+ expect(container1s.length, 1);
+
+ final c1Obj = container1s.first;
+
+ c1Obj.successors.forEach((element) {
+ print(element.klass.name);
+ });
+ snapshot1Foo = c1Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Foo',
+ );
+ expect(
+ snapshot1Foo.identityHashCode != 0,
+ true,
+ );
+
+ snapshot1Bar = c1Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Bar',
+ );
+ expect(
+ snapshot1Bar.identityHashCode != 0,
+ true,
+ );
+ },
+ (Isolate isolate) async {
+ snapshot2 = await isolate.fetchHeapSnapshot().done;
+ Iterable<SnapshotObject> container2s = snapshot2.objects.where(
+ (SnapshotObject obj) => obj.klass.name == 'Container2',
+ );
+ expect(container2s.length, 1);
+
+ final c2Obj = container2s.first;
+
+ snapshot2Foo = c2Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Foo',
+ );
+ expect(
+ snapshot2Foo.identityHashCode != 0,
+ true,
+ );
+ expect(
+ snapshot1Foo.identityHashCode == snapshot2Foo.identityHashCode,
+ true,
+ );
+
+ snapshot2Bar = c2Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Bar',
+ );
+ expect(
+ snapshot2Bar.identityHashCode != 0,
+ true,
+ );
+ expect(
+ snapshot1Bar.identityHashCode != snapshot2Bar.identityHashCode,
+ true,
+ );
+ },
+ (Isolate isolate) async {
+ snapshot3 = await isolate.fetchHeapSnapshot().done;
+ Iterable<SnapshotObject> container3s = snapshot3.objects.where(
+ (SnapshotObject obj) => obj.klass.name == 'Container3',
+ );
+ expect(container3s.length, 1);
+ final c3Obj = container3s.first;
+ for (final successor in c3Obj.successors) {
+ expect(successor.identityHashCode, 0);
+ }
+ },
+];
+
+main(args) => runIsolateTests(
+ args,
+ tests,
+ testeeBefore: script,
+ pause_on_exit: true,
+ );
diff --git a/runtime/observatory_2/lib/object_graph.dart b/runtime/observatory_2/lib/object_graph.dart
index 0e891ce..21b426a 100644
--- a/runtime/observatory_2/lib/object_graph.dart
+++ b/runtime/observatory_2/lib/object_graph.dart
@@ -202,11 +202,16 @@
/// An object in a heap snapshot.
abstract class SnapshotObject {
- // If this object has been obtained from [successors] or [predecessors], the
- // name of slot. Otherwise, the empty string.
+ /// 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.
+ /// The value for primitives. Otherwise, the class name.
String get description;
/// [internalSize] + [externalSize].
@@ -255,10 +260,12 @@
class _SnapshotObject implements SnapshotObject {
final int _id;
+ final int identityHashCode;
final _SnapshotGraph _graph;
final String label;
- _SnapshotObject._new(this._id, this._graph, this.label);
+ _SnapshotObject._new(this._id, this._graph, this.label)
+ : identityHashCode = _graph._identityHashes[_id];
bool operator ==(Object other) {
if (other is _SnapshotObject) {
@@ -344,6 +351,7 @@
SnapshotObject _parent;
List<SnapshotObject> _children;
+ int get identityHashCode => 0;
String get label => null;
String get description => _description;
SnapshotClass get klass => _klass;
@@ -794,6 +802,9 @@
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...");
@@ -866,6 +877,7 @@
Uint32List _externalSizes;
Uint32List _firstSuccs;
Uint32List _succs;
+ Uint32List _identityHashes;
// Intermediates.
Uint32List _vertex;
@@ -1025,6 +1037,15 @@
_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;
diff --git a/runtime/observatory_2/tests/service_2/object_graph_identity_hash_test.dart b/runtime/observatory_2/tests/service_2/object_graph_identity_hash_test.dart
new file mode 100644
index 0000000..ed5c71b
--- /dev/null
+++ b/runtime/observatory_2/tests/service_2/object_graph_identity_hash_test.dart
@@ -0,0 +1,160 @@
+// Copyright (c) 2021, 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.
+
+import 'package:observatory_2/object_graph.dart';
+import 'package:observatory_2/service_io.dart';
+import 'package:test/test.dart';
+import 'service_test_common.dart';
+import 'test_helper.dart';
+
+class Foo {}
+
+class Bar {}
+
+class Container1 {
+ @pragma("vm:entry-point")
+ Foo foo = Foo();
+ @pragma("vm:entry-point")
+ Bar bar = Bar();
+}
+
+class Container2 {
+ Container2(this.foo);
+
+ @pragma("vm:entry-point")
+ Foo foo;
+ @pragma("vm:entry-point")
+ Bar bar = Bar();
+}
+
+class Container3 {
+ @pragma("vm:entry-point")
+ int number = 42;
+ @pragma("vm:entry-point")
+ double doub = 3.14;
+ @pragma("vm:entry-point")
+ String foo = 'foobar';
+ @pragma("vm:entry-point")
+ bool bar = false;
+ @pragma("vm:entry-point")
+ Map baz;
+ @pragma("vm:entry-point")
+ List list;
+ @pragma("vm:entry-point")
+ List unmodifiableList;
+
+ Container3() {
+ baz = {
+ 'a': 'b',
+ };
+ list = [1, 2, 3];
+ unmodifiableList = List.empty();
+ }
+}
+
+@pragma("vm:entry-point")
+Container1 c1;
+@pragma("vm:entry-point")
+Container2 c2;
+@pragma("vm:entry-point")
+Container3 c3;
+
+void script() {
+ c1 = Container1();
+ c2 = Container2(c1.foo);
+ c3 = Container3();
+}
+
+SnapshotGraph snapshot1;
+SnapshotObject snapshot1Foo;
+SnapshotObject snapshot1Bar;
+
+SnapshotGraph snapshot2;
+SnapshotObject snapshot2Foo;
+SnapshotObject snapshot2Bar;
+
+SnapshotGraph snapshot3;
+
+final tests = <IsolateTest>[
+ (Isolate isolate) async {
+ snapshot1 = await isolate.fetchHeapSnapshot().done;
+
+ Iterable<SnapshotObject> container1s = snapshot1.objects.where(
+ (SnapshotObject obj) => obj.klass.name == 'Container1',
+ );
+ expect(container1s.length, 1);
+
+ final c1Obj = container1s.first;
+
+ c1Obj.successors.forEach((element) {
+ print(element.klass.name);
+ });
+ snapshot1Foo = c1Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Foo',
+ );
+ expect(
+ snapshot1Foo.identityHashCode != 0,
+ true,
+ );
+
+ snapshot1Bar = c1Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Bar',
+ );
+ expect(
+ snapshot1Bar.identityHashCode != 0,
+ true,
+ );
+ },
+ (Isolate isolate) async {
+ snapshot2 = await isolate.fetchHeapSnapshot().done;
+ Iterable<SnapshotObject> container2s = snapshot2.objects.where(
+ (SnapshotObject obj) => obj.klass.name == 'Container2',
+ );
+ expect(container2s.length, 1);
+
+ final c2Obj = container2s.first;
+
+ snapshot2Foo = c2Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Foo',
+ );
+ expect(
+ snapshot2Foo.identityHashCode != 0,
+ true,
+ );
+ expect(
+ snapshot1Foo.identityHashCode == snapshot2Foo.identityHashCode,
+ true,
+ );
+
+ snapshot2Bar = c2Obj.successors.firstWhere(
+ (element) => element.klass.name == 'Bar',
+ );
+ expect(
+ snapshot2Bar.identityHashCode != 0,
+ true,
+ );
+ expect(
+ snapshot1Bar.identityHashCode != snapshot2Bar.identityHashCode,
+ true,
+ );
+ },
+ (Isolate isolate) async {
+ snapshot3 = await isolate.fetchHeapSnapshot().done;
+ Iterable<SnapshotObject> container3s = snapshot3.objects.where(
+ (SnapshotObject obj) => obj.klass.name == 'Container3',
+ );
+ expect(container3s.length, 1);
+ final c3Obj = container3s.first;
+ for (final successor in c3Obj.successors) {
+ expect(successor.identityHashCode, 0);
+ }
+ },
+];
+
+main(args) => runIsolateTests(
+ args,
+ tests,
+ testeeBefore: script,
+ pause_on_exit: true,
+ );
diff --git a/runtime/vm/object_graph.cc b/runtime/vm/object_graph.cc
index 489b5e1..f71a1745 100644
--- a/runtime/vm/object_graph.cc
+++ b/runtime/vm/object_graph.cc
@@ -1013,6 +1013,90 @@
DISALLOW_COPY_AND_ASSIGN(Pass2Visitor);
};
+class Pass3Visitor : public ObjectVisitor {
+ public:
+ explicit Pass3Visitor(HeapSnapshotWriter* writer)
+ : ObjectVisitor(), isolate_(Isolate::Current()), writer_(writer) {}
+
+ void VisitObject(ObjectPtr obj) {
+ if (obj->IsPseudoObject()) {
+ return;
+ }
+ writer_->WriteUnsigned(GetHash(obj));
+ }
+
+ private:
+ uint32_t GetHash(ObjectPtr obj) {
+ if (!obj->IsHeapObject()) return 0;
+ intptr_t cid = obj->GetClassId();
+ uint32_t hash = 0;
+ switch (cid) {
+ case kForwardingCorpse:
+ case kFreeListElement:
+ case kSmiCid:
+ UNREACHABLE();
+ case kArrayCid:
+ case kBoolCid:
+ case kCodeSourceMapCid:
+ case kCompressedStackMapsCid:
+ case kDoubleCid:
+ case kExternalOneByteStringCid:
+ case kExternalTwoByteStringCid:
+ case kGrowableObjectArrayCid:
+ case kImmutableArrayCid:
+ case kInstructionsCid:
+ case kInstructionsSectionCid:
+ case kLinkedHashMapCid:
+ case kMintCid:
+ case kNeverCid:
+ case kNullCid:
+ case kObjectPoolCid:
+ case kOneByteStringCid:
+ case kPcDescriptorsCid:
+ case kTwoByteStringCid:
+ case kVoidCid:
+ // Don't provide hash codes for objects with the above CIDs in order
+ // to try and avoid having to initialize identity hash codes for common
+ // primitives and types that don't have hash codes.
+ break;
+ default: {
+ hash = GetHashHelper(obj);
+ }
+ }
+ return hash;
+ }
+
+ uint32_t GetHashHelper(ObjectPtr obj) {
+ uint32_t hash;
+#if defined(HASH_IN_OBJECT_HEADER)
+ hash = Object::GetCachedHash(obj);
+ if (hash == 0) {
+ ASSERT(
+ !isolate_->group()->heap()->old_space()->IsObjectFromImagePages(obj));
+ hash = isolate_->random()->NextUInt32();
+ Object::SetCachedHash(obj, hash);
+ hash = Object::GetCachedHash(obj);
+ }
+#else
+ Heap* heap = isolate_->group()->heap();
+ hash = heap->GetHash(obj);
+ if (hash == 0) {
+ ASSERT(!heap->old_space()->IsObjectFromImagePages(obj));
+ heap->SetHash(obj, isolate_->random()->NextUInt32());
+ hash = heap->GetHash(obj);
+ }
+#endif
+ return hash;
+ }
+
+ // TODO(dartbug.com/36097): Once the shared class table contains more
+ // information than just the size (i.e. includes an immutable class
+ // descriptor), we can remove this dependency on the current isolate.
+ Isolate* isolate_;
+ HeapSnapshotWriter* const writer_;
+
+ DISALLOW_COPY_AND_ASSIGN(Pass3Visitor);
+};
void HeapSnapshotWriter::Write() {
HeapIterationScope iteration(thread());
@@ -1181,6 +1265,18 @@
isolate()->group()->VisitWeakPersistentHandles(&visitor);
}
+ {
+ // Identity hash codes
+ Pass3Visitor visitor(this);
+
+ // Handle root object.
+ WriteUnsigned(0);
+
+ // Handle visit rest of the objects.
+ iteration.IterateVMIsolateObjects(&visitor);
+ iteration.IterateObjects(&visitor);
+ }
+
ClearObjectIds();
Flush(true);
}
diff --git a/runtime/vm/service/heap_snapshot.md b/runtime/vm/service/heap_snapshot.md
index c32d562..b2c167f 100644
--- a/runtime/vm/service/heap_snapshot.md
+++ b/runtime/vm/service/heap_snapshot.md
@@ -41,7 +41,7 @@
// The amount of memory reserved for this heap. At least as large as |shallowSize|.
capacity : uleb128,
- // The sum of sizes of all external properites in this graph.
+ // The sum of sizes of all external properties in this graph.
externalSize : uleb128,
classCount : uleb128,
@@ -54,6 +54,14 @@
externalPropertyCount : uleb128,
externalProperties : SnapshotExternalProperty[externalPropertyCount],
+
+ // The list of identity hash codes corresponding to each entry in objects.
+ // A hash code of zero is invalid and cannot be used to determine equality
+ // between objects. If the same object is included in multiple
+ // HeapSnapshots, it will report the same identityHashCode. The converse is
+ // not true: two different objects may report the same identityHashCode
+ // (with low probability).
+ identityHashCodes: uint32[objectCount],
}
```