Skip to content

Commit 957083e

Browse files
ppislsteve-s
authored andcommitted
[GR-21590] Update imports
PullRequest: graalpython/3581
2 parents 6d429b0 + 2fcb295 commit 957083e

File tree

16 files changed

+427
-181
lines changed

16 files changed

+427
-181
lines changed

ci.jsonnet

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "overlay": "9b569abc4c15b4de648248809d31293b16f90182" }
1+
{ "overlay": "87f37c05aaccb6ab3df2b605a9b846cb70734bc9" }

docs/contributor/IMPLEMENTATION_DETAILS.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,25 +259,39 @@ until Java GC runs we do not know if they are garbage or not.
259259
* We cannot traverse the managed objects: since we don't do refcounting on the managed
260260
side, we cannot traverse them and decrement refcounts to see if there is a cycle.
261261

262-
The high level solution is that when we see a "dead" cycle going through a managed object
263-
(i.e., cycle not referenced by any native object from the "outside" of the collected set),
262+
The high level solution is that when we see a "dead" cycle going through an object with a managed reference,
263+
(i.e., cycle not referenced by any native object from the "outside" of the collected set,
264+
which may be, however, referenced from managed),
264265
we fully replicate the object graphs (and the cycle) on the managed side (refcounts of native objects
265266
in the cycle, which were not referenced from managed yet, will get new `NativeObjectReference`
266267
created and refcount incremented by `MANAGED_REFCNT`). Managed objects already refer
267268
to the `PythonAbstractNativeObject` wrappers of the native objects (e.g., some Python container
268269
with managed storage), but we also make the native wrappers refer to whatever their referents
269270
are on the Java side (we use `tp_traverse` to find their referents).
270271

271-
Then we make the managed objects in the cycle only weakly referenced on the Java side.
272+
As part of that, we make the objects in the cycle only weakly referenced on the Java side.
272273
One can think about this as pushing the baseline reference count when the
273274
object is eligible for being GC'ed and thus freed. Normally when the object has
274275
`refcount > MANAGED_REFCNT` we keep it alive with a strong reference assuming that
275276
there are some native references to it. In this case, we know that all the native
276277
references to that object are part of potentially dead cycle, and we do not
277278
count them into this limit. Let us call this limit *weak to strong limit*.
278279

279-
After this, if the managed objects are garbage, eventually Java GC will collect them
280-
together with the whole cycle.
280+
After this, if the objects on the managed side (the managed objects or `PythonAbstractNativeObject`
281+
mirrors of native objects) are garbage, eventually Java GC will collect them.
282+
This will push their references to the reference queue. When polled from the queue (`CApiTransitions#pollReferenceQueue`),
283+
we decrement the refcount by `MANAGED_REFCNT` (no managed references anymore) and
284+
if their refcount falls to `0`, they are freed - as part of that, we call the
285+
`tp_clear` slot for native objects, which should call `Py_CLEAR` for their references,
286+
which does `Py_DecRef` - eventually all objects in the cycle should fall to refcount `0`.
287+
288+
*Example: managed object `o1` has refcount `MANAGED_REFCNT+1`: `MANAGED_REFCNT` representing all managed
289+
references, and `+1` for some native object `o2` referencing it. Native object `o2` has
290+
refcount `MANAGED_REFCNT`, because it is referenced only from managed (from `o1`).
291+
Both `o1` and `o2` form a cycle that was already transformed to managed during cycle GC.
292+
The reference queue processing will subtract `MANAGED_REFCNT` from `o1`'s refcount making it `1`.
293+
Then the reference queue processing will subtract `MANAGED_REFCNT` from `o2`'s refcount making it fall
294+
to `0` - this triggers the `tp_clear` of `o2`, which should subtract the final `1` from `o1`'s refcount.*
281295

282296
If some of the managed objects are not garbage, and they passed back to native code,
283297
the native code can then access and resurrect the whole cycle. W.r.t. the refcounts

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,20 @@ private static boolean mimeTypesComplete(ArrayList<String> mimeJavaStrings) {
313313

314314
private static final LanguageReference<PythonLanguage> REFERENCE = LanguageReference.create(PythonLanguage.class);
315315

316+
// Should be removed when GR-59720 - allow null location for safepoint is implemented
317+
@CompilationFinal public RootNode unavailableSafepointLocation;
318+
319+
private static final class DummyRootNode extends RootNode {
320+
protected DummyRootNode(TruffleLanguage<?> language) {
321+
super(language);
322+
}
323+
324+
@Override
325+
public Object execute(VirtualFrame frame) {
326+
return null;
327+
}
328+
}
329+
316330
/**
317331
* This assumption will be valid if no context set a trace or profile function at any point.
318332
* Calling sys.settrace(None) or sys.setprofile(None) will not invalidate it
@@ -534,6 +548,7 @@ protected void initializeContext(PythonContext context) {
534548
private synchronized void initializeLanguage() {
535549
if (!isLanguageInitialized) {
536550
TpSlots.initializeBuiltinSlots(this);
551+
unavailableSafepointLocation = new DummyRootNode(this);
537552
isLanguageInitialized = true;
538553
}
539554
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBuiltins.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1569,7 +1569,8 @@ static Object doNative(Object weakCandidates,
15691569
PythonNativeWrapper wrapper = nativePtrToPythonWrapperNode.execute(inliningTarget, op, true);
15701570
if (wrapper instanceof PythonAbstractObjectNativeWrapper abstractObjectNativeWrapper) {
15711571
if (GC_LOGGER.isLoggable(Level.FINE)) {
1572-
GC_LOGGER.fine(PythonUtils.formatJString("Breaking reference cycle for %s", abstractObjectNativeWrapper.ref));
1572+
GC_LOGGER.fine(PythonUtils.formatJString("Transitioning to weak reference to break a reference cycle for %s, refcount=%d",
1573+
abstractObjectNativeWrapper.ref, abstractObjectNativeWrapper.getRefCount()));
15731574
}
15741575
updateRefNode.clearStrongRef(inliningTarget, abstractObjectNativeWrapper);
15751576
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiContext.java

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
import com.oracle.graal.python.runtime.exception.PException;
116116
import com.oracle.graal.python.util.Function;
117117
import com.oracle.graal.python.util.PythonUtils;
118+
import com.oracle.graal.python.util.PythonSystemThreadTask;
118119
import com.oracle.graal.python.util.Supplier;
119120
import com.oracle.graal.python.util.SuppressFBWarnings;
120121
import com.oracle.graal.python.util.WeakIdentityHashMap;
@@ -613,9 +614,10 @@ public void untrackObject(Object ptr, PFrame.Reference curFrame, TruffleString c
613614
// TODO(fa): implement untracking of container objects
614615
}
615616

616-
private static final class BackgroundGCTask implements Runnable {
617+
private static final class BackgroundGCTask extends PythonSystemThreadTask {
617618

618619
private BackgroundGCTask(PythonContext context) {
620+
super("Python GC", LOGGER);
619621
this.ctx = new WeakReference<>(context);
620622
this.rssInterval = context.getOption(PythonOptions.BackgroundGCTaskInterval);
621623
this.gcRSSThreshold = context.getOption(PythonOptions.BackgroundGCTaskThreshold) / (double) 100;
@@ -667,17 +669,25 @@ Long getCurrentRSS() {
667669
}
668670

669671
@Override
670-
public void run() {
671-
try {
672-
while (true) {
673-
Thread.sleep(rssInterval);
674-
perform();
675-
}
676-
} catch (InterruptedException e) {
677-
Thread.currentThread().interrupt();
672+
protected void doRun() {
673+
Node location = getSafepointLocation();
674+
if (location == null) {
675+
return;
676+
}
677+
while (true) {
678+
TruffleSafepoint.setBlockedThreadInterruptible(location, Thread::sleep, rssInterval);
679+
perform();
678680
}
679681
}
680682

683+
private Node getSafepointLocation() {
684+
PythonContext context = ctx.get();
685+
if (context == null) {
686+
return null;
687+
}
688+
return context.getLanguage().unavailableSafepointLocation;
689+
}
690+
681691
private void perform() {
682692
PythonContext context = ctx.get();
683693
if (context == null) {
@@ -764,9 +774,7 @@ void runBackgroundGCTask(PythonContext context) {
764774
|| !context.getOption(PythonOptions.BackgroundGCTask)) {
765775
return;
766776
}
767-
backgroundGCTaskThread = context.getEnv().newTruffleThreadBuilder(gcTask).context(context.getEnv().getContext()).build();
768-
backgroundGCTaskThread.setDaemon(true);
769-
backgroundGCTaskThread.setName("python-gc-task");
777+
backgroundGCTaskThread = context.createSystemThread(gcTask);
770778
backgroundGCTaskThread.start();
771779
}
772780

@@ -865,7 +873,7 @@ public static CApiContext ensureCapiWasLoaded(Node node, PythonContext context,
865873
Object finalizeSignature = env.parseInternal(Source.newBuilder(J_NFI_LANGUAGE, "():POINTER", "exec").build()).call();
866874
Object finalizingPointer = SignatureLibrary.getUncached().call(finalizeSignature, finalizeFunction);
867875
try {
868-
cApiContext.addNativeFinalizer(env, finalizingPointer);
876+
cApiContext.addNativeFinalizer(context, finalizingPointer);
869877
cApiContext.runBackgroundGCTask(context);
870878
} catch (RuntimeException e) {
871879
// This can happen when other languages restrict multithreading
@@ -898,14 +906,16 @@ public static CApiContext ensureCapiWasLoaded(Node node, PythonContext context,
898906
* We need to do it in a VM shutdown hook to make sure C atexit won't crash even if our context
899907
* finalization didn't run.
900908
*/
901-
private void addNativeFinalizer(Env env, Object finalizingPointerObj) {
902-
final Unsafe unsafe = getContext().getUnsafe();
909+
private void addNativeFinalizer(PythonContext context, Object finalizingPointerObj) {
910+
final Unsafe unsafe = context.getUnsafe();
903911
InteropLibrary lib = InteropLibrary.getUncached(finalizingPointerObj);
904912
if (!lib.isNull(finalizingPointerObj) && lib.isPointer(finalizingPointerObj)) {
905913
try {
906914
long finalizingPointer = lib.asPointer(finalizingPointerObj);
915+
// We are writing off heap memory and registering a VM shutdown hook, there is no
916+
// point in creating this thread via Truffle sandbox at this point
907917
nativeFinalizerRunnable = () -> unsafe.putInt(finalizingPointer, 1);
908-
nativeFinalizerShutdownHook = env.newTruffleThreadBuilder(nativeFinalizerRunnable).build();
918+
nativeFinalizerShutdownHook = new Thread(nativeFinalizerRunnable);
909919
Runtime.getRuntime().addShutdownHook(nativeFinalizerShutdownHook);
910920
} catch (UnsupportedMessageException e) {
911921
throw new RuntimeException(e);
@@ -946,14 +956,16 @@ public void exitCApiContext() {
946956
@SuppressWarnings("try")
947957
public void finalizeCApi() {
948958
CompilerAsserts.neverPartOfCompilation();
949-
HandleContext handleContext = getContext().nativeContext;
959+
PythonContext context = getContext();
960+
HandleContext handleContext = context.nativeContext;
950961
if (backgroundGCTaskThread != null && backgroundGCTaskThread.isAlive()) {
962+
context.killSystemThread(backgroundGCTaskThread);
951963
try {
952-
backgroundGCTaskThread.interrupt();
953-
backgroundGCTaskThread.join();
964+
backgroundGCTaskThread.join(10);
954965
} catch (InterruptedException e) {
955-
Thread.currentThread().interrupt();
966+
LOGGER.finest("got interrupt while joining GC thread before cleaning up C API state");
956967
}
968+
backgroundGCTaskThread = null;
957969
}
958970

959971
/*

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/transitions/CApiTransitions.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,10 +456,11 @@ public static int pollReferenceQueue() {
456456
assert handleContext.referenceQueuePollActive;
457457
}
458458
count++;
459+
LOGGER.fine(() -> PythonUtils.formatJString("releasing %s, no remaining managed references", entry));
459460
if (entry instanceof PythonObjectReference reference) {
460-
LOGGER.fine(() -> PythonUtils.formatJString("releasing %s", reference.toString()));
461461
if (HandlePointerConverter.pointsToPyHandleSpace(reference.pointer)) {
462462
assert nativeStubLookupGet(handleContext, reference.pointer, reference.handleTableIndex) != null : Long.toHexString(reference.pointer);
463+
LOGGER.finer(() -> PythonUtils.formatJString("releasing native stub lookup for managed object %x => %s", reference.pointer, reference));
463464
nativeStubLookupRemove(handleContext, reference);
464465
/*
465466
* We may only free native object stubs if their reference count is
@@ -468,9 +469,12 @@ public static int pollReferenceQueue() {
468469
* be free'd at context finalization.
469470
*/
470471
long stubPointer = HandlePointerConverter.pointerToStub(reference.pointer);
471-
if (subNativeRefCount(stubPointer, MANAGED_REFCNT) == 0) {
472+
long newRefCount = subNativeRefCount(stubPointer, MANAGED_REFCNT);
473+
if (newRefCount == 0) {
474+
LOGGER.finer(() -> PythonUtils.formatJString("No more references for %s (refcount->0): freeing native stub", reference));
472475
freeNativeStub(reference);
473476
} else {
477+
LOGGER.finer(() -> PythonUtils.formatJString("Some native references to %s remain (refcount=%d): not freeing native stub yet", reference, newRefCount));
474478
/*
475479
* In this case, the object is no longer referenced from managed
476480
* but still from native code (since the reference count is
@@ -493,12 +497,15 @@ public static int pollReferenceQueue() {
493497
}
494498
} else {
495499
assert nativeLookupGet(handleContext, reference.pointer) != null : Long.toHexString(reference.pointer);
500+
LOGGER.finer(() -> PythonUtils.formatJString("releasing native stub lookup for managed object with replacement %x => %s", reference.pointer, reference));
496501
nativeLookupRemove(handleContext, reference.pointer);
497502
if (reference.isFreeAtCollection()) {
503+
LOGGER.finer(() -> PythonUtils.formatJString("freeing managed object %s replacement", reference));
498504
freeNativeStruct(reference);
499505
}
500506
}
501507
} else if (entry instanceof NativeObjectReference reference) {
508+
LOGGER.finer(() -> PythonUtils.formatJString("releasing native lookup for native object %x => %s", reference.pointer, reference));
502509
nativeLookupRemove(handleContext, reference.pointer);
503510
processNativeObjectReference(reference, referencesToBeFreed);
504511
} else if (entry instanceof NativeStorageReference reference) {

0 commit comments

Comments
 (0)