Reuses caches on SDM relaunch, makes first compile ~4x faster.

Previously MinimalRebuildCache instances were not persisted or loaded
from disk at all and a new PersistentUnitCache was built from scratch
for each SDM launch.

Now SDM takes care to choose a location for its PersistentUnitCache in
a way that will be consistent when SDM is relaunched in the same project
and targetting the same module.

Going further MinimalRebuildCache instances are now read from disk at
the beginning and persisted to disk after every compile. Similarly these
cache instances are saved to a folder whose name is consistent for the
same project and module and are saved to a file named consistently using
a combination of project, module and compiler version.

Because these caches are tied to the compiler version and because the
managed objects are tied to .java class modification dates, they should
always be correct. But in the event that something does become corrupt
there is now a SDM button to clear disk caches.

Change-Id: Ib995c18fe6842dc8914457ceff43e17808bfa550
Review-Link: https://gwt-review.googlesource.com/#/c/9450/
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java
index 945296f..3099d2b 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java
@@ -19,6 +19,10 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.MinimalRebuildCacheManager;
+import com.google.gwt.dev.javac.UnitCache;
+import com.google.gwt.dev.javac.UnitCacheSingleton;
+import com.google.gwt.dev.util.DiskCachingUtil;
 import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 import com.google.gwt.util.tools.Utility;
 
@@ -61,7 +65,12 @@
       OutboxTable outboxes;
 
       try {
-        outboxes = makeOutboxes(options, logger);
+        File baseCacheDir =
+            DiskCachingUtil.computePreferredCacheDir(options.getModuleNames(), logger);
+        UnitCache unitCache = UnitCacheSingleton.get(logger, null, baseCacheDir);
+        MinimalRebuildCacheManager minimalRebuildCacheManager =
+            new MinimalRebuildCacheManager(logger, baseCacheDir);
+        outboxes = makeOutboxes(options, logger, unitCache, minimalRebuildCacheManager);
       } catch (Throwable t) {
         t.printStackTrace();
         System.out.println("FAIL");
@@ -115,10 +124,16 @@
     topLogger.setMaxDetail(options.getLogLevel());
 
     TreeLogger startupLogger = topLogger.branch(Type.INFO, "Super Dev Mode starting up");
-    OutboxTable outboxes = makeOutboxes(options, startupLogger);
+    File baseCacheDir =
+        DiskCachingUtil.computePreferredCacheDir(options.getModuleNames(), startupLogger);
+    UnitCache unitCache = UnitCacheSingleton.get(startupLogger, null, baseCacheDir);
+    MinimalRebuildCacheManager minimalRebuildCacheManager =
+        new MinimalRebuildCacheManager(topLogger, baseCacheDir);
+    OutboxTable outboxes =
+        makeOutboxes(options, startupLogger, unitCache, minimalRebuildCacheManager);
 
     JobEventTable eventTable = new JobEventTable();
-    JobRunner runner = new JobRunner(eventTable);
+    JobRunner runner = new JobRunner(eventTable, minimalRebuildCacheManager);
 
     JsonExporter exporter = new JsonExporter(options, outboxes);
 
@@ -133,7 +148,8 @@
   /**
    * Configures and compiles all the modules (unless {@link Options#getNoPrecompile} is false).
    */
-  private static OutboxTable makeOutboxes(Options options, TreeLogger logger)
+  private static OutboxTable makeOutboxes(Options options, TreeLogger logger,
+      UnitCache unitCache, MinimalRebuildCacheManager minimalRebuildCacheManager)
       throws IOException, UnableToCompleteException {
 
     File workDir = ensureWorkDir(options);
@@ -146,7 +162,8 @@
     for (String moduleName : options.getModuleNames()) {
       OutboxDir outboxDir = OutboxDir.create(new File(workDir, moduleName), logger);
 
-      Recompiler recompiler = new Recompiler(outboxDir, launcherDir, moduleName, options);
+      Recompiler recompiler = new Recompiler(outboxDir, launcherDir, moduleName,
+          options, unitCache, minimalRebuildCacheManager);
 
       // The id should be treated as an opaque string since we will change it again.
       // TODO: change outbox id to include binding properties.
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/JobRunner.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/JobRunner.java
index d07f360..f7b2e45 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/JobRunner.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/JobRunner.java
@@ -17,6 +17,9 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.MinimalRebuildCacheManager;
+import com.google.gwt.dev.javac.UnitCacheSingleton;
 
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -32,15 +35,30 @@
  * <p>JobRunners are thread-safe.
  */
 public class JobRunner {
+
   private final JobEventTable table;
+  private final MinimalRebuildCacheManager minimalRebuildCacheManager;
   private final ExecutorService executor = Executors.newSingleThreadExecutor();
 
-  JobRunner(JobEventTable table) {
+  JobRunner(JobEventTable table, MinimalRebuildCacheManager minimalRebuildCacheManager) {
     this.table = table;
+    this.minimalRebuildCacheManager = minimalRebuildCacheManager;
   }
 
   /**
-   * Return fresh Js that knows how to request the specific permutation recompile for the given box.
+   * Submits a cleaner job to be executed. (Waits for completion.)
+   */
+  void clean(TreeLogger logger) throws ExecutionException {
+    try {
+      TreeLogger branch = logger.branch(TreeLogger.INFO, "Cleaning disk caches.");
+      executor.submit(new CleanerJob(branch)).get();
+    } catch (InterruptedException e) {
+      // Allow the JVM to shutdown.
+    }
+  }
+
+  /**
+   * Submits a recompile js creation job to be executed. (Waits for completion and returns JS.).
    */
   public String getRecompileJs(final TreeLogger logger, final Outbox box)
       throws ExecutionException {
@@ -94,4 +112,29 @@
     job.getLogger().log(Type.INFO, "starting job: " + job.getId());
     job.getOutbox().recompile(job);
   }
+
+  /**
+   * A callable for clearing both unit and minimalRebuild caches.
+   * <p>
+   * By packaging it as a callable and running it in the ExecutorService any danger of clearing
+   * caches at the same time as an active compile job is avoided.
+   */
+  private class CleanerJob implements Callable<Void> {
+
+    private TreeLogger logger;
+
+    public CleanerJob(TreeLogger logger) {
+      this.logger = logger;
+    }
+
+    @Override
+    public Void call() throws UnableToCompleteException {
+      long beforeMs = System.nanoTime() / 1000000L;
+      minimalRebuildCacheManager.deleteCaches();
+      UnitCacheSingleton.clearCache();
+      long afterMs = System.nanoTime() / 1000000L;
+      logger.log(TreeLogger.INFO, String.format("Cleaned in %sms.", (afterMs - beforeMs)));
+      return null;
+    }
+  }
 }
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/JsonExporter.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/JsonExporter.java
index 7a376da..9e4152d 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/JsonExporter.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/JsonExporter.java
@@ -88,6 +88,26 @@
   }
 
   /**
+   * Exports the template variables for success.
+   */
+  JsonObject exportOk(String message) {
+    JsonObject out = JsonObject.create();
+    out.put("status", "ok");
+    out.put("message", message);
+    return out;
+  }
+
+  /**
+   * Exports the template variables for failure.
+   */
+  JsonObject exportError(String message) {
+    JsonObject out = JsonObject.create();
+    out.put("status", "error");
+    out.put("message", message);
+    return out;
+  }
+
+  /**
    * Returns a JSON representation of the directories containing at least one source file
    * in the source map.
    * (These directories are relative to a classpath entry or -sourceDir argument.)
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxDir.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxDir.java
index ee15477..e521fb6 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxDir.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxDir.java
@@ -48,10 +48,6 @@
     return new File(root, "speedtracer.html");
   }
 
-  File getUnitCacheDir() {
-    return new File(root, "gwt-unitcache");
-  }
-
   /**
    * Creates a fresh, empty compile directory.
    */
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
index 180afba..21fceb4 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
@@ -26,6 +26,7 @@
 import com.google.gwt.dev.CompilerContext;
 import com.google.gwt.dev.CompilerOptions;
 import com.google.gwt.dev.MinimalRebuildCache;
+import com.google.gwt.dev.MinimalRebuildCacheManager;
 import com.google.gwt.dev.NullRebuildCache;
 import com.google.gwt.dev.cfg.BindingProperty;
 import com.google.gwt.dev.cfg.ConfigProps;
@@ -36,7 +37,7 @@
 import com.google.gwt.dev.cfg.ResourceLoaders;
 import com.google.gwt.dev.codeserver.Job.Result;
 import com.google.gwt.dev.codeserver.JobEvent.CompileStrategy;
-import com.google.gwt.dev.javac.UnitCacheSingleton;
+import com.google.gwt.dev.javac.UnitCache;
 import com.google.gwt.dev.resource.impl.ResourceOracleImpl;
 import com.google.gwt.dev.resource.impl.ZipFileClassPathEntry;
 import com.google.gwt.dev.util.log.CompositeTreeLogger;
@@ -57,16 +58,15 @@
 /**
  * Recompiles a GWT module on demand.
  */
-class Recompiler {
+public class Recompiler {
 
   private final OutboxDir outboxDir;
   private final LauncherDir launcherDir;
+  private final MinimalRebuildCacheManager minimalRebuildCacheManager;
   private final String inputModuleName;
 
   private String serverPrefix;
   private int compilesDone = 0;
-  private Map<Map<String, String>, MinimalRebuildCache> minimalRebuildCacheForProperties =
-      Maps.newHashMap();
 
   // after renaming
   private AtomicReference<String> outputModuleName = new AtomicReference<String>(null);
@@ -81,12 +81,17 @@
   private final CompilerContext.Builder compilerContextBuilder = new CompilerContext.Builder();
   private CompilerContext compilerContext;
   private final Options options;
+  private final UnitCache unitCache;
 
-  Recompiler(OutboxDir outboxDir, LauncherDir launcherDir, String inputModuleName, Options options) {
+  Recompiler(OutboxDir outboxDir, LauncherDir launcherDir,
+      String inputModuleName, Options options,
+      UnitCache unitCache, MinimalRebuildCacheManager minimalRebuildCacheManager) {
     this.outboxDir = outboxDir;
     this.launcherDir = launcherDir;
     this.inputModuleName = inputModuleName;
     this.options = options;
+    this.unitCache = unitCache;
+    this.minimalRebuildCacheManager = minimalRebuildCacheManager;
     this.serverPrefix = options.getPreferredHost() + ":" + options.getPort();
     compilerContext = compilerContextBuilder.build();
   }
@@ -148,8 +153,7 @@
         System.setProperty("gwt.speedtracerlog",
             outboxDir.getSpeedTracerLogFile().getAbsolutePath());
       }
-      compilerContext = compilerContextBuilder.unitCache(
-          UnitCacheSingleton.get(job.getLogger(), outboxDir.getUnitCacheDir())).build();
+      compilerContext = compilerContextBuilder.unitCache(unitCache).build();
     }
 
     long startTime = System.currentTimeMillis();
@@ -191,8 +195,7 @@
 
       logger.log(TreeLogger.INFO, "Loading Java files in " + inputModuleName + ".");
       CompilerOptions loadOptions = new CompilerOptionsImpl(compileDir, inputModuleName, options);
-      compilerContext = compilerContextBuilder.options(loadOptions).unitCache(
-          Compiler.getOrCreateUnitCache(logger, loadOptions)).build();
+      compilerContext = compilerContextBuilder.options(loadOptions).unitCache(unitCache).build();
 
       // Loads and parses all the Java files in the GWT application using the JDT.
       // (This is warmup to make compiling faster later; we stop at this point to avoid
@@ -321,21 +324,22 @@
     CompilerOptions runOptions = new CompilerOptionsImpl(compileDir, newModuleName, options);
     compilerContext = compilerContextBuilder.options(runOptions).build();
 
-    // Looks up the matching rebuild cache using the final set of overridden binding properties.
-    MinimalRebuildCache knownGoodMinimalRebuildCache =
-        getKnownGoodMinimalRebuildCache(bindingProperties);
-    job.setCompileStrategy(knownGoodMinimalRebuildCache.isPopulated() ? CompileStrategy.INCREMENTAL
+    MinimalRebuildCache minimalRebuildCache = new NullRebuildCache();
+    if (options.isIncrementalCompileEnabled()) {
+      // Returns a copy of the intended cache, which is safe to modify in this compile.
+      minimalRebuildCache = minimalRebuildCacheManager.getCache(inputModuleName, bindingProperties);
+    }
+    job.setCompileStrategy(minimalRebuildCache.isPopulated() ? CompileStrategy.INCREMENTAL
         : CompileStrategy.FULL);
 
-    // Takes care to transactionally replace the saved cache only after a successful compile.
-    MinimalRebuildCache mutableMinimalRebuildCache = new MinimalRebuildCache();
-    mutableMinimalRebuildCache.copyFrom(knownGoodMinimalRebuildCache);
-    boolean success =
-        new Compiler(runOptions, mutableMinimalRebuildCache).run(compileLogger, module);
+    boolean success = new Compiler(runOptions, minimalRebuildCache).run(compileLogger, module);
     if (success) {
       publishedCompileDir = compileDir;
       lastBuildInput = input;
-      saveKnownGoodMinimalRebuildCache(bindingProperties, mutableMinimalRebuildCache);
+      if (options.isIncrementalCompileEnabled()) {
+        minimalRebuildCacheManager.putCache(inputModuleName, bindingProperties,
+            minimalRebuildCache);
+      }
       String moduleName = outputModuleName.get();
       writeRecompileNoCacheJs(new File(publishedCompileDir.getWarDir(), moduleName), moduleName,
           recompileJs, compileLogger);
@@ -397,30 +401,6 @@
     }
   }
 
-  private MinimalRebuildCache getKnownGoodMinimalRebuildCache(
-      Map<String, String> bindingProperties) {
-    if (!options.isIncrementalCompileEnabled()) {
-      return new NullRebuildCache();
-    }
-
-    MinimalRebuildCache minimalRebuildCache =
-        minimalRebuildCacheForProperties.get(bindingProperties);
-    if (minimalRebuildCache == null) {
-      minimalRebuildCache = new MinimalRebuildCache();
-      minimalRebuildCacheForProperties.put(bindingProperties, minimalRebuildCache);
-    }
-    return minimalRebuildCache;
-  }
-
-  private void saveKnownGoodMinimalRebuildCache(Map<String, String> bindingProperties,
-      MinimalRebuildCache knownGoodMinimalRebuildCache) {
-    if (!options.isIncrementalCompileEnabled()) {
-      return;
-    }
-
-    minimalRebuildCacheForProperties.put(bindingProperties, knownGoodMinimalRebuildCache);
-  }
-
   /**
    * Loads the module and configures it for SuperDevMode. (Does not restrict permutations.)
    */
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
index 2677493..5b99081 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
@@ -228,6 +228,17 @@
       return Responses.newJsonResponse(json);
     }
 
+    if (target.startsWith("/clean")) {
+      JsonObject json = null;
+      try {
+        runner.clean(logger);
+        json = jsonExporter.exportOk("Cleaned disk caches.");
+      } catch (ExecutionException e) {
+        json = jsonExporter.exportError(e.getMessage());
+      }
+      return Responses.newJsonResponse(json);
+    }
+
     // GET the Js that knows how to request the specific permutation recompile.
     if (target.startsWith("/recompile-requester/")) {
       String moduleName = target.substring("/recompile-requester/".length());
@@ -442,7 +453,7 @@
     };
   }
 
-  private Response makePolicyFilePage(String target) throws IOException {
+  private Response makePolicyFilePage(String target) {
 
     int secondSlash = target.indexOf('/', 1);
     if (secondSlash < 1) {
@@ -465,8 +476,7 @@
   /**
    * Sends the log file as html with errors highlighted in red.
    */
-  private Response makeLogPage(final Outbox box)
-       throws IOException {
+  private Response makeLogPage(final Outbox box) {
     final File file = box.getCompileLog();
     if (!file.isFile()) {
       return new ErrorPage("log file not found");
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/frontpage.html b/dev/codeserver/java/com/google/gwt/dev/codeserver/frontpage.html
index 1a3d6c0..e940649 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/frontpage.html
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/frontpage.html
@@ -105,6 +105,25 @@
 else if (window.attachEvent) {
   window.attachEvent("onload", onPageLoad);
 }
+
+function clean(config) {
+  var xhr = new XMLHttpRequest();
+  xhr.open("GET", "clean/" + config.moduleNames[0], true);
+  xhr.onload = function (e) {
+    if (xhr.readyState === 4) {
+      var responseData = JSON.parse(xhr.responseText);
+      window.alert(responseData.status);
+    }
+  };
+  xhr.onerror = function (e) {
+    var statusText = xhr.statusText;
+    if (!statusText) {
+      statusText = "The server did not respond.";
+    }
+    window.alert("Cache clean failed: " + statusText);
+  };
+  xhr.send(null);
+}
   </script>
 
   <style>
@@ -137,6 +156,22 @@
       padding: 3pt;
       margin: 8pt;
     }
+
+    #clean {
+      background-color: #eee;
+      border: 1px solid #ccc;
+      padding: 8px;
+    }
+
+    #clean * {
+      color: #444;
+      font-size: 10pt;
+      font-family: arial;
+    }
+
+    #clean button {
+      margin: 8px 0px 0px 0px;
+    }
   </style>
 </head>
 <body>
@@ -158,4 +193,12 @@
 
 </ol>
 
+<br />
+<div id="clean">
+  <div>
+  Clears all compiler caches so the next compile will be a full compile.
+  </div>
+  <button onclick="clean(config)">Clean</button>
+</div>
+
 </body></html>
diff --git a/dev/codeserver/javatests/com/google/gwt/dev/codeserver/RecompilerTest.java b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/RecompilerTest.java
index 023551b..32f6bcf 100644
--- a/dev/codeserver/javatests/com/google/gwt/dev/codeserver/RecompilerTest.java
+++ b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/RecompilerTest.java
@@ -17,7 +17,9 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.MinimalRebuildCacheManager;
 import com.google.gwt.dev.codeserver.Job.Result;
+import com.google.gwt.dev.javac.UnitCache;
 import com.google.gwt.dev.javac.UnitCacheSingleton;
 import com.google.gwt.dev.javac.testing.impl.JavaResourceBase;
 import com.google.gwt.dev.javac.testing.impl.MockJavaResource;
@@ -113,13 +115,16 @@
         fooResource);
     writeResourcesTo(originalResources, sourcePath);
 
-    Recompiler recompiler =
-        new Recompiler(OutboxDir.create(Files.createTempDir(), logger), null,
-            "com.foo.SimpleModule", options);
+    File baseCacheDir = Files.createTempDir();
+    UnitCache unitCache = UnitCacheSingleton.get(logger, null, baseCacheDir);
+    MinimalRebuildCacheManager minimalRebuildCacheManager =
+        new MinimalRebuildCacheManager(logger, baseCacheDir);
+    Recompiler recompiler = new Recompiler(OutboxDir.create(Files.createTempDir(), logger), null,
+        "com.foo.SimpleModule", options, unitCache, minimalRebuildCacheManager);
     Outbox outbox = new Outbox("Transactional Cache", recompiler, options, logger);
     OutboxTable outboxes = new OutboxTable();
     outboxes.addOutbox(outbox);
-    JobRunner runner = new JobRunner(new JobEventTable());
+    JobRunner runner = new JobRunner(new JobEventTable(), minimalRebuildCacheManager);
 
     // Perform a first compile. This should pass since all resources are valid.
     Result result =
diff --git a/dev/core/src/com/google/gwt/dev/javac/CompilationStateBuilder.java b/dev/core/src/com/google/gwt/dev/javac/CompilationStateBuilder.java
index c8ce531..d16f682 100644
--- a/dev/core/src/com/google/gwt/dev/javac/CompilationStateBuilder.java
+++ b/dev/core/src/com/google/gwt/dev/javac/CompilationStateBuilder.java
@@ -29,6 +29,7 @@
 import com.google.gwt.dev.js.ast.JsRootScope;
 import com.google.gwt.dev.resource.Resource;
 import com.google.gwt.dev.util.StringInterner;
+import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.log.speedtracer.CompilerEventType;
 import com.google.gwt.dev.util.log.speedtracer.DevModeEventType;
 import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
@@ -49,6 +50,9 @@
 import org.eclipse.jdt.internal.compiler.lookup.Binding;
 import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
@@ -531,6 +535,7 @@
       AdditionalTypeProviderDelegate compilerDelegate)
     throws UnableToCompleteException {
     UnitCache unitCache = compilerContext.getUnitCache();
+    assert unitCache != null : "CompilerContext should always contain a unit cache.";
 
     // Units we definitely want to build.
     List<CompilationUnitBuilder> builders = Lists.newArrayList();
@@ -547,6 +552,14 @@
 
       CompilationUnit cachedUnit = unitCache.find(resource.getPathPrefix() + resource.getPath());
 
+      ContentId resourceContentId = getResourceContentId(resource);
+      if (cachedUnit != null && cachedUnit.getLastModified() == resource.getLastModified()
+          && !cachedUnit.getContentId().equals(resourceContentId)) {
+        logger.log(TreeLogger.WARN,
+            "Modification date hasn't changed but contentId has changed for "
+            + resource.getLocation());
+      }
+
       // Try to rescue cached units from previous sessions where a jar has been
       // recompiled.
       if (cachedUnit != null && cachedUnit.getLastModified() != resource.getLastModified()) {
@@ -599,6 +612,26 @@
     return compilationState;
   }
 
+  private ContentId getResourceContentId(Resource resource) {
+    ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
+    try {
+      InputStream in = resource.openContents();
+      /**
+       * In most cases openContents() will throw an exception, however in the case of a
+       * ZipFileResource it might return null causing an NPE in Util.copyNoClose(), see issue 4359.
+       */
+      if (in == null) {
+        throw new RuntimeException("Unexpected error reading resource '" + resource + "'");
+      }
+      // TODO: deprecate com.google.gwt.dev.util.Util and use Guava.
+      Util.copy(in, out);
+    } catch (IOException e) {
+      throw new RuntimeException("Unexpected error reading resource '" + resource + "'", e);
+    }
+    byte[] content = out.toByteArray();
+    return new ContentId(Shared.getTypeName(resource), Util.computeStrongName(content));
+  }
+
   public CompilationState doBuildFrom(
       TreeLogger logger, CompilerContext compilerContext, Set<Resource> resources)
       throws UnableToCompleteException {
diff --git a/dev/core/src/com/google/gwt/dev/javac/MemoryUnitCache.java b/dev/core/src/com/google/gwt/dev/javac/MemoryUnitCache.java
index 362f271..b327b1c 100644
--- a/dev/core/src/com/google/gwt/dev/javac/MemoryUnitCache.java
+++ b/dev/core/src/com/google/gwt/dev/javac/MemoryUnitCache.java
@@ -16,6 +16,7 @@
 package com.google.gwt.dev.javac;
 
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.thirdparty.guava.common.collect.Maps;
 
 import java.util.Map;
@@ -110,6 +111,12 @@
   }
 
   @Override
+  public void clear() throws UnableToCompleteException {
+    unitMap.clear();
+    unitMapByContentId.clear();
+  }
+
+  @Override
   public CompilationUnit find(ContentId contentId) {
     UnitCacheEntry entry = unitMapByContentId.get(contentId);
     if (entry != null) {
diff --git a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
index e6fddd5..44a1320 100644
--- a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
+++ b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
@@ -120,6 +120,16 @@
     return backgroundService.asyncWriteUnit(newUnit);
   }
 
+  @Override
+  public void clear() throws UnableToCompleteException {
+    backgroundService.asyncClearCache();
+    backgroundService.finishAndShutdown();
+    synchronized (this) {
+      super.clear();
+    }
+    backgroundService.start();
+  }
+
   /**
    * Rotates to a new file and/or starts garbage collection if needed after a compile is finished.
    *
@@ -254,7 +264,8 @@
 
     private final TreeLogger logger;
     private final PersistentUnitCacheDir cacheDir;
-    private final ExecutorService service;
+    private ExecutorService service;
+    private PersistentUnitCache cacheToLoad;
 
     /**
      * Non-null while the unit cache is loading.
@@ -268,7 +279,29 @@
         throws UnableToCompleteException {
       this.logger = logger;
       this.cacheDir = new PersistentUnitCacheDir(logger, parentDir);
+      this.cacheToLoad = cacheToLoad;
 
+      start();
+    }
+
+    /**
+     * Blocks addition of any further tasks and waits for current tasks to finish.
+     */
+    public void finishAndShutdown() throws UnableToCompleteException {
+      service.shutdown();
+      try {
+        if (!service.awaitTermination(30, TimeUnit.SECONDS)) {
+          logger.log(TreeLogger.WARN,
+              "Persistent Unit Cache shutdown tasks took longer than 30 seconds to complete.");
+          throw new UnableToCompleteException();
+        }
+      } catch (InterruptedException e) {
+        // JVM is shutting down, ignore it.
+      }
+    }
+
+    private void start() {
+      assert service == null || service.isTerminated();
       service = Executors.newSingleThreadExecutor();
       Runtime.getRuntime().addShutdownHook(new Thread() {
         @Override
@@ -392,6 +425,18 @@
       });
     }
 
+    Future<?> asyncClearCache() {
+      Future<?> status = service.submit(new Runnable() {
+        @Override
+        public void run() {
+          cacheDir.closeCurrentFile();
+          cacheDir.deleteClosedCacheFiles();
+        }
+      });
+      service.shutdown(); // Don't allow more tasks to be scheduled.
+      return status;
+    }
+
     Future<?> asyncWriteUnit(final CompilationUnit unit) {
       try {
         return service.submit(new Runnable() {
diff --git a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCacheDir.java b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCacheDir.java
index 94a8283..8b04f19 100644
--- a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCacheDir.java
+++ b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCacheDir.java
@@ -18,16 +18,14 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.dev.jjs.ast.JNode;
 import com.google.gwt.dev.jjs.impl.GwtAstBuilder;
+import com.google.gwt.dev.util.CompilerVersion;
 import com.google.gwt.dev.util.StringInterningObjectInputStream;
 import com.google.gwt.dev.util.log.speedtracer.DevModeEventType;
 import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
 import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
 import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
 import com.google.gwt.thirdparty.guava.common.collect.Lists;
-import com.google.gwt.thirdparty.guava.common.hash.Hashing;
-import com.google.gwt.thirdparty.guava.common.io.Files;
 import com.google.gwt.util.tools.Utility;
 
 import java.io.BufferedInputStream;
@@ -39,8 +37,6 @@
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
-import java.net.JarURLConnection;
-import java.net.URLConnection;
 import java.util.Collections;
 import java.util.List;
 
@@ -54,7 +50,7 @@
   private static final String CACHE_FILE_PREFIX = "gwt-unitCache-";
 
   static final String CURRENT_VERSION_CACHE_FILE_PREFIX =
-      CACHE_FILE_PREFIX + compilerVersion() + "-";
+      CACHE_FILE_PREFIX + CompilerVersion.getHash() + "-";
 
   private final TreeLogger logger;
   private final File dir;
@@ -339,27 +335,6 @@
     return newFile;
   }
 
-  // TODO: use CompilerVersion class after it's committed.
-  private static String compilerVersion() {
-    String hash = "unknown";
-    try {
-      URLConnection urlConnection =
-          JNode.class.getResource("JNode.class").openConnection();
-      if (urlConnection instanceof JarURLConnection) {
-        String gwtdevJar = ((JarURLConnection) urlConnection).getJarFile().getName();
-        hash = Files.hash(new File(gwtdevJar), Hashing.sha1()).toString();
-      } else {
-        System.err.println("Couldn't find the GWT compiler jar file. "
-            + "Serialization errors might occur when accessing the persistent unit cache.");
-      }
-    } catch (IOException e) {
-      System.err.println("Couldn't compute the hash for the GWT compiler jar file."
-          + "Serialization errors might occur when accessing the persistent unit cache.");
-      e.printStackTrace();
-    }
-    return hash;
-  }
-
   /**
    * The current file and stream being written to by the persistent unit cache, if any.
    *
diff --git a/dev/core/src/com/google/gwt/dev/javac/UnitCache.java b/dev/core/src/com/google/gwt/dev/javac/UnitCache.java
index 93bab56..dbda6b9 100644
--- a/dev/core/src/com/google/gwt/dev/javac/UnitCache.java
+++ b/dev/core/src/com/google/gwt/dev/javac/UnitCache.java
@@ -16,6 +16,7 @@
 package com.google.gwt.dev.javac;
 
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
 
 /**
  * An interface for caching {@link CompilationUnit}s. Alternate implementations may cache only in
@@ -46,6 +47,11 @@
   void cleanup(TreeLogger logger);
 
   /**
+   * Wipe the contents of the cache.
+   */
+  void clear() throws UnableToCompleteException;
+
+  /**
    * Lookup a {@link CompilationUnit} by {@link ContentId}.
    */
   CompilationUnit find(ContentId contentId);
diff --git a/dev/core/src/com/google/gwt/dev/javac/UnitCacheSingleton.java b/dev/core/src/com/google/gwt/dev/javac/UnitCacheSingleton.java
index 2259a9e..bce16c7 100644
--- a/dev/core/src/com/google/gwt/dev/javac/UnitCacheSingleton.java
+++ b/dev/core/src/com/google/gwt/dev/javac/UnitCacheSingleton.java
@@ -26,6 +26,7 @@
 public class UnitCacheSingleton {
 
   public static final String GWT_PERSISTENTUNITCACHE = "gwt.persistentunitcache";
+  private static final String GWT_PERSISTENTUNITCACHEDIR = "gwt.persistentunitcachedir";
 
   /**
    * The API must be enabled explicitly for persistent caching to be live.
@@ -37,29 +38,64 @@
   private static UnitCache instance = null;
 
   /**
+   * If a cache exists, asks it to clear its contents.
+   */
+  public static synchronized void clearCache() throws UnableToCompleteException {
+    if (instance == null) {
+      return;
+    }
+    instance.clear();
+  }
+
+  /**
    * If the cache is enabled, instantiates the cache and begins loading units
    * into memory in a background thread. If the cache is not enabled, it clears
    * out any old cached files.
-   *
+   * <p>
    * Only one instance of the cache is instantiated. If a previously created
    * cache exists, the previous instance is returned.
+   * <p>
+   * The specified cache dir parameter is optional.
    */
-  public static synchronized UnitCache get(TreeLogger logger, File cacheDir) {
+  public static synchronized UnitCache get(TreeLogger logger, File specifiedCacheDir) {
+    return get(logger, specifiedCacheDir, null);
+  }
+
+  /**
+   * If the cache is enabled, instantiates the cache and begins loading units
+   * into memory in a background thread. If the cache is not enabled, it clears
+   * out any old cached files.
+   * <p>
+   * Only one instance of the cache is instantiated. If a previously created
+   * cache exists, the previous instance is returned.
+   * <p>
+   * Both specified and fallback cache dir parameters are optional.
+   */
+  public static synchronized UnitCache get(TreeLogger logger, File specifiedCacheDir,
+      File fallbackCacheDir) {
     assert logger != null;
     if (instance == null) {
+      String propertyCachePath = System.getProperty(GWT_PERSISTENTUNITCACHEDIR);
+      File propertyCacheDir = propertyCachePath != null ? new File(propertyCachePath) : null;
+
       if (usePersistent) {
-        String dirProp = "gwt.persistentunitcachedir";
-        String propertyCacheDir = System.getProperty(dirProp);
-        if (propertyCacheDir != null) {
-          cacheDir = new File(propertyCacheDir);
-        } else if (cacheDir == null) {
+        File actualCacheDir = null;
+
+        // Pick the highest priority cache dir that is available.
+        if (specifiedCacheDir != null) {
+          actualCacheDir = specifiedCacheDir;
+        } else if (propertyCacheDir != null) {
+          actualCacheDir = propertyCacheDir;
+        } else if (fallbackCacheDir != null) {
+          actualCacheDir = fallbackCacheDir;
+        } else {
           logger.log(TreeLogger.TRACE, "Persistent caching disabled - no directory specified.\n"
               + "To enable persistent unit caching use -Dgwt.persistentunitcachedir=<dir>");
         }
-        if (cacheDir != null) {
+
+        if (actualCacheDir != null) {
           try {
-            instance = new PersistentUnitCache(logger, cacheDir);
-            return instance;
+            return instance = new PersistentUnitCache(logger, actualCacheDir);
           } catch (UnableToCompleteException ignored) {
           }
         }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java
index 7cd3cfb..92cb4c0 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java
@@ -76,8 +76,8 @@
 
   @Override
   public void exec() {
-    logger = logger.branch(TreeLogger.DEBUG,
-        "Linking per-type JS with " + typeRanges.size() + " new types.");
+    logger = logger.branch(TreeLogger.INFO,
+        "Linking per-type JS with " + typeRanges.size() + " new/changed types.");
     linkAll(computeReachableTypes());
   }
 
@@ -123,15 +123,15 @@
     }
     linkOne(FOOTER_NAME);
 
-    logger.log(TreeLogger.INFO, "prelink JS size = " + js.length());
-    logger.log(TreeLogger.INFO, "prelink sourcemap = " + sourceInfoMap.getBytes() + " bytes and "
+    logger.log(TreeLogger.TRACE, "prelink JS size = " + js.length());
+    logger.log(TreeLogger.TRACE, "prelink sourcemap = " + sourceInfoMap.getBytes() + " bytes and "
         + sourceInfoMap.getLines() + " lines");
     js = jsBuilder.toString();
     statementRanges = statementRangesBuilder.build();
     sourceInfoMap = jsSourceMapBuilder.build();
     minimalRebuildCache.setLastLinkedJsBytes(js.length());
-    logger.log(TreeLogger.INFO, "postlink JS size = " + js.length());
-    logger.log(TreeLogger.INFO, "postlink sourcemap = " + sourceInfoMap.getBytes() + " bytes and "
+    logger.log(TreeLogger.TRACE, "postlink JS size = " + js.length());
+    logger.log(TreeLogger.TRACE, "postlink sourcemap = " + sourceInfoMap.getBytes() + " bytes and "
         + sourceInfoMap.getLines() + " lines");
   }
 
diff --git a/dev/core/src/com/google/gwt/dev/util/CompilerVersion.java b/dev/core/src/com/google/gwt/dev/util/CompilerVersion.java
index 0397cf3..027810a 100644
--- a/dev/core/src/com/google/gwt/dev/util/CompilerVersion.java
+++ b/dev/core/src/com/google/gwt/dev/util/CompilerVersion.java
@@ -13,15 +13,48 @@
  */
 package com.google.gwt.dev.util;
 
+import com.google.gwt.dev.jjs.ast.JNode;
+import com.google.gwt.thirdparty.guava.common.hash.Hashing;
+import com.google.gwt.thirdparty.guava.common.io.Files;
+import com.google.gwt.util.tools.shared.Md5Utils;
+import com.google.gwt.util.tools.shared.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.JarURLConnection;
+import java.net.URLConnection;
+
 /**
  * Utility for uniquely identifying the current compiler version.
  */
 public class CompilerVersion {
 
+  private static String versionHash;
+
   /**
    * Calculates and returns a hash to uniquely identify the current compiler version if possible.
+   * <p>
+   * If the compiler jar can not be found then a random hash is returned.
    */
   public static synchronized String getHash() {
-    return "version-unknown";
+    if (versionHash == null) {
+      versionHash = "unknown-version-" + StringUtils.toHexString(
+          Md5Utils.getMd5Digest(Long.toString((long) (Long.MAX_VALUE * Math.random()))));
+
+      try {
+        URLConnection urlConnection = JNode.class.getResource("JNode.class").openConnection();
+        if (urlConnection instanceof JarURLConnection) {
+          String gwtdevJar = ((JarURLConnection) urlConnection).getJarFile().getName();
+          versionHash = Files.hash(new File(gwtdevJar), Hashing.sha1()).toString();
+        } else {
+          System.err.println("Could not find the GWT compiler jarfile. "
+              + "Serialization errors might occur when accessing the persistent unit cache.");
+        }
+      } catch (IOException e) {
+        System.err.println("Could not compute the hash for the GWT compiler jarfile."
+            + "Serialization errors might occur when accessing the persistent unit cache.");
+      }
+    }
+    return versionHash;
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/util/DiskCachingUtil.java b/dev/core/src/com/google/gwt/dev/util/DiskCachingUtil.java
new file mode 100644
index 0000000..a4b3191
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/DiskCachingUtil.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.util;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.thirdparty.guava.common.base.Joiner;
+import com.google.gwt.util.tools.shared.Md5Utils;
+import com.google.gwt.util.tools.shared.StringUtils;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * General utility functions for disk caching.
+ */
+public class DiskCachingUtil {
+
+  /**
+   * Computes and returns a consistent preferred cache dir based on the given set of module names
+   * and the current working directory.
+   * <p>
+   * Using a consistent cache dir has performance advantages since caches can be reused between JVM
+   * process launches.
+   */
+  public static synchronized File computePreferredCacheDir(List<String> moduleNames,
+      TreeLogger logger) {
+    String tempDir = System.getProperty("java.io.tmpdir");
+    String currentWorkingDirectory = System.getProperty("user.dir");
+    String preferredCacheDirName = "gwt-cache-" + StringUtils.toHexString(
+        Md5Utils.getMd5Digest(currentWorkingDirectory + Joiner.on(", ").join(moduleNames)));
+
+    File preferredCacheDir = new File(tempDir, preferredCacheDirName);
+    if (!preferredCacheDir.exists() && !preferredCacheDir.mkdir()) {
+      logger.log(TreeLogger.WARN, "Can't create cache directory: " + preferredCacheDir);
+      return null;
+    }
+    return preferredCacheDir;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/util/Util.java b/dev/core/src/com/google/gwt/dev/util/Util.java
index a12411e..debb2c3 100644
--- a/dev/core/src/com/google/gwt/dev/util/Util.java
+++ b/dev/core/src/com/google/gwt/dev/util/Util.java
@@ -78,6 +78,7 @@
  * being moved to {@link com.google.gwt.util.tools.Utility} if they would be
  * generally useful to tool writers, and don't involve TreeLogger.
  */
+// TODO: remove stream functions and replace with Guava.
 public final class Util {
 
   public static String DEFAULT_ENCODING = "UTF-8";
diff --git a/dev/core/src/com/google/gwt/util/tools/shared/Md5Utils.java b/dev/core/src/com/google/gwt/util/tools/shared/Md5Utils.java
index 8151017..f4d21c2 100644
--- a/dev/core/src/com/google/gwt/util/tools/shared/Md5Utils.java
+++ b/dev/core/src/com/google/gwt/util/tools/shared/Md5Utils.java
@@ -51,4 +51,11 @@
     md5.update(input);
     return md5.digest();
   }
+
+  /**
+   * Generate MD5 digest.
+   */
+  public static byte[] getMd5Digest(String string) {
+    return getMd5Digest(string.getBytes());
+  }
 }
diff --git a/dev/core/test/com/google/gwt/dev/jjs/LibraryJavaToJavaScriptCompilerTest.java b/dev/core/test/com/google/gwt/dev/jjs/LibraryJavaToJavaScriptCompilerTest.java
index 008abf1..ca3d0c2 100644
--- a/dev/core/test/com/google/gwt/dev/jjs/LibraryJavaToJavaScriptCompilerTest.java
+++ b/dev/core/test/com/google/gwt/dev/jjs/LibraryJavaToJavaScriptCompilerTest.java
@@ -223,7 +223,7 @@
       }
 
       /**
-       * Overridden to avoid the complexity of mocking out a LibraryGroupUnitCache.
+       * Overridden to avoid the complexity of mocking out a UnitCache.
        */
       @Override
       protected JDeclaredType ensureFullTypeLoaded(JDeclaredType type) {