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..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 @@ -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,7 +66,9 @@ 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.builtins.objects.cext.capi.transitions.CApiTransitions.HandleContext; import com.oracle.graal.python.test.integration.Utils; import com.sun.management.HotSpotDiagnosticMXBean; @@ -100,6 +102,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 +164,100 @@ 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(HandleContext.class.getName()); + if (cls == null) { + System.err.println("Could not find " + HandleContext.class.getName() + " in heap dump"); + return true; + } + boolean fail = false; + for (Object i : cls.getInstances()) { + Instance inst = (Instance) i; + if (!isReachable(inst)) { + continue; + } + List residues = new ArrayList<>(); + 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() + ": " + + String.join(", ", residues)); + } + } + return fail; + } + + 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"); + 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 +272,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 +359,8 @@ protected List preprocessArguments(List arguments, Map= 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(["--code", 'import _testcapi, mmap, bz2; print(memoryview(b"").nbytes)']) + run_leak_launcher([ + "--forbid-capi-residue", "--code", + c_api_leak_test, + ]) # 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", + 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", "--forbidden-class", "com.oracle.graal.python.runtime.native_memory.NativePrimitiveReference"]) @@ -2924,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")