diff --git a/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp b/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp index 4367eba4c43..31534efb89d 100644 --- a/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp @@ -25,6 +25,8 @@ #include "cgroupV2Subsystem_linux.hpp" #include "cgroupUtil_linux.hpp" +#include + // Constructor CgroupV2Controller::CgroupV2Controller(char* mount_path, char *cgroup_path, @@ -60,22 +62,39 @@ int CgroupV2CpuController::cpu_shares() { log_debug(os, container)("CPU Shares is: %d", -1); return -1; } + // cg v2 values must be in range [1-10000] + assert(shares_int >= 1 && shares_int <= 10000, "invariant"); // CPU shares (OCI) value needs to get translated into // a proper Cgroups v2 value. See: - // https://github.com/containers/crun/blob/master/crun.1.md#cpu-controller + // https://github.com/containers/crun/blob/1.24/crun.1.md#cpu-controller // // Use the inverse of (x == OCI value, y == cgroupsv2 value): - // ((262142 * y - 1)/9999) + 2 = x + // y = 10^(log2(x)^2/612 + 125/612 * log2(x) - 7.0/34.0) + // + // By re-arranging it to the standard quadratic form: + // log2(x)^2 + 125 * log2(x) - (126 + 612 * log_10(y)) = 0 + // + // Therefore, log2(x) = (-125 + sqrt( 125^2 - 4 * (-(126 + 612 * log_10(y)))))/2 + // + // As a result we have the inverse (we can discount substraction of the + // square root value since those values result in very small numbers and the + // cpu shares values - OCI - are in range [2,262144]): + // + // x = 2^((-125 + sqrt(16129 + 2448* log10(y)))/2) // - int x = 262142 * shares_int - 1; - double frac = x/9999.0; - x = ((int)frac) + 2; + double log_multiplicand = log10(shares_int); + double discriminant = 16129 + 2448 * log_multiplicand; + double square_root = sqrt(discriminant); + double exponent = (-125 + square_root)/2; + double scaled_val = pow(2, exponent); + int x = (int) scaled_val; log_trace(os, container)("Scaled CPU shares value is: %d", x); // Since the scaled value is not precise, return the closest // multiple of PER_CPU_SHARES for a more conservative mapping if ( x <= PER_CPU_SHARES ) { - // will always map to 1 CPU + // Don't do the multiples of PER_CPU_SHARES mapping since we + // have a value <= PER_CPU_SHARES log_debug(os, container)("CPU Shares is: %d", x); return x; } diff --git a/src/java.base/linux/classes/jdk/internal/platform/cgroupv2/CgroupV2Subsystem.java b/src/java.base/linux/classes/jdk/internal/platform/cgroupv2/CgroupV2Subsystem.java index ddb4d8e2718..3bdd7dc2a64 100644 --- a/src/java.base/linux/classes/jdk/internal/platform/cgroupv2/CgroupV2Subsystem.java +++ b/src/java.base/linux/classes/jdk/internal/platform/cgroupv2/CgroupV2Subsystem.java @@ -154,22 +154,39 @@ private long getFromCpuMax(int tokenIdx) { @Override public long getCpuShares() { long sharesRaw = getLongVal("cpu.weight"); - if (sharesRaw == 100 || sharesRaw <= 0) { + // cg v2 value must be in range [1,10000] + if (sharesRaw == 100 || sharesRaw <= 0 || sharesRaw > 10000) { return CgroupSubsystem.LONG_RETVAL_UNLIMITED; } int shares = (int)sharesRaw; // CPU shares (OCI) value needs to get translated into // a proper Cgroups v2 value. See: - // https://github.com/containers/crun/blob/master/crun.1.md#cpu-controller + // https://github.com/containers/crun/blob/1.24/crun.1.md#cpu-controller // // Use the inverse of (x == OCI value, y == cgroupsv2 value): - // ((262142 * y - 1)/9999) + 2 = x + // y = 10^(log2(x)^2/612 + 125/612 * log2(x) - 7.0/34.0) // - int x = 262142 * shares - 1; - double frac = x/9999.0; - x = ((int)frac) + 2; + // By re-arranging it to the standard quadratic form: + // log2(x)^2 + 125 * log2(x) - (126 + 612 * log_10(y)) = 0 + // + // Therefore, log2(x) = (-125 + sqrt( 125^2 - 4 * (-(126 + 612 * log_10(y)))))/2 + // + // As a result we have the inverse (we can discount substraction of the + // square root value since those values result in very small numbers and the + // cpu shares values - OCI - are in range [2-262144]) + // + // x = 2^((-125 + sqrt(16129 + 2448* log10(y)))/2) + // + double logMultiplicand = Math.log10(shares); + double discriminant = 16129 + 2448 * logMultiplicand; + double squareRoot = Math.sqrt(discriminant); + double exponent = (-125 + squareRoot)/2; + double scaledValue = Math.pow(2, exponent); + + int x = (int)scaledValue; if ( x <= PER_CPU_SHARES ) { - return PER_CPU_SHARES; // mimic cgroups v1 + // Return the back-mapped value. + return x; } int f = x/PER_CPU_SHARES; int lower_multiple = f * PER_CPU_SHARES; diff --git a/test/hotspot/jtreg/containers/docker/TestMisc.java b/test/hotspot/jtreg/containers/docker/TestMisc.java index 4da13067399..1db65b61558 100644 --- a/test/hotspot/jtreg/containers/docker/TestMisc.java +++ b/test/hotspot/jtreg/containers/docker/TestMisc.java @@ -29,20 +29,24 @@ * @requires !vm.asan * @library /test/lib * @modules java.base/jdk.internal.misc + * java.base/jdk.internal.platform * java.management * jdk.jartool/sun.tools.jar * @build CheckContainerized jdk.test.whitebox.WhiteBox PrintContainerInfo * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar whitebox.jar jdk.test.whitebox.WhiteBox * @run driver TestMisc */ +import jdk.internal.platform.Metrics; import jdk.test.lib.containers.docker.Common; import jdk.test.lib.containers.docker.DockerTestUtils; import jdk.test.lib.containers.docker.DockerRunOptions; import jdk.test.lib.process.OutputAnalyzer; import jdk.test.lib.process.ProcessTools; +import jtreg.SkippedException; public class TestMisc { + private static final Metrics metrics = Metrics.systemMetrics(); private static final String imageName = Common.imageName("misc"); public static void main(String[] args) throws Exception { @@ -58,6 +62,7 @@ public static void main(String[] args) throws Exception { testIsContainerized(); testPrintContainerInfo(); testPrintContainerInfoActiveProcessorCount(); + testPrintContainerInfoCPUShares(); } finally { DockerTestUtils.removeDockerImage(imageName); } @@ -94,8 +99,53 @@ private static void testPrintContainerInfo() throws Exception { checkContainerInfo(Common.run(opts)); } + // Test the mapping function on cgroups v2. Should also pass on cgroups v1 as it's + // a direct mapping there. + private static void testPrintContainerInfoCPUShares() throws Exception { + // Test won't work on cgv1 rootless podman since resource limits don't + // work there. + if ("cgroupv1".equals(metrics.getProvider()) && + DockerTestUtils.isPodman() && + DockerTestUtils.isRootless()) { + throw new SkippedException("Resource limits required for testPrintContainerInfoCPUShares(). " + + "This is cgv1 with podman in rootless mode. Test skipped."); + } + // Anything less than 1024 should return the back-mapped cpu-shares value without + // rounding to next multiple of 1024 (on cg v2). Only ensure that we get + // 'cpu_shares: ' over 'cpu_shares: no shares'. + printContainerInfo(512, 1024, false); + // Don't use 1024 exactly so as to avoid mapping to the unlimited/uset case. + // Use a value > 100 post-mapping so as to hit the non-default branch: 1052 => 103 + printContainerInfo(1052, 1024, true); + // need at least 2 CPU cores for this test to work + if (Runtime.getRuntime().availableProcessors() >= 2) { + printContainerInfo(2048, 2048, true); + } + } + + private static void printContainerInfo(int cpuShares, int expected, boolean numberMatch) throws Exception { + Common.logNewTestCase("Test print_container_info() - cpu shares - given: " + cpuShares + ", expected: " + expected); + + DockerRunOptions opts = Common.newOpts(imageName, "PrintContainerInfo"); + Common.addWhiteBoxOpts(opts); + opts.addDockerOpts("--cpu-shares", Integer.valueOf(cpuShares).toString()); + + OutputAnalyzer out = Common.run(opts); + String str = out.getOutput(); + boolean isCgroupV2 = str.contains("cgroupv2"); + // cg v1 maps cpu shares values verbatim. Only cg v2 uses the + // mapping function. + if (numberMatch) { + int valueExpected = isCgroupV2 ? expected : cpuShares; + out.shouldContain("cpu_shares: " + valueExpected); + } else { + // must not print "no shares" + out.shouldNotContain("cpu_shares: no shares"); + } + } + private static void testPrintContainerInfoActiveProcessorCount() throws Exception { - Common.logNewTestCase("Test print_container_info()"); + Common.logNewTestCase("Test print_container_info() - ActiveProcessorCount"); DockerRunOptions opts = Common.newOpts(imageName, "PrintContainerInfo").addJavaOpts("-XX:ActiveProcessorCount=2"); Common.addWhiteBoxOpts(opts); diff --git a/test/jdk/jdk/internal/platform/docker/MetricsCpuTester.java b/test/jdk/jdk/internal/platform/docker/MetricsCpuTester.java index ff5d52d95a6..62549e7b518 100644 --- a/test/jdk/jdk/internal/platform/docker/MetricsCpuTester.java +++ b/test/jdk/jdk/internal/platform/docker/MetricsCpuTester.java @@ -145,9 +145,13 @@ private static void testCpuSetMemNodes(String cpusetMems) { private static void testCpuShares(long shares) { Metrics metrics = Metrics.systemMetrics(); if ("cgroupv2".equals(metrics.getProvider()) && shares < 1024) { - // Adjust input shares for < 1024 cpu shares as the - // impl. rounds up to the next multiple of 1024 - shares = 1024; + // Don't assert for shares values less than 1024 as we don't + // have a 1-to-1 mapping from the cgroup v2 value to the OCI + // value. + System.out.println("Debug: cgv2 - Got CPU shares of: " + + metrics.getCpuShares() + " - Skipping assert."); + System.out.println("TEST PASSED!!!"); + return; } long newShares = metrics.getCpuShares(); if (newShares != shares) {