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"])