Revert "Introduce PersistentUnitCache.BackgroundService"

This reverts commit bf556abdfb0cef9d2e49e20d27bab0bd221ebfb7.

Adding synchronization introduced deadlock

Change-Id: Ie0d030dcb2ed683962d2ca44bc308d2b1bd8da04
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 661ec1a..8e09d02 100644
--- a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
+++ b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
@@ -19,21 +19,29 @@
 import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.dev.jjs.InternalCompilerException;
+import com.google.gwt.dev.jjs.impl.GwtAstBuilder;
+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.base.Preconditions;
-import com.google.gwt.thirdparty.guava.common.collect.Lists;
+import com.google.gwt.util.tools.Utility;
 
+import java.io.BufferedInputStream;
+import java.io.EOFException;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * A class that manages a persistent cache of {@link CompilationUnit} instances.
@@ -91,33 +99,100 @@
    */
   static final int CACHE_FILE_THRESHOLD = 40;
 
-  private final BackgroundService backgroundService;
+  /**
+   * There is no significance in the return value, we just want to be able
+   * to tell if the purgeOldCacheFilesTask has completed.
+   */
+  private Future<?> purgeTaskStatus;
+  private AtomicBoolean purgeInProgress = new AtomicBoolean(false);
 
-  private Semaphore cleanupInProgress = new Semaphore(1);
-  private AtomicInteger newUnitsSinceLastCleanup = new AtomicInteger();
+  private final Runnable shutdownThreadTask = new Runnable() {
+    @Override
+    public void run() {
+      logger.log(Type.TRACE, "Shutdown hook called for persistent unit cache");
+      cacheDir.closeCurrentFile();
+      logger.log(TreeLogger.TRACE, "Shutting down PersistentUnitCache thread");
+      backgroundService.shutdownNow();
+    }
+  };
+
+  /**
+   * Saved to be able to wait for UNIT_MAP_LOAD_TASK to complete.
+   */
+  private Future<?> unitMapLoadStatus;
+
+  /**
+   * Used to execute the above Runnables in a background thread.
+   */
+  private final ExecutorService backgroundService;
+
+  private int addedSinceLastCleanup = 0;
+
+  /**
+   * A directory to store the cache files that should persist between
+   * invocations.
+   */
+  private final PersistentUnitCacheDir cacheDir;
+  private final TreeLogger logger;
 
   PersistentUnitCache(final TreeLogger logger, File parentDir) throws UnableToCompleteException {
-    this.backgroundService = new BackgroundService(logger, parentDir, this);
+    this.logger = logger;
+    cacheDir = new PersistentUnitCacheDir(logger, parentDir);
+
+    backgroundService = Executors.newSingleThreadExecutor();
+    Runtime.getRuntime().addShutdownHook(new Thread() {
+      @Override
+      public void run() {
+        try {
+          Future<Boolean> status = backgroundService.submit(shutdownThreadTask, Boolean.TRUE);
+          // Don't let the shutdown hang more than 5 seconds
+          status.get(5, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+          // ignore
+        } catch (RejectedExecutionException e) {
+          // already shutdown, ignore
+        } catch (ExecutionException e) {
+          logger.log(TreeLogger.ERROR, "Error during shutdown", e);
+        } catch (TimeoutException e) {
+          // ignore
+        } finally {
+          backgroundService.shutdownNow();
+        }
+      }
+    });
+
+    /**
+     * Load up cached units from the persistent store in the background. The
+     * {@link #add(CompilationUnit)} and {@link #find(String)} methods block if
+     * invoked before this thread finishes.
+     */
+    unitMapLoadStatus = backgroundService.submit(new Runnable() {
+      @Override
+      public void run() {
+        loadUnitMap();
+      }
+    });
   }
 
   /**
    * Enqueue a unit to be written by the background thread.
    */
   @Override
-  public synchronized  void add(CompilationUnit newUnit) {
+  public void add(CompilationUnit newUnit) {
     internalAdd(newUnit);
   }
 
   @VisibleForTesting
   Future<?> internalAdd(CompilationUnit newUnit) {
-    Preconditions.checkNotNull(newUnit);
-    backgroundService.waitForCacheToLoad();
-    addNewUnit(newUnit);
-    return backgroundService.asyncWriteUnit(newUnit);
+    awaitUnitCacheMapLoad();
+    addedSinceLastCleanup++;
+    super.add(newUnit);
+    return addImpl(unitMap.get(newUnit.getResourcePath()));
   }
 
   /**
-   * Rotates to a new file and/or starts garbage collection if needed after a compile is finished.
+   * Cleans up old cache files in the directory, migrating everything previously
+   * loaded in them to the current cache file.
    *
    * Normally, only newly compiled units are written to the current log, but
    * when it is time to cleanup, valid units from older log files need to be
@@ -125,297 +200,238 @@
    */
   @Override
   public void cleanup(TreeLogger logger) {
-    logger.log(Type.TRACE, "PersistentUnitCache cleanup requested");
-    backgroundService.waitForCacheToLoad();
+    logger.log(Type.TRACE, "Cleanup called");
+    awaitUnitCacheMapLoad();
 
     if (backgroundService.isShutdown()) {
-      logger.log(TreeLogger.TRACE, "Skipped PersistentUnitCache cleanup because it's shut down");
+      logger.log(TreeLogger.TRACE, "Skipped cleanup");
       return;
     }
+    boolean shouldRotate = addedSinceLastCleanup > 0;
+    logger.log(TreeLogger.TRACE, "Added " + addedSinceLastCleanup +
+        " units to cache since last cleanup.");
+    addedSinceLastCleanup = 0;
+    try {
+      List<File> cacheFiles = cacheDir.listCacheFilesToLoad();
+      logger.log(TreeLogger.TRACE, cacheFiles.size() + " persistent unit files in directory");
+      if (cacheFiles.size() < CACHE_FILE_THRESHOLD) {
+        if (shouldRotate) {
+          startRotating();
+        }
+        return;
+      }
 
-    if (!cleanupInProgress.tryAcquire()) {
-      return; // some other thread is already doing this.
+      // Check to see if the previous purge task finished.
+      boolean inProgress = purgeInProgress.getAndSet(true);
+      if (inProgress) {
+        try {
+          purgeTaskStatus.get(0, TimeUnit.NANOSECONDS);
+        } catch (InterruptedException ex) {
+          Thread.currentThread().interrupt();
+        } catch (TimeoutException ex) {
+          // purge is currently in progress.
+          return;
+        }
+      }
+
+      logger.log(Type.TRACE, "Cleaning up persistent unit cache files");
+      /*
+       * Resend all units read in from the in-memory cache to the background
+       * thread. They will be re-written out and the old cache files removed.
+       */
+      synchronized (unitMap) {
+        for (UnitCacheEntry unitCacheEntry : unitMap.values()) {
+          if (unitCacheEntry.getOrigin() == UnitOrigin.ARCHIVE) {
+            // Units from GWTAR archives should not be kept in the persistent unit cache on disk
+            // because they are already being kept in their original GWTAR file location.
+            continue;
+          }
+          addImpl(unitCacheEntry);
+        }
+      }
+
+      purgeTaskStatus = backgroundService.submit(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            cacheDir.deleteClosedCacheFiles();
+            cacheDir.rotate();
+          } catch (UnableToCompleteException e) {
+            backgroundService.shutdownNow();
+          } finally {
+            purgeInProgress.set(false);
+          }
+        }
+      }, Boolean.TRUE);
+
+    } catch (ExecutionException ex) {
+      throw new InternalCompilerException("Error purging cache", ex);
+    } catch (RejectedExecutionException ex) {
+      // Cache background thread is not running - ignore
     }
-
-    int addCallCount = newUnitsSinceLastCleanup.getAndSet(0);
-    logger.log(TreeLogger.TRACE, "Added " + addCallCount +
-        " units to PersistentUnitCache since last cleanup");
-    if (addCallCount == 0) {
-      // Don't clean up until we compiled something.
-      logger.log(TreeLogger.TRACE, "Skipped PersistentUnitCache because no units were added");
-      cleanupInProgress.release();
-      return;
-    }
-
-    int closedCount = backgroundService.getClosedCacheFileCount();
-    if (closedCount < CACHE_FILE_THRESHOLD) {
-      // Not enough files yet, so just rotate to a new file.
-      logger.log(TreeLogger.TRACE, "Rotating PersistentUnitCache file because only " +
-          closedCount + " files were added.");
-      backgroundService.asyncRotate(cleanupInProgress);
-      return;
-    }
-
-    logger.log(Type.TRACE, "Compacting persistent unit cache files");
-    backgroundService.asyncCompact(getUnitsToSaveToDisk(), cleanupInProgress);
-  }
-
-  /**
-   * Waits for any cleanup in progress to finish.
-   */
-  @VisibleForTesting
-  void waitForCleanup() throws InterruptedException {
-    cleanupInProgress.acquire();
-    cleanupInProgress.release();
   }
 
   @VisibleForTesting
-  void shutdown() throws InterruptedException, ExecutionException {
-    backgroundService.shutdown();
+  Future<?> startRotating() {
+    return backgroundService.submit(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          cacheDir.rotate();
+        } catch (UnableToCompleteException e) {
+          backgroundService.shutdownNow();
+        }
+      }
+    });
   }
 
-  // Methods that read or write the in-memory cache
-
   @Override
-  public synchronized CompilationUnit find(ContentId contentId) {
-    backgroundService.waitForCacheToLoad();
+  public CompilationUnit find(ContentId contentId) {
+    awaitUnitCacheMapLoad();
     return super.find(contentId);
   }
 
   @Override
-  public synchronized CompilationUnit find(String resourcePath) {
-    backgroundService.waitForCacheToLoad();
+  public CompilationUnit find(String resourcePath) {
+    awaitUnitCacheMapLoad();
     return super.find(resourcePath);
   }
 
-  @Override
-  public synchronized void remove(CompilationUnit unit) {
-    super.remove(unit);
-  }
-
   /**
-   * Saves a newly compiled unit to the in-memory cache.
+   * For Unit testing - shutdown the persistent cache.
+   *
+   * @throws ExecutionException
+   * @throws InterruptedException
    */
-  private synchronized void addNewUnit(CompilationUnit unit) {
-    newUnitsSinceLastCleanup.incrementAndGet();
-    super.add(unit);
-  }
-
-  /**
-   * Adds a compilation unit from disk into the in-memory cache.
-   * (Callback from {@link PersistentUnitCacheDir}.)
-   */
-  synchronized void maybeAddLoadedUnit(CachedCompilationUnit unit) {
-    UnitCacheEntry entry = new UnitCacheEntry(unit, UnitOrigin.PERSISTENT);
-    UnitCacheEntry existingEntry = unitMap.get(unit.getResourcePath());
-        /*
-         * Don't assume that an existing entry is stale - an entry might
-         * have been loaded already from another source like a
-         * CompilationUnitArchive that is more up to date. If the
-         * timestamps are the same, accept the latest version. If it turns
-         * out to be stale, it will be recompiled and the updated unit
-         * will win this test the next time the session starts.
-         */
-    if (existingEntry != null
-        && unit.getLastModified() >= existingEntry.getUnit().getLastModified()) {
-      super.remove(existingEntry.getUnit());
-      unitMap.put(unit.getResourcePath(), entry);
-      unitMapByContentId.put(unit.getContentId(), entry);
-    } else if (existingEntry == null) {
-      unitMap.put(unit.getResourcePath(), entry);
-      unitMapByContentId.put(unit.getContentId(), entry);
+  void shutdown() throws InterruptedException, ExecutionException {
+    logger.log(Type.INFO, "shutdown called");
+    try {
+      Future<?> future = backgroundService.submit(shutdownThreadTask);
+      backgroundService.shutdown();
+      future.get();
+    } catch (RejectedExecutionException ex) {
+      // background thread is not running - ignore
     }
   }
 
-  private synchronized List<CompilationUnit> getUnitsToSaveToDisk() {
-    List<CompilationUnit> result = Lists.newArrayList();
-    for (UnitCacheEntry entry : unitMap.values()) {
-      // Units from GWTAR archives should not be kept in the persistent unit cache on disk
-      // because they are already being kept in their original GWTAR file location.
-      if (entry.getOrigin() != UnitOrigin.ARCHIVE) {
-        result.add(Preconditions.checkNotNull(entry.getUnit()));
-      }
-    }
-    return result;
-  }
-
-  /**
-   * Implements async methods that run in the background.
-   */
-  private static class BackgroundService {
-
-    private final TreeLogger logger;
-    private final PersistentUnitCacheDir cacheDir;
-    private final ExecutorService service;
-
-    /**
-     * Non-null while the unit cache is loading.
-     */
-    private Future<?> loadingDone;
-
-    /**
-     * Starts the background thread and starts loading the given unit cache in the background.
-     */
-    BackgroundService(TreeLogger logger, File parentDir, final PersistentUnitCache cacheToLoad)
-        throws UnableToCompleteException {
-      this.logger = logger;
-      this.cacheDir = new PersistentUnitCacheDir(logger, parentDir);
-
-      service = Executors.newSingleThreadExecutor();
-      Runtime.getRuntime().addShutdownHook(new Thread() {
+  private Future<?> addImpl(final UnitCacheEntry entry) {
+    try {
+      return backgroundService.submit(new Runnable() {
         @Override
         public void run() {
           try {
-            Future<?> status = asyncShutdown();
-            // Don't let the shutdown hang more than 5 seconds
-            status.get(5, TimeUnit.SECONDS);
-          } catch (InterruptedException e) {
-            // ignore
-          } catch (RejectedExecutionException e) {
-            // already shutdown, ignore
-          } catch (ExecutionException e) {
-            BackgroundService.this.logger.log(TreeLogger.ERROR, "Error during shutdown", e);
-          } catch (TimeoutException e) {
-            // ignore
-          } finally {
-            shutdownNow();
-          }
-        }
-      });
-
-      /**
-       * Load up cached units from the persistent store in the background. The
-       * {@link #add(CompilationUnit)} and {@link #find(String)} methods block if
-       * invoked before this thread finishes.
-       */
-      loadingDone = service.submit(new Runnable() {
-        @Override
-        public void run() {
-          cacheDir.loadUnitMap(cacheToLoad);
-        }
-      });
-    }
-
-    /**
-     * Blocks until the background service is done loading units into the in-memory cache.
-     */
-    synchronized void waitForCacheToLoad() {
-      if (loadingDone == null) {
-        return; // fast path
-      }
-
-      try {
-        loadingDone.get();
-        loadingDone = null;
-      } catch (InterruptedException e) {
-        throw new InternalCompilerException(
-            "Interrupted waiting for PersistentUnitCache to load.", e);
-      } catch (ExecutionException e) {
-        logger.log(TreeLogger.ERROR, "Failed to load PersistentUnitCache.", e);
-        // Keep going. We didn't load anything but will still save units to the cache.
-        loadingDone = null;
-      }
-    }
-
-    boolean isShutdown() {
-      return service.isShutdown();
-    }
-
-    @VisibleForTesting
-    void shutdown() throws InterruptedException, ExecutionException {
-      logger.log(Type.INFO, "PersistentUnitCache shutdown requested");
-      try {
-        asyncShutdown().get();
-      } catch (RejectedExecutionException ex) {
-        // background thread is not running - ignore
-      }
-    }
-
-    int getClosedCacheFileCount() {
-      return cacheDir.getClosedCacheFileCount();
-    }
-
-    /**
-     * Rotates to a new file.
-     * @param cleanupInProgress a semaphore to release when done.
-     * (The permit must already be acquired.)
-     */
-    Future<?> asyncRotate(final Semaphore cleanupInProgress) {
-      return service.submit(new Runnable() {
-        @Override
-        public void run() {
-          try {
-            cacheDir.rotate();
+            cacheDir.writeObject(entry);
           } catch (UnableToCompleteException e) {
-            shutdownNow();
-          } finally {
-            cleanupInProgress.release();
+            backgroundService.shutdownNow();
           }
         }
       });
+    } catch (RejectedExecutionException ex) {
+      // background thread is not running, ignore
+      return null;
     }
+  }
 
-    /**
-     * Compacts the persistent unit cache and then rotates to a new file.
-     * There will be one closed file and one empty, open file when done.
-     * @param unitsToSave all compilation units to keep
-     * @param cleanupInProgress a semaphore to release when done.
-     * (The permit must already be acquired.)
-     */
-    Future<?> asyncCompact(final List<CompilationUnit> unitsToSave,
-        final Semaphore cleanupInProgress) {
+  private synchronized void awaitUnitCacheMapLoad() {
+    // wait on initial load of unit map to complete.
+    try {
+      if (unitMapLoadStatus != null) {
+        unitMapLoadStatus.get();
+        // no need to check any more.
+        unitMapLoadStatus = null;
+      }
+    } catch (InterruptedException e) {
+      throw new InternalCompilerException("Interrupted waiting for unit cache map to load.", e);
+    } catch (ExecutionException e) {
+      logger.log(TreeLogger.ERROR, "Failure in unit cache map load.", e);
+      // keep going
+      unitMapLoadStatus = null;
+    }
+  }
 
-      return service.submit(new Runnable() {
-        @Override
-        public void run() {
-          try {
-            for (CompilationUnit unit : unitsToSave) {
-              cacheDir.writeUnit(unit);
+  /**
+   * Load everything cached on disk into memory.
+   */
+  private void loadUnitMap() {
+    Event loadPersistentUnitEvent =
+        SpeedTracerLogger.start(DevModeEventType.LOAD_PERSISTENT_UNIT_CACHE);
+    if (logger.isLoggable(TreeLogger.TRACE)) {
+      logger.log(TreeLogger.TRACE, "Looking for previously cached Compilation Units in "
+          + cacheDir.getPath());
+    }
+    try {
+      List<File> files = cacheDir.listCacheFilesToLoad();
+      for (File cacheFile : files) {
+        FileInputStream fis = null;
+        BufferedInputStream bis = null;
+        ObjectInputStream inputStream = null;
+        boolean deleteCacheFile = false;
+        try {
+          fis = new FileInputStream(cacheFile);
+          bis = new BufferedInputStream(fis);
+          /*
+           * It is possible for the next call to throw an exception, leaving
+           * inputStream null and fis still live.
+           */
+          inputStream = new StringInterningObjectInputStream(bis);
+          while (true) {
+            CachedCompilationUnit unit = (CachedCompilationUnit) inputStream.readObject();
+            if (unit == null) {
+              break;
             }
-            cacheDir.deleteClosedCacheFiles();
-            cacheDir.rotate(); // Move to a new, empty file.
-          } catch (UnableToCompleteException e) {
-            shutdownNow();
-          } finally {
-            cleanupInProgress.release();
-          }
-        }
-      });
-    }
-
-    Future<?> asyncWriteUnit(final CompilationUnit unit) {
-      try {
-        return service.submit(new Runnable() {
-          @Override
-          public void run() {
-            try {
-              cacheDir.writeUnit(unit);
-            } catch (UnableToCompleteException e) {
-              shutdownNow();
+            if (unit.getTypesSerializedVersion() != GwtAstBuilder.getSerializationVersion()) {
+              continue;
+            }
+            UnitCacheEntry entry = new UnitCacheEntry(unit, UnitOrigin.PERSISTENT);
+            UnitCacheEntry existingEntry = unitMap.get(unit.getResourcePath());
+            /*
+             * Don't assume that an existing entry is stale - an entry might
+             * have been loaded already from another source like a
+             * CompilationUnitArchive that is more up to date. If the
+             * timestamps are the same, accept the latest version. If it turns
+             * out to be stale, it will be recompiled and the updated unit
+             * will win this test the next time the session starts.
+             */
+            if (existingEntry != null
+                && unit.getLastModified() >= existingEntry.getUnit().getLastModified()) {
+              super.remove(existingEntry.getUnit());
+              unitMap.put(unit.getResourcePath(), entry);
+              unitMapByContentId.put(unit.getContentId(), entry);
+            } else if (existingEntry == null) {
+              unitMap.put(unit.getResourcePath(), entry);
+              unitMapByContentId.put(unit.getContentId(), entry);
             }
           }
-        });
-      } catch (RejectedExecutionException ex) {
-        // background thread is not running, ignore
-        return null;
-      }
-    }
-
-    Future<?> asyncShutdown() {
-      Future<?> status = service.submit(new Runnable() {
-        @Override
-        public void run() {
-          cacheDir.closeCurrentFile();
-          shutdownNow();
+        } catch (EOFException ex) {
+          // Go on to the next file.
+        } catch (IOException ex) {
+          deleteCacheFile = true;
+          if (logger.isLoggable(TreeLogger.TRACE)) {
+            logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log "
+                + cacheFile.getAbsolutePath() + " due to read error.", ex);
+          }
+        } catch (ClassNotFoundException ex) {
+          deleteCacheFile = true;
+          if (logger.isLoggable(TreeLogger.TRACE)) {
+            logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log "
+                + cacheFile.getAbsolutePath() + " due to deserialization error.", ex);
+          }
+        } finally {
+          Utility.close(inputStream);
+          Utility.close(bis);
+          Utility.close(fis);
         }
-      });
-      service.shutdown(); // Don't allow more tasks to be scheduled.
-      return status;
-    }
-
-    private void shutdownNow() {
-      logger.log(TreeLogger.TRACE, "Shutting down PersistentUnitCache thread");
-      service.shutdownNow();
+        if (deleteCacheFile) {
+          logger.log(Type.WARN, "Deleting " + cacheFile + " due to an exception");
+          cacheDir.deleteUnlessOpen(cacheFile);
+        } else {
+          if (logger.isLoggable(TreeLogger.TRACE)) {
+            logger.log(TreeLogger.TRACE, cacheFile.getName() + ": Load complete");
+          }
+        }
+      }
+    } finally {
+      loadPersistentUnitEvent.end();
     }
   }
 }
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..e072f19 100644
--- a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCacheDir.java
+++ b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCacheDir.java
@@ -18,26 +18,21 @@
 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.javac.MemoryUnitCache.UnitCacheEntry;
 import com.google.gwt.dev.jjs.ast.JNode;
-import com.google.gwt.dev.jjs.impl.GwtAstBuilder;
-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.base.Preconditions;
 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;
 import java.io.BufferedOutputStream;
-import java.io.EOFException;
 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.net.JarURLConnection;
 import java.net.URLConnection;
@@ -111,33 +106,6 @@
   }
 
   /**
-   * Returns the number of files written to the cache directory and closed.
-   */
-  synchronized int getClosedCacheFileCount() {
-    return selectClosedFiles(listFiles(CURRENT_VERSION_CACHE_FILE_PREFIX)).size();
-  }
-
-  /**
-   * Load everything cached on disk into memory.
-   */
-  synchronized void loadUnitMap(PersistentUnitCache destination) {
-    Event loadPersistentUnitEvent =
-        SpeedTracerLogger.start(DevModeEventType.LOAD_PERSISTENT_UNIT_CACHE);
-    if (logger.isLoggable(TreeLogger.TRACE)) {
-      logger.log(TreeLogger.TRACE, "Looking for previously cached Compilation Units in "
-          + getPath());
-    }
-    try {
-      List<File> files = selectClosedFiles(listFiles(CURRENT_VERSION_CACHE_FILE_PREFIX));
-      for (File cacheFile : files) {
-        loadOrDeleteCacheFile(cacheFile, destination);
-      }
-    } finally {
-      loadPersistentUnitEvent.end();
-    }
-  }
-
-  /**
    * Delete all cache files in the directory except for the currently open file.
    */
   synchronized void deleteClosedCacheFiles() {
@@ -170,6 +138,13 @@
   }
 
   /**
+   * Returns the files that should be loaded at startup to refill the cache.
+   */
+  synchronized List<File> listCacheFilesToLoad() {
+    return selectClosedFiles(listFiles(CURRENT_VERSION_CACHE_FILE_PREFIX));
+  }
+
+  /**
    * Deletes the given file unless it's currently open for writing.
    */
   synchronized boolean deleteUnlessOpen(File cacheFile) {
@@ -185,14 +160,14 @@
   }
 
   /**
-   * Writes a compilation unit to the disk cache.
+   * Writes an entry to the cache.
    */
-  synchronized void writeUnit(CompilationUnit unit) throws UnableToCompleteException {
+  synchronized void writeObject(UnitCacheEntry entry) throws UnableToCompleteException {
     if (openFile == null) {
       logger.log(Type.TRACE, "Skipped writing compilation unit to cache because no file is open");
       return;
     }
-    openFile.writeUnit(logger, unit);
+    openFile.writeObject(logger, entry);
   }
 
   /**
@@ -216,66 +191,6 @@
   }
 
   /**
-   * Loads all the units in a cache file into the given cache.
-   * Delete it if unable to read it.
-   */
-  private void loadOrDeleteCacheFile(File cacheFile, PersistentUnitCache destination) {
-    FileInputStream fis = null;
-    BufferedInputStream bis = null;
-    ObjectInputStream inputStream = null;
-
-    boolean ok = false;
-    int unitsLoaded = 0;
-    try {
-      fis = new FileInputStream(cacheFile);
-      bis = new BufferedInputStream(fis);
-      /*
-       * It is possible for the next call to throw an exception, leaving
-       * inputStream null and fis still live.
-       */
-      inputStream = new StringInterningObjectInputStream(bis);
-
-      // Read objects until we get an EOF exception.
-      while (true) {
-        CachedCompilationUnit unit = (CachedCompilationUnit) inputStream.readObject();
-        if (unit == null) {
-          // Won't normally get here. Not sure why this check was here before.
-          logger.log(Type.WARN, "unexpected null in cache file: " + cacheFile);
-          break;
-        }
-        if (unit.getTypesSerializedVersion() != GwtAstBuilder.getSerializationVersion()) {
-          continue;
-        }
-        destination.maybeAddLoadedUnit(unit);
-        unitsLoaded++;
-      }
-
-    } catch (EOFException ignored) {
-      // This is a normal exit. Go on to the next file.
-      ok = true;
-    } catch (IOException e) {
-      logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log "
-          + cacheFile.getAbsolutePath() + " due to read error.", e);
-    } catch (ClassNotFoundException e) {
-      logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log "
-          + cacheFile.getAbsolutePath() + " due to deserialization error.", e);
-    } finally {
-      Utility.close(inputStream);
-      Utility.close(bis);
-      Utility.close(fis);
-    }
-
-    if (ok) {
-      logger.log(TreeLogger.TRACE, "Loaded " + unitsLoaded +
-          " units from cache file: " + cacheFile.getName());
-    } else {
-      deleteUnlessOpen(cacheFile);
-      logger.log(TreeLogger.TRACE, "Loaded " + unitsLoaded +
-          " units from invalid cache file before deleting it: " + cacheFile.getName());
-    }
-  }
-
-  /**
    * Lists files in the cache directory that start with the given prefix.
    *
    * <p>The files will be sorted according to {@link java.io.File#compareTo}, which
@@ -386,12 +301,13 @@
     }
 
     /**
-     * Writes a compilation unit to the currently open file, if any.
+     * Writes an entry to the currently open file, if any.
      * @return true if written
      * @throws UnableToCompleteException if the file was open but we can't append.
      */
-    boolean writeUnit(TreeLogger logger, CompilationUnit unit)
+    boolean writeObject(TreeLogger logger, UnitCacheEntry entry)
         throws UnableToCompleteException {
+      CompilationUnit unit = Preconditions.checkNotNull(entry.getUnit());
       try {
         stream.writeObject(unit);
         unitsWritten++;
diff --git a/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java b/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
index 59f3e81..5dadf9c 100644
--- a/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
+++ b/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
@@ -19,6 +19,7 @@
 import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.dev.util.Util;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 import com.google.gwt.thirdparty.guava.common.util.concurrent.Futures;
 
 import junit.framework.TestCase;
@@ -32,6 +33,7 @@
 import java.io.Serializable;
 import java.util.Arrays;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
 
 /**
  * Unit test for {@link PersistentUnitCache}.
@@ -58,8 +60,8 @@
   @Override
   protected void setUp() throws Exception {
     super.setUp();
-    logger = TreeLogger.NULL;
-    // logger = new PrintWriterTreeLogger(); // uncomment for debugging
+//    logger = TreeLogger.NULL; // Change to PrintWriterTreeLogger to debug tests
+    logger = new PrintWriterTreeLogger();
     logger.log(Type.INFO, "\n\n*** Running " + getName());
   }
 
@@ -125,7 +127,7 @@
     File parentDir = lastParentDir = File.createTempFile("persistentCacheTest", "");
     File unitCacheDir = mkCacheDir(parentDir);
 
-    PersistentUnitCache cache = new PersistentUnitCache(logger, parentDir);
+    PersistentUnitCache cache = makeUnitCache(parentDir);
 
     MockCompilationUnit foo1 = new MockCompilationUnit("com.example.Foo", "Foo: source1");
     cache.add(foo1);
@@ -162,7 +164,6 @@
     assertEquals("com.example.Foo", result.getTypeName());
     assertEquals(foo2.getContentId(), result.getContentId());
     cache.cleanup(logger);
-    cache.waitForCleanup();
 
     // Shutdown the cache and re -load it
     cache.shutdown();
@@ -172,7 +173,7 @@
 
     // Fire up the cache again. It should be pre-populated.
     // Search by type name
-    cache = new PersistentUnitCache(logger, parentDir);
+    cache = makeUnitCache(parentDir);
     result = cache.find("com/example/Foo.java");
     assertNotNull(result);
     assertEquals("com.example.Foo", result.getTypeName());
@@ -195,7 +196,6 @@
     assertEquals(foo2.getTypeName(), result.getTypeName());
     assertEquals(foo2.getContentId(), result.getContentId());
     cache.cleanup(logger);
-    cache.waitForCleanup();
 
     // We didn't write anything, still 1 file.
     cache.shutdown();
@@ -204,7 +204,7 @@
     assertNumCacheFiles(unitCacheDir, 1);
 
     // Fire up the cache again. (Creates a second file in the background.)
-    cache = new PersistentUnitCache(logger, parentDir);
+    cache = makeUnitCache(parentDir);
 
     // keep making more files
     MockCompilationUnit lastUnit = null;
@@ -216,12 +216,11 @@
       // force rotation to a new file
       // (Normally async but we overrode it.)
       cache.cleanup(logger);
-      cache.waitForCleanup();
       assertNumCacheFiles(unitCacheDir, i + 1);
     }
 
     // One last check, we should load the last unit added to the cache.
-    cache = new PersistentUnitCache(logger, parentDir);
+    cache = makeUnitCache(parentDir);
     result = cache.find(lastUnit.getContentId());
     assertNotNull(result);
     assertEquals("com.example.Foo", result.getTypeName());
@@ -245,7 +244,6 @@
     lastUnit = new MockCompilationUnit("com.example.Baz", "Baz Source");
     cache.add(lastUnit);
     cache.cleanup(logger);
-    cache.waitForCleanup();
 
     // Add one more to put us over the top.
     lastUnit = new MockCompilationUnit("com.example.Qux", "Qux Source");
@@ -255,14 +253,13 @@
     assertNumCacheFiles(unitCacheDir, PersistentUnitCache.CACHE_FILE_THRESHOLD + 1);
 
     cache.cleanup(logger);
-    cache.waitForCleanup();
     cache.shutdown();
 
     // There should be a single file in the cache dir.
     assertNumCacheFiles(unitCacheDir, 1);
 
     // Fire up the cache on this one coalesced file.
-    cache = new PersistentUnitCache(logger, parentDir);
+    cache = makeUnitCache(parentDir);
 
     // Verify that we can still find the content that was coalesced.
     assertNotNull(cache.find("com/example/Foo.java"));
@@ -271,6 +268,17 @@
     assertNotNull(cache.find("com/example/Qux.java"));
   }
 
+  private PersistentUnitCache makeUnitCache(File cacheDir) throws UnableToCompleteException {
+    return new PersistentUnitCache(logger, cacheDir) {
+      @Override
+      Future<Void> startRotating() {
+        // wait for rotation to finish to avoid flakiness
+        Futures.getUnchecked(super.startRotating());
+        return Futures.immediateFuture(null);
+      }
+    };
+  }
+
   private void assertNumCacheFiles(File unitCacheDir, int expected) {
     String[] actualFiles = unitCacheDir.list();
     if (expected == actualFiles.length) {