From e03f2f89f382c0db8c240310a923e6d38b0f8176 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Fri, 8 May 2026 15:02:32 +0200 Subject: [PATCH 1/2] [GR-53387] Check C API residue in leak tests --- .../graal/python/test/advanced/LeakTest.java | 85 ++++++++++++++++++- mx.graalpython/mx_graalpython.py | 10 ++- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/advanced/LeakTest.java b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/advanced/LeakTest.java index 2bef8eaf9b..2998ce1c01 100644 --- a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/advanced/LeakTest.java +++ b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/advanced/LeakTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -66,6 +66,7 @@ import org.netbeans.lib.profiler.heap.HeapFactory; import org.netbeans.lib.profiler.heap.Instance; import org.netbeans.lib.profiler.heap.JavaClass; +import org.netbeans.lib.profiler.heap.ObjectArrayInstance; import com.oracle.graal.python.test.integration.Utils; import com.sun.management.HotSpotDiagnosticMXBean; @@ -100,6 +101,7 @@ public static void main(String[] args) { private boolean keepDump = false; private int repeatAndCheckSize = -1; private boolean nullStdout = false; + private boolean forbidCApiResidue = false; private String languageId; private String code; private List forbiddenClasses = new ArrayList<>(); @@ -161,19 +163,89 @@ private boolean checkForLeaks(Path dumpFile) { } } } + if (forbidCApiResidue && checkCApiResidue(heap)) { + fail = true; + } } catch (IOException e) { throw new RuntimeException(e); } return fail; } + private boolean checkCApiResidue(Heap heap) { + JavaClass cls = heap.getJavaClassByName( + "com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions$HandleContext"); + if (cls == null) { + return false; + } + boolean fail = false; + for (Object i : cls.getInstances()) { + Instance inst = (Instance) i; + if (!isReachable(inst)) { + continue; + } + List residues = new ArrayList<>(); + addResidue(residues, "referencesToBeFreed", collectionSize(inst.getValueOfField("referencesToBeFreed"))); + addResidue(residues, "nativeLookup", collectionSize(inst.getValueOfField("nativeLookup"))); + addResidue(residues, "nativeWeakRef", collectionSize(inst.getValueOfField("nativeWeakRef"))); + addResidue(residues, "managedNativeLookup", collectionSize(inst.getValueOfField("managedNativeLookup"))); + addResidue(residues, "nativeTypeLookup", objectArraySize(inst.getValueOfField("nativeTypeLookup"))); + addResidue(residues, "nativeStubLookup", objectArraySize(inst.getValueOfField("nativeStubLookup"))); + addResidue(residues, "nativeStorageReferences", collectionSize(inst.getValueOfField("nativeStorageReferences"))); + addResidue(residues, "pyCapsuleReferences", collectionSize(inst.getValueOfField("pyCapsuleReferences"))); + if (!residues.isEmpty()) { + fail = true; + System.err.println("C API residue in reachable HandleContext " + inst.getInstanceId() + ": " + + String.join(", ", residues)); + } + } + return fail; + } + + private void addResidue(List residues, String name, int size) { + if (size > 0) { + residues.add(name + "=" + size); + } + } + + private int collectionSize(Object object) { + if (object instanceof Instance instance) { + Object size = instance.getValueOfField("size"); + if (size instanceof Number n) { + return n.intValue(); + } + Object baseCount = instance.getValueOfField("baseCount"); + if (baseCount instanceof Number n) { + return n.intValue(); + } + Object map = instance.getValueOfField("map"); + if (map instanceof Instance mapInstance) { + return collectionSize(mapInstance); + } + } + return 0; + } + + private int objectArraySize(Object object) { + if (object instanceof ObjectArrayInstance array) { + int size = 0; + for (Object value : array.getValues()) { + if (value != null) { + size++; + } + } + return size; + } + return 0; + } + private int getCntAndErrors(JavaClass cls, List errors) { int cnt = cls.getInstancesCount(); if (cnt > 0) { boolean realLeak = false; for (Object i : cls.getInstances()) { Instance inst = (Instance) i; - if (inst.isGCRoot() || inst.getNearestGCRootPointer() != null) { + if (isReachable(inst)) { realLeak = true; break; } @@ -188,6 +260,10 @@ private int getCntAndErrors(JavaClass cls, List errors) { return cnt; } + private boolean isReachable(Instance inst) { + return inst.isGCRoot() || inst.getNearestGCRootPointer() != null; + } + @SuppressWarnings("sync-override") @Override public final Throwable fillInStackTrace() { @@ -271,6 +347,8 @@ protected List preprocessArguments(List arguments, Map= mx.VersionSpec("22.0.0") # test leaks when some C module code is involved if has_jep_454: - run_leak_launcher(["--code", 'import _testcapi, mmap, bz2; print(memoryview(b"").nbytes)']) + run_leak_launcher([ + "--forbid-capi-residue", "--code", + 'import _testcapi, mmap, bz2; print(memoryview(b"").nbytes)', + ]) # test leaks with shared engine Python code only run_leak_launcher(["--shared-engine", "--code", "pass"]) run_leak_launcher(["--shared-engine", "--repeat-and-check-size", "250", "--null-stdout", "--code", "print('hello')"]) # test leaks with shared engine when some C module code is involved if has_jep_454: - run_leak_launcher(["--shared-engine", "--code", 'import _testcapi, mmap, bz2; print(memoryview(b"").nbytes)']) + run_leak_launcher([ + "--shared-engine", "--forbid-capi-residue", "--code", + 'import _testcapi, mmap, bz2; print(memoryview(b"").nbytes)', + ]) run_leak_launcher(["--shared-engine", "--code", '[10, 20]', "--python.UseNativePrimitiveStorageStrategy=true", "--forbidden-class", "com.oracle.graal.python.runtime.sequence.storage.NativePrimitiveSequenceStorage", "--forbidden-class", "com.oracle.graal.python.runtime.native_memory.NativePrimitiveReference"]) From 1b731033187e2aa9645f1e07bda9b16b13461772 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Mon, 11 May 2026 11:45:51 +0200 Subject: [PATCH 2/2] [GR-53387] Address C API leak test review --- .../graal/python/test/advanced/LeakTest.java | 36 ++++++++++++------- mx.graalpython/mx_graalpython.py | 10 ++++-- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/advanced/LeakTest.java b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/advanced/LeakTest.java index 2998ce1c01..ce8438d096 100644 --- a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/advanced/LeakTest.java +++ b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/advanced/LeakTest.java @@ -68,6 +68,7 @@ import org.netbeans.lib.profiler.heap.JavaClass; import org.netbeans.lib.profiler.heap.ObjectArrayInstance; +import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.HandleContext; import com.oracle.graal.python.test.integration.Utils; import com.sun.management.HotSpotDiagnosticMXBean; @@ -173,10 +174,10 @@ private boolean checkForLeaks(Path dumpFile) { } private boolean checkCApiResidue(Heap heap) { - JavaClass cls = heap.getJavaClassByName( - "com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions$HandleContext"); + JavaClass cls = heap.getJavaClassByName(HandleContext.class.getName()); if (cls == null) { - return false; + System.err.println("Could not find " + HandleContext.class.getName() + " in heap dump"); + return true; } boolean fail = false; for (Object i : cls.getInstances()) { @@ -185,14 +186,14 @@ private boolean checkCApiResidue(Heap heap) { continue; } List residues = new ArrayList<>(); - addResidue(residues, "referencesToBeFreed", collectionSize(inst.getValueOfField("referencesToBeFreed"))); - addResidue(residues, "nativeLookup", collectionSize(inst.getValueOfField("nativeLookup"))); - addResidue(residues, "nativeWeakRef", collectionSize(inst.getValueOfField("nativeWeakRef"))); - addResidue(residues, "managedNativeLookup", collectionSize(inst.getValueOfField("managedNativeLookup"))); - addResidue(residues, "nativeTypeLookup", objectArraySize(inst.getValueOfField("nativeTypeLookup"))); - addResidue(residues, "nativeStubLookup", objectArraySize(inst.getValueOfField("nativeStubLookup"))); - addResidue(residues, "nativeStorageReferences", collectionSize(inst.getValueOfField("nativeStorageReferences"))); - addResidue(residues, "pyCapsuleReferences", collectionSize(inst.getValueOfField("pyCapsuleReferences"))); + addResidue(residues, inst, "referencesToBeFreed", this::collectionSize); + addResidue(residues, inst, "nativeLookup", this::collectionSize); + addResidue(residues, inst, "nativeWeakRef", this::collectionSize); + addResidue(residues, inst, "managedNativeLookup", this::collectionSize); + addResidue(residues, inst, "nativeTypeLookup", this::objectArraySize); + addResidue(residues, inst, "nativeStubLookup", this::objectArraySize); + addResidue(residues, inst, "nativeStorageReferences", this::collectionSize); + addResidue(residues, inst, "pyCapsuleReferences", this::collectionSize); if (!residues.isEmpty()) { fail = true; System.err.println("C API residue in reachable HandleContext " + inst.getInstanceId() + ": " + @@ -202,12 +203,23 @@ private boolean checkCApiResidue(Heap heap) { return fail; } - private void addResidue(List residues, String name, int size) { + private void addResidue(List residues, Instance inst, String name, FieldSize fieldSize) { + Object fieldValue = inst.getValueOfField(name); + if (fieldValue == null) { + residues.add(name + "=missing"); + return; + } + int size = fieldSize.apply(fieldValue); if (size > 0) { residues.add(name + "=" + size); } } + @FunctionalInterface + private interface FieldSize { + int apply(Object object); + } + private int collectionSize(Object object) { if (object instanceof Instance instance) { Object size = instance.getValueOfField("size"); diff --git a/mx.graalpython/mx_graalpython.py b/mx.graalpython/mx_graalpython.py index 661022c69d..b473a902be 100644 --- a/mx.graalpython/mx_graalpython.py +++ b/mx.graalpython/mx_graalpython.py @@ -708,11 +708,16 @@ def __post_init__(self): run_leak_launcher(["--code", "pass", ]) run_leak_launcher(["--repeat-and-check-size", "250", "--null-stdout", "--code", "print('hello')"]) has_jep_454 = mx.get_jdk().version >= mx.VersionSpec("22.0.0") + c_api_leak_test = ( + 'import _testcapi; ' + 't = _testcapi.tuple_pack(2, "a", "b"); ' + 'assert _testcapi.tuple_get_item(t, 1) == "b"' + ) # test leaks when some C module code is involved if has_jep_454: run_leak_launcher([ "--forbid-capi-residue", "--code", - 'import _testcapi, mmap, bz2; print(memoryview(b"").nbytes)', + c_api_leak_test, ]) # test leaks with shared engine Python code only run_leak_launcher(["--shared-engine", "--code", "pass"]) @@ -721,7 +726,7 @@ def __post_init__(self): if has_jep_454: run_leak_launcher([ "--shared-engine", "--forbid-capi-residue", "--code", - 'import _testcapi, mmap, bz2; print(memoryview(b"").nbytes)', + c_api_leak_test, ]) run_leak_launcher(["--shared-engine", "--code", '[10, 20]', "--python.UseNativePrimitiveStorageStrategy=true", "--forbidden-class", "com.oracle.graal.python.runtime.sequence.storage.NativePrimitiveSequenceStorage", @@ -2930,6 +2935,7 @@ def run_leak_launcher(input_args): vm_args, graalpython_args = mx.extract_VM_args(args, useDoubleDash=True, defaultAllVMArgs=False) vm_args += mx.get_runtime_jvm_args(dists) vm_args += ['--add-exports', 'org.graalvm.py/com.oracle.graal.python.builtins=ALL-UNNAMED'] + vm_args += ['--add-exports', 'org.graalvm.py/com.oracle.graal.python.builtins.objects.cext.capi.transitions=ALL-UNNAMED'] vm_args.append('-Dpolyglot.engine.WarnInterpreterOnly=false') jdk = get_jdk() vm_args.append("com.oracle.graal.python.test.advanced.LeakTest")