Skip to content

Commit b3e40f9

Browse files
committed
Update compatibility and thread handling of FinalizationRegistry
By default, finalization callbacks will not be called, and nothing will be enqueued on a ReferenceQueue, although the API will continue to function and appear to "register" and "unregister" callbacks to match the spec. If finalization is enabled, embedders of Rhino must somehow ensure that "processMicrotasks" is periodically called to prevent a too-large ReferenceQueue. How this is done depends on the framework in question, although microtask processing happens automatically when Promises are used.
1 parent 5d39d90 commit b3e40f9

10 files changed

Lines changed: 426 additions & 208 deletions

File tree

rhino-tools/src/main/java/org/mozilla/javascript/tools/shell/ShellContextFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ protected void onContextCreated(Context cx) {
4949
cx.setErrorReporter(errorReporter);
5050
}
5151
cx.setGeneratingDebug(generatingDebug);
52+
cx.setFinalizationEnabled(true);
5253
super.onContextCreated(cx);
5354
}
5455

rhino/src/main/java/org/mozilla/javascript/Context.java

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import java.io.Reader;
1717
import java.io.StringWriter;
1818
import java.io.Writer;
19+
import java.lang.ref.Reference;
20+
import java.lang.ref.ReferenceQueue;
1921
import java.lang.reflect.Constructor;
2022
import java.lang.reflect.InvocationTargetException;
2123
import java.lang.reflect.Method;
@@ -508,7 +510,7 @@ public static void exit() {
508510
}
509511
if (cx.enterCount < 1) Kit.codeBug();
510512
if (--cx.enterCount == 0) {
511-
releaseContext(cx);
513+
cx.releaseContext();
512514
}
513515
}
514516

@@ -518,15 +520,17 @@ public void close() {
518520
if (--enterCount == 0) {
519521
assert (currentContext.get() == this)
520522
: "currentContext: " + currentContext.get() + ", this: " + this;
521-
releaseContext(this);
523+
releaseContext();
522524
}
523525
}
524526

525-
private static void releaseContext(Context cx) {
527+
private void releaseContext() {
528+
// Drain the reference queue to prevent memory leaks
529+
cleanUpReferences();
526530
// do not use contextLocal.remove() here, as this might be much slower, when the same thread
527531
// creates a new context. See ContextThreadLocalBenchmark.
528532
currentContext.set(null);
529-
cx.factory.onContextReleased(cx);
533+
factory.onContextReleased(this);
530534
}
531535

532536
/**
@@ -2530,19 +2534,42 @@ public void enqueueMicrotask(Runnable task) {
25302534
* Run all the microtasks for the current context to completion. This is called by the various
25312535
* "evaluate" functions. Frameworks that call Function objects directly should call this
25322536
* function to ensure that everything completes if they want all Promises to eventually resolve.
2533-
* This function is idempotent, but the microtask queue is not thread-safe.
2537+
* This function is idempotent, but the microtask queue is not thread-safe. If references are
2538+
* registered using a FinalizationRegistry, finalization callbacks will be called in this method
2539+
* as well.
25342540
*
25352541
* <p>Nothing will happen if suspendMicrotaskProcessing was called.
25362542
*
25372543
* @see #suspendMicrotaskProcessing()
25382544
*/
25392545
public void processMicrotasks() {
2546+
if (microtaskSuspendCount > 0) {
2547+
return;
2548+
}
2549+
// Clean up references in a microtask in case a finalization call
2550+
// registers a microtask
2551+
microtasks.add(this::cleanUpReferences);
25402552
Runnable head;
2541-
while (microtaskSuspendCount == 0 && (head = microtasks.poll()) != null) {
2553+
while ((head = microtasks.poll()) != null) {
25422554
head.run();
25432555
}
25442556
}
25452557

2558+
/**
2559+
* Clean up an item that has been removed from the reference queue and check if there are more.
2560+
*/
2561+
private void cleanUpReferences() {
2562+
Reference<?> ref;
2563+
while ((ref = referenceQueue.poll()) != null) {
2564+
if (ref instanceof NativeFinalizationRegistry.Registration registration) {
2565+
var registry = registration.getRegistry();
2566+
if (registry != null) {
2567+
registry.cleanup(this, registration);
2568+
}
2569+
}
2570+
}
2571+
}
2572+
25462573
/**
25472574
* Temporarily suspend microtask processing. Tasks will be resumed again when
25482575
* resumeMicrotaskProcessing() is called. Suspensions are cumulative -- each call to this method
@@ -2571,6 +2598,10 @@ public void resumeMicrotaskProcessing() {
25712598
}
25722599
}
25732600

2601+
ReferenceQueue<Object> getReferenceQueue() {
2602+
return referenceQueue;
2603+
}
2604+
25742605
/**
25752606
* Control whether to track unhandled promise rejections. If "track" is set to true, then the
25762607
* tracker returned by "getUnhandledPromiseTracker" must be periodically used to process the
@@ -2591,6 +2622,23 @@ public UnhandledRejectionTracker getUnhandledPromiseTracker() {
25912622
return unhandledPromises;
25922623
}
25932624

2625+
/**
2626+
* Control whether finalization callbacks are dispatched when registered targets are garbage
2627+
* collected. Defaults to false. If set to true, then "processMicrotasks" must periodically be
2628+
* called to ensure that finalization callbacks are fired, or a memory leak may result.
2629+
* (Microtasks are always processed before scripts exit and as a normal part of handling
2630+
* Promises.) It set to false (the default) then the FinalizationRegistry API will still work as
2631+
* the spec describes but callbacks will never fire.
2632+
*/
2633+
public void setFinalizationEnabled(boolean enabled) {
2634+
finalizationEnabled = enabled;
2635+
}
2636+
2637+
/** Returns whether cleanup callbacks will be dispatched at microtask checkpoints. */
2638+
public boolean isFinalizationEnabled() {
2639+
return finalizationEnabled;
2640+
}
2641+
25942642
/* ******** end of API ********* */
25952643

25962644
/** Internal method that reports an error for missing calls to enter(). */
@@ -3024,8 +3072,10 @@ public static EvaluationMethod forLevel(int level) {
30243072
private ClassLoader applicationClassLoader;
30253073
private UnaryOperator<Object> javaToJSONConverter;
30263074
private final ArrayDeque<Runnable> microtasks = new ArrayDeque<>();
3075+
private final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
30273076
private int microtaskSuspendCount;
30283077
private final UnhandledRejectionTracker unhandledPromises = new UnhandledRejectionTracker();
3078+
private boolean finalizationEnabled = false;
30293079

30303080
/** This is the list of names of objects forcing the creation of function activation records. */
30313081
Set<String> activationNames;

0 commit comments

Comments
 (0)