1 /*
   2  * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 
  25 /*
  26  * @test
  27  * @summary Test JVM's CPU resource awareness when running inside docker container
  28  * @requires docker.support
  29  * @library /test/lib
  30  * @modules java.base/jdk.internal.misc
  31  *          java.management
  32  *          jdk.jartool/sun.tools.jar
  33  * @run driver TestCPUAwareness
  34  */
  35 import java.util.List;
  36 import java.util.Optional;
  37 import jdk.test.lib.containers.docker.Common;
  38 import jdk.test.lib.containers.docker.DockerRunOptions;
  39 import jdk.test.lib.containers.docker.DockerTestUtils;
  40 import jdk.test.lib.containers.cgroup.CPUSetsReader;
  41 import jdk.test.lib.process.OutputAnalyzer;
  42 
  43 public class TestCPUAwareness {
  44     private static final String imageName = Common.imageName("cpu");
  45     private static final int availableCPUs = Runtime.getRuntime().availableProcessors();
  46 
  47     public static void main(String[] args) throws Exception {
  48         if (!DockerTestUtils.canTestDocker()) {
  49             return;
  50         }
  51 
  52         System.out.println("Test Environment: detected availableCPUs = " + availableCPUs);
  53         DockerTestUtils.buildJdkDockerImage(imageName, "Dockerfile-BasicTest", "jdk-docker");
  54 
  55         try {
  56             // cpuset, period, shares, expected Active Processor Count
  57             testComboWithCpuSets();
  58 
  59             // cpu shares - it should be safe to use CPU shares exceeding available CPUs
  60             testCpuShares(256, 1);
  61             testCpuShares(2048, 2);
  62             testCpuShares(4096, 4);
  63 
  64             // leave one CPU for system and tools, otherwise this test may be unstable
  65             int maxNrOfAvailableCpus =  availableCPUs - 1;
  66             for (int i=1; i < maxNrOfAvailableCpus; i = i * 2) {
  67                 testCpus(i, i);
  68             }
  69 
  70             // If ActiveProcessorCount is set, the VM should use it, regardless of other
  71             // container settings, host settings or available CPUs on the host.
  72             testActiveProcessorCount(1, 1);
  73             testActiveProcessorCount(2, 2);
  74 
  75             // cpu quota and period
  76             testCpuQuotaAndPeriod(50*1000, 100*1000);
  77             testCpuQuotaAndPeriod(100*1000, 100*1000);
  78             testCpuQuotaAndPeriod(150*1000, 100*1000);
  79             testCpuQuotaAndPeriod(400*1000, 100*1000);
  80 
  81         } finally {
  82             DockerTestUtils.removeDockerImage(imageName);
  83         }
  84     }
  85 
  86 
  87     private static void testComboWithCpuSets() throws Exception {
  88         String cpuSetStr = CPUSetsReader.readFromProcStatus("Cpus_allowed_list");
  89         System.out.println("cpuSetStr = " + cpuSetStr);
  90 
  91         if (cpuSetStr == null) {
  92             System.out.printf("The cpuset test cases are skipped");
  93         } else {
  94             List<Integer> cpuSet = CPUSetsReader.parseCpuSet(cpuSetStr);
  95 
  96             // Test subset of cpuset with one element
  97             if (cpuSet.size() >= 1) {
  98                 String testCpuSet = CPUSetsReader.listToString(cpuSet, 1);
  99                 testAPCCombo(testCpuSet, 200*1000, 100*1000,   4*1024, true, 1);
 100             }
 101 
 102             // Test subset of cpuset with two elements
 103             if (cpuSet.size() >= 2) {
 104                 String testCpuSet = CPUSetsReader.listToString(cpuSet, 2);
 105                 testAPCCombo(testCpuSet, 200*1000, 100*1000, 4*1024, true, 2);
 106                 testAPCCombo(testCpuSet, 200*1000, 100*1000, 1023,   true, 2);
 107                 testAPCCombo(testCpuSet, 200*1000, 100*1000, 1023,   false,  1);
 108             }
 109 
 110             // Test subset of cpuset with three elements
 111             if (cpuSet.size() >= 3) {
 112                 String testCpuSet = CPUSetsReader.listToString(cpuSet, 3);
 113                 testAPCCombo(testCpuSet, 100*1000, 100*1000, 2*1024, true, 1);
 114                 testAPCCombo(testCpuSet, 200*1000, 100*1000, 1023,   true, 2);
 115                 testAPCCombo(testCpuSet, 200*1000, 100*1000, 1023,   false,  1);
 116             }
 117         }
 118     }
 119 
 120 
 121     private static void testActiveProcessorCount(int valueToSet, int expectedValue) throws Exception {
 122         Common.logNewTestCase("Test ActiveProcessorCount: valueToSet = " + valueToSet);
 123 
 124         DockerRunOptions opts = Common.newOpts(imageName)
 125             .addJavaOpts("-XX:ActiveProcessorCount=" + valueToSet, "-Xlog:os=trace");
 126         Common.run(opts)
 127             .shouldMatch("active processor count set by user.*" + expectedValue);
 128     }
 129 
 130 
 131     private static void testCpus(int valueToSet, int expectedTraceValue) throws Exception {
 132         Common.logNewTestCase("test cpus: " + valueToSet);
 133         DockerRunOptions opts = Common.newOpts(imageName)
 134             .addDockerOpts("--cpu-period=" + 10000)
 135             .addDockerOpts("--cpu-quota=" + valueToSet * 10000);
 136         Common.run(opts)
 137             .shouldMatch("active_processor_count.*" + expectedTraceValue);
 138     }
 139 
 140 
 141     // Expected active processor count can not exceed available CPU count
 142     private static int adjustExpectedAPCForAvailableCPUs(int expectedAPC) {
 143         if (expectedAPC > availableCPUs) {
 144             expectedAPC = availableCPUs;
 145             System.out.println("Adjusted expectedAPC = " + expectedAPC);
 146         }
 147         return expectedAPC;
 148     }
 149 
 150 
 151     private static void testCpuQuotaAndPeriod(int quota, int period)
 152         throws Exception {
 153         Common.logNewTestCase("test cpu quota and period: ");
 154         System.out.println("quota = " + quota);
 155         System.out.println("period = " + period);
 156 
 157         int expectedAPC = (int) Math.ceil((float) quota / (float) period);
 158         System.out.println("expectedAPC = " + expectedAPC);
 159         expectedAPC = adjustExpectedAPCForAvailableCPUs(expectedAPC);
 160 
 161         DockerRunOptions opts = Common.newOpts(imageName)
 162             .addDockerOpts("--cpu-period=" + period)
 163             .addDockerOpts("--cpu-quota=" + quota);
 164 
 165         Common.run(opts)
 166             .shouldMatch("CPU Period is.*" + period)
 167             .shouldMatch("CPU Quota is.*" + quota)
 168             .shouldMatch("active_processor_count.*" + expectedAPC);
 169     }
 170 
 171 
 172     // Test correctess of automatically selected active processor cound
 173     private static void testAPCCombo(String cpuset, int quota, int period, int shares,
 174                                      boolean usePreferContainerQuotaForCPUCount,
 175                                      int expectedAPC) throws Exception {
 176         Common.logNewTestCase("test APC Combo");
 177         System.out.println("cpuset = " + cpuset);
 178         System.out.println("quota = " + quota);
 179         System.out.println("period = " + period);
 180         System.out.println("shares = " + shares);
 181         System.out.println("usePreferContainerQuotaForCPUCount = " + usePreferContainerQuotaForCPUCount);
 182         System.out.println("expectedAPC = " + expectedAPC);
 183 
 184         expectedAPC = adjustExpectedAPCForAvailableCPUs(expectedAPC);
 185 
 186         DockerRunOptions opts = Common.newOpts(imageName)
 187             .addDockerOpts("--cpuset-cpus", "" + cpuset)
 188             .addDockerOpts("--cpu-period=" + period)
 189             .addDockerOpts("--cpu-quota=" + quota)
 190             .addDockerOpts("--cpu-shares=" + shares);
 191 
 192         if (!usePreferContainerQuotaForCPUCount) opts.addJavaOpts("-XX:-PreferContainerQuotaForCPUCount");
 193 
 194         Common.run(opts)
 195             .shouldMatch("active_processor_count.*" + expectedAPC);
 196     }
 197 
 198 
 199     private static void testCpuShares(int shares, int expectedAPC) throws Exception {
 200         Common.logNewTestCase("test cpu shares, shares = " + shares);
 201         System.out.println("expectedAPC = " + expectedAPC);
 202 
 203         expectedAPC = adjustExpectedAPCForAvailableCPUs(expectedAPC);
 204 
 205         DockerRunOptions opts = Common.newOpts(imageName)
 206             .addDockerOpts("--cpu-shares=" + shares);
 207         OutputAnalyzer out = Common.run(opts);
 208         String cgroupVer = getDetectedCgroupVersion(out);
 209         if (cgroupVer != null ) {
 210             if ("cgroupv1".equals(cgroupVer)) {
 211                 out.shouldMatch("CPU Shares is.*" + shares);
 212             } else if ("cgroupsv2".equals(cgroupVer)) {
 213                 out.shouldMatch("Scaled CPU Shares value is:.*");
 214             }
 215         }
 216         out.shouldMatch("active_processor_count.*" + expectedAPC);
 217     }
 218 
 219     private static String getDetectedCgroupVersion(OutputAnalyzer out) throws Exception {
 220         Optional<String> cgroupVersString = out.asLines()
 221                                                 .stream()
 222                                                 .filter( l -> l.startsWith("Detected CGroups version is:") )
 223                                                 .findFirst();
 224         if (cgroupVersString.isPresent()) { // only non-product builds have this
 225             return cgroupVersString.get().split(": ")[1].trim();
 226         } else {
 227             return null;
 228         }
 229     }
 230 
 231 }