Skip to content

Commit 931560f

Browse files
committed
Improve GC docs and logging
1 parent 3ca4fb0 commit 931560f

File tree

3 files changed

+30
-8
lines changed

3 files changed

+30
-8
lines changed

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/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/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)