Skip to content

Commit fbfac54

Browse files
committed
Extend LeakTest to do repeated executions
1 parent ba66049 commit fbfac54

File tree

2 files changed

+136
-83
lines changed

2 files changed

+136
-83
lines changed

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

Lines changed: 108 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -41,6 +41,7 @@
4141
package com.oracle.graal.python.test.advanced;
4242

4343
import java.io.IOException;
44+
import java.io.OutputStream;
4445
import java.lang.management.ManagementFactory;
4546
import java.lang.management.ThreadInfo;
4647
import java.lang.management.ThreadMXBean;
@@ -98,10 +99,14 @@ public static void main(String[] args) {
9899

99100
private boolean sharedEngine = false;
100101
private boolean keepDump = false;
102+
private int repeatAndCheckSize = -1;
103+
private boolean nullStdout = false;
101104
private String languageId;
102105
private String code;
103106
private List<String> forbiddenClasses = new ArrayList<>();
104107

108+
private static final int REPEAT_AND_CHECK_BASLINE_ITERATION = 32;
109+
105110
private final class SystemExit extends RuntimeException {
106111
private static final long serialVersionUID = 1L;
107112

@@ -123,7 +128,7 @@ private void dumpAndAnalyze() {
123128

124129
MBeanServer server = doFullGC();
125130
String threadDump = getThreadDump();
126-
Path dumpFile = dumpHeap(server);
131+
Path dumpFile = dumpHeap(server, keepDump);
127132
boolean fail = checkForLeaks(dumpFile);
128133
if (fail) {
129134
System.err.print(threadDump);
@@ -181,50 +186,6 @@ private boolean checkForLeaks(Path dumpFile) {
181186
return fail;
182187
}
183188

184-
private Path dumpHeap(MBeanServer server) {
185-
Path dumpFile = null;
186-
try {
187-
Path p = Files.createTempDirectory("leakTest");
188-
if (!keepDump) {
189-
p.toFile().deleteOnExit();
190-
}
191-
dumpFile = p.resolve("heapdump.hprof");
192-
if (!keepDump) {
193-
dumpFile.toFile().deleteOnExit();
194-
} else {
195-
System.out.println("Dump file: " + dumpFile.toString());
196-
}
197-
HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(server,
198-
"com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class);
199-
mxBean.dumpHeap(dumpFile.toString(), true);
200-
} catch (IOException e) {
201-
throw new RuntimeException(e);
202-
}
203-
return dumpFile;
204-
}
205-
206-
private MBeanServer doFullGC() {
207-
// do this a few times to dump a small heap if we can
208-
MBeanServer server = null;
209-
for (int i = 0; i < 10; i++) {
210-
System.gc();
211-
Runtime.getRuntime().freeMemory();
212-
server = ManagementFactory.getPlatformMBeanServer();
213-
try {
214-
ObjectName objectName = new ObjectName("com.sun.management:type=DiagnosticCommand");
215-
server.invoke(objectName, "gcRun", new Object[]{null}, new String[]{String[].class.getName()});
216-
} catch (MalformedObjectNameException | InstanceNotFoundException | ReflectionException | MBeanException e) {
217-
throw new RuntimeException(e);
218-
}
219-
try {
220-
Thread.sleep(2000);
221-
} catch (InterruptedException e1) {
222-
// do nothing
223-
}
224-
}
225-
return server;
226-
}
227-
228189
private int getCntAndErrors(JavaClass cls, List<String> errors) {
229190
int cnt = cls.getInstancesCount();
230191
if (cnt > 0) {
@@ -253,6 +214,58 @@ public final Throwable fillInStackTrace() {
253214
}
254215
}
255216

217+
private static MBeanServer doFullGC() {
218+
// do this a few times to dump a small heap if we can
219+
MBeanServer server = null;
220+
for (int i = 0; i < 10; i++) {
221+
System.gc();
222+
Runtime.getRuntime().freeMemory();
223+
server = ManagementFactory.getPlatformMBeanServer();
224+
try {
225+
ObjectName objectName = new ObjectName("com.sun.management:type=DiagnosticCommand");
226+
server.invoke(objectName, "gcRun", new Object[]{null}, new String[]{String[].class.getName()});
227+
} catch (MalformedObjectNameException | InstanceNotFoundException | ReflectionException | MBeanException e) {
228+
throw new RuntimeException(e);
229+
}
230+
try {
231+
Thread.sleep(2000);
232+
} catch (InterruptedException e1) {
233+
// do nothing
234+
}
235+
}
236+
return server;
237+
}
238+
239+
private static Path dumpHeap(MBeanServer server, boolean keepDump) {
240+
Path dumpFile = null;
241+
try {
242+
Path p = Files.createTempDirectory("leakTest");
243+
if (!keepDump) {
244+
p.toFile().deleteOnExit();
245+
}
246+
dumpFile = p.resolve("heapdump.hprof");
247+
if (!keepDump) {
248+
dumpFile.toFile().deleteOnExit();
249+
} else {
250+
System.out.println("Dump file: " + dumpFile.toString());
251+
}
252+
HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(server,
253+
"com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class);
254+
mxBean.dumpHeap(dumpFile.toString(), true);
255+
} catch (IOException e) {
256+
throw new RuntimeException(e);
257+
}
258+
return dumpFile;
259+
}
260+
261+
private static long getJavaHeapSize(boolean createHeapDump) {
262+
MBeanServer server = doFullGC();
263+
if (createHeapDump) {
264+
dumpHeap(server, true);
265+
}
266+
return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
267+
}
268+
256269
@Override
257270
protected List<String> preprocessArguments(List<String> arguments, Map<String, String> polyglotOptions) {
258271
ArrayList<String> unrecognized = new ArrayList<>();
@@ -269,6 +282,14 @@ protected List<String> preprocessArguments(List<String> arguments, Map<String, S
269282
code = arguments.get(++i);
270283
} else if (arg.equals("--forbidden-class")) {
271284
forbiddenClasses.add(arguments.get(++i));
285+
} else if (arg.equals("--repeat-and-check-size")) {
286+
repeatAndCheckSize = Integer.parseInt(arguments.get(++i));
287+
if (repeatAndCheckSize <= REPEAT_AND_CHECK_BASLINE_ITERATION) {
288+
System.err.printf("--repeat-and-check-size must be at least %d\n", REPEAT_AND_CHECK_BASLINE_ITERATION);
289+
System.exit(1);
290+
}
291+
} else if (arg.equals("--null-stdout")) {
292+
nullStdout = true;
272293
} else {
273294
unrecognized.add(arg);
274295
}
@@ -284,26 +305,59 @@ protected void launch(Builder contextBuilder) {
284305
contextBuilder.engine(engine);
285306
}
286307
contextBuilder.allowExperimentalOptions(true).allowAllAccess(true);
308+
if (nullStdout) {
309+
contextBuilder.out(OutputStream.nullOutputStream());
310+
}
287311

288312
try (Context c = contextBuilder.build()) {
289313
try {
290314
c.eval(getLanguageId(), code);
291315
} catch (PolyglotException e) {
292-
if (e.isExit()) {
293-
if (e.getExitStatus() == 0) {
294-
throw new SystemExit();
295-
} else {
296-
exit(e.getExitStatus());
316+
handleException(e);
317+
}
318+
}
319+
320+
if (repeatAndCheckSize > 0) {
321+
long initialSize = -1;
322+
for (int i = 0; i < repeatAndCheckSize; i++) {
323+
if (i == REPEAT_AND_CHECK_BASLINE_ITERATION) {
324+
// Give the system some time to stabilize, fill caches, etc.
325+
initialSize = getJavaHeapSize(keepDump);
326+
System.out.printf("Baseline heap size: %,d\n", initialSize);
327+
}
328+
try (Context c = contextBuilder.build()) {
329+
try {
330+
c.eval(getLanguageId(), code);
331+
} catch (PolyglotException e) {
332+
handleException(e);
297333
}
298-
} else {
299-
e.printStackTrace();
300-
exit(255);
301334
}
302335
}
336+
// the check at the end will make a dump anyway, so no createHeapDump flag here
337+
long currentSize = getJavaHeapSize(false);
338+
System.out.printf("Heap size after all repetitions: %,d\n", currentSize);
339+
if (currentSize > initialSize * 1.1) {
340+
System.err.printf("Heap size grew too much after repeated context creations and invocations. From %,d bytes to %,d bytes.\n", initialSize, currentSize);
341+
System.exit(255);
342+
}
303343
}
344+
304345
throw new SystemExit();
305346
}
306347

348+
private void handleException(PolyglotException e) {
349+
if (e.isExit()) {
350+
if (e.getExitStatus() == 0) {
351+
throw new SystemExit();
352+
} else {
353+
exit(e.getExitStatus());
354+
}
355+
} else {
356+
e.printStackTrace();
357+
exit(255);
358+
}
359+
}
360+
307361
@Override
308362
protected String getLanguageId() {
309363
return languageId;

mx.graalpython/mx_graalpython.py

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -410,21 +410,16 @@ def __str__(self):
410410
if skip_leak_tests:
411411
return
412412

413-
common_args = ["--lang", "python",
414-
"--forbidden-class", "com.oracle.graal.python.builtins.objects.object.PythonObject",
415-
"--python.ForceImportSite", "--python.TRegexUsesSREFallback=false"]
416-
417-
if not all([
418-
# test leaks with Python code only
419-
run_leak_launcher(common_args + ["--code", "pass", ]),
420-
# test leaks when some C module code is involved
421-
run_leak_launcher(common_args + ["--code", 'import _testcapi, mmap, bz2; print(memoryview(b"").nbytes)']),
422-
# test leaks with shared engine Python code only
423-
run_leak_launcher(common_args + ["--shared-engine", "--code", "pass"]),
424-
# test leaks with shared engine when some C module code is involved
425-
run_leak_launcher(common_args + ["--shared-engine", "--code", 'import _testcapi, mmap, bz2; print(memoryview(b"").nbytes)'])
426-
]):
427-
mx.abort(1)
413+
# test leaks with Python code only
414+
run_leak_launcher(["--code", "pass", ]),
415+
run_leak_launcher(["--repeat-and-check-size", "--code", "--null-stdout", "print('hello')"]),
416+
# test leaks when some C module code is involved
417+
run_leak_launcher(["--code", 'import _testcapi, mmap, bz2; print(memoryview(b"").nbytes)']),
418+
# test leaks with shared engine Python code only
419+
run_leak_launcher(["--shared-engine", "--code", "pass"]),
420+
run_leak_launcher(["--shared-engine", "--repeat-and-check-size", "--code", "--null-stdout", "print('hello')"]),
421+
# test leaks with shared engine when some C module code is involved
422+
run_leak_launcher(["--shared-engine", "--code", 'import _testcapi, mmap, bz2; print(memoryview(b"").nbytes)'])
428423

429424

430425
PYTHON_ARCHIVES = ["GRAALPYTHON_GRAALVM_SUPPORT"]
@@ -2996,7 +2991,10 @@ def exclude_files(*files):
29962991
def run_leak_launcher(input_args):
29972992
print(shlex.join(["mx", "python-leak-test", *input_args]))
29982993

2999-
args = input_args
2994+
args = ["--lang", "python",
2995+
"--forbidden-class", "com.oracle.graal.python.builtins.objects.object.PythonObject",
2996+
"--python.ForceImportSite", "--python.TRegexUsesSREFallback=false"]
2997+
args += input_args
30002998
args = [
30012999
"--keep-dump",
30023000
"--experimental-options",
@@ -3015,24 +3013,25 @@ def run_leak_launcher(input_args):
30153013
vm_args.append("com.oracle.graal.python.test.advanced.LeakTest")
30163014
out = mx.OutputCapture()
30173015
retval = mx.run_java(vm_args + graalpython_args, jdk=jdk, env=env, nonZeroIsFatal=False, out=mx.TeeOutputCapture(out))
3018-
dump_path = out.data.strip().partition("Dump file:")[2].strip()
3016+
dump_paths = re.findall(r'Dump file: (\S+)', out.data.strip())
30193017
if retval == 0:
30203018
print("PASSED")
3021-
if dump_path:
3019+
if dump_paths:
30223020
print("Removing heapdump for passed test")
3023-
os.unlink(dump_path)
3024-
return True
3021+
for p in dump_paths:
3022+
os.unlink(p)
30253023
else:
30263024
print("FAILED")
3027-
if 'CI' in os.environ and dump_path:
3028-
save_path = os.path.join(SUITE.dir, "dumps", "leak_test")
3029-
try:
3030-
os.makedirs(save_path)
3031-
except OSError:
3032-
pass
3033-
dest = shutil.copy(dump_path, save_path)
3034-
print(f"Heapdump file kept in {dest}")
3035-
return False
3025+
if 'CI' in os.environ and dump_paths:
3026+
for i, dump_path in enumerate(dump_paths):
3027+
save_path = os.path.join(SUITE.dir, "dumps", f"leak_test{i}")
3028+
try:
3029+
os.makedirs(save_path)
3030+
except OSError:
3031+
pass
3032+
dest = shutil.copy(dump_path, save_path)
3033+
print(f"Heapdump file {dump_path} kept in {dest}")
3034+
mx.abort(1)
30363035

30373036

30383037
def no_return(fn):

0 commit comments

Comments
 (0)