Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,12 @@ static final Context enter(Context cx, ContextFactory factory) {
currentContext.set(cx);
}
++cx.enterCount;

// Poll for finalized objects when Context becomes active
if (cx.enterCount == 1) {
FinalizationQueue.pollAndScheduleCleanups(cx, 100);
}

return cx;
}

Expand Down Expand Up @@ -555,6 +561,10 @@ public void close() {
}

private static void releaseContext(Context cx) {
// Process any pending microtasks and finalization cleanups before releasing
cx.processMicrotasks();
cx.processFinalizationCleanups();

// do not use contextLocal.remove() here, as this might be much slower, when the same thread
// creates a new context. See ContextThreadLocalBenchmark.
currentContext.set(null);
Expand Down Expand Up @@ -2520,6 +2530,44 @@ public void processMicrotasks() {
} while (head != null);
}

/**
* Schedule a finalization cleanup task to run after microtasks. This is used by
* FinalizationRegistry to schedule cleanup callbacks. The finalization cleanup queue is not
* thread-safe.
*
* @param cleanup the cleanup task to schedule
*/
void scheduleFinalizationCleanup(Runnable cleanup) {
finalizationCleanups.add(cleanup);
}

/**
* Process all pending finalization cleanup tasks. This is called after microtasks to ensure
* proper execution order. Errors in cleanup callbacks are caught and logged but not propagated.
*/
void processFinalizationCleanups() {
// First check for newly finalized objects
FinalizationQueue.pollAndScheduleCleanups(this, 100);

Runnable cleanup;
int processed = 0;
// Process up to 100 cleanups to avoid blocking
while ((cleanup = finalizationCleanups.poll()) != null && processed < 100) {
try {
cleanup.run();
processed++;
} catch (Exception e) {
// Log but don't propagate errors from cleanup callbacks
if (hasFeature(Context.FEATURE_ENHANCED_JAVA_ACCESS)) {
e.printStackTrace();
}
}
}

// If we still have cleanups pending after processing 100,
// let them run in the next cycle to avoid blocking
}

/**
* Control whether to track unhandled promise rejections. If "track" is set to true, then the
* tracker returned by "getUnhandledPromiseTracker" must be periodically used to process the
Expand Down Expand Up @@ -2842,6 +2890,7 @@ public static boolean isCurrentContextStrict() {
private ClassLoader applicationClassLoader;
private UnaryOperator<Object> javaToJSONConverter;
private final ArrayDeque<Runnable> microtasks = new ArrayDeque<>();
private final ArrayDeque<Runnable> finalizationCleanups = new ArrayDeque<>();
private final UnhandledRejectionTracker unhandledPromises = new UnhandledRejectionTracker();

/** This is the list of names of objects forcing the creation of function activation records. */
Expand Down
83 changes: 83 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/FinalizationQueue.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;

/**
* Shared finalization queue infrastructure for FinalizationRegistry.
*
* <p>Provides shared ReferenceQueue for JVM efficiency and GC detection polling. Uses JSCode
* architecture for Context-safe cleanup execution.
*/
public class FinalizationQueue {

// Single shared queue for all registries (JVM efficient)
private static final ReferenceQueue<Object> SHARED_QUEUE = new ReferenceQueue<>();

/**
* Get the shared reference queue for PhantomReference registration.
*
* @return the shared ReferenceQueue used by all FinalizationRegistry instances
*/
public static ReferenceQueue<Object> getSharedQueue() {
return SHARED_QUEUE;
}

/**
* Poll for finalized objects and schedule cleanups using JSCode architecture.
*
* <p>Called from Context during JavaScript execution to process recently finalized objects.
* Processes at most maxCleanups items to bound execution time. Uses JSCode execution for
* Context safety per aardvark179's architecture patterns.
*
* @param cx the JavaScript execution context
* @param maxCleanups maximum number of cleanup tasks to process
*/
public static void pollAndScheduleCleanups(Context cx, int maxCleanups) {
if (cx == null) return;

int processed = 0;
Reference<?> ref;

while (processed < maxCleanups && (ref = SHARED_QUEUE.poll()) != null) {
if (ref instanceof TrackedPhantomReference) {
TrackedPhantomReference trackedRef = (TrackedPhantomReference) ref;
// Use JSCode execution instead of direct Context capture
trackedRef.scheduleJSCodeCleanup(cx);
processed++;
}
ref.clear();
}
}

/**
* PhantomReference that knows how to schedule its own cleanup using JSCode architecture.
*
* <p>Base class for references that need to perform cleanup when their target is garbage
* collected. Automatically registers with the shared queue. Uses JSCode execution for
* Context-safe cleanup per aardvark179's architecture patterns.
*/
public abstract static class TrackedPhantomReference extends PhantomReference<Object> {

protected TrackedPhantomReference(Object target) {
super(target, SHARED_QUEUE);
}

/**
* Schedule cleanup using JSCode execution (Context-safe).
*
* <p>Called when the referenced object has been garbage collected. Uses JSCode architecture
* to avoid Context capture issues.
*
* @param cx the JavaScript execution context (fresh, never stored)
*/
protected abstract void scheduleJSCodeCleanup(Context cx);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
* Manages registrations and cleanup scheduling for FinalizationRegistry.
*
* <p>Handles the complex logic of tracking PhantomReferences, managing unregister token indexes,
* and coordinating with the shared FinalizationQueue for efficient cleanup processing.
*/
final class FinalizationRegistrationManager {

/** Active registrations using thread-safe concurrent storage */
private final Set<RegistrationReference> activeRegistrations = ConcurrentHashMap.newKeySet();

/** Token-based lookup index for efficient unregistration */
private final Map<TokenKey, Set<RegistrationReference>> tokenIndex =
new ConcurrentHashMap<>(16);

/**
* Register a target for finalization cleanup.
*
* @param target the object to track
* @param heldValue the value to pass to cleanup
* @param unregisterToken optional token for later removal
* @param registry the FinalizationRegistry instance
*/
void register(
Object target,
Object heldValue,
Object unregisterToken,
NativeFinalizationRegistry registry) {
RegistrationReference ref = new RegistrationReference(target, registry, heldValue);

activeRegistrations.add(ref);

if (unregisterToken != null && !Undefined.isUndefined(unregisterToken)) {
TokenKey key = new TokenKey(unregisterToken);
Set<RegistrationReference> refs =
tokenIndex.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet());
refs.add(ref);
}
}

/**
* Unregister all registrations associated with a token.
*
* @param token the unregister token
* @return true if any registrations were removed
*/
boolean unregister(Object token) {
TokenKey key = new TokenKey(token);
Set<RegistrationReference> refs = tokenIndex.remove(key);

if (refs == null || refs.isEmpty()) {
return false;
}

// Remove from active registrations and clear references
for (RegistrationReference ref : refs) {
activeRegistrations.remove(ref);
ref.clear();
}

return true;
}

/**
* Process cleanup callbacks for finalized objects.
*
* @param maxCleanups maximum number to process
* @param cleanupExecutor callback to execute cleanup
*/
void processCleanups(int maxCleanups, CleanupExecutor cleanupExecutor) {
Iterator<RegistrationReference> iterator = activeRegistrations.iterator();
int processed = 0;

while (iterator.hasNext() && processed < maxCleanups) {
RegistrationReference ref = iterator.next();

if (ref.isEnqueued()) {
iterator.remove();
cleanupExecutor.executeCleanup(ref.getHeldValue());
ref.clear();
processed++;
}
}
}

/** Get the current number of active registrations. */
int getActiveCount() {
return activeRegistrations.size();
}

/** Clear all registrations (for finalization cleanup). */
void clear() {
activeRegistrations.clear();
tokenIndex.clear();
}

/** Callback interface for executing cleanup operations. */
interface CleanupExecutor {
void executeCleanup(Object heldValue);
}

/** PhantomReference that tracks target objects for finalization. */
private static final class RegistrationReference
extends FinalizationQueue.TrackedPhantomReference {
private final NativeFinalizationRegistry registry;
private final Object heldValue;

RegistrationReference(
Object target, NativeFinalizationRegistry registry, Object heldValue) {
super(target);
this.registry = registry;
this.heldValue = heldValue;
}

Object getHeldValue() {
return heldValue;
}

@Override
protected void scheduleJSCodeCleanup(Context cx) {
// Schedule cleanup to be executed in the Context processing loop
cx.scheduleFinalizationCleanup(
() -> {
registry.executeCleanupCallback(cx, heldValue);
});
}
}

/** Wrapper for unregister tokens providing identity-based equality. */
private static final class TokenKey {
private final Object token;

TokenKey(Object token) {
this.token = token;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof TokenKey)) return false;
return token == ((TokenKey) obj).token;
}

@Override
public int hashCode() {
return System.identityHashCode(token);
}
}
}
Loading