Introduces MinimalRebuildCache management.

Adds a memory and disk cache management class for MinimalRebuildCache 
instances.

Change-Id: I7d3bce27b22d150991ca009f454015a3d3d75a8a
Review-Link: https://gwt-review.googlesource.com/#/c/10261/
diff --git a/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java b/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
index 6e76884..124ce48 100644
--- a/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
+++ b/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
@@ -318,10 +318,10 @@
     }
 
     /*
-     * Filter for just those stale types that are actually reachable. Since if they're not
-     * reachable we don't want to artificially traverse them and unnecessarily reveal dependency
-     * problems. And if they have become reachable, since they're missing JS, they will already be
-     * fully traversed when seen in Unify.
+     * Filter for just those stale types that are actually reachable. Since if they're not reachable
+     * we don't want to artificially traverse them and unnecessarily reveal dependency problems. And
+     * if they have become reachable, since they're missing JS, they will already be fully traversed
+     * when seen in Unify.
      */
     copyCollection(filterUnreachableTypeNames(staleTypeNames), staleTypeNames);
 
@@ -443,6 +443,50 @@
     copyCollection(that.staleTypeNames, this.staleTypeNames);
   }
 
+  @VisibleForTesting
+  boolean hasSameContent(MinimalRebuildCache that) {
+    // Ignoring processedStaleTypeNames since it is transient.
+    return
+        this.immediateTypeRelations.hasSameContent(that.immediateTypeRelations) && Objects.equal(
+            this.compilationUnitTypeNameByNestedTypeName,
+            that.compilationUnitTypeNameByNestedTypeName)
+        && Objects.equal(this.contentHashByGeneratedTypeName, that.contentHashByGeneratedTypeName)
+        && Objects.equal(this.deletedCompilationUnitNames, that.deletedCompilationUnitNames)
+        && Objects.equal(this.deletedDiskSourcePaths, that.deletedDiskSourcePaths)
+        && Objects.equal(this.deletedResourcePaths, that.deletedResourcePaths)
+        && Objects.equal(this.dualJsoImplInterfaceNames, that.dualJsoImplInterfaceNames)
+        && Objects.equal(this.generatedArtifacts, that.generatedArtifacts) && Objects.equal(
+            this.generatedCompilationUnitNamesByReboundTypeNames,
+            that.generatedCompilationUnitNamesByReboundTypeNames)
+        && this.intTypeMapper.hasSameContent(that.intTypeMapper)
+        && Objects.equal(this.jsByTypeName, that.jsByTypeName)
+        && Objects.equal(this.jsoStatusChangedTypeNames, that.jsoStatusChangedTypeNames)
+        && Objects.equal(this.jsoTypeNames, that.jsoTypeNames)
+        && Objects.equal(this.lastLinkedJsBytes, that.lastLinkedJsBytes)
+        && Objects.equal(this.lastModifiedByDiskSourcePath, that.lastModifiedByDiskSourcePath)
+        && Objects.equal(this.lastModifiedByResourcePath, that.lastModifiedByResourcePath)
+        && Objects.equal(this.lastReachableTypeNames, that.lastReachableTypeNames)
+        && Objects.equal(this.modifiedCompilationUnitNames, that.modifiedCompilationUnitNames)
+        && Objects.equal(this.modifiedDiskSourcePaths, that.modifiedDiskSourcePaths)
+        && Objects.equal(this.modifiedResourcePaths, that.modifiedResourcePaths)
+        && Objects.equal(this.nestedTypeNamesByUnitTypeName, that.nestedTypeNamesByUnitTypeName)
+        && this.persistentPrettyNamerState.hasSameContent(that.persistentPrettyNamerState)
+        && Objects.equal(this.preambleTypeNames, that.preambleTypeNames) && Objects.equal(
+            this.rebinderTypeNamesByReboundTypeName, that.rebinderTypeNamesByReboundTypeName)
+        && Objects.equal(this.reboundTypeNamesByGeneratedCompilationUnitNames,
+            that.reboundTypeNamesByGeneratedCompilationUnitNames) && Objects.equal(
+            this.reboundTypeNamesByInputResource, that.reboundTypeNamesByInputResource)
+        && Objects.equal(this.referencedTypeNamesByTypeName, that.referencedTypeNamesByTypeName)
+        && Objects.equal(this.rootTypeNames, that.rootTypeNames)
+        && Objects.equal(this.singleJsoImplInterfaceNames, that.singleJsoImplInterfaceNames)
+        && Objects.equal(this.sourceCompilationUnitNames, that.sourceCompilationUnitNames)
+        && Objects.equal(this.sourceMapsByTypeName, that.sourceMapsByTypeName)
+        && Objects.equal(this.staleTypeNames, that.staleTypeNames)
+        && Objects.equal(this.statementRangesByTypeName, that.statementRangesByTypeName)
+        && Objects.equal(this.typeNamesByReferencingTypeName,
+            that.typeNamesByReferencingTypeName);
+  }
+
   /**
    * Return the set of provided typeNames with unreachable types filtered out.
    */
@@ -458,10 +502,6 @@
     return immediateTypeRelations;
   }
 
-  public IntTypeMapper getTypeMapper() {
-    return intTypeMapper;
-  }
-
   public String getJs(String typeName) {
     return jsByTypeName.get(typeName);
   }
@@ -505,6 +545,10 @@
     return statementRangesByTypeName.get(typeName);
   }
 
+  public IntTypeMapper getTypeMapper() {
+    return intTypeMapper;
+  }
+
   public boolean hasJs(String typeName) {
     return jsByTypeName.containsKey(typeName);
   }
diff --git a/dev/core/src/com/google/gwt/dev/MinimalRebuildCacheManager.java b/dev/core/src/com/google/gwt/dev/MinimalRebuildCacheManager.java
new file mode 100644
index 0000000..5149769
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/MinimalRebuildCacheManager.java
@@ -0,0 +1,290 @@
+/*
+ * 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;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.dev.util.CompilerVersion;
+import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
+import com.google.gwt.thirdparty.guava.common.cache.Cache;
+import com.google.gwt.thirdparty.guava.common.cache.CacheBuilder;
+import com.google.gwt.thirdparty.guava.common.util.concurrent.Futures;
+import com.google.gwt.thirdparty.guava.common.util.concurrent.MoreExecutors;
+import com.google.gwt.util.tools.Utility;
+import com.google.gwt.util.tools.shared.Md5Utils;
+import com.google.gwt.util.tools.shared.StringUtils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Manages caching of MinimalRebuildCache instances.
+ * <p>
+ * Changes are immediately performed in memory and are asynchronously persisted to disk in original
+ * request order.
+ */
+public class MinimalRebuildCacheManager {
+
+  private static final int MEMORY_CACHE_COUNT_LIMIT = 3;
+  private static final String REBUILD_CACHE_PREFIX = "gwt-rebuildCache";
+
+  private final ExecutorService executorService =
+      MoreExecutors.getExitingExecutorService((ThreadPoolExecutor) Executors.newFixedThreadPool(1));
+  private final TreeLogger logger;
+  private final File minimalRebuildCacheDir;
+  private final Cache<String, MinimalRebuildCache> minimalRebuildCachesByName =
+      CacheBuilder.newBuilder().maximumSize(MEMORY_CACHE_COUNT_LIMIT).build();
+
+  public MinimalRebuildCacheManager(TreeLogger logger, File baseCacheDir) {
+    this.logger = logger;
+    if (baseCacheDir != null) {
+      minimalRebuildCacheDir = new File(baseCacheDir, REBUILD_CACHE_PREFIX);
+      minimalRebuildCacheDir.mkdir();
+    } else {
+      minimalRebuildCacheDir = null;
+    }
+  }
+
+  /**
+   * Synchronously delete all in memory caches managed here and all on disk in the managed folder.
+   */
+  public synchronized void deleteCaches() {
+    syncDeleteMemoryCaches();
+    if (haveCacheDir()) {
+      Futures.getUnchecked(enqueueAsyncDeleteDiskCaches());
+    }
+  }
+
+  /**
+   * Synchronously return the MinimalRebuildCache specific to the given module and binding
+   * properties.
+   * <p>
+   * If no cache is found in memory then it will be synchronously loaded from disk.
+   * <p>
+   * If it is still not found a new empty cache will be returned.
+   */
+  public synchronized MinimalRebuildCache getCache(String moduleName,
+      Map<String, String> bindingProperties) {
+    String cacheName = computeMinimalRebuildCacheName(moduleName, bindingProperties);
+
+    MinimalRebuildCache minimalRebuildCache = minimalRebuildCachesByName.getIfPresent(cacheName);
+
+    // If there's no cache already in memory, try to load a cache from disk.
+    if (minimalRebuildCache == null && haveCacheDir()) {
+      // Might return null.
+      minimalRebuildCache = syncReadDiskCache(moduleName, bindingProperties);
+      if (minimalRebuildCache != null) {
+        minimalRebuildCachesByName.put(cacheName, minimalRebuildCache);
+      }
+    }
+
+    // If there's still no cache loaded, just create a blank one.
+    if (minimalRebuildCache == null) {
+      minimalRebuildCache = new MinimalRebuildCache();
+      minimalRebuildCachesByName.put(cacheName, minimalRebuildCache);
+      return minimalRebuildCache;
+    }
+
+    // Return a copy.
+    MinimalRebuildCache mutableMinimalRebuildCache = new MinimalRebuildCache();
+    mutableMinimalRebuildCache.copyFrom(minimalRebuildCache);
+    return mutableMinimalRebuildCache;
+  }
+
+  /**
+   * Stores a MinimalRebuildCache specific to the given module and binding properties.
+   * <p>
+   * A copy of the cache will be lazily persisted to disk as well.
+   */
+  public synchronized void putCache(String moduleName, Map<String, String> bindingProperties,
+      MinimalRebuildCache knownGoodMinimalRebuildCache) {
+    syncPutMemoryCache(moduleName, bindingProperties, knownGoodMinimalRebuildCache);
+    if (haveCacheDir()) {
+      enqueueAsyncWriteDiskCache(moduleName, bindingProperties, knownGoodMinimalRebuildCache);
+    }
+  }
+
+  /**
+   * Enqueue to asynchronously delete all on disk caches in the managed cache folder.
+   */
+  @VisibleForTesting
+  synchronized Future<Void> enqueueAsyncDeleteDiskCaches() {
+    return executorService.submit(new Callable<Void>() {
+      @Override
+      public Void call() {
+        for (File cacheFile : minimalRebuildCacheDir.listFiles()) {
+          if (!cacheFile.delete()) {
+            logger.log(TreeLogger.WARN, "Couldn't delete " + cacheFile);
+          }
+        }
+        return null;
+      }
+    });
+  }
+
+  /**
+   * Enqueue to asynchronously find, read and return the MinimalRebuildCache unique to this module
+   * and binding properties combination in the managed cache folder.
+   */
+  @VisibleForTesting
+  synchronized Future<MinimalRebuildCache> enqueueAsyncReadDiskCache(final String moduleName,
+      final Map<String, String> bindingProperties) {
+    return executorService.submit(new Callable<MinimalRebuildCache>() {
+      @Override
+      public MinimalRebuildCache call() {
+        // Find the cache file unique to this module, binding properties and working directory.
+        File minimalRebuildCacheFile =
+            computeMinimalRebuildCacheFile(moduleName, bindingProperties);
+
+        // If the file exists.
+        if (minimalRebuildCacheFile.exists()) {
+          ObjectInputStream objectInputStream = null;
+          // Try to read it.
+          try {
+            objectInputStream = new ObjectInputStream(
+                new BufferedInputStream(new FileInputStream(minimalRebuildCacheFile)));
+            return (MinimalRebuildCache) objectInputStream.readObject();
+          } catch (IOException e) {
+            logger.log(TreeLogger.WARN,
+                "Unable to read the rebuild cache in " + minimalRebuildCacheFile + ".");
+            Utility.close(objectInputStream);
+            minimalRebuildCacheFile.delete();
+          } catch (ClassNotFoundException e) {
+            logger.log(TreeLogger.WARN,
+                "Unable to read the rebuild cache in " + minimalRebuildCacheFile + ".");
+            Utility.close(objectInputStream);
+            minimalRebuildCacheFile.delete();
+          } finally {
+            Utility.close(objectInputStream);
+          }
+        }
+        return null;
+      }
+    });
+  }
+
+  /**
+   * Enqueue to asynchronously write the provided MinimalRebuildCache to disk.
+   * <p>
+   * Persisted caches are uniquely named based on the compiler version, current module name, binding
+   * properties and the location where the JVM was launched.
+   * <p>
+   * Care is taken to completely and successfully write a new cache (to a different location on
+   * disk) before replacing the old cache (at the regular location on disk).
+   * <p>
+   * Write requests will occur in the order requested and will queue up if requests are made faster
+   * than they can be completed.
+   */
+  @VisibleForTesting
+  synchronized Future<Void> enqueueAsyncWriteDiskCache(final String moduleName,
+      final Map<String, String> bindingProperties, final MinimalRebuildCache minimalRebuildCache) {
+    return executorService.submit(new Callable<Void>() {
+      @Override
+      public Void call() {
+        File oldMinimalRebuildCacheFile =
+            computeMinimalRebuildCacheFile(moduleName, bindingProperties);
+        File newMinimalRebuildCacheFile =
+            new File(oldMinimalRebuildCacheFile.getAbsoluteFile() + ".new");
+
+        // Ensure the cache folder exists.
+        oldMinimalRebuildCacheFile.getParentFile().mkdirs();
+
+        // Write the new cache to disk.
+        ObjectOutputStream objectOutputStream = null;
+        try {
+          objectOutputStream = new ObjectOutputStream(
+              new BufferedOutputStream(new FileOutputStream(newMinimalRebuildCacheFile)));
+          objectOutputStream.writeObject(minimalRebuildCache);
+          Utility.close(objectOutputStream);
+
+          // Replace the old cache file with the new one.
+          oldMinimalRebuildCacheFile.delete();
+          newMinimalRebuildCacheFile.renameTo(oldMinimalRebuildCacheFile);
+        } catch (IOException e) {
+          logger.log(TreeLogger.WARN,
+              "Unable to update the cache in " + oldMinimalRebuildCacheFile + ".");
+          newMinimalRebuildCacheFile.delete();
+        } finally {
+          if (objectOutputStream != null) {
+            Utility.close(objectOutputStream);
+          }
+        }
+        return null;
+      }
+    });
+  }
+
+  /**
+   * For testing only. Stops accepting any new tasks and waits for current tasks to complete.
+   */
+  @VisibleForTesting
+  boolean shutdown() throws InterruptedException {
+    executorService.shutdown();
+    return executorService.awaitTermination(30, TimeUnit.SECONDS);
+  }
+
+  /**
+   * Find, read and return the MinimalRebuildCache unique to this module, binding properties and
+   * working directory.
+   */
+  @VisibleForTesting
+  synchronized MinimalRebuildCache syncReadDiskCache(String moduleName,
+      Map<String, String> bindingProperties) {
+    return Futures.getUnchecked(enqueueAsyncReadDiskCache(moduleName, bindingProperties));
+  }
+
+  private File computeMinimalRebuildCacheFile(String moduleName,
+      Map<String, String> bindingProperties) {
+    return new File(minimalRebuildCacheDir,
+        computeMinimalRebuildCacheName(moduleName, bindingProperties));
+  }
+
+  private String computeMinimalRebuildCacheName(String moduleName,
+      Map<String, String> bindingProperties) {
+    String currentWorkingDirectory = System.getProperty("user.dir");
+    String compilerVersionHash = CompilerVersion.getHash();
+    String bindingPropertiesString = bindingProperties.toString();
+
+    String consistentHash = StringUtils.toHexString(Md5Utils.getMd5Digest((
+        compilerVersionHash + moduleName + currentWorkingDirectory + bindingPropertiesString)
+        .getBytes()));
+    return REBUILD_CACHE_PREFIX + "-" + consistentHash;
+  }
+
+  private boolean haveCacheDir() {
+    return minimalRebuildCacheDir != null && minimalRebuildCacheDir.isDirectory();
+  }
+
+  private void syncDeleteMemoryCaches() {
+    minimalRebuildCachesByName.invalidateAll();
+  }
+
+  private void syncPutMemoryCache(String moduleName, Map<String, String> bindingProperties,
+      MinimalRebuildCache knownGoodMinimalRebuildCache) {
+    String cacheName = computeMinimalRebuildCacheName(moduleName, bindingProperties);
+    minimalRebuildCachesByName.put(cacheName, knownGoodMinimalRebuildCache);
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JsSourceMap.java b/dev/core/src/com/google/gwt/dev/jjs/JsSourceMap.java
index 4603ae6..6af0d24 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JsSourceMap.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JsSourceMap.java
@@ -18,21 +18,26 @@
 import com.google.gwt.core.ext.linker.impl.JsSourceMapExtractor;
 import com.google.gwt.core.ext.soyc.Range;
 
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
  * An unmodifiable container of Ranges that map from JavaScript to the Java it came from.
  */
-public class JsSourceMap {
+public class JsSourceMap implements Serializable {
 
-  private final int bytes;
-  private final int lines;
+  private int bytes;
+  private int lines;
 
   /**
    * Maps JS ranges to Java ranges. The mapping is sparse thus the need for separately tracking
    * total bytes and lines.
    */
-  private final List<Range> ranges;
+  private List<Range> ranges;
 
   public JsSourceMap(List<Range> ranges, int bytes, int lines) {
     this.ranges = ranges;
@@ -48,15 +53,50 @@
     return bytes;
   }
 
-  public List<Range> getRanges() {
-    return ranges;
-  }
-
   public int getLines() {
     return lines;
   }
 
+  public List<Range> getRanges() {
+    return ranges;
+  }
+
   public int size() {
     return ranges.size();
   }
+
+  private void readObject(ObjectInputStream inStream) throws IOException, ClassNotFoundException {
+    bytes = inStream.readInt();
+    lines = inStream.readInt();
+
+    int rangeCount = inStream.readInt();
+    ranges = new ArrayList<Range>(rangeCount);
+    for (int i = 0; i < rangeCount; i++) {
+      int start = inStream.readInt();
+      int end = inStream.readInt();
+      int startLine = inStream.readInt();
+      int startColumn = inStream.readInt();
+      int endLine = inStream.readInt();
+      int endColumn = inStream.readInt();
+      SourceInfo sourceInfo = (SourceInfo) inStream.readObject();
+
+      ranges.add(new Range(start, end, startLine, startColumn, endLine, endColumn, sourceInfo));
+    }
+  }
+
+  private void writeObject(ObjectOutputStream outStream) throws IOException {
+    outStream.writeInt(bytes);
+    outStream.writeInt(lines);
+
+    outStream.writeInt(ranges.size());
+    for (Range range : ranges) {
+      outStream.writeInt(range.getStart());
+      outStream.writeInt(range.getEnd());
+      outStream.writeInt(range.getStartLine());
+      outStream.writeInt(range.getStartColumn());
+      outStream.writeInt(range.getEndLine());
+      outStream.writeInt(range.getEndColumn());
+      outStream.writeObject(range.getSourceInfo());
+    }
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java
index 7cc1d9b..a1f049c 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java
@@ -20,6 +20,7 @@
 import com.google.gwt.dev.util.arg.OptionJsInteropMode;
 import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
 import com.google.gwt.thirdparty.guava.common.base.Function;
+import com.google.gwt.thirdparty.guava.common.base.Objects;
 import com.google.gwt.thirdparty.guava.common.base.Predicate;
 import com.google.gwt.thirdparty.guava.common.base.Strings;
 import com.google.gwt.thirdparty.guava.common.collect.HashMultimap;
@@ -89,6 +90,15 @@
     }
 
     @VisibleForTesting
+    public boolean hasSameContent(ImmediateTypeRelations that) {
+      return Objects.equal(this.immediateImplementedInterfacesByClass,
+          that.immediateImplementedInterfacesByClass)
+          && Objects.equal(this.immediateSuperclassesByClass, that.immediateSuperclassesByClass)
+          && Objects.equal(this.immediateSuperInterfacesByInterface,
+              that.immediateSuperInterfacesByInterface);
+    }
+
+    @VisibleForTesting
     public Map<String, String> getImmediateSuperclassesByClass() {
       return immediateSuperclassesByClass;
     }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ResolveRuntimeTypeReferences.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ResolveRuntimeTypeReferences.java
index ca941cb..77e8f56 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/ResolveRuntimeTypeReferences.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ResolveRuntimeTypeReferences.java
@@ -26,12 +26,15 @@
 import com.google.gwt.dev.jjs.ast.JRuntimeTypeReference;
 import com.google.gwt.dev.jjs.ast.JType;
 import com.google.gwt.dev.jjs.ast.JVisitor;
+import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
+import com.google.gwt.thirdparty.guava.common.base.Objects;
 import com.google.gwt.thirdparty.guava.common.collect.LinkedHashMultiset;
 import com.google.gwt.thirdparty.guava.common.collect.Lists;
 import com.google.gwt.thirdparty.guava.common.collect.Maps;
 import com.google.gwt.thirdparty.guava.common.collect.Multiset;
 import com.google.gwt.thirdparty.guava.common.collect.Multisets;
 
+import java.io.Serializable;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -63,7 +66,7 @@
   /**
    * Sequentially creates int type ids for types.
    */
-  public static class IntTypeMapper implements TypeMapper<Integer> {
+  public static class IntTypeMapper implements Serializable, TypeMapper<Integer> {
 
     // NOTE: DO NOT STORE ANY AST REFERENCE. Objects of this type persist across compiles.
     private final Map<String, Integer> typeIdByTypeName = Maps.newHashMap();
@@ -81,6 +84,12 @@
       this.typeIdByTypeName.putAll(from.typeIdByTypeName);
     }
 
+    @VisibleForTesting
+    public boolean hasSameContent(IntTypeMapper that) {
+      return Objects.equal(this.typeIdByTypeName, that.typeIdByTypeName)
+          && Objects.equal(this.nextAvailableId, that.nextAvailableId);
+    }
+
     @Override
     public Integer get(JType type) {
       return typeIdByTypeName.get(type.getName());
diff --git a/dev/core/src/com/google/gwt/dev/js/JsPersistentPrettyNamer.java b/dev/core/src/com/google/gwt/dev/js/JsPersistentPrettyNamer.java
index f045f11..fb27f6b 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsPersistentPrettyNamer.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsPersistentPrettyNamer.java
@@ -18,11 +18,13 @@
 import com.google.gwt.dev.js.ast.JsProgram;
 import com.google.gwt.dev.js.ast.JsScope;
 import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
+import com.google.gwt.thirdparty.guava.common.base.Objects;
 import com.google.gwt.thirdparty.guava.common.collect.HashMultiset;
 import com.google.gwt.thirdparty.guava.common.collect.Maps;
 import com.google.gwt.thirdparty.guava.common.collect.Multiset;
 import com.google.gwt.thirdparty.guava.common.collect.Sets;
 
+import java.io.Serializable;
 import java.util.Map;
 import java.util.Set;
 
@@ -35,12 +37,10 @@
   /**
    * Encapsulates the complete state of this namer so that state can be persisted and reused.
    */
-  public static class PersistentPrettyNamerState {
+  public static class PersistentPrettyNamerState implements Serializable {
 
     private Multiset<String> shortIdentCollisionCounts = HashMultiset.create();
-
     private Map<String, String> prettyIdentByOriginalIdent = Maps.newHashMap();
-
     private Set<String> usedPrettyIdents = Sets.newHashSet();
 
     public void copyFrom(PersistentPrettyNamerState that) {
@@ -52,6 +52,13 @@
       this.prettyIdentByOriginalIdent.putAll(that.prettyIdentByOriginalIdent);
       this.usedPrettyIdents.addAll(that.usedPrettyIdents);
     }
+
+    @VisibleForTesting
+    public boolean hasSameContent(PersistentPrettyNamerState that) {
+      return Objects.equal(this.shortIdentCollisionCounts, that.shortIdentCollisionCounts)
+          && Objects.equal(this.prettyIdentByOriginalIdent, that.prettyIdentByOriginalIdent)
+          && Objects.equal(this.usedPrettyIdents, that.usedPrettyIdents);
+    }
   }
 
   @VisibleForTesting
diff --git a/dev/core/src/com/google/gwt/dev/util/CompilerVersion.java b/dev/core/src/com/google/gwt/dev/util/CompilerVersion.java
new file mode 100644
index 0000000..0397cf3
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/CompilerVersion.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+/**
+ * Utility for uniquely identifying the current compiler version.
+ */
+public class CompilerVersion {
+
+  /**
+   * Calculates and returns a hash to uniquely identify the current compiler version if possible.
+   */
+  public static synchronized String getHash() {
+    return "version-unknown";
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/MinimalRebuildCacheManagerTest.java b/dev/core/test/com/google/gwt/dev/MinimalRebuildCacheManagerTest.java
new file mode 100644
index 0000000..84ab280
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/MinimalRebuildCacheManagerTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.dev.jjs.ast.JTypeOracle;
+import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
+import com.google.gwt.thirdparty.guava.common.collect.Maps;
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+import com.google.gwt.thirdparty.guava.common.io.Files;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * Tests for {@link MinimalRebuildCacheManager}.
+ */
+public class MinimalRebuildCacheManagerTest extends TestCase {
+
+  public void testNoSuchCache() {
+    MinimalRebuildCacheManager minimalRebuildCacheManager =
+        new MinimalRebuildCacheManager(TreeLogger.NULL, Files.createTempDir());
+
+    // Make sure we start with a blank slate.
+    minimalRebuildCacheManager.deleteCaches();
+
+    // Construct and empty cache and also ask the manager to get a cache which does not exist.
+    MinimalRebuildCache emptyCache = new MinimalRebuildCache();
+    MinimalRebuildCache noSuchCache = minimalRebuildCacheManager.getCache("com.google.FooModule",
+        Maps.<String, String> newHashMap());
+
+    // Show that the manager created a new empty cache for the request of a cache that does not
+    // exist.
+    assertFalse(emptyCache == noSuchCache);
+    assertTrue(emptyCache.hasSameContent(noSuchCache));
+  }
+
+  public void testReload() throws InterruptedException {
+    File cacheDir = Files.createTempDir();
+
+    String moduleName = "com.google.FooModule";
+    MinimalRebuildCacheManager minimalRebuildCacheManager =
+        new MinimalRebuildCacheManager(TreeLogger.NULL, cacheDir);
+    Map<String, String> bindingProperites = Maps.<String, String> newHashMap();
+
+    // Make sure we start with a blank slate.
+    minimalRebuildCacheManager.deleteCaches();
+
+    MinimalRebuildCache startingCache =
+        minimalRebuildCacheManager.getCache(moduleName, bindingProperites);
+
+    // Record and compute a bunch of random data.
+    Map<String, Long> currentModifiedBySourcePath = new ImmutableMap.Builder<String, Long>().put(
+        "Foo.java", 0L).put("Bar.java", 0L).put("Baz.java", 0L).build();
+    startingCache.recordDiskSourceResources(currentModifiedBySourcePath);
+    startingCache.recordNestedTypeName("Foo", "Foo");
+    startingCache.setJsForType(TreeLogger.NULL, "Foo", "Some Js for Foo");
+    startingCache.addTypeReference("Bar", "Foo");
+    startingCache.getImmediateTypeRelations().getImmediateSuperclassesByClass().put("Baz", "Foo");
+    startingCache.addTypeReference("Foo", "Foo$Inner");
+    Map<String, Long> laterModifiedBySourcePath = new ImmutableMap.Builder<String, Long>().put(
+        "Foo.java", 9999L).put("Bar.java", 0L).put("Baz.java", 0L).build();
+    startingCache.recordDiskSourceResources(laterModifiedBySourcePath);
+    startingCache.setRootTypeNames(Sets.newHashSet("Foo", "Bar", "Baz"));
+    startingCache.computeReachableTypeNames();
+    startingCache.computeAndClearStaleTypesCache(TreeLogger.NULL,
+        new JTypeOracle(null, startingCache, true));
+
+    // Save and reload the cache.
+    minimalRebuildCacheManager.putCache(moduleName, bindingProperites, startingCache);
+
+    // Shutdown the cache manager and make sure it was successful.
+    assertTrue(minimalRebuildCacheManager.shutdown());
+
+    // Start a new cache manager in the same folder.
+    MinimalRebuildCacheManager reloadedMinimalRebuildCacheManager =
+        new MinimalRebuildCacheManager(TreeLogger.NULL, cacheDir);
+
+    // Reread the previously saved cache.
+    MinimalRebuildCache reloadedCache =
+        reloadedMinimalRebuildCacheManager.syncReadDiskCache(moduleName, bindingProperites);
+
+    // Show that the reread cache is a different instance.
+    assertFalse(startingCache == reloadedCache);
+    // Show that the reread cache contains the same data as the original.
+    assertTrue(startingCache.hasSameContent(reloadedCache));
+  }
+}