-
Notifications
You must be signed in to change notification settings - Fork 140
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
8312182: THPs cause huge RSS due to thread start timing issue
8310687: JDK-8303215 is incomplete Backport-of: 84b325b844c08809448a9c073a11443d9e3c3f8e
- Loading branch information
Showing
3 changed files
with
301 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
241 changes: 241 additions & 0 deletions
241
test/hotspot/jtreg/runtime/os/THPsInThreadStackPreventionTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
/* | ||
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. | ||
* Copyright (c) 2023, Red Hat Inc. | ||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | ||
* | ||
* This code is free software; you can redistribute it and/or modify it | ||
* under the terms of the GNU General Public License version 2 only, as | ||
* published by the Free Software Foundation. | ||
* | ||
* This code is distributed in the hope that it will be useful, but WITHOUT | ||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | ||
* version 2 for more details (a copy is included in the LICENSE file that | ||
* accompanied this code). | ||
* | ||
* You should have received a copy of the GNU General Public License version | ||
* 2 along with this work; if not, write to the Free Software Foundation, | ||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | ||
* | ||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | ||
* or visit www.oracle.com if you need additional information or have any | ||
* questions. | ||
*/ | ||
|
||
/* | ||
* @test id=ENABLED | ||
* @bug 8303215 8312182 | ||
* @summary On THP=always systems, we prevent THPs from forming within thread stacks | ||
* @library /test/lib | ||
* @requires os.family == "linux" | ||
* @requires vm.debug | ||
* @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64" | ||
* @modules java.base/jdk.internal.misc | ||
* java.management | ||
* @run driver THPsInThreadStackPreventionTest PATCH-ENABLED | ||
*/ | ||
|
||
/* | ||
* @test id=DISABLED | ||
* @bug 8303215 8312182 | ||
* @summary On THP=always systems, we prevent THPs from forming within thread stacks (negative test) | ||
* @library /test/lib | ||
* @requires os.family == "linux" | ||
* @requires vm.debug | ||
* @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64" | ||
* @modules java.base/jdk.internal.misc | ||
* java.management | ||
* @run main/manual THPsInThreadStackPreventionTest PATCH-DISABLED | ||
*/ | ||
import jdk.test.lib.process.OutputAnalyzer; | ||
import jdk.test.lib.process.ProcessTools; | ||
import jtreg.SkippedException; | ||
|
||
import java.io.BufferedReader; | ||
import java.io.FileReader; | ||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Objects; | ||
import java.util.concurrent.BrokenBarrierException; | ||
import java.util.concurrent.CyclicBarrier; | ||
|
||
public class THPsInThreadStackPreventionTest { | ||
|
||
// We test the mitigation for "huge rss for THP=always" introduced with JDK-8312182 and JDK-8302015: | ||
// | ||
// We start a program that spawns a ton of threads with a stack size close to THP page size. The threads | ||
// are idle and should not build up a lot of stack. The threads are started with an artificial delay | ||
// between thread start and stack guardpage creation, which exacerbates the RSS bloat (for explanation | ||
// please see 8312182). | ||
// | ||
// We then observe RSS of that program. We expect it to stay below a reasonable maximum. The unpatched | ||
// version should show an RSS of ~2 GB (paying for the fully paged in thread stacks). The fixed variant should | ||
// cost only ~200-400 MB. | ||
|
||
static final int numThreads = 1000; | ||
static final long threadStackSizeMB = 2; // must be 2M | ||
static final long heapSizeMB = 64; | ||
static final long basicRSSOverheadMB = heapSizeMB + 150; | ||
// A successful completion of this test would show not more than X KB per thread stack. | ||
static final long acceptableRSSPerThreadStack = 128 * 1024; | ||
static final long acceptableRSSForAllThreadStacks = numThreads * acceptableRSSPerThreadStack; | ||
static final long acceptableRSSLimitMB = (acceptableRSSForAllThreadStacks / (1024 * 1024)) + basicRSSOverheadMB; | ||
|
||
private static class TestMain { | ||
|
||
static class Sleeper extends Thread { | ||
CyclicBarrier barrier; | ||
public Sleeper(CyclicBarrier barrier) { | ||
this.barrier = barrier; | ||
} | ||
@Override | ||
public void run() { | ||
try { | ||
barrier.await(); // wait for all siblings | ||
barrier.await(); // wait main thread to print status | ||
} catch (InterruptedException | BrokenBarrierException e) { | ||
e.printStackTrace(); | ||
} | ||
} | ||
} | ||
|
||
public static void main(String[] args) throws BrokenBarrierException, InterruptedException { | ||
|
||
// Fire up 1000 threads with 2M stack size each. | ||
Sleeper[] threads = new Sleeper[numThreads]; | ||
CyclicBarrier barrier = new CyclicBarrier(numThreads + 1); | ||
|
||
for (int i = 0; i < numThreads; i++) { | ||
threads[i] = new Sleeper(barrier); | ||
threads[i].start(); | ||
} | ||
|
||
// Wait for all threads to come up | ||
barrier.await(); | ||
|
||
// print status | ||
String file = "/proc/self/status"; | ||
try (FileReader fr = new FileReader(file); | ||
BufferedReader reader = new BufferedReader(fr)) { | ||
String line; | ||
while ((line = reader.readLine()) != null) { | ||
System.out.println(line); | ||
} | ||
} catch (IOException | NumberFormatException e) { /* ignored */ } | ||
|
||
// Signal threads to stop | ||
barrier.await(); | ||
|
||
} | ||
} | ||
|
||
static class ProcSelfStatus { | ||
|
||
public long rssMB; | ||
public long swapMB; | ||
public int numLifeThreads; | ||
|
||
// Parse output from /proc/self/status | ||
public static ProcSelfStatus parse(OutputAnalyzer o) { | ||
ProcSelfStatus status = new ProcSelfStatus(); | ||
String s = o.firstMatch("Threads:\\s*(\\d+)", 1); | ||
Objects.requireNonNull(s); | ||
status.numLifeThreads = Integer.parseInt(s); | ||
s = o.firstMatch("VmRSS:\\s*(\\d+) kB", 1); | ||
Objects.requireNonNull(s); | ||
status.rssMB = Long.parseLong(s) / 1024; | ||
s = o.firstMatch("VmSwap:\\s*(\\d+) kB", 1); | ||
Objects.requireNonNull(s); | ||
status.swapMB = Long.parseLong(s) / 1024; | ||
return status; | ||
} | ||
} | ||
|
||
public static void main(String[] args) throws Exception { | ||
|
||
HugePageConfiguration config = HugePageConfiguration.readFromOS(); | ||
// This issue is bound to THP=always | ||
if (config.getThpMode() != HugePageConfiguration.THPMode.always) { | ||
throw new SkippedException("Test only makes sense in THP \"always\" mode"); | ||
} | ||
|
||
String[] defaultArgs = { | ||
"-Xlog:pagesize", | ||
"-Xmx" + heapSizeMB + "m", "-Xms" + heapSizeMB + "m", "-XX:+AlwaysPreTouch", // stabilize RSS | ||
"-Xss" + threadStackSizeMB + "m", | ||
"-XX:-CreateCoredumpOnCrash", | ||
// This will delay the child threads before they create guard pages, thereby greatly increasing the | ||
// chance of large VMA formation + hugepage coalescation; see JDK-8312182 | ||
"-XX:+DelayThreadStartALot" | ||
}; | ||
ArrayList<String> finalargs = new ArrayList<>(Arrays.asList(defaultArgs)); | ||
|
||
switch (args[0]) { | ||
case "PATCH-ENABLED": { | ||
finalargs.add(TestMain.class.getName()); | ||
ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(finalargs); | ||
|
||
OutputAnalyzer output = new OutputAnalyzer(pb.start()); | ||
output.shouldHaveExitValue(0); | ||
|
||
// this line indicates the mitigation is active: | ||
output.shouldContain("[pagesize] JVM will attempt to prevent THPs in thread stacks."); | ||
|
||
ProcSelfStatus status = ProcSelfStatus.parse(output); | ||
if (status.numLifeThreads < numThreads) { | ||
throw new RuntimeException("Number of live threads lower than expected: " + status.numLifeThreads + ", expected " + numThreads); | ||
} else { | ||
System.out.println("Found " + status.numLifeThreads + " to be alive. Ok."); | ||
} | ||
|
||
long rssPlusSwapMB = status.swapMB + status.rssMB; | ||
|
||
if (rssPlusSwapMB > acceptableRSSLimitMB) { | ||
throw new RuntimeException("RSS+Swap larger than expected: " + rssPlusSwapMB + "m, expected at most " + acceptableRSSLimitMB + "m"); | ||
} else { | ||
if (rssPlusSwapMB < heapSizeMB) { // we pretouch the java heap, so we expect to see at least that: | ||
throw new RuntimeException("RSS+Swap suspiciously low: " + rssPlusSwapMB + "m, expected at least " + heapSizeMB + "m"); | ||
} | ||
System.out.println("Okay: RSS+Swap=" + rssPlusSwapMB + ", within acceptable limit of " + acceptableRSSLimitMB); | ||
} | ||
} | ||
break; | ||
|
||
case "PATCH-DISABLED": { | ||
|
||
// Only execute manually! this will allocate ~2gb of memory! | ||
|
||
// explicitly disable the no-THP-workaround: | ||
finalargs.add("-XX:+UnlockDiagnosticVMOptions"); | ||
finalargs.add("-XX:+DisableTHPStackMitigation"); | ||
|
||
finalargs.add(TestMain.class.getName()); | ||
ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(finalargs); | ||
OutputAnalyzer output = new OutputAnalyzer(pb.start()); | ||
|
||
output.shouldHaveExitValue(0); | ||
|
||
// We deliberately switched off mitigation, VM should tell us: | ||
output.shouldContain("[pagesize] JVM will *not* prevent THPs in thread stacks. This may cause high RSS."); | ||
|
||
// Parse output from self/status | ||
ProcSelfStatus status = ProcSelfStatus.parse(output); | ||
if (status.numLifeThreads < numThreads) { | ||
throw new RuntimeException("Number of live threads lower than expected (" + status.numLifeThreads + ", expected " + numThreads +")"); | ||
} else { | ||
System.out.println("Found " + status.numLifeThreads + " to be alive. Ok."); | ||
} | ||
|
||
long rssPlusSwapMB = status.swapMB + status.rssMB; | ||
|
||
if (rssPlusSwapMB < acceptableRSSLimitMB) { | ||
throw new RuntimeException("RSS+Swap lower than expected: " + rssPlusSwapMB + "m, expected more than " + acceptableRSSLimitMB + "m"); | ||
} | ||
break; | ||
} | ||
|
||
default: throw new RuntimeException("Bad argument: " + args[0]); | ||
} | ||
} | ||
} |
4cca633
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review
Issues