diff --git a/src/hotspot/share/prims/whitebox.cpp b/src/hotspot/share/prims/whitebox.cpp index 8a08a12a0b0..87f4a751e80 100644 --- a/src/hotspot/share/prims/whitebox.cpp +++ b/src/hotspot/share/prims/whitebox.cpp @@ -2497,6 +2497,13 @@ WB_ENTRY(jint, WB_ValidateCgroup(JNIEnv* env, return ret; WB_END +// Available cpus of the host machine, Linux only. +// Used in container testing. +WB_ENTRY(jint, WB_HostCPUs(JNIEnv* env, jobject o)) + LINUX_ONLY(return os::Linux::active_processor_count();) + return -1; // Not used/implemented on other platforms +WB_END + WB_ENTRY(void, WB_PrintOsInfo(JNIEnv* env, jobject o)) os::print_os_info(tty); WB_END @@ -2938,6 +2945,7 @@ static JNINativeMethod methods[] = { (void*)&WB_ValidateCgroup }, {CC"hostPhysicalMemory", CC"()J", (void*)&WB_HostPhysicalMemory }, {CC"hostPhysicalSwap", CC"()J", (void*)&WB_HostPhysicalSwap }, + {CC"hostCPUs", CC"()I", (void*)&WB_HostCPUs }, {CC"printOsInfo", CC"()V", (void*)&WB_PrintOsInfo }, {CC"disableElfSectionCache", CC"()V", (void*)&WB_DisableElfSectionCache }, {CC"resolvedMethodItemsCount", CC"()J", (void*)&WB_ResolvedMethodItemsCount }, diff --git a/test/hotspot/jtreg/TEST.ROOT b/test/hotspot/jtreg/TEST.ROOT index 5f4c52186a7..74540b6ad45 100644 --- a/test/hotspot/jtreg/TEST.ROOT +++ b/test/hotspot/jtreg/TEST.ROOT @@ -87,6 +87,7 @@ requires.properties= \ vm.musl \ vm.flagless \ docker.support \ + systemd.support \ jdk.containerized # Minimum jtreg version diff --git a/test/hotspot/jtreg/containers/systemd/HelloSystemd.java b/test/hotspot/jtreg/containers/systemd/HelloSystemd.java new file mode 100644 index 00000000000..1b320131bf5 --- /dev/null +++ b/test/hotspot/jtreg/containers/systemd/HelloSystemd.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024, 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. + */ + +public class HelloSystemd { + public static void main(String args[]) { + System.out.println("Hello Systemd"); + } +} diff --git a/test/hotspot/jtreg/containers/systemd/SystemdMemoryAwarenessTest.java b/test/hotspot/jtreg/containers/systemd/SystemdMemoryAwarenessTest.java new file mode 100644 index 00000000000..2c06d48bb1c --- /dev/null +++ b/test/hotspot/jtreg/containers/systemd/SystemdMemoryAwarenessTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024, 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. + */ + +import jdk.test.lib.containers.systemd.SystemdRunOptions; +import jdk.test.lib.containers.systemd.SystemdTestUtils; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.whitebox.WhiteBox; +import jtreg.SkippedException; + +/* + * @test + * @bug 8322420 8217338 + * @summary Memory/CPU awareness test for JDK-under-test inside a systemd slice. + * @requires systemd.support + * @library /test/lib + * @modules java.base/jdk.internal.platform + * @build HelloSystemd jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar whitebox.jar jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:whitebox.jar -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI SystemdMemoryAwarenessTest + */ +public class SystemdMemoryAwarenessTest { + + private static final int MB = 1024 * 1024; + private static final WhiteBox wb = WhiteBox.getWhiteBox(); + private static final String TEST_SLICE_NAME = SystemdMemoryAwarenessTest.class.getSimpleName() + "HS"; + + public static void main(String[] args) throws Exception { + testHelloSystemd(); + } + + private static void testHelloSystemd() throws Exception { + SystemdRunOptions opts = SystemdTestUtils.newOpts("HelloSystemd"); + // 1 GB memory, but the limit in the lower hierarchy is 512M + opts.memoryLimit("1024M"); + int expectedMemLimit = 512; + // expected detected limit we test for, 512MB + opts.sliceDMemoryLimit(String.format("%dM", expectedMemLimit)); + int physicalCpus = wb.hostCPUs(); + if (physicalCpus < 2) { + System.err.println("WARNING: host system only has " + physicalCpus + " cpus. Expected >= 2"); + System.err.println("The active_processor_count assertion will trivially pass."); + } + // Use a CPU core limit of 1 for best coverage + int coreLimit = 1; + System.out.println("DEBUG: Running test with a CPU limit of " + coreLimit); + opts.cpuLimit(String.format("%d%%", coreLimit * 100)); + opts.sliceName(TEST_SLICE_NAME); + + OutputAnalyzer out = SystemdTestUtils.buildAndRunSystemdJava(opts); + out.shouldHaveExitValue(0) + .shouldContain("Hello Systemd") + .shouldContain(String.format("Memory Limit is: %d", (expectedMemLimit * MB))); + try { + out.shouldContain("OSContainer::active_processor_count: " + coreLimit); + } catch (RuntimeException e) { + // CPU delegation needs to be enabled when run as user on cg v2 + if (SystemdTestUtils.RUN_AS_USER) { + String hint = "When run as user on cg v2 cpu delegation needs to be configured!"; + throw new SkippedException(hint); + } + throw e; + } + } + +} diff --git a/test/jdk/TEST.ROOT b/test/jdk/TEST.ROOT index 67eceaa5a3e..6198d332142 100644 --- a/test/jdk/TEST.ROOT +++ b/test/jdk/TEST.ROOT @@ -101,6 +101,7 @@ requires.properties= \ vm.jvmti \ vm.cpu.features \ docker.support \ + systemd.support \ release.implementor \ jdk.containerized \ jdk.foreign.linker diff --git a/test/jtreg-ext/requires/VMProps.java b/test/jtreg-ext/requires/VMProps.java index 49cc6e14311..795a658afa5 100644 --- a/test/jtreg-ext/requires/VMProps.java +++ b/test/jtreg-ext/requires/VMProps.java @@ -133,6 +133,7 @@ public class VMProps implements Callable> { map.put("vm.compiler1.enabled", this::isCompiler1Enabled); map.put("vm.compiler2.enabled", this::isCompiler2Enabled); map.put("docker.support", this::dockerSupport); + map.put("systemd.support", this::systemdSupport); map.put("vm.musl", this::isMusl); map.put("release.implementor", this::implementor); map.put("jdk.containerized", this::jdkContainerized); @@ -610,7 +611,7 @@ public class VMProps implements Callable> { if (isSupported) { try { - isSupported = checkDockerSupport(); + isSupported = checkProgramSupport("checkDockerSupport()", Container.ENGINE_COMMAND); } catch (Exception e) { isSupported = false; } @@ -620,6 +621,27 @@ public class VMProps implements Callable> { return "" + isSupported; } + /** + * A simple check for systemd support + * + * @return true if systemd is supported in a given environment + */ + protected String systemdSupport() { + log("Entering systemdSupport()"); + + boolean isSupported = Platform.isLinux(); + if (isSupported) { + try { + isSupported = checkProgramSupport("checkSystemdSupport()", "systemd-run"); + } catch (Exception e) { + isSupported = false; + } + } + + log("systemdSupport(): returning isSupported = " + isSupported); + return "" + isSupported; + } + // Configures process builder to redirect process stdout and stderr to a file. // Returns file names for stdout and stderr. private Map redirectOutputToLogFile(String msg, ProcessBuilder pb, String fileNameBase) { @@ -654,17 +676,17 @@ public class VMProps implements Callable> { }); } - private boolean checkDockerSupport() throws IOException, InterruptedException { - log("checkDockerSupport(): entering"); - ProcessBuilder pb = new ProcessBuilder("which", Container.ENGINE_COMMAND); + private boolean checkProgramSupport(String logString, String cmd) throws IOException, InterruptedException { + log(logString + ": entering"); + ProcessBuilder pb = new ProcessBuilder("which", cmd); Map logFileNames = - redirectOutputToLogFile("checkDockerSupport(): which " + Container.ENGINE_COMMAND, - pb, "which-container"); + redirectOutputToLogFile(logString + ": which " + cmd, + pb, "which-cmd"); Process p = pb.start(); p.waitFor(10, TimeUnit.SECONDS); int exitValue = p.exitValue(); - log(String.format("checkDockerSupport(): exitValue = %s, pid = %s", exitValue, p.pid())); + log(String.format("%s: exitValue = %s, pid = %s", logString, exitValue, p.pid())); if (exitValue != 0) { printLogfileContent(logFileNames); } diff --git a/test/lib/jdk/test/lib/containers/systemd/SystemdRunOptions.java b/test/lib/jdk/test/lib/containers/systemd/SystemdRunOptions.java new file mode 100644 index 00000000000..5f5d513a8b2 --- /dev/null +++ b/test/lib/jdk/test/lib/containers/systemd/SystemdRunOptions.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2024, 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. + */ + +package jdk.test.lib.containers.systemd; + +import static jdk.test.lib.Asserts.assertNotNull; + +import java.util.ArrayList; +import java.util.Collections; + + +// This class represents options for running java inside systemd slices +// in test environment. +public class SystemdRunOptions { + public ArrayList javaOpts = new ArrayList<>(); + public String classToRun; // class or "-version" + public ArrayList classParams = new ArrayList<>(); + public String memoryLimit; // used in slice for MemoryLimit property + public String cpuLimit; // used in slice for CPUQuota property + public String sliceName; // name of the slice (nests CPU in memory) + public String sliceDMemoryLimit; // used in jdk_internal.slice.d + public String sliceDCpuLimit; // used in jdk_internal.slice.d + + /** + * Convenience constructor for most common use cases in testing. + * @param classToRun a class to run, or "-version" + * @param javaOpts java options to use + * + * @return Default docker run options + */ + public SystemdRunOptions(String classToRun, String... javaOpts) { + this.classToRun = classToRun; + Collections.addAll(this.javaOpts, javaOpts); + this.sliceName = defaultSliceName(); + } + + private static String defaultSliceName() { + // Create a unique name for a systemd slice + // jtreg guarantees that test.name is unique among all concurrently executing + // tests. For example, if you have two test roots: + // + // $ find test -type f + // test/foo/TEST.ROOT + // test/foo/my/TestCase.java + // test/bar/TEST.ROOT + // test/bar/my/TestCase.java + // $ jtreg -concur:2 test/foo test/bar + // + // jtreg will first run all the tests under test/foo. When they are all finished, then + // jtreg will run all the tests under test/bar. So you will never have two concurrent + // test cases whose test.name is "my/TestCase.java" + String testname = System.getProperty("test.name"); + assertNotNull(testname, "must be set by jtreg"); + testname = testname.replace(".java", ""); + testname = testname.replace("/", "_"); + testname = testname.replace("\\", "_"); + testname = testname.replace("-", "_"); + + // Example: + // Memory: "test_containers_systemd_TestMemoryAwareness" + // CPU: "test_containers_systemd_TestMemoryAwareness-cpu" => derived + return testname; + } + + /** + * The memory limit set with a .slice file in the systemd + * config directory. + * + * @param memLimit The memory limit to set (e.g. 1000M). + * @return The run options. + */ + public SystemdRunOptions memoryLimit(String memLimit) { + this.memoryLimit = memLimit; + return this; + } + + /** + * The memory limit to set in the top-level jdk_internal.slice.d + * systemd config directory. + * + * @param memoryLimit The memory limit to set. + * @return The run options. + */ + public SystemdRunOptions sliceDMemoryLimit(String memoryLimit) { + this.sliceDMemoryLimit = memoryLimit; + return this; + } + + /** + * The CPU limit set with a .slice file in the systemd + * config directory. + * + * @param cpuLimit + * @return The run options. + */ + public SystemdRunOptions cpuLimit(String cpuLimit) { + this.cpuLimit = cpuLimit; + return this; + } + + /** + * The Cpu limit set in the top-level jdk_internal.slice.d + * systemd config directory. + * + * @param cpuLimit The CPU limit to set to. + * @return The run options. + */ + public SystemdRunOptions sliceDCpuLimit(String cpuLimit) { + this.sliceDCpuLimit = cpuLimit; + return this; + } + + public SystemdRunOptions sliceName(String name) { + this.sliceName = name; + return this; + } + + public SystemdRunOptions addJavaOpts(String... opts) { + Collections.addAll(javaOpts, opts); + return this; + } + + public SystemdRunOptions addClassOptions(String... opts) { + Collections.addAll(classParams,opts); + return this; + } + + public boolean hasSliceDLimit() { + return this.sliceDMemoryLimit != null || + this.sliceDCpuLimit != null; + } +} diff --git a/test/lib/jdk/test/lib/containers/systemd/SystemdTestUtils.java b/test/lib/jdk/test/lib/containers/systemd/SystemdTestUtils.java new file mode 100644 index 00000000000..341b24c3e05 --- /dev/null +++ b/test/lib/jdk/test/lib/containers/systemd/SystemdTestUtils.java @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2024, 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. + */ + +package jdk.test.lib.containers.systemd; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import jdk.internal.platform.Metrics; +import jdk.test.lib.Platform; +import jdk.test.lib.Utils; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.util.FileUtils; +import jtreg.SkippedException; + +public class SystemdTestUtils { + + private static final String CGROUPS_PROVIDER = Metrics.systemMetrics().getProvider(); + private static boolean CGROUPS_V2 = "cgroupv2".equals(CGROUPS_PROVIDER); + public static boolean RUN_AS_USER = !Platform.isRoot() && CGROUPS_V2; + private static final String SLICE_NAMESPACE_PREFIX = "jdk_internal"; + private static final String SLICE_D_MEM_CONFIG_FILE = "memory-limit.conf"; + private static final String SLICE_D_CPU_CONFIG_FILE = "cpu-limit.conf"; + private static final String USER_HOME = System.getProperty("user.home"); + private static final Path SYSTEMD_CONFIG_HOME_ROOT = Path.of("/", "etc", "systemd", "system"); + private static final Path SYSTEMD_CONFIG_HOME_USER = Path.of(USER_HOME, ".config", "systemd", "user"); + private static final Path SYSTEMD_CONFIG_HOME = Platform.isRoot() ? SYSTEMD_CONFIG_HOME_ROOT : SYSTEMD_CONFIG_HOME_USER; + + // Specifies how many lines to copy from child STDOUT to main test output. + // Having too many lines in the main test output will result + // in JT harness trimming the output, and can lead to loss of useful + // diagnostic information. + private static final int MAX_LINES_TO_COPY_FOR_CHILD_STDOUT = 100; + + public record ResultFiles(Path memory, Path cpu, Path sliceDotDDir) {} + + /** + * Create commonly used options with the class to be launched inside the + * systemd slice + * + * @param testClass The test class or {@code -version} + * @return The basic options. + */ + public static SystemdRunOptions newOpts(String testClass) { + return new SystemdRunOptions(testClass, + "-Xlog:os+container=trace", + "-cp", + Utils.TEST_CLASSES); + } + + /** + * Run Java inside a systemd slice with specified parameters and options. + * + * @param opts The systemd slice options when running java + * @return An OutputAnalyzer of the output of the command than ran. + * @throws Exception If something went wrong. + * @throws SkippedException If the test cannot be run (i.e. non-root user + * on cgroups v1). + */ + public static OutputAnalyzer buildAndRunSystemdJava(SystemdRunOptions opts) throws Exception, SkippedException { + if (!Platform.isRoot() && !CGROUPS_V2) { + throw new SkippedException("Systemd tests require root on cgroup v1. Test skipped!"); + } + ResultFiles files = SystemdTestUtils.buildSystemdSlices(opts); + + try { + return SystemdTestUtils.systemdRunJava(opts); + } finally { + try { + if (files.memory() != null) { + Files.delete(files.memory()); + } + if (files.cpu() != null) { + Files.delete(files.cpu()); + } + if (files.sliceDotDDir() != null) { + FileUtils.deleteFileTreeUnchecked(files.sliceDotDDir()); + } + } catch (NoSuchFileException e) { + // ignore + } + } + } + + private static OutputAnalyzer systemdRunJava(SystemdRunOptions opts) throws Exception { + return execute(buildJavaCommand(opts)); + } + + /** + * Create systemd slice files under /etc/systemd/system. + * + * The JDK will then run within that slice as provided by the SystemdRunOptions. + * + * @param runOpts The systemd slice options to use when running the test. + * @return The systemd slice files (for cleanup-purposes later). + * @throws Exception + */ + private static ResultFiles buildSystemdSlices(SystemdRunOptions runOpts) throws Exception { + String sliceName = sliceName(runOpts); + String sliceNameCpu = sliceNameCpu(runOpts); + + // Generate systemd slices for cpu/memory + String memorySliceContent = getMemorySlice(runOpts, sliceName); + String cpuSliceContent = getCpuSlice(runOpts, sliceName); + + // Ensure base directory exists + Files.createDirectories(SYSTEMD_CONFIG_HOME); + Path sliceDotDDir = null; + if (runOpts.hasSliceDLimit()) { + String dirName = String.format("%s.slice.d", SLICE_NAMESPACE_PREFIX); + sliceDotDDir = SYSTEMD_CONFIG_HOME.resolve(Path.of(dirName)); + Files.createDirectory(sliceDotDDir); + + if (runOpts.sliceDMemoryLimit != null) { + Path memoryConfig = sliceDotDDir.resolve(Path.of(SLICE_D_MEM_CONFIG_FILE)); + Files.writeString(memoryConfig, getMemoryDSliceContent(runOpts)); + } + if (runOpts.sliceDCpuLimit != null) { + Path cpuConfig = sliceDotDDir.resolve(Path.of(SLICE_D_CPU_CONFIG_FILE)); + Files.writeString(cpuConfig, getCPUDSliceContent(runOpts)); + } + } + + Path memory, cpu; + try { + // memory slice + memory = SYSTEMD_CONFIG_HOME.resolve(Path.of(sliceFileName(sliceName))); + // cpu slice nested in memory + cpu = SYSTEMD_CONFIG_HOME.resolve(Path.of(sliceFileName(sliceNameCpu))); + Files.writeString(memory, memorySliceContent); + Files.writeString(cpu, cpuSliceContent); + } catch (IOException e) { + throw new AssertionError("Failed to write systemd slice files"); + } + + systemdDaemonReload(cpu); + + return new ResultFiles(memory, cpu, sliceDotDDir); + } + + private static String sliceName(SystemdRunOptions runOpts) { + // Slice name may include '-' which is a hierarchical slice indicator. + // Replace '-' with '_' to avoid side-effects. + return SLICE_NAMESPACE_PREFIX + "-" + runOpts.sliceName.replace("-", "_"); + } + + private static String sliceNameCpu(SystemdRunOptions runOpts) { + String slice = sliceName(runOpts); + return String.format("%s-cpu", slice); + } + + private static void systemdDaemonReload(Path cpu) throws Exception { + List daemonReload = systemCtl(); + daemonReload.add("daemon-reload"); + + if (execute(daemonReload).getExitValue() != 0) { + throw new AssertionError("Failed to reload systemd daemon"); + } + } + + private static List systemCtl() { + return commandWithUser("systemctl"); + } + + /** + * 'baseCommand' or 'baseCommand --user' as list, depending on the cgroups + * version and running user. + * + * @return 'baseCommand' if we are the root user, 'systemctl --user' if + * the current user is non-root and we are on cgroups v2. Note: + * Cgroups v1 and non-root is not possible as tests are skipped then. + */ + private static List commandWithUser(String baseCommand) { + List command = new ArrayList<>(); + command.add(baseCommand); + if (RUN_AS_USER) { + command.add("--user"); + } + return command; + } + + private static String getCpuSlice(SystemdRunOptions runOpts, String sliceName) { + String basicSliceFormat = getBasicSliceFormat(); + return String.format(basicSliceFormat, sliceName, getCPUSliceContent(runOpts)); + } + + private static String getCPUSliceContent(SystemdRunOptions runOpts) { + String format = basicCPUContentFormat(); + return String.format(format, runOpts.cpuLimit); + } + + private static String getMemorySlice(SystemdRunOptions runOpts, String sliceName) { + String basicSliceFormat = getBasicSliceFormat(); + return String.format(basicSliceFormat, sliceName, getMemorySliceContent(runOpts)); + } + + private static String getMemoryDSliceContent(SystemdRunOptions runOpts) { + String format = "[Slice]\n" + basicMemoryContentFormat(); + return String.format(format, runOpts.sliceDMemoryLimit); + } + + private static String getCPUDSliceContent(SystemdRunOptions runOpts) { + String format = "[Slice]\n" + basicCPUContentFormat(); + return String.format(format, runOpts.sliceDCpuLimit); + } + + private static String basicCPUContentFormat() { + return """ + CPUAccounting=true + CPUQuota=%s + """; + } + + private static String basicMemoryContentFormat() { + return """ + MemoryAccounting=true + MemoryLimit=%s + """; + } + + private static String getMemorySliceContent(SystemdRunOptions runOpts) { + String format = basicMemoryContentFormat(); + + return String.format(format, runOpts.memoryLimit); + } + + private static String getBasicSliceFormat() { + return """ + [Unit] + Description=OpenJDK Tests Slice for %s + Before=slices.target + + [Slice] + %s + """; + } + + private static String sliceFileName(String sliceName) { + return String.format("%s.slice", sliceName); + } + + /** + * Build the java command to run inside a systemd slice + * + * @param SystemdRunOptions options for running the systemd slice test + * + * @return command + * @throws Exception + */ + private static List buildJavaCommand(SystemdRunOptions opts) throws Exception { + // systemd-run [--user] --slice .slice --scope + List javaCmd = systemdRun(); + javaCmd.add("--slice"); + javaCmd.add(sliceFileName(sliceNameCpu(opts))); + javaCmd.add("--scope"); + javaCmd.add(Path.of(Utils.TEST_JDK, "bin", "java").toString()); + javaCmd.addAll(opts.javaOpts); + javaCmd.add(opts.classToRun); + javaCmd.addAll(opts.classParams); + return javaCmd; + } + + private static List systemdRun() { + return commandWithUser("systemd-run"); + } + + /** + * Execute a specified command in a process, report diagnostic info. + * + * @param command to be executed + * @return The output from the process + * @throws Exception + */ + private static OutputAnalyzer execute(List command) throws Exception { + return execute(command.toArray(String[]::new)); + } + + /** + * Execute a specified command in a process, report diagnostic info. + * + * @param command to be executed + * @return The output from the process + * @throws Exception + */ + private static OutputAnalyzer execute(String... command) throws Exception { + ProcessBuilder pb = new ProcessBuilder(command); + System.out.println("[COMMAND]\n" + Utils.getCommandLine(pb)); + + Process p = pb.start(); + long pid = p.pid(); + OutputAnalyzer output = new OutputAnalyzer(p); + + int max = MAX_LINES_TO_COPY_FOR_CHILD_STDOUT; + String stdout = output.getStdout(); + String stdoutLimited = limitLines(stdout, max); + System.out.println("[STDERR]\n" + output.getStderr()); + System.out.println("[STDOUT]\n" + stdoutLimited); + if (stdout != stdoutLimited) { + System.out.printf("Child process STDOUT is limited to %d lines\n", + max); + } + + String stdoutLogFile = String.format("systemd-stdout-%d.log", pid); + writeOutputToFile(stdout, stdoutLogFile); + System.out.println("Full child process STDOUT was saved to " + stdoutLogFile); + + return output; + } + + private static void writeOutputToFile(String output, String fileName) throws Exception { + try (FileWriter fw = new FileWriter(fileName)) { + fw.write(output, 0, output.length()); + } + } + + private static String limitLines(String buffer, int nrOfLines) { + List l = Arrays.asList(buffer.split("\\R")); + if (l.size() < nrOfLines) { + return buffer; + } + + return String.join("\n", l.subList(0, nrOfLines)); + } +} diff --git a/test/lib/jdk/test/whitebox/WhiteBox.java b/test/lib/jdk/test/whitebox/WhiteBox.java index 607e9552945..8883e8332d5 100644 --- a/test/lib/jdk/test/whitebox/WhiteBox.java +++ b/test/lib/jdk/test/whitebox/WhiteBox.java @@ -769,6 +769,7 @@ public class WhiteBox { public native void printOsInfo(); public native long hostPhysicalMemory(); public native long hostPhysicalSwap(); + public native int hostCPUs(); // Decoder public native void disableElfSectionCache();