1 /* 2 * Copyright (c) 2020, Red Hat Inc. 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 package jdk.test.lib.containers.cgroup; 25 26 import java.io.IOException; 27 import java.nio.file.Files; 28 import java.nio.file.Path; 29 import java.nio.file.Paths; 30 import java.util.Arrays; 31 import java.util.List; 32 import java.util.concurrent.TimeUnit; 33 import java.util.stream.Collectors; 34 35 import jdk.internal.platform.CgroupSubsystem; 36 import jdk.internal.platform.Metrics; 37 38 public class MetricsTesterCgroupV2 implements CgroupMetricsTester { 39 40 private static final long UNLIMITED = -1; 41 private static final UnifiedController UNIFIED = new UnifiedController(); 42 private static final String MAX = "max"; 43 private static final int PER_CPU_SHARES = 1024; 44 45 private final long startSysVal; 46 private final long startUserVal; 47 private final long startUsage; 48 49 static class UnifiedController { 50 51 private static final String NAME = "unified"; 52 private final String path; 53 54 UnifiedController() { 55 path = constructPath(); 56 } 57 58 String getPath() { 59 return path; 60 } 61 62 private static String constructPath() { 63 String mountPath; 64 String cgroupPath; 65 try { 66 List<String> fifthTokens = Files.lines(Paths.get("/proc/self/mountinfo")) 67 .filter( l -> l.contains("- cgroup2")) 68 .map(UnifiedController::splitAndMountPath) 69 .collect(Collectors.toList()); 70 if (fifthTokens.size() != 1) { 71 throw new AssertionError("Expected only one cgroup2 line"); 72 } 73 mountPath = fifthTokens.get(0); 74 75 List<String> cgroupPaths = Files.lines(Paths.get("/proc/self/cgroup")) 76 .filter( l -> l.startsWith("0:")) 77 .map(UnifiedController::splitAndCgroupPath) 78 .collect(Collectors.toList()); 79 if (cgroupPaths.size() != 1) { 80 throw new AssertionError("Expected only one unified controller line"); 81 } 82 cgroupPath = cgroupPaths.get(0); 83 return Paths.get(mountPath, cgroupPath).toString(); 84 } catch (IOException e) { 85 return null; 86 } 87 } 88 89 public static String splitAndMountPath(String input) { 90 String[] tokens = input.split("\\s+"); 91 return tokens[4]; // fifth entry is the mount path 92 } 93 94 public static String splitAndCgroupPath(String input) { 95 String[] tokens = input.split(":"); 96 return tokens[2]; 97 } 98 } 99 100 private long getLongLimitValueFromFile(String file) { 101 String strVal = getStringVal(file); 102 if (MAX.equals(strVal)) { 103 return UNLIMITED; 104 } 105 return convertStringToLong(strVal); 106 } 107 108 public MetricsTesterCgroupV2() { 109 Metrics metrics = Metrics.systemMetrics(); 110 // Initialize CPU usage metrics before we do any testing. 111 startSysVal = metrics.getCpuSystemUsage(); 112 startUserVal = metrics.getCpuUserUsage(); 113 startUsage = metrics.getCpuUsage(); 114 } 115 116 private long getLongValueFromFile(String file) { 117 return convertStringToLong(getStringVal(file)); 118 } 119 120 private long getLongValueEntryFromFile(String file, String metric) { 121 Path filePath = Paths.get(UNIFIED.getPath(), file); 122 try { 123 String strVal = Files.lines(filePath).filter(l -> l.startsWith(metric)).collect(Collectors.joining()); 124 String[] keyValues = strVal.split("\\s+"); 125 String value = keyValues[1]; 126 return convertStringToLong(value); 127 } catch (IOException e) { 128 return 0; 129 } 130 } 131 132 private String getStringVal(String file) { 133 Path filePath = Paths.get(UNIFIED.getPath(), file); 134 try { 135 return Files.lines(filePath).collect(Collectors.joining()); 136 } catch (IOException e) { 137 return null; 138 } 139 } 140 141 private void fail(String metric, long oldVal, long newVal) { 142 CgroupMetricsTester.fail(UnifiedController.NAME, metric, oldVal, newVal); 143 } 144 145 private void fail(String metric, String oldVal, String newVal) { 146 CgroupMetricsTester.fail(UnifiedController.NAME, metric, oldVal, newVal); 147 } 148 149 private void warn(String metric, long oldVal, long newVal) { 150 CgroupMetricsTester.warn(UnifiedController.NAME, metric, oldVal, newVal); 151 } 152 153 private long getCpuShares(String file) { 154 long rawVal = getLongValueFromFile(file); 155 if (rawVal == 0 || rawVal == 100) { 156 return UNLIMITED; 157 } 158 int shares = (int)rawVal; 159 // CPU shares (OCI) value needs to get translated into 160 // a proper Cgroups v2 value. See: 161 // https://github.com/containers/crun/blob/master/crun.1.md#cpu-controller 162 // 163 // Use the inverse of (x == OCI value, y == cgroupsv2 value): 164 // ((262142 * y - 1)/9999) + 2 = x 165 // 166 int x = 262142 * shares - 1; 167 double frac = x/9999.0; 168 x = ((int)frac) + 2; 169 if ( x <= PER_CPU_SHARES ) { 170 return PER_CPU_SHARES; // mimic cgroups v1 171 } 172 int f = x/PER_CPU_SHARES; 173 int lower_multiple = f * PER_CPU_SHARES; 174 int upper_multiple = (f + 1) * PER_CPU_SHARES; 175 int distance_lower = Math.max(lower_multiple, x) - Math.min(lower_multiple, x); 176 int distance_upper = Math.max(upper_multiple, x) - Math.min(upper_multiple, x); 177 x = distance_lower <= distance_upper ? lower_multiple : upper_multiple; 178 return x; 179 } 180 181 private long getCpuMaxValueFromFile(String file) { 182 return getCpuValueFromFile(file, 0 /* $MAX index */); 183 } 184 185 private long getCpuPeriodValueFromFile(String file) { 186 return getCpuValueFromFile(file, 1 /* $PERIOD index */); 187 } 188 189 private long getCpuValueFromFile(String file, int index) { 190 String maxPeriod = getStringVal(file); 191 if (maxPeriod == null) { 192 return UNLIMITED; 193 } 194 String[] tokens = maxPeriod.split("\\s+"); 195 String val = tokens[index]; 196 if (MAX.equals(val)) { 197 return UNLIMITED; 198 } 199 return convertStringToLong(val); 200 } 201 202 private long convertStringToLong(String val) { 203 return CgroupMetricsTester.convertStringToLong(val, UNLIMITED); 204 } 205 206 @Override 207 public void testMemorySubsystem() { 208 Metrics metrics = Metrics.systemMetrics(); 209 210 // User Memory 211 long oldVal = metrics.getMemoryFailCount(); 212 long newVal = getLongValueEntryFromFile("memory.events", "max"); 213 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 214 fail("memory.events[max]", oldVal, newVal); 215 } 216 217 oldVal = metrics.getMemoryLimit(); 218 newVal = getLongLimitValueFromFile("memory.max"); 219 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 220 fail("memory.max", oldVal, newVal); 221 } 222 223 oldVal = metrics.getMemoryUsage(); 224 newVal = getLongValueFromFile("memory.current"); 225 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 226 fail("memory.current", oldVal, newVal); 227 } 228 229 oldVal = metrics.getTcpMemoryUsage(); 230 newVal = getLongValueEntryFromFile("memory.stat", "sock"); 231 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 232 fail("memory.stat[sock]", oldVal, newVal); 233 } 234 235 oldVal = metrics.getMemoryAndSwapLimit(); 236 newVal = getLongLimitValueFromFile("memory.swap.max"); 237 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 238 fail("memory.swap.max", oldVal, newVal); 239 } 240 241 oldVal = metrics.getMemoryAndSwapUsage(); 242 newVal = getLongValueFromFile("memory.swap.current"); 243 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 244 fail("memory.swap.current", oldVal, newVal); 245 } 246 247 oldVal = metrics.getMemorySoftLimit(); 248 newVal = getLongLimitValueFromFile("memory.high"); 249 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 250 fail("memory.high", oldVal, newVal); 251 } 252 253 } 254 255 @Override 256 public void testCpuAccounting() { 257 Metrics metrics = Metrics.systemMetrics(); 258 long oldVal = metrics.getCpuUsage(); 259 long newVal = TimeUnit.MICROSECONDS.toNanos(getLongValueEntryFromFile("cpu.stat", "usage_usec")); 260 261 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 262 warn("cpu.stat[usage_usec]", oldVal, newVal); 263 } 264 265 oldVal = metrics.getCpuUserUsage(); 266 newVal = TimeUnit.MICROSECONDS.toNanos(getLongValueEntryFromFile("cpu.stat", "user_usec")); 267 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 268 warn("cpu.stat[user_usec]", oldVal, newVal); 269 } 270 271 oldVal = metrics.getCpuSystemUsage(); 272 newVal = TimeUnit.MICROSECONDS.toNanos(getLongValueEntryFromFile("cpu.stat", "system_usec")); 273 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 274 warn("cpu.stat[system_usec]", oldVal, newVal); 275 } 276 } 277 278 @Override 279 public void testCpuSchedulingMetrics() { 280 Metrics metrics = Metrics.systemMetrics(); 281 long oldVal = metrics.getCpuPeriod(); 282 long newVal = getCpuPeriodValueFromFile("cpu.max"); 283 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 284 fail("cpu.max[$PERIOD]", oldVal, newVal); 285 } 286 287 oldVal = metrics.getCpuQuota(); 288 newVal = getCpuMaxValueFromFile("cpu.max"); 289 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 290 fail("cpu.max[$MAX]", oldVal, newVal); 291 } 292 293 oldVal = metrics.getCpuShares(); 294 newVal = getCpuShares("cpu.weight"); 295 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 296 fail("cpu.weight", oldVal, newVal); 297 } 298 299 oldVal = metrics.getCpuNumPeriods(); 300 newVal = getLongValueEntryFromFile("cpu.stat", "nr_periods"); 301 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 302 fail("cpu.stat[nr_periods]", oldVal, newVal); 303 } 304 305 oldVal = metrics.getCpuNumThrottled(); 306 newVal = getLongValueEntryFromFile("cpu.stat", "nr_throttled"); 307 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 308 fail("cpu.stat[nr_throttled]", oldVal, newVal); 309 } 310 311 oldVal = metrics.getCpuThrottledTime(); 312 newVal = TimeUnit.MICROSECONDS.toNanos(getLongValueEntryFromFile("cpu.stat", "throttled_usec")); 313 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 314 fail("cpu.stat[throttled_usec]", oldVal, newVal); 315 } 316 } 317 318 @Override 319 public void testCpuSets() { 320 Metrics metrics = Metrics.systemMetrics(); 321 int[] cpus = mapNullToEmpty(metrics.getCpuSetCpus()); 322 Integer[] oldVal = Arrays.stream(cpus).boxed().toArray(Integer[]::new); 323 Arrays.sort(oldVal); 324 325 String cpusstr = getStringVal("cpuset.cpus"); 326 // Parse range string in the format 1,2-6,7 327 Integer[] newVal = CgroupMetricsTester.convertCpuSetsToArray(cpusstr); 328 Arrays.sort(newVal); 329 if (Arrays.compare(oldVal, newVal) != 0) { 330 fail("cpuset.cpus", Arrays.toString(oldVal), 331 Arrays.toString(newVal)); 332 } 333 334 cpus = mapNullToEmpty(metrics.getEffectiveCpuSetCpus()); 335 oldVal = Arrays.stream(cpus).boxed().toArray(Integer[]::new); 336 Arrays.sort(oldVal); 337 cpusstr = getStringVal("cpuset.cpus.effective"); 338 newVal = CgroupMetricsTester.convertCpuSetsToArray(cpusstr); 339 Arrays.sort(newVal); 340 if (Arrays.compare(oldVal, newVal) != 0) { 341 fail("cpuset.cpus.effective", Arrays.toString(oldVal), 342 Arrays.toString(newVal)); 343 } 344 345 cpus = mapNullToEmpty(metrics.getCpuSetMems()); 346 oldVal = Arrays.stream(cpus).boxed().toArray(Integer[]::new); 347 Arrays.sort(oldVal); 348 cpusstr = getStringVal("cpuset.mems"); 349 newVal = CgroupMetricsTester.convertCpuSetsToArray(cpusstr); 350 Arrays.sort(newVal); 351 if (Arrays.compare(oldVal, newVal) != 0) { 352 fail("cpuset.mems", Arrays.toString(oldVal), 353 Arrays.toString(newVal)); 354 } 355 356 cpus = mapNullToEmpty(metrics.getEffectiveCpuSetMems()); 357 oldVal = Arrays.stream(cpus).boxed().toArray(Integer[]::new); 358 Arrays.sort(oldVal); 359 cpusstr = getStringVal("cpuset.mems.effective"); 360 newVal = CgroupMetricsTester.convertCpuSetsToArray(cpusstr); 361 Arrays.sort(newVal); 362 if (Arrays.compare(oldVal, newVal) != 0) { 363 fail("cpuset.mems.effective", Arrays.toString(oldVal), 364 Arrays.toString(newVal)); 365 } 366 } 367 368 private int[] mapNullToEmpty(int[] cpus) { 369 if (cpus == null) { 370 // Not available. For sake of testing continue with an 371 // empty array. 372 cpus = new int[0]; 373 } 374 return cpus; 375 } 376 377 @Override 378 public void testCpuConsumption() { 379 Metrics metrics = Metrics.systemMetrics(); 380 // make system call 381 long newSysVal = metrics.getCpuSystemUsage(); 382 long newUserVal = metrics.getCpuUserUsage(); 383 long newUsage = metrics.getCpuUsage(); 384 385 // system/user CPU usage counters may be slowly increasing. 386 // allow for equal values for a pass 387 if (newSysVal < startSysVal) { 388 fail("getCpuSystemUsage", newSysVal, startSysVal); 389 } 390 391 // system/user CPU usage counters may be slowly increasing. 392 // allow for equal values for a pass 393 if (newUserVal < startUserVal) { 394 fail("getCpuUserUsage", newUserVal, startUserVal); 395 } 396 397 if (newUsage <= startUsage) { 398 fail("getCpuUsage", newUsage, startUsage); 399 } 400 } 401 402 @Override 403 public void testMemoryUsage() { 404 Metrics metrics = Metrics.systemMetrics(); 405 long memoryUsage = metrics.getMemoryUsage(); 406 long newMemoryUsage = 0; 407 408 // allocate memory in a loop and check more than once for new values 409 // otherwise we might occasionally see the effect of decreasing new memory 410 // values. For example because the system could free up memory 411 byte[][] bytes = new byte[32][]; 412 for (int i = 0; i < 32; i++) { 413 bytes[i] = new byte[8*1024*1024]; 414 newMemoryUsage = metrics.getMemoryUsage(); 415 if (newMemoryUsage > memoryUsage) { 416 break; 417 } 418 } 419 420 if (newMemoryUsage < memoryUsage) { 421 fail("getMemoryUsage", memoryUsage, newMemoryUsage); 422 } 423 } 424 425 @Override 426 public void testMisc() { 427 testIOStat(); 428 } 429 430 private void testIOStat() { 431 Metrics metrics = Metrics.systemMetrics(); 432 long oldVal = metrics.getBlkIOServiceCount(); 433 long newVal = getIoStatAccumulate(new String[] { "rios", "wios" }); 434 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 435 fail("io.stat->rios/wios: ", oldVal, newVal); 436 } 437 438 oldVal = metrics.getBlkIOServiced(); 439 newVal = getIoStatAccumulate(new String[] { "rbytes", "wbytes" }); 440 if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { 441 fail("io.stat->rbytes/wbytes: ", oldVal, newVal); 442 } 443 } 444 445 private long getIoStatAccumulate(String[] matchNames) { 446 try { 447 return Files.lines(Paths.get(UNIFIED.getPath(), "io.stat")) 448 .map(line -> { 449 long accumulator = 0; 450 String[] tokens = line.split("\\s+"); 451 for (String t: tokens) { 452 String[] keyVal = t.split("="); 453 if (keyVal.length != 2) { 454 continue; 455 } 456 for (String match: matchNames) { 457 if (match.equals(keyVal[0])) { 458 accumulator += Long.parseLong(keyVal[1]); 459 } 460 } 461 } 462 return accumulator; 463 }).collect(Collectors.summingLong(e -> e)); 464 } catch (IOException e) { 465 return CgroupSubsystem.LONG_RETVAL_UNLIMITED; 466 } 467 } 468 }