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