Skip to content

Commit b655a42

Browse files
committed
[GR-21296] Fix memory leaks in both single and multi context mode
PullRequest: graalpython/1283
2 parents 7dd4567 + 84bec56 commit b655a42

File tree

72 files changed

+1002
-586
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1002
-586
lines changed

docs/contributor/CONTRIBUTING.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,21 @@ To run the JVM configuration:
289289
To run the Native Image configuration:
290290

291291
mx --env ../../graal/vm/mx.vm/ce --exclude-components=slgm --dynamicimports /vm benchmark meso:nbody3 -- --python-vm=graalpython --jvm=graalvm-ce-python --jvm-config=native --python-vm-config=default --
292+
293+
### Finding Memory Leaks
294+
295+
For best performance we keep references to long-lived user objects (mostly
296+
functions, classes, and modules) directly in the AST nodes when using the
297+
default configuration of a single Python context (as is used when running the
298+
launcher). For better sharing of warmup and where absolutely best peak
299+
performance is not needed, contexts can be configured with a shared engine and
300+
the ASTs will be shared across contexts. However, that implies we *must* not
301+
store any user objects strongly in the ASTs. We test that we have no
302+
PythonObjects alive after a Context is closed that are run as part of our JUnit
303+
tests. These can be run by themselves, for example, like so:
304+
305+
$ mx python-leak-test --lang python --shared-engine --code 'import site, json' --forbidden-class com.oracle.graal.python.builtins.objects.object.PythonObject --keep-dump
306+
307+
The `--keep-dump` option will print the heapdump location and leave the file
308+
there rather than deleting it. It can then be opened for example with VisualVM
309+
to check for the paths of any leaked object, if there are any.
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
/*
2+
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.graal.python.test.advance;
42+
43+
import java.io.IOException;
44+
import java.lang.management.ManagementFactory;
45+
import java.nio.file.Files;
46+
import java.nio.file.Path;
47+
import java.util.ArrayList;
48+
import java.util.List;
49+
import java.util.Map;
50+
51+
import javax.management.InstanceNotFoundException;
52+
import javax.management.MBeanException;
53+
import javax.management.MBeanServer;
54+
import javax.management.MalformedObjectNameException;
55+
import javax.management.ObjectName;
56+
import javax.management.ReflectionException;
57+
58+
import org.graalvm.launcher.AbstractLanguageLauncher;
59+
import org.graalvm.options.OptionCategory;
60+
import org.graalvm.polyglot.Context;
61+
import org.graalvm.polyglot.Context.Builder;
62+
import org.graalvm.polyglot.Engine;
63+
import org.graalvm.polyglot.PolyglotException;
64+
import org.netbeans.lib.profiler.heap.Heap;
65+
import org.netbeans.lib.profiler.heap.HeapFactory;
66+
import org.netbeans.lib.profiler.heap.Instance;
67+
import org.netbeans.lib.profiler.heap.JavaClass;
68+
69+
import com.sun.management.HotSpotDiagnosticMXBean;
70+
71+
public class LeakTest extends AbstractLanguageLauncher {
72+
public static void main(String[] args) {
73+
String languageId = null;
74+
for (int i = 0; i < args.length; i++) {
75+
if (args[i].equals("--lang")) {
76+
languageId = args[i + 1];
77+
break;
78+
}
79+
}
80+
new LeakTest(languageId).launch(args);
81+
}
82+
83+
LeakTest(String languageId) {
84+
super();
85+
if (languageId == null) {
86+
printHelp(null);
87+
System.exit(255);
88+
}
89+
this.languageId = languageId;
90+
}
91+
92+
// sharedEngine is an instance variable explicitly so we definitely keep the ASTs alive. This is
93+
// to ensure that we actually test that even when the engine is still alive, as the Context is
94+
// closed, no objects should still be reachable
95+
private Engine engine;
96+
97+
private boolean sharedEngine = false;
98+
private boolean keepDump = false;
99+
private String languageId;
100+
private String code;
101+
private List<String> forbiddenClasses = new ArrayList<>();
102+
103+
private final class SystemExit extends RuntimeException {
104+
private static final long serialVersionUID = 1L;
105+
106+
public SystemExit() {
107+
super(null, null);
108+
}
109+
110+
@Override
111+
public synchronized Throwable getCause() {
112+
dumpAndAnalyze();
113+
System.exit(0);
114+
return null;
115+
}
116+
117+
private void dumpAndAnalyze() {
118+
if (sharedEngine && engine == null) {
119+
throw new AssertionError("the engine is no longer alive!");
120+
}
121+
122+
MBeanServer server = doFullGC();
123+
Path dumpFile = dumpHeap(server);
124+
boolean fail = checkForLeaks(dumpFile);
125+
if (fail) {
126+
System.exit(255);
127+
} else {
128+
System.exit(0);
129+
}
130+
}
131+
132+
private boolean checkForLeaks(Path dumpFile) {
133+
boolean fail = false;
134+
try {
135+
Heap heap = HeapFactory.createHeap(dumpFile.toFile());
136+
for (String fqn : forbiddenClasses) {
137+
List<String> errors = new ArrayList<>();
138+
JavaClass cls = heap.getJavaClassByName(fqn);
139+
if (cls == null) {
140+
System.err.println("No such class: " + fqn);
141+
fail = true;
142+
continue;
143+
}
144+
int cnt = getCntAndErrors(cls, errors);
145+
for (Object subCls : cls.getSubClasses()) {
146+
cnt += getCntAndErrors((JavaClass) subCls, errors);
147+
}
148+
if (cnt > 0) {
149+
fail = true;
150+
System.err.println("More instances of " + fqn + " than expected: " + cnt);
151+
for (String e : errors) {
152+
System.err.println(e);
153+
}
154+
}
155+
}
156+
} catch (IOException e) {
157+
throw new RuntimeException(e);
158+
}
159+
return fail;
160+
}
161+
162+
private Path dumpHeap(MBeanServer server) {
163+
Path dumpFile = null;
164+
try {
165+
Path p = Files.createTempDirectory("leakTest");
166+
if (!keepDump) {
167+
p.toFile().deleteOnExit();
168+
}
169+
dumpFile = p.resolve("heapdump.hprof");
170+
if (!keepDump) {
171+
dumpFile.toFile().deleteOnExit();
172+
} else {
173+
System.out.println("Dump file: " + dumpFile.toString());
174+
}
175+
HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(server,
176+
"com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class);
177+
mxBean.dumpHeap(dumpFile.toString(), true);
178+
} catch (IOException e) {
179+
throw new RuntimeException(e);
180+
}
181+
return dumpFile;
182+
}
183+
184+
private MBeanServer doFullGC() {
185+
// do this a few times to dump a small heap if we can
186+
MBeanServer server = null;
187+
for (int i = 0; i < 10; i++) {
188+
System.gc();
189+
Runtime.getRuntime().freeMemory();
190+
server = ManagementFactory.getPlatformMBeanServer();
191+
try {
192+
ObjectName objectName = new ObjectName("com.sun.management:type=DiagnosticCommand");
193+
server.invoke(objectName, "gcRun", new Object[]{null}, new String[]{String[].class.getName()});
194+
} catch (MalformedObjectNameException | InstanceNotFoundException | ReflectionException | MBeanException e) {
195+
throw new RuntimeException(e);
196+
}
197+
}
198+
return server;
199+
}
200+
201+
private int getCntAndErrors(JavaClass cls, List<String> errors) {
202+
int cnt = cls.getInstancesCount();
203+
if (cnt > 0) {
204+
boolean realLeak = false;
205+
for (Object i : cls.getInstances()) {
206+
Instance inst = (Instance) i;
207+
if (inst.isGCRoot() || inst.getNearestGCRootPointer() != null) {
208+
realLeak = true;
209+
break;
210+
}
211+
}
212+
if (!realLeak) {
213+
return 0;
214+
}
215+
StringBuilder sb = new StringBuilder();
216+
sb.append(cls.getName()).append(" ").append(cnt).append(" instance(s)");
217+
errors.add(sb.toString());
218+
}
219+
return cnt;
220+
}
221+
222+
@SuppressWarnings("sync-override")
223+
@Override
224+
public final Throwable fillInStackTrace() {
225+
return this;
226+
}
227+
}
228+
229+
@Override
230+
protected List<String> preprocessArguments(List<String> arguments, Map<String, String> polyglotOptions) {
231+
ArrayList<String> unrecognized = new ArrayList<>();
232+
for (int i = 0; i < arguments.size(); i++) {
233+
String arg = arguments.get(i);
234+
if (arg.equals("--shared-engine")) {
235+
sharedEngine = true;
236+
} else if (arg.equals("--lang")) {
237+
// already parsed
238+
i++;
239+
} else if (arg.equals("--keep-dump")) {
240+
keepDump = true;
241+
} else if (arg.equals("--code")) {
242+
code = arguments.get(++i);
243+
} else if (arg.equals("--forbidden-class")) {
244+
forbiddenClasses.add(arguments.get(++i));
245+
} else {
246+
unrecognized.add(arg);
247+
}
248+
}
249+
unrecognized.add("--experimental-options");
250+
return unrecognized;
251+
}
252+
253+
@Override
254+
protected void launch(Builder contextBuilder) {
255+
if (sharedEngine) {
256+
engine = Engine.newBuilder().build();
257+
contextBuilder.engine(engine);
258+
}
259+
contextBuilder.allowExperimentalOptions(true).allowAllAccess(true);
260+
261+
try (Context c = contextBuilder.build()) {
262+
try {
263+
c.eval(getLanguageId(), code);
264+
} catch (PolyglotException e) {
265+
if (e.isExit()) {
266+
if (e.getExitStatus() == 0) {
267+
throw new SystemExit();
268+
} else {
269+
exit(e.getExitStatus());
270+
}
271+
} else {
272+
e.printStackTrace();
273+
exit(255);
274+
}
275+
}
276+
}
277+
throw new SystemExit();
278+
}
279+
280+
@Override
281+
protected String getLanguageId() {
282+
return languageId;
283+
}
284+
285+
@Override
286+
protected String[] getDefaultLanguages() {
287+
return new String[]{languageId};
288+
}
289+
290+
@Override
291+
protected void printHelp(OptionCategory maxCategory) {
292+
System.out.println("--lang ID --code CODE --forbidden-class FQN [--forbidden-class FQN]* [--shared-engine] [--keep-dump] [POLYGLOT-OPTIONS]");
293+
}
294+
}

graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_object.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ def test_str_subclass(self):
484484
* PyObject_New() so we are able to allocate space for the object and
485485
* it's data buffer.
486486
*/
487-
obj = (PyUnicodeObject *) malloc(struct_size + (size + 1) * char_size);
487+
obj = (PyUnicodeObject *) PyObject_MALLOC(struct_size + (size + 1) * char_size);
488488
if (obj == NULL)
489489
return NULL;
490490
obj = (PyUnicodeObject *) PyObject_INIT(obj, testStrSubclassPtr);

0 commit comments

Comments
 (0)