|
| 1 | +/* |
| 2 | + * @test |
| 3 | + * @summary Measure impact of skipping hasEffectiveAnnotation(NONNULL) fast-path on many `new` sites. |
| 4 | + * |
| 5 | + * @run NewClassPerf |
| 6 | + */ |
| 7 | + |
| 8 | +import java.io.BufferedWriter; |
| 9 | +import java.io.File; |
| 10 | +import java.io.FileWriter; |
| 11 | +import java.io.IOException; |
| 12 | +import java.nio.charset.StandardCharsets; |
| 13 | +import java.nio.file.Files; |
| 14 | +import java.nio.file.Path; |
| 15 | +import java.nio.file.Paths; |
| 16 | +import java.util.ArrayList; |
| 17 | +import java.util.Arrays; |
| 18 | +import java.util.Collections; |
| 19 | +import java.util.List; |
| 20 | + |
| 21 | +public class NewClassPerf { |
| 22 | + |
| 23 | + private static final int RUNS = Integer.getInteger("perf.runs", 10); |
| 24 | + private static final int GROUPS = Integer.getInteger("perf.groups", 400); // ~2k new-exprs total (5 per group) |
| 25 | + // Protocol: AB | BA | BOTH | SEPARATE. Default BOTH runs interleaved AB and BA to cancel order bias. |
| 26 | + private static final String PROTOCOL = System.getProperty("perf.protocol", "BOTH"); |
| 27 | + private static final int WARMUP = Integer.getInteger("perf.warmupPerVariant", 1); |
| 28 | + |
| 29 | + public static void main(String[] args) throws Exception { |
| 30 | + Path workDir = Paths.get(".").toAbsolutePath().normalize(); |
| 31 | + Path src = workDir.resolve("ManyNew.java"); |
| 32 | + writeManyNewSource(src, GROUPS); |
| 33 | + |
| 34 | + switch (PROTOCOL.toUpperCase()) { |
| 35 | + case "AB": { |
| 36 | + Result[] ab = timeInterleaved(src, "AB"); |
| 37 | + printResults("Interleaved AB", ab[0], ab[1]); |
| 38 | + break; |
| 39 | + } |
| 40 | + case "BA": { |
| 41 | + Result[] ba = timeInterleaved(src, "BA"); |
| 42 | + printResults("Interleaved BA", ba[0], ba[1]); |
| 43 | + break; |
| 44 | + } |
| 45 | + case "SEPARATE": { |
| 46 | + Result[] sep = timeSeparate(src, WARMUP); |
| 47 | + printResults("Separate (warmup=" + WARMUP + ")", sep[0], sep[1]); |
| 48 | + break; |
| 49 | + } |
| 50 | + case "BOTH": |
| 51 | + default: { |
| 52 | + Result[] ab = timeInterleaved(src, "AB"); |
| 53 | + Result[] ba = timeInterleaved(src, "BA"); |
| 54 | + System.out.println("==== Interleaved AB ===="); |
| 55 | + printResults("AB", ab[0], ab[1]); |
| 56 | + System.out.println(); |
| 57 | + System.out.println("==== Interleaved BA ===="); |
| 58 | + printResults("BA", ba[0], ba[1]); |
| 59 | + // Consistency check |
| 60 | + double sign1 = Math.signum((ab[1].median() - ab[0].median())); |
| 61 | + double sign2 = Math.signum((ba[1].median() - ba[0].median())); |
| 62 | + System.out.println(); |
| 63 | + if (sign1 == 0 || sign2 == 0 || Math.signum(sign1) != Math.signum(sign2)) { |
| 64 | + System.out.println("Direction: ORDER-SENSITIVE (results differ between AB and BA)"); |
| 65 | + } else { |
| 66 | + System.out.println("Direction: CONSISTENT across AB and BA"); |
| 67 | + } |
| 68 | + break; |
| 69 | + } |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + private static void writeManyNewSource(Path file, int groups) throws IOException { |
| 74 | + StringBuilder sb = new StringBuilder(1024 * 1024); |
| 75 | + sb.append("import java.util.*;\n"); |
| 76 | + sb.append("public class ManyNew {\n"); |
| 77 | + // Avoid initialization checker errors; use @Nullable type variable upper bound. |
| 78 | + sb.append(" static class Box<T extends Object> { T t; Box(){ this.t = (T) (Object) new Object(); } }\n"); |
| 79 | + sb.append(" void f() {\n"); |
| 80 | + for (int i = 0; i < groups; i++) { |
| 81 | + sb.append(" Object o").append(i).append(" = new Object();\n"); |
| 82 | + sb.append(" ArrayList<String> l").append(i).append(" = new ArrayList<>();\n"); |
| 83 | + sb.append(" Box<String> b").append(i).append(" = new Box<>();\n"); |
| 84 | + sb.append(" int[] ai").append(i).append(" = new int[10];\n"); |
| 85 | + sb.append(" String[] as").append(i).append(" = new String[10];\n"); |
| 86 | + sb.append(" Runnable r").append(i) |
| 87 | + .append(" = new Runnable(){ public void run(){} };\n"); |
| 88 | + } |
| 89 | + sb.append(" }\n"); |
| 90 | + sb.append("}\n"); |
| 91 | + try (BufferedWriter w = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { |
| 92 | + w.write(sb.toString()); |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + private static Result timeVariant(Path src, boolean skipFastPath) throws Exception { |
| 97 | + List<Long> timings = new ArrayList<>(); |
| 98 | + for (int i = 0; i < RUNS; i++) { |
| 99 | + timings.add(runOnceMs(src, skipFastPath)); |
| 100 | + } |
| 101 | + return new Result(timings); |
| 102 | + } |
| 103 | + |
| 104 | + private static Result[] timeInterleaved(Path src, String order) throws Exception { |
| 105 | + List<Long> aTimes = new ArrayList<>(); |
| 106 | + List<Long> bTimes = new ArrayList<>(); |
| 107 | + boolean firstA = !"BA".equalsIgnoreCase(order); |
| 108 | + for (int i = 0; i < RUNS; i++) { |
| 109 | + if (firstA) { |
| 110 | + aTimes.add(runOnceMs(src, /*skipFastPath=*/false)); |
| 111 | + bTimes.add(runOnceMs(src, /*skipFastPath=*/true)); |
| 112 | + } else { |
| 113 | + bTimes.add(runOnceMs(src, /*skipFastPath=*/true)); |
| 114 | + aTimes.add(runOnceMs(src, /*skipFastPath=*/false)); |
| 115 | + } |
| 116 | + } |
| 117 | + return new Result[] { new Result(aTimes), new Result(bTimes) }; |
| 118 | + } |
| 119 | + |
| 120 | + private static long runOnceMs(Path src, boolean skipFastPath) throws Exception { |
| 121 | + long start = System.nanoTime(); |
| 122 | + int exit = runJavac(src, skipFastPath); |
| 123 | + long end = System.nanoTime(); |
| 124 | + if (exit != 0) { |
| 125 | + throw new RuntimeException("javac failed with exit code " + exit); |
| 126 | + } |
| 127 | + return (end - start) / 1_000_000L; |
| 128 | + } |
| 129 | + |
| 130 | + private static Result[] timeSeparate(Path src, int warmup) throws Exception { |
| 131 | + // Variant A (fast-path enabled) |
| 132 | + for (int i = 0; i < warmup; i++) { |
| 133 | + runOnceMs(src, /*skipFastPath=*/false); |
| 134 | + } |
| 135 | + List<Long> aTimes = new ArrayList<>(); |
| 136 | + for (int i = 0; i < RUNS; i++) { |
| 137 | + aTimes.add(runOnceMs(src, /*skipFastPath=*/false)); |
| 138 | + } |
| 139 | + |
| 140 | + // Variant B (fast-path disabled) |
| 141 | + for (int i = 0; i < warmup; i++) { |
| 142 | + runOnceMs(src, /*skipFastPath=*/true); |
| 143 | + } |
| 144 | + List<Long> bTimes = new ArrayList<>(); |
| 145 | + for (int i = 0; i < RUNS; i++) { |
| 146 | + bTimes.add(runOnceMs(src, /*skipFastPath=*/true)); |
| 147 | + } |
| 148 | + return new Result[] { new Result(aTimes), new Result(bTimes) }; |
| 149 | + } |
| 150 | + |
| 151 | + private static void printResults(String label, Result a, Result b) { |
| 152 | + // Print raw timings |
| 153 | + System.out.println("Variant A (fast-path enabled) timings ms: " + a.timingsMs); |
| 154 | + System.out.println("Variant B (fast-path disabled) timings ms: " + b.timingsMs); |
| 155 | + |
| 156 | + // Summary table and deltas |
| 157 | + System.out.println(); |
| 158 | + System.out.println("Results (ms) - " + label + ":"); |
| 159 | + System.out.println("Variant | median | average"); |
| 160 | + System.out.println(String.format("A | %7.2f | %7.2f", a.median(), a.average())); |
| 161 | + System.out.println(String.format("B | %7.2f | %7.2f", b.median(), b.average())); |
| 162 | + double medA = a.median(), medB = b.median(); |
| 163 | + double avgA = a.average(), avgB = b.average(); |
| 164 | + System.out.println(); |
| 165 | + System.out.printf("Median delta (B vs A): %.3f%%%n", (medB - medA) / medA * 100.0); |
| 166 | + System.out.printf("Average delta (B vs A): %.3f%%%n", (avgB - avgA) / avgA * 100.0); |
| 167 | + } |
| 168 | + |
| 169 | + private static int runJavac(Path src, boolean skipFastPath) throws Exception { |
| 170 | + String checkerJar = locateCheckerJar(); |
| 171 | + |
| 172 | + List<String> cmd = new ArrayList<>(); |
| 173 | + cmd.add(findJavacExecutable()); |
| 174 | + // Add required --add-opens for running Checker Framework on JDK 9+ |
| 175 | + String[] javacPkgs = new String[] { |
| 176 | + "com.sun.tools.javac.api", |
| 177 | + "com.sun.tools.javac.code", |
| 178 | + "com.sun.tools.javac.comp", |
| 179 | + "com.sun.tools.javac.file", |
| 180 | + "com.sun.tools.javac.main", |
| 181 | + "com.sun.tools.javac.parser", |
| 182 | + "com.sun.tools.javac.processing", |
| 183 | + "com.sun.tools.javac.tree", |
| 184 | + "com.sun.tools.javac.util" |
| 185 | + }; |
| 186 | + for (String p : javacPkgs) { |
| 187 | + cmd.add("-J--add-opens=jdk.compiler/" + p + "=ALL-UNNAMED"); |
| 188 | + } |
| 189 | + if (skipFastPath) { |
| 190 | + cmd.add("-J-Dcf.skipNonnullFastPath=true"); |
| 191 | + } |
| 192 | + cmd.addAll(Arrays.asList( |
| 193 | + "-classpath", checkerJar, |
| 194 | + "-processor", "org.checkerframework.checker.nullness.NullnessChecker", |
| 195 | + "-proc:only", |
| 196 | + "-source", "8", |
| 197 | + "-target", "8", |
| 198 | + "-Xlint:-options", |
| 199 | + src.getFileName().toString())); |
| 200 | + |
| 201 | + ProcessBuilder pb = new ProcessBuilder(cmd); |
| 202 | + pb.directory(src.getParent().toFile()); |
| 203 | + pb.redirectErrorStream(true); |
| 204 | + Process p = pb.start(); |
| 205 | + // Consume output to avoid buffer blockage. |
| 206 | + byte[] out = p.getInputStream().readAllBytes(); |
| 207 | + int code = p.waitFor(); |
| 208 | + // In case of failure, print compiler output for debugging. |
| 209 | + if (code != 0) { |
| 210 | + System.out.write(out); |
| 211 | + } |
| 212 | + return code; |
| 213 | + } |
| 214 | + |
| 215 | + private static String locateCheckerJar() { |
| 216 | + try { |
| 217 | + String testRoot = System.getProperty("test.root"); |
| 218 | + if (testRoot != null) { |
| 219 | + Path p = Paths.get(testRoot).toAbsolutePath().normalize().getParent().resolve("dist/checker.jar"); |
| 220 | + if (Files.exists(p)) { |
| 221 | + return p.toString(); |
| 222 | + } |
| 223 | + } |
| 224 | + Path p1 = Paths.get("checker/dist/checker.jar").toAbsolutePath().normalize(); |
| 225 | + if (Files.exists(p1)) { |
| 226 | + return p1.toString(); |
| 227 | + } |
| 228 | + Path p2 = Paths.get("../../../dist/checker.jar").toAbsolutePath().normalize(); |
| 229 | + if (Files.exists(p2)) { |
| 230 | + return p2.toString(); |
| 231 | + } |
| 232 | + // Fallback: walk up from the current working dir (jtreg scratch) |
| 233 | + Path cwd = Paths.get(".").toAbsolutePath().normalize(); |
| 234 | + for (int i = 0; i < 8; i++) { |
| 235 | + Path cand = cwd; |
| 236 | + for (int j = 0; j < i; j++) cand = cand.getParent(); |
| 237 | + if (cand == null) break; |
| 238 | + Path jar = cand.resolve("checker/dist/checker.jar"); |
| 239 | + if (Files.exists(jar)) return jar.toString(); |
| 240 | + } |
| 241 | + } catch (Throwable ignore) { |
| 242 | + } |
| 243 | + return "checker/dist/checker.jar"; |
| 244 | + } |
| 245 | + |
| 246 | + private static String findJavacExecutable() { |
| 247 | + String javaHome = System.getProperty("java.home"); |
| 248 | + // Typical JDK layout: $JAVA_HOME/bin/javac |
| 249 | + File jh = new File(javaHome); |
| 250 | + File bin = new File(jh.getParentFile(), "bin"); |
| 251 | + File javac = new File(bin, isWindows() ? "javac.exe" : "javac"); |
| 252 | + if (javac.exists()) { |
| 253 | + return javac.getAbsolutePath(); |
| 254 | + } |
| 255 | + // Fallback to PATH |
| 256 | + return isWindows() ? "javac.exe" : "javac"; |
| 257 | + } |
| 258 | + |
| 259 | + private static boolean isWindows() { |
| 260 | + String os = System.getProperty("os.name", "").toLowerCase(); |
| 261 | + return os.contains("win"); |
| 262 | + } |
| 263 | + |
| 264 | + private static final class Result { |
| 265 | + final List<Long> timingsMs; |
| 266 | + Result(List<Long> t) { this.timingsMs = Collections.unmodifiableList(new ArrayList<>(t)); } |
| 267 | + double average() { return timingsMs.stream().mapToLong(Long::longValue).average().orElse(0); } |
| 268 | + double median() { |
| 269 | + if (timingsMs.isEmpty()) return 0; |
| 270 | + List<Long> copy = new ArrayList<>(timingsMs); |
| 271 | + Collections.sort(copy); |
| 272 | + int n = copy.size(); |
| 273 | + if ((n & 1) == 1) { |
| 274 | + return copy.get(n/2); |
| 275 | + } else { |
| 276 | + return (copy.get(n/2 - 1) + copy.get(n/2)) / 2.0; |
| 277 | + } |
| 278 | + } |
| 279 | + } |
| 280 | +} |
| 281 | + |
| 282 | + |
0 commit comments