Skip to content

Commit e29d806

Browse files
committed
[GR-54727] Fix leak of StructSequence builtin nodes and call targets.
PullRequest: graalpython/3376
2 parents 61ee57d + 5e54734 commit e29d806

File tree

22 files changed

+383
-317
lines changed

22 files changed

+383
-317
lines changed

graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/advanced/LeakTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,11 +333,13 @@ protected void launch(Builder contextBuilder) {
333333
}
334334
}
335335
}
336-
// the check at the end will make a dump anyway, so no createHeapDump flag here
337336
long currentSize = getJavaHeapSize(false);
338337
System.out.printf("Heap size after all repetitions: %,d\n", currentSize);
339338
if (currentSize > initialSize * 1.1) {
340339
System.err.printf("Heap size grew too much after repeated context creations and invocations. From %,d bytes to %,d bytes.\n", initialSize, currentSize);
340+
if (keepDump) {
341+
dumpHeap(ManagementFactory.getPlatformMBeanServer(), true);
342+
}
341343
System.exit(255);
342344
}
343345
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.io.IOException;
3838
import java.io.InputStream;
3939
import java.lang.invoke.VarHandle;
40+
import java.lang.ref.WeakReference;
4041
import java.nio.charset.StandardCharsets;
4142
import java.nio.file.InvalidPathException;
4243
import java.util.ArrayList;
@@ -70,6 +71,10 @@
7071
import com.oracle.graal.python.builtins.objects.function.PArguments;
7172
import com.oracle.graal.python.builtins.objects.module.PythonModule;
7273
import com.oracle.graal.python.builtins.objects.object.PythonObject;
74+
import com.oracle.graal.python.builtins.objects.tuple.StructSequence;
75+
import com.oracle.graal.python.builtins.objects.tuple.StructSequence.BuiltinTypeDescriptor;
76+
import com.oracle.graal.python.builtins.objects.tuple.StructSequence.Descriptor;
77+
import com.oracle.graal.python.builtins.objects.tuple.StructSequence.DescriptorCallTargets;
7378
import com.oracle.graal.python.builtins.objects.type.MroShape;
7479
import com.oracle.graal.python.builtins.objects.type.PythonAbstractClass;
7580
import com.oracle.graal.python.builtins.objects.type.PythonManagedClass;
@@ -333,6 +338,46 @@ public boolean isSingleContext() {
333338

334339
@CompilationFinal(dimensions = 1) private final RootCallTarget[] builtinSlotsCallTargets;
335340

341+
/**
342+
* Weak hash map of call targets for builtin functions associated with named tuples generated at
343+
* runtime from C extensions. We hold the cached call targets also weakly, because otherwise we
344+
* would have a cycle from the value (call targets reference builtin nodes which wrap the
345+
* descriptor) to the key. The key should be GC'ed when the corresponding generated named tuple
346+
* class is GC'ed.
347+
*/
348+
private final WeakHashMap<StructSequence.Descriptor, WeakReference<StructSequence.DescriptorCallTargets>> structSequenceTargets = new WeakHashMap<>();
349+
350+
/**
351+
* The same as {@link #structSequenceTargets}, but for builtin named tuples. There is a bounded
352+
* statically known number of builtin named tuples.
353+
*/
354+
private final ConcurrentHashMap<StructSequence.Descriptor, StructSequence.DescriptorCallTargets> structSequenceBuiltinTargets = new ConcurrentHashMap<>();
355+
356+
public StructSequence.DescriptorCallTargets getOrCreateStructSequenceCallTargets(StructSequence.Descriptor descriptor,
357+
Function<StructSequence.Descriptor, StructSequence.DescriptorCallTargets> factory) {
358+
if (singleContext) {
359+
return factory.apply(descriptor);
360+
}
361+
if (descriptor instanceof BuiltinTypeDescriptor builtinDescriptor) {
362+
// There must be finite set of objects initialized at build time, no need for a weak map
363+
assert !ImageInfo.inImageCode() || builtinDescriptor.wasInitializedAtBuildTime();
364+
return structSequenceBuiltinTargets.computeIfAbsent(builtinDescriptor, factory);
365+
}
366+
return getOrCreateStructSeqNonBuiltinTargets(descriptor, factory);
367+
}
368+
369+
private DescriptorCallTargets getOrCreateStructSeqNonBuiltinTargets(Descriptor descriptor, Function<Descriptor, DescriptorCallTargets> factory) {
370+
synchronized (structSequenceTargets) {
371+
WeakReference<DescriptorCallTargets> weakResult = structSequenceTargets.computeIfAbsent(descriptor, d -> new WeakReference<>(factory.apply(d)));
372+
DescriptorCallTargets result = weakResult.get();
373+
if (result == null) {
374+
result = factory.apply(descriptor);
375+
structSequenceTargets.put(descriptor, new WeakReference<>(result));
376+
}
377+
return result;
378+
}
379+
}
380+
336381
/**
337382
* We cannot initialize call targets in language ctor and the next suitable hook is context
338383
* initialization, but that is called multiple times. We use this flag to run the language
@@ -410,6 +455,9 @@ public MroShape getMroShapeRoot() {
410455
protected void finalizeContext(PythonContext context) {
411456
context.finalizeContext();
412457
super.finalizeContext(context);
458+
// trigger cleanup of stale entries in weak hash maps
459+
structSequenceTargets.size();
460+
indirectCallDataMap.size();
413461
}
414462

415463
@Override
@@ -1000,9 +1048,87 @@ public void setBuiltinSlotCallTarget(int index, RootCallTarget callTarget) {
10001048
}
10011049

10021050
/**
1003-
* Cache call targets that are created for every new context, based on a single key.
1051+
* Caches call target that wraps a node that is not parametrized, i.e., has only a parameterless
1052+
* ctor and all its instances implement the same logic. Parametrized nodes must include the
1053+
* parameters that alter their behavior as part of the cache key.
1054+
*/
1055+
public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Class<? extends Node> key) {
1056+
// It's complicated with RootNodes, but regular nodes should have only parameterless ctor to
1057+
// be appropriate keys for the cache
1058+
assert RootNode.class.isAssignableFrom(key) || key.getConstructors().length <= 1;
1059+
assert RootNode.class.isAssignableFrom(key) || key.getConstructors().length == 0 || key.getConstructors()[0].getParameterCount() == 0;
1060+
return createCachedCallTargetUnsafe(rootNodeFunction, key);
1061+
}
1062+
1063+
public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Enum<?> key) {
1064+
return createCachedCallTargetUnsafe(rootNodeFunction, key);
1065+
}
1066+
1067+
public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Class<? extends Node> nodeClass, String key) {
1068+
// for builtins: name is needed to distinguish builtins that share the same underlying node
1069+
// in general: a String may be parameter of the node wrapped in the root node or the root
1070+
// node itself, there must be finite number of strings that can appear here (i.e., must not
1071+
// be dynamically generated unless their number is bounded).
1072+
return createCachedCallTargetUnsafe(rootNodeFunction, nodeClass, key);
1073+
}
1074+
1075+
public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Class<? extends Node> nodeClass, TruffleString key) {
1076+
// See the String overload
1077+
return createCachedCallTargetUnsafe(rootNodeFunction, nodeClass, key);
1078+
}
1079+
1080+
public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Class<? extends Node> nodeClass, int key) {
1081+
return createCachedCallTargetUnsafe(rootNodeFunction, nodeClass, key);
1082+
}
1083+
1084+
public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Class<? extends Node> nodeClass1, Class<?> nodeClass2, String name) {
1085+
// for slot wrappers: the root node may be wrapping a helper wrapper node implementing the
1086+
// wrapper logic and the bare slot node itself
1087+
return createCachedCallTargetUnsafe(rootNodeFunction, nodeClass1, nodeClass2, name);
1088+
}
1089+
1090+
/**
1091+
* Caches call targets for external C functions created by extensions at runtime.
1092+
* <p>
1093+
* For the time being, we assume finite/limited number of extensions and their external
1094+
* functions. This may hold onto call targets created by one extension used in a context that
1095+
* was closed in the meanwhile and no other context ever loads the extension.
10041096
*/
1005-
public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Object key) {
1097+
public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Class<? extends RootNode> klass, Enum<?> signature, TruffleString name,
1098+
boolean doArgumentAndResultConversion) {
1099+
return createCachedCallTargetUnsafe(rootNodeFunction, klass, signature, name, doArgumentAndResultConversion);
1100+
}
1101+
1102+
public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Enum<?> signature, TruffleString name,
1103+
boolean doArgumentAndResultConversion) {
1104+
return createCachedCallTargetUnsafe(rootNodeFunction, signature, name, doArgumentAndResultConversion);
1105+
}
1106+
1107+
public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Enum<?> signature, TruffleString name) {
1108+
return createCachedCallTargetUnsafe(rootNodeFunction, signature, name);
1109+
}
1110+
1111+
public RootCallTarget createStructSeqIndexedMemberAccessCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, int memberIndex) {
1112+
return createCachedCallTargetUnsafe(rootNodeFunction, StructSequence.class, memberIndex);
1113+
}
1114+
1115+
public RootCallTarget createCachedPropAccessCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Class<?> nodeClass, String name, int type, int offset) {
1116+
// For the time being, we assume finite/limited number of cext/hpy types members, their
1117+
// types and offsets
1118+
return createCachedCallTargetUnsafe(rootNodeFunction, nodeClass, name, type, offset);
1119+
}
1120+
1121+
/**
1122+
* Keys in any caches held by {@link PythonLanguage} must be context independent objects and
1123+
* there must be either finite number of their instances, or if the key is a context independent
1124+
* mirror of some runtime data structure, it must be cached weakly. This call targets cache is
1125+
* strong.
1126+
* <p>
1127+
* To avoid memory leaks, all key types must be known to have finite number of possible
1128+
* instances. Public methods for adding to the cache must take concrete key type(s) so that all
1129+
* possible cache keys are explicit and documented.
1130+
*/
1131+
private RootCallTarget createCachedCallTargetUnsafe(Function<PythonLanguage, RootNode> rootNodeFunction, Object key) {
10061132
CompilerAsserts.neverPartOfCompilation();
10071133
if (!singleContext) {
10081134
return cachedCallTargets.computeIfAbsent(key, k -> PythonUtils.getOrCreateCallTarget(rootNodeFunction.apply(this)));
@@ -1011,11 +1137,8 @@ public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode>
10111137
}
10121138
}
10131139

1014-
/**
1015-
* Cache call targets that are created for every new context, based on a list of keys.
1016-
*/
1017-
public RootCallTarget createCachedCallTarget(Function<PythonLanguage, RootNode> rootNodeFunction, Object... cacheKeys) {
1018-
return createCachedCallTarget(rootNodeFunction, Arrays.asList(cacheKeys));
1140+
private RootCallTarget createCachedCallTargetUnsafe(Function<PythonLanguage, RootNode> rootNodeFunction, Object... cacheKeys) {
1141+
return createCachedCallTargetUnsafe(rootNodeFunction, Arrays.asList(cacheKeys));
10191142
}
10201143

10211144
public void registerBuiltinDescriptorCallTarget(BuiltinMethodDescriptor descriptor, RootCallTarget callTarget) {

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/Builtin.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2023, Oracle and/or its affiliates.
2+
* Copyright (c) 2017, 2024, Oracle and/or its affiliates.
33
* Copyright (c) 2013, Regents of the University of California
44
*
55
* All rights reserved.
@@ -109,4 +109,14 @@
109109
String raiseErrorName() default StringLiterals.J_EMPTY_STRING;
110110

111111
boolean forceSplitDirectCalls() default false;
112+
113+
/**
114+
* If set to {@code true}, then this builtin will be initialized and registered in
115+
* {@link PythonBuiltins#initialize(Python3Core)}. Otherwise, it will be ignored even when it
116+
* has a generated node factory. This is useful when we want to initialize the builtin manually
117+
* in different way (e.g., wrap it in method, descriptor, ...). By convention set this to
118+
* {@code false} also for builtins declared outside of {@link PythonBuiltins} subclass to
119+
* document the intent to not initialize them automatically.
120+
*/
121+
boolean autoRegister() default true;
112122
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/PythonBuiltins.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,12 @@ private void initializeEachFactoryWith(BiConsumer<NodeFactory<? extends PythonBu
244244
PythonOS currentOs = PythonOS.getPythonOS();
245245
for (NodeFactory<? extends PythonBuiltinBaseNode> factory : factories) {
246246
Boolean needsFrame = null;
247+
boolean initialized = false;
247248
for (Builtin builtin : factory.getNodeClass().getAnnotationsByType(Builtin.class)) {
249+
if (!builtin.autoRegister()) {
250+
assert !initialized : "Builtin annotations on " + factory.getNodeClass().getName() + " do not agree on 'autoInitialize' property.";
251+
break;
252+
}
248253
if (builtin.os() == PythonOS.PLATFORM_ANY || builtin.os() == currentOs) {
249254
if (needsFrame == null) {
250255
needsFrame = builtin.needsFrame();

0 commit comments

Comments
 (0)