Skip to content

Commit 7dca34c

Browse files
dcharkesCommit Bot
authored and
Commit Bot
committed
[vm] Implement Finalizer
This CL implements the `Finalizer` in the GC. (This CL does not yet implement `NativeFinalizer`.) The GC is specially aware of two types of objects for the purposes of running finalizers. 1) `FinalizerEntry` 2) `Finalizer` (`FinalizerBase`, `_FinalizerImpl`) A `FinalizerEntry` contains the `value`, the optional `detach` key, and the `token`, and a reference to the `finalizer`. An entry only holds on weakly to the value, detach key, and finalizer. (Similar to how `WeakReference` only holds on weakly to target). A `Finalizer` contains all entries, a list of entries of which the value is collected, and a reference to the isolate. When a the value of an entry is GCed, the enry is added over to the collected list. If any entry is moved to the collected list, a message is sent that invokes the finalizer to call the callback on all entries in that list. When a finalizer is detached by the user, the entry token is set to the entry itself and is removed from the all entries set. This ensures that if the entry was already moved to the collected list, the finalizer is not executed. To speed up detaching, we use a weak map from detach keys to list of entries. This ensures entries can be GCed. Both the scavenger and marker tasks process finalizer entries in parallel. Parallel tasks use an atomic exchange on the head of the collected entries list, ensuring no entries get lost. The mutator thread is guaranteed to be stopped when processing entries. This ensures that we do not need barriers for moving entries into the finalizers collected list. Dart reads and replaces the collected entries list also with an atomic exchange, ensuring the GC doesn't run in between a load/store. When a finalizer gets posted a message to process finalized objects, it is being kept alive by the message. An alternative design would be to pre-allocate a `WeakReference` in the finalizer pointing to the finalizer, and send that itself. This would be at the cost of an extra object. Send and exit is not supported in this CL, support will be added in a follow up CL. Trying to send will throw. Bug: #47777 TEST=runtime/tests/vm/dart/finalizer/* TEST=runtime/tests/vm/dart_2/isolates/fast_object_copy_test.dart TEST=runtime/vm/object_test.cc Change-Id: I03e6b4a46212316254bf46ba3f2df333abaa686c Cq-Include-Trybots: luci.dart.try:vm-kernel-reload-rollback-linux-debug-x64-try,vm-kernel-reload-linux-debug-x64-try,vm-ffi-android-debug-arm64c-try,dart-sdk-mac-arm64-try,vm-kernel-mac-release-arm64-try,pkg-mac-release-arm64-try,vm-kernel-precomp-nnbd-mac-release-arm64-try,vm-kernel-win-debug-x64c-try,vm-kernel-win-debug-x64-try,vm-kernel-precomp-win-debug-x64c-try,vm-kernel-nnbd-win-release-ia32-try,vm-ffi-android-debug-arm-try,vm-precomp-ffi-qemu-linux-release-arm-try,vm-kernel-mac-debug-x64-try,vm-kernel-nnbd-mac-debug-x64-try,vm-kernel-nnbd-linux-debug-ia32-try,benchmark-linux-try,flutter-analyze-try,flutter-frontend-try,pkg-linux-debug-try,vm-kernel-asan-linux-release-x64-try,vm-kernel-gcc-linux-try,vm-kernel-optcounter-threshold-linux-release-x64-try,vm-kernel-precomp-linux-debug-simarm_x64-try,vm-kernel-precomp-obfuscate-linux-release-x64-try,vm-kernel-precomp-linux-debug-x64-try,vm-kernel-precomp-linux-debug-x64c-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/229544 Reviewed-by: Lasse Nielsen <[email protected]> Reviewed-by: Slava Egorov <[email protected]> Reviewed-by: Martin Kustermann <[email protected]> Reviewed-by: Ryan Macnak <[email protected]> Commit-Queue: Daco Harkes <[email protected]>
1 parent a999d21 commit 7dca34c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+4318
-446
lines changed

runtime/docs/gc.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ A tag of 1 has no penalty on heap object access because removing the tag can be
1313
Heap objects are always allocated in double-word increments. Objects in old-space are kept at double-word alignment (address % double-word == 0), and objects in new-space are kept offset from double-word alignment (address % double-word == word). This allows checking an object's age without comparing to a boundary address, avoiding restrictions on heap placement and avoiding loading the boundary from thread-local storage. Additionally, the scavenger can quickly skip over both immediates and old objects with a single branch.
1414

1515
| Pointer | Referent |
16-
| --- | --- |
16+
| ---------- | --------------------------------------- |
1717
| 0x00000002 | Small integer 1 |
1818
| 0xFFFFFFFE | Small integer -1 |
1919
| 0x00A00001 | Heap object at 0x00A00000, in old-space |
@@ -75,7 +75,7 @@ The Dart VM includes a sliding compactor. The forwarding table is compactly repr
7575

7676
## Concurrent Marking
7777

78-
To reduce the time the mutator is paused for old-space GCs, we allow the mutator to continue running during most of the marking work.
78+
To reduce the time the mutator is paused for old-space GCs, we allow the mutator to continue running during most of the marking work.
7979

8080
### Barrier
8181

@@ -204,3 +204,35 @@ container <- AllocateObject
204204
<instructions that cannot directly call Dart functions>
205205
StoreInstanceField(container, value, NoBarrier)
206206
```
207+
208+
## Finalizers
209+
210+
The GC is aware of two types of objects for the purposes of running finalizers.
211+
212+
1) `FinalizerEntry`
213+
2) `Finalizer` (`FinalizerBase`, `_FinalizerImpl`)
214+
215+
A `FinalizerEntry` contains the `value`, the optional `detach` key, and the `token`, and a reference to the `finalizer`.
216+
An entry only holds on weakly to the value, detach key, and finalizer. (Similar to how `WeakReference` only holds on weakly to target).
217+
218+
A `Finalizer` contains all entries, a list of entries of which the value is collected, and a reference to the isolate.
219+
220+
When the value of an entry is GCed, the entry is added over to the collected list.
221+
If any entry is moved to the collected list, a message is sent that invokes the finalizer to call the callback on all entries in that list.
222+
223+
When a finalizer is detached by the user, the entry token is set to the entry itself and is removed from the all entries set.
224+
This ensures that if the entry was already moved to the collected list, the finalizer is not executed.
225+
226+
To speed up detaching, we use a weak map from detach keys to list of entries. This ensures entries can be GCed.
227+
228+
Both the scavenger and marker can process finalizer entries in parallel.
229+
Parallel tasks use an atomic exchange on the head of the collected entries list, ensuring no entries get lost.
230+
Mutator threads are guaranteed to be stopped when processing entries.
231+
This ensures that we do not need barriers for moving entries into the finalizers collected list.
232+
Dart reads and replaces the collected entries list also with an atomic exchange, ensuring the GC doesn't run in between a load/store.
233+
234+
When a finalizer gets posted a message to process finalized objects, it is being kept alive by the message.
235+
An alternative design would be to pre-allocate a `WeakReference` in the finalizer pointing to the finalizer, and send that itself.
236+
This would be at the cost of an extra object.
237+
238+
If the finalizer object itself is GCed, the callback is not run for any of the attachments.

runtime/lib/isolate.cc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ static ObjectPtr ValidateMessageObject(Zone* zone,
242242
break;
243243

244244
MESSAGE_SNAPSHOT_ILLEGAL(DynamicLibrary);
245+
// TODO(http://dartbug.com/47777): Send and exit support: remove this.
246+
MESSAGE_SNAPSHOT_ILLEGAL(Finalizer);
245247
MESSAGE_SNAPSHOT_ILLEGAL(MirrorReference);
246248
MESSAGE_SNAPSHOT_ILLEGAL(Pointer);
247249
MESSAGE_SNAPSHOT_ILLEGAL(ReceivePort);
@@ -284,6 +286,7 @@ static ObjectPtr ValidateMessageObject(Zone* zone,
284286
return obj.ptr();
285287
}
286288

289+
// TODO(http://dartbug.com/47777): Add support for Finalizers.
287290
DEFINE_NATIVE_ENTRY(Isolate_exit_, 0, 2) {
288291
GET_NATIVE_ARGUMENT(SendPort, port, arguments->NativeArgAt(0));
289292
if (!port.IsNull()) {
@@ -638,9 +641,10 @@ class SpawnIsolateTask : public ThreadPool::Task {
638641
// Make a copy of the state's isolate flags and hand it to the callback.
639642
Dart_IsolateFlags api_flags = *(state_->isolate_flags());
640643
api_flags.is_system_isolate = false;
641-
Dart_Isolate isolate = (create_group_callback)(
642-
state_->script_url(), name, nullptr, state_->package_config(),
643-
&api_flags, parent_isolate_->init_callback_data(), &error);
644+
Dart_Isolate isolate =
645+
(create_group_callback)(state_->script_url(), name, nullptr,
646+
state_->package_config(), &api_flags,
647+
parent_isolate_->init_callback_data(), &error);
644648
parent_isolate_->DecrementSpawnCount();
645649
parent_isolate_ = nullptr;
646650

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:isolate';
6+
7+
import 'helpers.dart';
8+
9+
int callbackCount = 0;
10+
11+
void callback(Nonce token) {
12+
callbackCount++;
13+
print('$name: Running finalizer: token: $token');
14+
}
15+
16+
final finalizer = Finalizer<Nonce>(callback);
17+
18+
late String name;
19+
20+
void main(List<String> arguments, SendPort port) async {
21+
name = arguments[0];
22+
23+
final token = Nonce(42);
24+
makeObjectWithFinalizer(finalizer, token);
25+
26+
final awaitBeforeShuttingDown = ReceivePort();
27+
port.send(awaitBeforeShuttingDown.sendPort);
28+
final message = await awaitBeforeShuttingDown.first;
29+
print('$name: $message');
30+
31+
await Future.delayed(Duration(milliseconds: 1));
32+
print('$name: Awaited to see if there were any callbacks.');
33+
34+
print('$name: Helper isolate exiting. num callbacks: $callbackCount.');
35+
port.send(callbackCount);
36+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// VMOptions=
6+
// VMOptions=--use_compactor
7+
// VMOptions=--use_compactor --force_evacuation
8+
9+
import 'dart:async';
10+
import 'dart:isolate';
11+
12+
import 'package:async/async.dart';
13+
import 'package:expect/expect.dart';
14+
15+
import 'helpers.dart';
16+
17+
void main() async {
18+
await testFinalizerInOtherIsolateGroupGCBeforeExit();
19+
await testFinalizerInOtherIsolateGroupGCAfterExit();
20+
await testFinalizerInOtherIsolateGroupNoGC();
21+
22+
print('$name: End of test, shutting down.');
23+
}
24+
25+
const name = 'main';
26+
27+
late bool hotReloadBot;
28+
29+
Future<void> testFinalizerInOtherIsolateGroupGCBeforeExit() async {
30+
final receivePort = ReceivePort();
31+
final messagesQueue = StreamQueue(receivePort);
32+
33+
await Isolate.spawnUri(
34+
Uri.parse('finalizer_isolate_groups_run_gc_helper.dart'),
35+
['helper 1'],
36+
receivePort.sendPort,
37+
);
38+
final signalHelperIsolate = await messagesQueue.next as SendPort;
39+
40+
doGC(name: name);
41+
await yieldToMessageLoop(name: name);
42+
43+
signalHelperIsolate.send('Done GCing.');
44+
45+
final helperCallbacks = await messagesQueue.next as int;
46+
messagesQueue.cancel();
47+
print('$name: Helper exited.');
48+
// Different isolate group, so we don't expect a GC in this isolate to cause
49+
// collected objects in the helper.
50+
// Except for in --hot-reload-test-mode, then the GC is triggered.
51+
hotReloadBot = helperCallbacks == 1;
52+
}
53+
54+
Future<void> testFinalizerInOtherIsolateGroupGCAfterExit() async {
55+
final receivePort = ReceivePort();
56+
final messagesQueue = StreamQueue(receivePort);
57+
await Isolate.spawnUri(
58+
Uri.parse('finalizer_isolate_groups_run_gc_helper.dart'),
59+
['helper 2'],
60+
receivePort.sendPort,
61+
);
62+
63+
final signalHelperIsolate = await messagesQueue.next as SendPort;
64+
65+
signalHelperIsolate.send('Before GCing.');
66+
67+
final helperCallbacks = await messagesQueue.next as int;
68+
messagesQueue.cancel();
69+
print('$name: Helper exited.');
70+
Expect.equals(hotReloadBot ? 1 : 0, helperCallbacks);
71+
72+
doGC(name: name);
73+
await yieldToMessageLoop(name: name);
74+
}
75+
76+
Future<void> testFinalizerInOtherIsolateGroupNoGC() async {
77+
final receivePort = ReceivePort();
78+
final messagesQueue = StreamQueue(receivePort);
79+
80+
await Isolate.spawnUri(
81+
Uri.parse('finalizer_isolate_groups_run_gc_helper.dart'),
82+
['helper 3'],
83+
receivePort.sendPort,
84+
);
85+
final signalHelperIsolate = await messagesQueue.next as SendPort;
86+
87+
signalHelperIsolate.send('Before quitting main isolate.');
88+
89+
final helperCallbacks = await messagesQueue.next as int;
90+
messagesQueue.cancel();
91+
print('$name: Helper exited.');
92+
Expect.equals(hotReloadBot ? 1 : 0, helperCallbacks);
93+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// VMOptions=
6+
// VMOptions=--use_compactor
7+
// VMOptions=--use_compactor --force_evacuation
8+
9+
import 'dart:async';
10+
import 'dart:isolate';
11+
12+
import 'package:expect/expect.dart';
13+
14+
import 'helpers.dart';
15+
16+
void main() async {
17+
await testNormalExit();
18+
await testSendAndExit();
19+
await testSendAndExitFinalizer();
20+
print('End of test, shutting down.');
21+
}
22+
23+
final finalizerTokens = <Nonce>{};
24+
25+
void callback(Nonce token) {
26+
print('Running finalizer: token: $token');
27+
finalizerTokens.add(token);
28+
}
29+
30+
void runIsolateAttachFinalizer(Object? message) {
31+
final finalizer = Finalizer<Nonce>(callback);
32+
final value = Nonce(1001);
33+
final token = Nonce(1002);
34+
finalizer.attach(value, token);
35+
final token9 = Nonce(9002);
36+
makeObjectWithFinalizer(finalizer, token9);
37+
if (message == null) {
38+
print('Isolate done.');
39+
return;
40+
}
41+
final list = message as List;
42+
assert(list.length == 2);
43+
final sendPort = list[0] as SendPort;
44+
final tryToSendFinalizer = list[1] as bool;
45+
if (tryToSendFinalizer) {
46+
Expect.throws(() {
47+
// TODO(http://dartbug.com/47777): Send and exit support.
48+
print('Trying to send and exit finalizer.');
49+
Isolate.exit(sendPort, [value, finalizer]);
50+
});
51+
}
52+
print('Isolate sending and exit.');
53+
Isolate.exit(sendPort, [value]);
54+
}
55+
56+
Future testNormalExit() async {
57+
final portExitMessage = ReceivePort();
58+
await Isolate.spawn(
59+
runIsolateAttachFinalizer,
60+
null,
61+
onExit: portExitMessage.sendPort,
62+
);
63+
await portExitMessage.first;
64+
65+
doGC();
66+
await yieldToMessageLoop();
67+
68+
Expect.equals(0, finalizerTokens.length);
69+
}
70+
71+
@pragma('vm:never-inline')
72+
Future<Finalizer?> testSendAndExitHelper(
73+
{bool trySendFinalizer = false}) async {
74+
final port = ReceivePort();
75+
await Isolate.spawn(
76+
runIsolateAttachFinalizer,
77+
[port.sendPort, trySendFinalizer],
78+
);
79+
final message = await port.first as List;
80+
print('Received message ($message).');
81+
final value = message[0] as Nonce;
82+
print('Received value ($value), but now forgetting about it.');
83+
84+
Expect.equals(1, message.length);
85+
// TODO(http://dartbug.com/47777): Send and exit support.
86+
return null;
87+
}
88+
89+
Future testSendAndExit() async {
90+
await testSendAndExitHelper(trySendFinalizer: false);
91+
92+
doGC();
93+
await yieldToMessageLoop();
94+
95+
Expect.equals(0, finalizerTokens.length);
96+
}
97+
98+
Future testSendAndExitFinalizer() async {
99+
final finalizer = await testSendAndExitHelper(trySendFinalizer: true);
100+
101+
// TODO(http://dartbug.com/47777): Send and exit support.
102+
Expect.isNull(finalizer);
103+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:expect/expect.dart';
6+
7+
import 'helpers.dart';
8+
9+
void main() {
10+
testFinalizer();
11+
}
12+
13+
void testFinalizer() async {
14+
final finalizerTokens = <Nonce?>{};
15+
void callback(Nonce? token) {
16+
print('Running finalizer: token: $token');
17+
finalizerTokens.add(token);
18+
}
19+
20+
final finalizer = Finalizer<Nonce?>(callback);
21+
22+
{
23+
final detach = Nonce(2022);
24+
final token = null;
25+
26+
makeObjectWithFinalizer(finalizer, token, detach: detach);
27+
28+
doGC();
29+
30+
// We haven't stopped running synchronous dart code yet.
31+
Expect.isFalse(finalizerTokens.contains(token));
32+
33+
await Future.delayed(Duration(milliseconds: 1));
34+
35+
// Now we have.
36+
Expect.isTrue(finalizerTokens.contains(token));
37+
38+
// Try detaching after finalizer ran.
39+
finalizer.detach(detach);
40+
}
41+
42+
print('End of test, shutting down.');
43+
}

0 commit comments

Comments
 (0)