8056913: Limit the size of type info cache on disk

Wed, 03 Sep 2014 14:33:34 +0200

author
attila
date
Wed, 03 Sep 2014 14:33:34 +0200
changeset 990
46647c4943ff
parent 987
34c17c956654
child 991
b7a2db4de254

8056913: Limit the size of type info cache on disk
Reviewed-by: jlaskey, lagergren

src/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java file | annotate | diff | comparison | revisions
src/jdk/nashorn/internal/codegen/types/Type.java file | annotate | diff | comparison | revisions
     1.1 --- a/src/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java	Thu Aug 28 16:10:38 2014 -0700
     1.2 +++ b/src/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java	Wed Sep 03 14:33:34 2014 +0200
     1.3 @@ -31,9 +31,11 @@
     1.4  import java.io.File;
     1.5  import java.io.FileInputStream;
     1.6  import java.io.FileOutputStream;
     1.7 +import java.io.IOException;
     1.8  import java.io.InputStream;
     1.9  import java.net.URL;
    1.10  import java.nio.file.Files;
    1.11 +import java.nio.file.Path;
    1.12  import java.security.AccessController;
    1.13  import java.security.MessageDigest;
    1.14  import java.security.PrivilegedAction;
    1.15 @@ -41,6 +43,14 @@
    1.16  import java.util.Base64;
    1.17  import java.util.Date;
    1.18  import java.util.Map;
    1.19 +import java.util.Timer;
    1.20 +import java.util.TimerTask;
    1.21 +import java.util.concurrent.TimeUnit;
    1.22 +import java.util.concurrent.atomic.AtomicBoolean;
    1.23 +import java.util.function.Function;
    1.24 +import java.util.function.IntFunction;
    1.25 +import java.util.function.Predicate;
    1.26 +import java.util.stream.Stream;
    1.27  import jdk.nashorn.internal.codegen.types.Type;
    1.28  import jdk.nashorn.internal.runtime.Context;
    1.29  import jdk.nashorn.internal.runtime.RecompilableScriptFunctionData;
    1.30 @@ -49,30 +59,66 @@
    1.31  import jdk.nashorn.internal.runtime.options.Options;
    1.32  
    1.33  /**
    1.34 - * Static utility that encapsulates persistence of decompilation information for functions. Normally, the type info
    1.35 - * persistence feature is enabled and operates in an operating-system specific per-user cache directory. You can
    1.36 - * override the directory by specifying it in the {@code nashorn.typeInfo.cacheDir} directory. Also, you can disable the
    1.37 - * type info persistence altogether by specifying the {@code nashorn.typeInfo.disabled} system property.
    1.38 + * Static utility that encapsulates persistence of type information for functions compiled with optimistic
    1.39 + * typing. With this feature enabled, when a JavaScript function is recompiled because it gets deoptimized,
    1.40 + * the type information for deoptimization is stored in a cache file. If the same function is compiled in a
    1.41 + * subsequent JVM invocation, the type information is used for initial compilation, thus allowing the system
    1.42 + * to skip a lot of intermediate recompilations and immediately emit a version of the code that has its
    1.43 + * optimistic types at (or near) the steady state.
    1.44 + * </p><p>
    1.45 + * Normally, the type info persistence feature is disabled. When the {@code nashorn.typeInfo.maxFiles} system
    1.46 + * property is specified with a value greater than 0, it is enabled and operates in an operating-system
    1.47 + * specific per-user cache directory. You can override the directory by specifying it in the
    1.48 + * {@code nashorn.typeInfo.cacheDir} directory. The maximum number of files is softly enforced by a task that
    1.49 + * cleans up the directory periodically on a separate thread. It is run after some delay after a new file is
    1.50 + * added to the cache. The default delay is 20 seconds, and can be set using the
    1.51 + * {@code nashorn.typeInfo.cleanupDelaySeconds} system property. You can also specify the word
    1.52 + * {@code unlimited} as the value for {@code nashorn.typeInfo.maxFiles} in which case the type info cache is
    1.53 + * allowed to grow without limits.
    1.54   */
    1.55  public final class OptimisticTypesPersistence {
    1.56 +    // Default is 0, for disabling the feature when not specified. A reasonable default when enabled is
    1.57 +    // dependent on the application; setting it to e.g. 20000 is probably good enough for most uses and will
    1.58 +    // usually cap the cache directory to about 80MB presuming a 4kB filesystem allocation unit. There is one
    1.59 +    // file per JavaScript function.
    1.60 +    private static final int DEFAULT_MAX_FILES = 0;
    1.61 +    // Constants for signifying that the cache should not be limited
    1.62 +    private static final int UNLIMITED_FILES = -1;
    1.63 +    // Maximum number of files that should be cached on disk. The maximum will be softly enforced.
    1.64 +    private static final int MAX_FILES = getMaxFiles();
    1.65 +    // Number of seconds to wait between adding a new file to the cache and running a cleanup process
    1.66 +    private static final int DEFAULT_CLEANUP_DELAY = 20;
    1.67 +    private static final int CLEANUP_DELAY = Math.max(0, Options.getIntProperty(
    1.68 +            "nashorn.typeInfo.cleanupDelaySeconds", DEFAULT_CLEANUP_DELAY));
    1.69      // The name of the default subdirectory within the system cache directory where we store type info.
    1.70      private static final String DEFAULT_CACHE_SUBDIR_NAME = "com.oracle.java.NashornTypeInfo";
    1.71      // The directory where we cache type info
    1.72 -    private static final File cacheDir = createCacheDir();
    1.73 +    private static final File baseCacheDir = createBaseCacheDir();
    1.74 +    private static final File cacheDir = createCacheDir(baseCacheDir);
    1.75      // In-process locks to make sure we don't have a cross-thread race condition manipulating any file.
    1.76      private static final Object[] locks = cacheDir == null ? null : createLockArray();
    1.77 -
    1.78      // Only report one read/write error every minute
    1.79      private static final long ERROR_REPORT_THRESHOLD = 60000L;
    1.80  
    1.81      private static volatile long lastReportedError;
    1.82 -
    1.83 +    private static final AtomicBoolean scheduledCleanup;
    1.84 +    private static final Timer cleanupTimer;
    1.85 +    static {
    1.86 +        if (baseCacheDir == null || MAX_FILES == UNLIMITED_FILES) {
    1.87 +            scheduledCleanup = null;
    1.88 +            cleanupTimer = null;
    1.89 +        } else {
    1.90 +            scheduledCleanup = new AtomicBoolean();
    1.91 +            cleanupTimer = new Timer(true);
    1.92 +        }
    1.93 +    }
    1.94      /**
    1.95 -     * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed to
    1.96 -     * {@link #load(Object)} and {@link #store(Object, Map)} methods.
    1.97 +     * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed
    1.98 +     * to {@link #load(Object)} and {@link #store(Object, Map)} methods.
    1.99       * @param source the source where the function comes from
   1.100       * @param functionId the unique ID number of the function within the source
   1.101 -     * @param paramTypes the types of the function parameters (as persistence is per parameter type specialization).
   1.102 +     * @param paramTypes the types of the function parameters (as persistence is per parameter type
   1.103 +     * specialization).
   1.104       * @return an opaque descriptor for the persistence location. Can be null if persistence is disabled.
   1.105       */
   1.106      public static Object getLocationDescriptor(final Source source, final int functionId, final Type[] paramTypes) {
   1.107 @@ -82,7 +128,8 @@
   1.108          final StringBuilder b = new StringBuilder(48);
   1.109          // Base64-encode the digest of the source, and append the function id.
   1.110          b.append(source.getDigest()).append('-').append(functionId);
   1.111 -        // Finally, if this is a parameter-type specialized version of the function, add the parameter types to the file name.
   1.112 +        // Finally, if this is a parameter-type specialized version of the function, add the parameter types
   1.113 +        // to the file name.
   1.114          if(paramTypes != null && paramTypes.length > 0) {
   1.115              b.append('-');
   1.116              for(final Type t: paramTypes) {
   1.117 @@ -118,6 +165,11 @@
   1.118              @Override
   1.119              public Void run() {
   1.120                  synchronized(getFileLock(file)) {
   1.121 +                    if (!file.exists()) {
   1.122 +                        // If the file already exists, we aren't increasing the number of cached files, so
   1.123 +                        // don't schedule cleanup.
   1.124 +                        scheduleCleanup();
   1.125 +                    }
   1.126                      try (final FileOutputStream out = new FileOutputStream(file)) {
   1.127                          out.getChannel().lock(); // lock exclusive
   1.128                          final DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(out));
   1.129 @@ -174,19 +226,19 @@
   1.130          }
   1.131      }
   1.132  
   1.133 -    private static File createCacheDir() {
   1.134 -        if(Options.getBooleanProperty("nashorn.typeInfo.disabled")) {
   1.135 +    private static File createBaseCacheDir() {
   1.136 +        if(MAX_FILES == 0 || Options.getBooleanProperty("nashorn.typeInfo.disabled")) {
   1.137              return null;
   1.138          }
   1.139          try {
   1.140 -            return createCacheDirPrivileged();
   1.141 +            return createBaseCacheDirPrivileged();
   1.142          } catch(final Exception e) {
   1.143              getLogger().warning("Failed to create cache dir", e);
   1.144              return null;
   1.145          }
   1.146      }
   1.147  
   1.148 -    private static File createCacheDirPrivileged() {
   1.149 +    private static File createBaseCacheDirPrivileged() {
   1.150          return AccessController.doPrivileged(new PrivilegedAction<File>() {
   1.151              @Override
   1.152              public File run() {
   1.153 @@ -195,14 +247,35 @@
   1.154                  if(explicitDir != null) {
   1.155                      dir = new File(explicitDir);
   1.156                  } else {
   1.157 -                    // When no directory is explicitly specified, get an operating system specific cache directory,
   1.158 -                    // and create "com.oracle.java.NashornTypeInfo" in it.
   1.159 +                    // When no directory is explicitly specified, get an operating system specific cache
   1.160 +                    // directory, and create "com.oracle.java.NashornTypeInfo" in it.
   1.161                      final File systemCacheDir = getSystemCacheDir();
   1.162                      dir = new File(systemCacheDir, DEFAULT_CACHE_SUBDIR_NAME);
   1.163                      if (isSymbolicLink(dir)) {
   1.164                          return null;
   1.165                      }
   1.166                  }
   1.167 +                return dir;
   1.168 +            }
   1.169 +        });
   1.170 +    }
   1.171 +
   1.172 +    private static File createCacheDir(final File baseDir) {
   1.173 +        if (baseDir == null) {
   1.174 +            return null;
   1.175 +        }
   1.176 +        try {
   1.177 +            return createCacheDirPrivileged(baseDir);
   1.178 +        } catch(final Exception e) {
   1.179 +            getLogger().warning("Failed to create cache dir", e);
   1.180 +            return null;
   1.181 +        }
   1.182 +    }
   1.183 +
   1.184 +    private static File createCacheDirPrivileged(final File baseDir) {
   1.185 +        return AccessController.doPrivileged(new PrivilegedAction<File>() {
   1.186 +            @Override
   1.187 +            public File run() {
   1.188                  final String versionDirName;
   1.189                  try {
   1.190                      versionDirName = getVersionDirName();
   1.191 @@ -210,12 +283,12 @@
   1.192                      getLogger().warning("Failed to calculate version dir name", e);
   1.193                      return null;
   1.194                  }
   1.195 -                final File versionDir = new File(dir, versionDirName);
   1.196 +                final File versionDir = new File(baseDir, versionDirName);
   1.197                  if (isSymbolicLink(versionDir)) {
   1.198                      return null;
   1.199                  }
   1.200                  versionDir.mkdirs();
   1.201 -                if(versionDir.isDirectory()) {
   1.202 +                if (versionDir.isDirectory()) {
   1.203                      getLogger().info("Optimistic type persistence directory is " + versionDir);
   1.204                      return versionDir;
   1.205                  }
   1.206 @@ -235,12 +308,12 @@
   1.207              // Mac OS X stores caches in ~/Library/Caches
   1.208              return new File(new File(System.getProperty("user.home"), "Library"), "Caches");
   1.209          } else if(os.startsWith("Windows")) {
   1.210 -            // On Windows, temp directory is the best approximation of a cache directory, as its contents persist across
   1.211 -            // reboots and various cleanup utilities know about it. java.io.tmpdir normally points to a user-specific
   1.212 -            // temp directory, %HOME%\LocalSettings\Temp.
   1.213 +            // On Windows, temp directory is the best approximation of a cache directory, as its contents
   1.214 +            // persist across reboots and various cleanup utilities know about it. java.io.tmpdir normally
   1.215 +            // points to a user-specific temp directory, %HOME%\LocalSettings\Temp.
   1.216              return new File(System.getProperty("java.io.tmpdir"));
   1.217          } else {
   1.218 -            // In all other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache"
   1.219 +            // In other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache"
   1.220              return new File(System.getProperty("user.home"), ".cache");
   1.221          }
   1.222      }
   1.223 @@ -278,7 +351,8 @@
   1.224              final int packageNameLen = className.lastIndexOf('.');
   1.225              final String dirStr = fileStr.substring(0, fileStr.length() - packageNameLen - 1);
   1.226              final File dir = new File(dirStr);
   1.227 -            return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile(dir, 0L)));
   1.228 +            return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile(
   1.229 +                    dir, 0L)));
   1.230          } else {
   1.231              throw new AssertionError();
   1.232          }
   1.233 @@ -335,4 +409,108 @@
   1.234              return DebugLogger.DISABLED_LOGGER;
   1.235          }
   1.236      }
   1.237 +
   1.238 +    private static void scheduleCleanup() {
   1.239 +        if (MAX_FILES != UNLIMITED_FILES && scheduledCleanup.compareAndSet(false, true)) {
   1.240 +            cleanupTimer.schedule(new TimerTask() {
   1.241 +                @Override
   1.242 +                public void run() {
   1.243 +                    scheduledCleanup.set(false);
   1.244 +                    try {
   1.245 +                        doCleanup();
   1.246 +                    } catch (final IOException e) {
   1.247 +                        // Ignore it. While this is unfortunate, we don't have good facility for reporting
   1.248 +                        // this, as we're running in a thread that has no access to Context, so we can't grab
   1.249 +                        // a DebugLogger.
   1.250 +                    }
   1.251 +                }
   1.252 +            }, TimeUnit.SECONDS.toMillis(CLEANUP_DELAY));
   1.253 +        }
   1.254 +    }
   1.255 +
   1.256 +    private static void doCleanup() throws IOException {
   1.257 +        final long start = System.nanoTime();
   1.258 +        final Path[] files = getAllRegularFilesInLastModifiedOrder();
   1.259 +        final int nFiles = files.length;
   1.260 +        final int filesToDelete = Math.max(0, nFiles - MAX_FILES);
   1.261 +        int filesDeleted = 0;
   1.262 +        for (int i = 0; i < nFiles && filesDeleted < filesToDelete; ++i) {
   1.263 +            try {
   1.264 +                Files.deleteIfExists(files[i]);
   1.265 +                // Even if it didn't exist, we increment filesDeleted; it existed a moment earlier; something
   1.266 +                // else deleted it for us; that's okay with us.
   1.267 +                filesDeleted++;
   1.268 +            } catch (final Exception e) {
   1.269 +                // does not increase filesDeleted
   1.270 +            }
   1.271 +            files[i] = null; // gc eligible
   1.272 +        };
   1.273 +        final long duration = System.nanoTime() - start;
   1.274 +    }
   1.275 +
   1.276 +    private static Path[] getAllRegularFilesInLastModifiedOrder() throws IOException {
   1.277 +        try (final Stream<Path> filesStream = Files.walk(baseCacheDir.toPath())) {
   1.278 +            // TODO: rewrite below once we can use JDK8 syntactic constructs
   1.279 +            return filesStream
   1.280 +            .filter(new Predicate<Path>() {
   1.281 +                @Override
   1.282 +                public boolean test(final Path path) {
   1.283 +                    return !Files.isDirectory(path);
   1.284 +                };
   1.285 +            })
   1.286 +            .map(new Function<Path, PathAndTime>() {
   1.287 +                @Override
   1.288 +                public PathAndTime apply(final Path path) {
   1.289 +                    return new PathAndTime(path);
   1.290 +                }
   1.291 +            })
   1.292 +            .sorted()
   1.293 +            .map(new Function<PathAndTime, Path>() {
   1.294 +                @Override
   1.295 +                public Path apply(final PathAndTime pathAndTime) {
   1.296 +                    return pathAndTime.path;
   1.297 +                }
   1.298 +            })
   1.299 +            .toArray(new IntFunction<Path[]>() { // Replace with Path::new
   1.300 +                @Override
   1.301 +                public Path[] apply(final int length) {
   1.302 +                    return new Path[length];
   1.303 +                }
   1.304 +            });
   1.305 +        }
   1.306 +    }
   1.307 +
   1.308 +    private static class PathAndTime implements Comparable<PathAndTime> {
   1.309 +        private final Path path;
   1.310 +        private final long time;
   1.311 +
   1.312 +        PathAndTime(final Path path) {
   1.313 +            this.path = path;
   1.314 +            this.time = getTime(path);
   1.315 +        }
   1.316 +
   1.317 +        @Override
   1.318 +        public int compareTo(final PathAndTime other) {
   1.319 +            return Long.compare(time, other.time);
   1.320 +        }
   1.321 +
   1.322 +        private static long getTime(final Path path) {
   1.323 +            try {
   1.324 +                return Files.getLastModifiedTime(path).toMillis();
   1.325 +            } catch (IOException e) {
   1.326 +                // All files for which we can't retrieve the last modified date will be considered oldest.
   1.327 +                return -1L;
   1.328 +            }
   1.329 +        }
   1.330 +    }
   1.331 +
   1.332 +    private static int getMaxFiles() {
   1.333 +        final String str = Options.getStringProperty("nashorn.typeInfo.maxFiles", null);
   1.334 +        if (str == null) {
   1.335 +            return DEFAULT_MAX_FILES;
   1.336 +        } else if ("unlimited".equals(str)) {
   1.337 +            return UNLIMITED_FILES;
   1.338 +        }
   1.339 +        return Math.max(0, Integer.parseInt(str));
   1.340 +    }
   1.341  }
     2.1 --- a/src/jdk/nashorn/internal/codegen/types/Type.java	Thu Aug 28 16:10:38 2014 -0700
     2.2 +++ b/src/jdk/nashorn/internal/codegen/types/Type.java	Wed Sep 03 14:33:34 2014 +0200
     2.3 @@ -333,7 +333,7 @@
     2.4       */
     2.5      public static Map<Integer, Type> readTypeMap(final DataInput input) throws IOException {
     2.6          final int size = input.readInt();
     2.7 -        if (size == 0) {
     2.8 +        if (size <= 0) {
     2.9              return null;
    2.10          }
    2.11          final Map<Integer, Type> map = new TreeMap<>();
    2.12 @@ -345,7 +345,7 @@
    2.13                  case 'L': type = Type.OBJECT; break;
    2.14                  case 'D': type = Type.NUMBER; break;
    2.15                  case 'J': type = Type.LONG; break;
    2.16 -                default: throw new AssertionError();
    2.17 +                default: continue;
    2.18              }
    2.19              map.put(pp, type);
    2.20          }

mercurial