1 /* 2 * Copyright (c) 2015, 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. 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 com.sun.tools.javac.api; 27 28 import java.io.PrintStream; 29 import java.io.Writer; 30 import java.util.ArrayDeque; 31 import java.util.ArrayList; 32 import java.util.Collection; 33 import java.util.HashMap; 34 import java.util.HashSet; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Queue; 38 import java.util.Set; 39 import java.util.stream.Collectors; 40 import java.util.stream.StreamSupport; 41 42 import javax.tools.Diagnostic; 43 import javax.tools.DiagnosticListener; 44 import javax.tools.JavaFileManager; 45 import javax.tools.JavaFileObject; 46 47 import com.sun.source.tree.ClassTree; 48 import com.sun.source.tree.CompilationUnitTree; 49 import com.sun.source.tree.Tree; 50 import com.sun.source.util.JavacTask; 51 import com.sun.source.util.TaskEvent; 52 import com.sun.source.util.TaskEvent.Kind; 53 import com.sun.source.util.TaskListener; 54 import com.sun.source.util.TreeScanner; 55 import com.sun.tools.javac.code.Kinds; 56 import com.sun.tools.javac.code.Preview; 57 import com.sun.tools.javac.code.Symbol; 58 import com.sun.tools.javac.code.Symtab; 59 import com.sun.tools.javac.code.Type; 60 import com.sun.tools.javac.code.Type.ClassType; 61 import com.sun.tools.javac.code.TypeTag; 62 import com.sun.tools.javac.code.Types; 63 import com.sun.tools.javac.comp.Annotate; 64 import com.sun.tools.javac.comp.Check; 65 import com.sun.tools.javac.comp.CompileStates; 66 import com.sun.tools.javac.comp.Enter; 67 import com.sun.tools.javac.comp.Modules; 68 import com.sun.tools.javac.main.Arguments; 69 import com.sun.tools.javac.main.JavaCompiler; 70 import com.sun.tools.javac.model.JavacElements; 71 import com.sun.tools.javac.platform.PlatformDescription; 72 import com.sun.tools.javac.tree.JCTree.JCClassDecl; 73 import com.sun.tools.javac.tree.JCTree.LetExpr; 74 import com.sun.tools.javac.util.Context; 75 import com.sun.tools.javac.util.DefinedBy; 76 import com.sun.tools.javac.util.DefinedBy.Api; 77 import com.sun.tools.javac.util.Log; 78 import com.sun.tools.javac.util.Options; 79 80 /** 81 * A pool of reusable JavacTasks. When a task is no valid anymore, it is returned to the pool, 82 * and its Context may be reused for future processing in some cases. The reuse is achieved 83 * by replacing some components (most notably JavaCompiler and Log) with reusable counterparts, 84 * and by cleaning up leftovers from previous compilation. 85 * <p> 86 * For each combination of options, a separate task/context is created and kept, as most option 87 * values are cached inside components themselves. 88 * <p> 89 * When the compilation redefines sensitive classes (e.g. classes in the the java.* packages), the 90 * task/context is not reused. 91 * <p> 92 * When the task is reused, then packages that were already listed won't be listed again. 93 * <p> 94 * Care must be taken to only return tasks that won't be used by the original caller. 95 * <p> 96 * Care must also be taken when custom components are installed, as those are not cleaned when the 97 * task/context is reused, and subsequent getTask may return a task based on a context with these 98 * custom components. 99 * 100 * <p><b>This is NOT part of any supported API. 101 * If you write code that depends on this, you do so at your own risk. 102 * This code and its internal interfaces are subject to change or 103 * deletion without notice.</b> 104 */ 105 public class JavacTaskPool { 106 107 private static final JavacTool systemProvider = JavacTool.create(); 108 private static final Queue<ReusableContext> EMPTY_QUEUE = new ArrayDeque<>(0); 109 110 private final int maxPoolSize; 111 private final Map<List<String>, Queue<ReusableContext>> options2Contexts = new HashMap<>(); 112 private int id; 113 114 private int statReused = 0; 115 private int statNew = 0; 116 private int statPolluted = 0; 117 private int statRemoved = 0; 118 119 /**Creates the pool. 120 * 121 * @param maxPoolSize maximum number of tasks/context that will be kept in the pool. 122 */ 123 public JavacTaskPool(int maxPoolSize) { 124 this.maxPoolSize = maxPoolSize; 125 } 126 127 /**Creates a new task as if by {@link javax.tools.JavaCompiler#getTask} and runs the provided 128 * worker with it. The task is only valid while the worker is running. The internal structures 129 * may be reused from some previous compilation. 130 * 131 * @param out a Writer for additional output from the compiler; 132 * use {@code System.err} if {@code null} 133 * @param fileManager a file manager; if {@code null} use the 134 * compiler's standard file manager 135 * @param diagnosticListener a diagnostic listener; if {@code 136 * null} use the compiler's default method for reporting 137 * diagnostics 138 * @param options compiler options, {@code null} means no options 139 * @param classes names of classes to be processed by annotation 140 * processing, {@code null} means no class names 141 * @param compilationUnits the compilation units to compile, {@code 142 * null} means no compilation units 143 * @param worker that should be run with the task 144 * @return an object representing the compilation 145 * @throws RuntimeException if an unrecoverable error 146 * occurred in a user supplied component. The 147 * {@linkplain Throwable#getCause() cause} will be the error in 148 * user code. 149 * @throws IllegalArgumentException if any of the options are invalid, 150 * or if any of the given compilation units are of other kind than 151 * {@linkplain JavaFileObject.Kind#SOURCE source} 152 */ 153 public <Z> Z getTask(Writer out, 154 JavaFileManager fileManager, 155 DiagnosticListener<? super JavaFileObject> diagnosticListener, 156 Iterable<String> options, 157 Iterable<String> classes, 158 Iterable<? extends JavaFileObject> compilationUnits, 159 Worker<Z> worker) { 160 List<String> opts = 161 StreamSupport.stream(options.spliterator(), false) 162 .collect(Collectors.toCollection(ArrayList::new)); 163 164 ReusableContext ctx; 165 166 synchronized (this) { 167 Queue<ReusableContext> cached = 168 options2Contexts.getOrDefault(opts, EMPTY_QUEUE); 169 170 if (cached.isEmpty()) { 171 ctx = new ReusableContext(opts); 172 statNew++; 173 } else { 174 ctx = cached.remove(); 175 statReused++; 176 } 177 } 178 179 ctx.useCount++; 180 181 JavacTaskImpl task = 182 (JavacTaskImpl) systemProvider.getTask(out, fileManager, diagnosticListener, 183 opts, classes, compilationUnits, ctx); 184 185 task.addTaskListener(ctx); 186 187 Z result = worker.withTask(task); 188 189 //not returning the context to the pool if task crashes with an exception 190 //the task/context may be in a broken state 191 ctx.clear(); 192 if (ctx.polluted) { 193 statPolluted++; 194 } else { 195 task.cleanup(); 196 synchronized (this) { 197 while (cacheSize() + 1 > maxPoolSize) { 198 ReusableContext toRemove = 199 options2Contexts.values() 200 .stream() 201 .flatMap(Collection::stream) 202 .sorted((c1, c2) -> c1.timeStamp < c2.timeStamp ? -1 : 1) 203 .findFirst() 204 .get(); 205 options2Contexts.get(toRemove.arguments).remove(toRemove); 206 statRemoved++; 207 } 208 options2Contexts.computeIfAbsent(ctx.arguments, x -> new ArrayDeque<>()).add(ctx); 209 ctx.timeStamp = id++; 210 } 211 } 212 213 return result; 214 } 215 //where: 216 private long cacheSize() { 217 return options2Contexts.values().stream().flatMap(Collection::stream).count(); 218 } 219 220 public void printStatistics(PrintStream out) { 221 out.println(statReused + " reused Contexts"); 222 out.println(statNew + " newly created Contexts"); 223 out.println(statPolluted + " polluted Contexts"); 224 out.println(statRemoved + " removed Contexts"); 225 } 226 227 public interface Worker<Z> { 228 public Z withTask(JavacTask task); 229 } 230 231 static class ReusableContext extends Context implements TaskListener { 232 233 Set<CompilationUnitTree> roots = new HashSet<>(); 234 235 List<String> arguments; 236 boolean polluted = false; 237 238 int useCount; 239 long timeStamp; 240 241 ReusableContext(List<String> arguments) { 242 super(); 243 this.arguments = arguments; 244 put(Log.logKey, ReusableLog.factory); 245 put(JavaCompiler.compilerKey, ReusableJavaCompiler.factory); 246 } 247 248 void clear() { 249 drop(Arguments.argsKey); 250 drop(DiagnosticListener.class); 251 drop(Log.outKey); 252 drop(Log.errKey); 253 drop(JavaFileManager.class); 254 drop(JavacTask.class); 255 drop(JavacTrees.class); 256 drop(JavacElements.class); 257 drop(PlatformDescription.class); 258 259 if (ht.get(Log.logKey) instanceof ReusableLog) { 260 //log already inited - not first round 261 ((ReusableLog)Log.instance(this)).clear(); 262 Enter.instance(this).newRound(); 263 ((ReusableJavaCompiler)ReusableJavaCompiler.instance(this)).clear(); 264 Types.instance(this).newRound(); 265 Check.instance(this).newRound(); 266 Check.instance(this).clear(); //clear mandatory warning handlers 267 Preview.instance(this).clear(); //clear mandatory warning handlers 268 Modules.instance(this).newRound(); 269 Annotate.instance(this).newRound(); 270 CompileStates.instance(this).clear(); 271 MultiTaskListener.instance(this).clear(); 272 Options.instance(this).clear(); 273 274 //find if any of the roots have redefined java.* classes 275 Symtab syms = Symtab.instance(this); 276 pollutionScanner.scan(roots, syms); 277 roots.clear(); 278 } 279 } 280 281 /** 282 * This scanner detects as to whether the shared context has been polluted. This happens 283 * whenever a compiled program redefines a core class (in 'java.*' package) or when 284 * (typically because of cyclic inheritance) the symbol kind of a core class has been touched. 285 */ 286 TreeScanner<Void, Symtab> pollutionScanner = new TreeScanner<Void, Symtab>() { 287 @Override @DefinedBy(Api.COMPILER_TREE) 288 public Void scan(Tree tree, Symtab syms) { 289 if (tree instanceof LetExpr) { 290 LetExpr le = (LetExpr) tree; 291 scan(le.defs, syms); 292 scan(le.expr, syms); 293 return null; 294 } else { 295 return super.scan(tree, syms); 296 } 297 } 298 299 @Override @DefinedBy(Api.COMPILER_TREE) 300 public Void visitClass(ClassTree node, Symtab syms) { 301 Symbol sym = ((JCClassDecl)node).sym; 302 if (sym != null) { 303 syms.removeClass(sym.packge().modle, sym.flatName()); 304 Type sup = supertype(sym); 305 if (isCoreClass(sym) || 306 (sup != null && isCoreClass(sup.tsym) && sup.tsym.kind != Kinds.Kind.TYP)) { 307 polluted = true; 308 } 309 } 310 return super.visitClass(node, syms); 311 } 312 313 private boolean isCoreClass(Symbol s) { 314 return s.flatName().toString().startsWith("java."); 315 } 316 317 private Type supertype(Symbol s) { 318 if (s.type == null || 319 !s.type.hasTag(TypeTag.CLASS)) { 320 return null; 321 } else { 322 ClassType ct = (ClassType)s.type; 323 return ct.supertype_field; 324 } 325 } 326 }; 327 328 @Override @DefinedBy(Api.COMPILER_TREE) 329 public void finished(TaskEvent e) { 330 if (e.getKind() == Kind.PARSE) { 331 roots.add(e.getCompilationUnit()); 332 } 333 } 334 335 @Override @DefinedBy(Api.COMPILER_TREE) 336 public void started(TaskEvent e) { 337 //do nothing 338 } 339 340 <T> void drop(Key<T> k) { 341 ht.remove(k); 342 } 343 344 <T> void drop(Class<T> c) { 345 ht.remove(key(c)); 346 } 347 348 /** 349 * Reusable JavaCompiler; exposes a method to clean up the component from leftovers associated with 350 * previous compilations. 351 */ 352 static class ReusableJavaCompiler extends JavaCompiler { 353 354 final static Factory<JavaCompiler> factory = ReusableJavaCompiler::new; 355 356 ReusableJavaCompiler(Context context) { 357 super(context); 358 } 359 360 @Override 361 public void close() { 362 //do nothing 363 } 364 365 void clear() { 366 newRound(); 367 } 368 369 @Override 370 protected void checkReusable() { 371 //do nothing - it's ok to reuse the compiler 372 } 373 } 374 375 /** 376 * Reusable Log; exposes a method to clean up the component from leftovers associated with 377 * previous compilations. 378 */ 379 static class ReusableLog extends Log { 380 381 final static Factory<Log> factory = ReusableLog::new; 382 383 Context context; 384 385 ReusableLog(Context context) { 386 super(context); 387 this.context = context; 388 } 389 390 void clear() { 391 recorded.clear(); 392 sourceMap.clear(); 393 nerrors = 0; 394 nwarnings = 0; 395 //Set a fake listener that will lazily lookup the context for the 'real' listener. Since 396 //this field is never updated when a new task is created, we cannot simply reset the field 397 //or keep old value. This is a hack to workaround the limitations in the current infrastructure. 398 diagListener = new DiagnosticListener<JavaFileObject>() { 399 DiagnosticListener<JavaFileObject> cachedListener; 400 401 @Override @DefinedBy(Api.COMPILER) 402 @SuppressWarnings("unchecked") 403 public void report(Diagnostic<? extends JavaFileObject> diagnostic) { 404 if (cachedListener == null) { 405 cachedListener = context.get(DiagnosticListener.class); 406 } 407 cachedListener.report(diagnostic); 408 } 409 }; 410 } 411 } 412 } 413 }