Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> forbiddenClasses = new ArrayList<>();
Expand Down Expand Up @@ -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<String> 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<String> 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<String> 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;
}
Expand All @@ -188,6 +260,10 @@ private int getCntAndErrors(JavaClass cls, List<String> errors) {
return cnt;
}

private boolean isReachable(Instance inst) {
return inst.isGCRoot() || inst.getNearestGCRootPointer() != null;
}

@SuppressWarnings("sync-override")
@Override
public final Throwable fillInStackTrace() {
Expand Down Expand Up @@ -271,6 +347,8 @@ protected List<String> preprocessArguments(List<String> arguments, Map<String, S
}
} else if (arg.equals("--null-stdout")) {
nullStdout = true;
} else if (arg.equals("--forbid-capi-residue")) {
forbidCApiResidue = true;
} else {
unrecognized.add(arg);
}
Expand Down Expand Up @@ -348,6 +426,7 @@ protected String getLanguageId() {

@Override
protected void printHelp(OptionCategory maxCategory) {
System.out.println("--lang ID --code CODE --forbidden-class FQN [--forbidden-class FQN]* [--shared-engine] [--keep-dump] [POLYGLOT-OPTIONS]");
System.out.println("--lang ID --code CODE --forbidden-class FQN [--forbidden-class FQN]* " +
"[--forbid-capi-residue] [--shared-engine] [--keep-dump] [POLYGLOT-OPTIONS]");
}
}
10 changes: 8 additions & 2 deletions mx.graalpython/mx_graalpython.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,13 +710,19 @@ def __post_init__(self):
has_jep_454 = mx.get_jdk().version >= 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"])
Expand Down
Loading