8056913: Limit the size of type info cache on disk
authorattila
Wed, 03 Sep 2014 14:33:34 +0200
changeset 26374 5bc67576b50e
parent 26373 cd907cf7bf7a
child 26375 a741f7808507
8056913: Limit the size of type info cache on disk Reviewed-by: jlaskey, lagergren
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/codegen/types/Type.java
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java	Mon Sep 01 17:34:37 2014 +0400
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java	Wed Sep 03 14:33:34 2014 +0200
@@ -31,9 +31,11 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.security.AccessController;
 import java.security.MessageDigest;
 import java.security.PrivilegedAction;
@@ -41,6 +43,14 @@
 import java.util.Base64;
 import java.util.Date;
 import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+import java.util.function.IntFunction;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
 import jdk.nashorn.internal.codegen.types.Type;
 import jdk.nashorn.internal.runtime.Context;
 import jdk.nashorn.internal.runtime.RecompilableScriptFunctionData;
@@ -49,30 +59,66 @@
 import jdk.nashorn.internal.runtime.options.Options;
 
 /**
- * Static utility that encapsulates persistence of decompilation information for functions. Normally, the type info
- * persistence feature is enabled and operates in an operating-system specific per-user cache directory. You can
- * override the directory by specifying it in the {@code nashorn.typeInfo.cacheDir} directory. Also, you can disable the
- * type info persistence altogether by specifying the {@code nashorn.typeInfo.disabled} system property.
+ * Static utility that encapsulates persistence of type information for functions compiled with optimistic
+ * typing. With this feature enabled, when a JavaScript function is recompiled because it gets deoptimized,
+ * the type information for deoptimization is stored in a cache file. If the same function is compiled in a
+ * subsequent JVM invocation, the type information is used for initial compilation, thus allowing the system
+ * to skip a lot of intermediate recompilations and immediately emit a version of the code that has its
+ * optimistic types at (or near) the steady state.
+ * </p><p>
+ * Normally, the type info persistence feature is disabled. When the {@code nashorn.typeInfo.maxFiles} system
+ * property is specified with a value greater than 0, it is enabled and operates in an operating-system
+ * specific per-user cache directory. You can override the directory by specifying it in the
+ * {@code nashorn.typeInfo.cacheDir} directory. The maximum number of files is softly enforced by a task that
+ * cleans up the directory periodically on a separate thread. It is run after some delay after a new file is
+ * added to the cache. The default delay is 20 seconds, and can be set using the
+ * {@code nashorn.typeInfo.cleanupDelaySeconds} system property. You can also specify the word
+ * {@code unlimited} as the value for {@code nashorn.typeInfo.maxFiles} in which case the type info cache is
+ * allowed to grow without limits.
  */
 public final class OptimisticTypesPersistence {
+    // Default is 0, for disabling the feature when not specified. A reasonable default when enabled is
+    // dependent on the application; setting it to e.g. 20000 is probably good enough for most uses and will
+    // usually cap the cache directory to about 80MB presuming a 4kB filesystem allocation unit. There is one
+    // file per JavaScript function.
+    private static final int DEFAULT_MAX_FILES = 0;
+    // Constants for signifying that the cache should not be limited
+    private static final int UNLIMITED_FILES = -1;
+    // Maximum number of files that should be cached on disk. The maximum will be softly enforced.
+    private static final int MAX_FILES = getMaxFiles();
+    // Number of seconds to wait between adding a new file to the cache and running a cleanup process
+    private static final int DEFAULT_CLEANUP_DELAY = 20;
+    private static final int CLEANUP_DELAY = Math.max(0, Options.getIntProperty(
+            "nashorn.typeInfo.cleanupDelaySeconds", DEFAULT_CLEANUP_DELAY));
     // The name of the default subdirectory within the system cache directory where we store type info.
     private static final String DEFAULT_CACHE_SUBDIR_NAME = "com.oracle.java.NashornTypeInfo";
     // The directory where we cache type info
-    private static final File cacheDir = createCacheDir();
+    private static final File baseCacheDir = createBaseCacheDir();
+    private static final File cacheDir = createCacheDir(baseCacheDir);
     // In-process locks to make sure we don't have a cross-thread race condition manipulating any file.
     private static final Object[] locks = cacheDir == null ? null : createLockArray();
-
     // Only report one read/write error every minute
     private static final long ERROR_REPORT_THRESHOLD = 60000L;
 
     private static volatile long lastReportedError;
-
+    private static final AtomicBoolean scheduledCleanup;
+    private static final Timer cleanupTimer;
+    static {
+        if (baseCacheDir == null || MAX_FILES == UNLIMITED_FILES) {
+            scheduledCleanup = null;
+            cleanupTimer = null;
+        } else {
+            scheduledCleanup = new AtomicBoolean();
+            cleanupTimer = new Timer(true);
+        }
+    }
     /**
-     * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed to
-     * {@link #load(Object)} and {@link #store(Object, Map)} methods.
+     * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed
+     * to {@link #load(Object)} and {@link #store(Object, Map)} methods.
      * @param source the source where the function comes from
      * @param functionId the unique ID number of the function within the source
-     * @param paramTypes the types of the function parameters (as persistence is per parameter type specialization).
+     * @param paramTypes the types of the function parameters (as persistence is per parameter type
+     * specialization).
      * @return an opaque descriptor for the persistence location. Can be null if persistence is disabled.
      */
     public static Object getLocationDescriptor(final Source source, final int functionId, final Type[] paramTypes) {
@@ -82,7 +128,8 @@
         final StringBuilder b = new StringBuilder(48);
         // Base64-encode the digest of the source, and append the function id.
         b.append(source.getDigest()).append('-').append(functionId);
-        // Finally, if this is a parameter-type specialized version of the function, add the parameter types to the file name.
+        // Finally, if this is a parameter-type specialized version of the function, add the parameter types
+        // to the file name.
         if(paramTypes != null && paramTypes.length > 0) {
             b.append('-');
             for(final Type t: paramTypes) {
@@ -118,6 +165,11 @@
             @Override
             public Void run() {
                 synchronized(getFileLock(file)) {
+                    if (!file.exists()) {
+                        // If the file already exists, we aren't increasing the number of cached files, so
+                        // don't schedule cleanup.
+                        scheduleCleanup();
+                    }
                     try (final FileOutputStream out = new FileOutputStream(file)) {
                         out.getChannel().lock(); // lock exclusive
                         final DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(out));
@@ -174,19 +226,19 @@
         }
     }
 
-    private static File createCacheDir() {
-        if(Options.getBooleanProperty("nashorn.typeInfo.disabled")) {
+    private static File createBaseCacheDir() {
+        if(MAX_FILES == 0 || Options.getBooleanProperty("nashorn.typeInfo.disabled")) {
             return null;
         }
         try {
-            return createCacheDirPrivileged();
+            return createBaseCacheDirPrivileged();
         } catch(final Exception e) {
             getLogger().warning("Failed to create cache dir", e);
             return null;
         }
     }
 
-    private static File createCacheDirPrivileged() {
+    private static File createBaseCacheDirPrivileged() {
         return AccessController.doPrivileged(new PrivilegedAction<File>() {
             @Override
             public File run() {
@@ -195,14 +247,35 @@
                 if(explicitDir != null) {
                     dir = new File(explicitDir);
                 } else {
-                    // When no directory is explicitly specified, get an operating system specific cache directory,
-                    // and create "com.oracle.java.NashornTypeInfo" in it.
+                    // When no directory is explicitly specified, get an operating system specific cache
+                    // directory, and create "com.oracle.java.NashornTypeInfo" in it.
                     final File systemCacheDir = getSystemCacheDir();
                     dir = new File(systemCacheDir, DEFAULT_CACHE_SUBDIR_NAME);
                     if (isSymbolicLink(dir)) {
                         return null;
                     }
                 }
+                return dir;
+            }
+        });
+    }
+
+    private static File createCacheDir(final File baseDir) {
+        if (baseDir == null) {
+            return null;
+        }
+        try {
+            return createCacheDirPrivileged(baseDir);
+        } catch(final Exception e) {
+            getLogger().warning("Failed to create cache dir", e);
+            return null;
+        }
+    }
+
+    private static File createCacheDirPrivileged(final File baseDir) {
+        return AccessController.doPrivileged(new PrivilegedAction<File>() {
+            @Override
+            public File run() {
                 final String versionDirName;
                 try {
                     versionDirName = getVersionDirName();
@@ -210,12 +283,12 @@
                     getLogger().warning("Failed to calculate version dir name", e);
                     return null;
                 }
-                final File versionDir = new File(dir, versionDirName);
+                final File versionDir = new File(baseDir, versionDirName);
                 if (isSymbolicLink(versionDir)) {
                     return null;
                 }
                 versionDir.mkdirs();
-                if(versionDir.isDirectory()) {
+                if (versionDir.isDirectory()) {
                     getLogger().info("Optimistic type persistence directory is " + versionDir);
                     return versionDir;
                 }
@@ -235,12 +308,12 @@
             // Mac OS X stores caches in ~/Library/Caches
             return new File(new File(System.getProperty("user.home"), "Library"), "Caches");
         } else if(os.startsWith("Windows")) {
-            // On Windows, temp directory is the best approximation of a cache directory, as its contents persist across
-            // reboots and various cleanup utilities know about it. java.io.tmpdir normally points to a user-specific
-            // temp directory, %HOME%\LocalSettings\Temp.
+            // On Windows, temp directory is the best approximation of a cache directory, as its contents
+            // persist across reboots and various cleanup utilities know about it. java.io.tmpdir normally
+            // points to a user-specific temp directory, %HOME%\LocalSettings\Temp.
             return new File(System.getProperty("java.io.tmpdir"));
         } else {
-            // In all other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache"
+            // In other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache"
             return new File(System.getProperty("user.home"), ".cache");
         }
     }
@@ -278,7 +351,8 @@
             final int packageNameLen = className.lastIndexOf('.');
             final String dirStr = fileStr.substring(0, fileStr.length() - packageNameLen - 1);
             final File dir = new File(dirStr);
-            return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile(dir, 0L)));
+            return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile(
+                    dir, 0L)));
         } else {
             throw new AssertionError();
         }
@@ -335,4 +409,108 @@
             return DebugLogger.DISABLED_LOGGER;
         }
     }
+
+    private static void scheduleCleanup() {
+        if (MAX_FILES != UNLIMITED_FILES && scheduledCleanup.compareAndSet(false, true)) {
+            cleanupTimer.schedule(new TimerTask() {
+                @Override
+                public void run() {
+                    scheduledCleanup.set(false);
+                    try {
+                        doCleanup();
+                    } catch (final IOException e) {
+                        // Ignore it. While this is unfortunate, we don't have good facility for reporting
+                        // this, as we're running in a thread that has no access to Context, so we can't grab
+                        // a DebugLogger.
+                    }
+                }
+            }, TimeUnit.SECONDS.toMillis(CLEANUP_DELAY));
+        }
+    }
+
+    private static void doCleanup() throws IOException {
+        final long start = System.nanoTime();
+        final Path[] files = getAllRegularFilesInLastModifiedOrder();
+        final int nFiles = files.length;
+        final int filesToDelete = Math.max(0, nFiles - MAX_FILES);
+        int filesDeleted = 0;
+        for (int i = 0; i < nFiles && filesDeleted < filesToDelete; ++i) {
+            try {
+                Files.deleteIfExists(files[i]);
+                // Even if it didn't exist, we increment filesDeleted; it existed a moment earlier; something
+                // else deleted it for us; that's okay with us.
+                filesDeleted++;
+            } catch (final Exception e) {
+                // does not increase filesDeleted
+            }
+            files[i] = null; // gc eligible
+        };
+        final long duration = System.nanoTime() - start;
+    }
+
+    private static Path[] getAllRegularFilesInLastModifiedOrder() throws IOException {
+        try (final Stream<Path> filesStream = Files.walk(baseCacheDir.toPath())) {
+            // TODO: rewrite below once we can use JDK8 syntactic constructs
+            return filesStream
+            .filter(new Predicate<Path>() {
+                @Override
+                public boolean test(final Path path) {
+                    return !Files.isDirectory(path);
+                };
+            })
+            .map(new Function<Path, PathAndTime>() {
+                @Override
+                public PathAndTime apply(final Path path) {
+                    return new PathAndTime(path);
+                }
+            })
+            .sorted()
+            .map(new Function<PathAndTime, Path>() {
+                @Override
+                public Path apply(final PathAndTime pathAndTime) {
+                    return pathAndTime.path;
+                }
+            })
+            .toArray(new IntFunction<Path[]>() { // Replace with Path::new
+                @Override
+                public Path[] apply(final int length) {
+                    return new Path[length];
+                }
+            });
+        }
+    }
+
+    private static class PathAndTime implements Comparable<PathAndTime> {
+        private final Path path;
+        private final long time;
+
+        PathAndTime(final Path path) {
+            this.path = path;
+            this.time = getTime(path);
+        }
+
+        @Override
+        public int compareTo(final PathAndTime other) {
+            return Long.compare(time, other.time);
+        }
+
+        private static long getTime(final Path path) {
+            try {
+                return Files.getLastModifiedTime(path).toMillis();
+            } catch (IOException e) {
+                // All files for which we can't retrieve the last modified date will be considered oldest.
+                return -1L;
+            }
+        }
+    }
+
+    private static int getMaxFiles() {
+        final String str = Options.getStringProperty("nashorn.typeInfo.maxFiles", null);
+        if (str == null) {
+            return DEFAULT_MAX_FILES;
+        } else if ("unlimited".equals(str)) {
+            return UNLIMITED_FILES;
+        }
+        return Math.max(0, Integer.parseInt(str));
+    }
 }
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/codegen/types/Type.java	Mon Sep 01 17:34:37 2014 +0400
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/codegen/types/Type.java	Wed Sep 03 14:33:34 2014 +0200
@@ -333,7 +333,7 @@
      */
     public static Map<Integer, Type> readTypeMap(final DataInput input) throws IOException {
         final int size = input.readInt();
-        if (size == 0) {
+        if (size <= 0) {
             return null;
         }
         final Map<Integer, Type> map = new TreeMap<>();
@@ -345,7 +345,7 @@
                 case 'L': type = Type.OBJECT; break;
                 case 'D': type = Type.NUMBER; break;
                 case 'J': type = Type.LONG; break;
-                default: throw new AssertionError();
+                default: continue;
             }
             map.put(pp, type);
         }