--- old/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java 2020-04-15 18:47:15.000000000 +0530 +++ /dev/null 2020-04-15 18:47:15.000000000 +0530 @@ -1,602 +0,0 @@ -/* - * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package jdk.nashorn.internal.codegen; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.io.UncheckedIOException; -import java.net.URI; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Path; -import java.security.AccessController; -import java.security.MessageDigest; -import java.security.PrivilegedAction; -import java.text.SimpleDateFormat; -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.Consumer; -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; -import jdk.nashorn.internal.runtime.Source; -import jdk.nashorn.internal.runtime.logging.DebugLogger; -import jdk.nashorn.internal.runtime.options.Options; - -/** - *

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. - *

- * 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 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. - * @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). - * @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) { - if(cacheDir == null) { - return null; - } - 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. - if(paramTypes != null && paramTypes.length > 0) { - b.append('-'); - for(final Type t: paramTypes) { - b.append(Type.getShortSignatureDescriptor(t)); - } - } - return new LocationDescriptor(new File(cacheDir, b.toString())); - } - - private static final class LocationDescriptor { - private final File file; - - LocationDescriptor(final File file) { - this.file = file; - } - } - - - /** - * Stores the map of optimistic types for a given function. - * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling - * {@link #getLocationDescriptor(Source, int, Type[])}. - * @param optimisticTypes the map of optimistic types. - */ - @SuppressWarnings("resource") - public static void store(final Object locationDescriptor, final Map optimisticTypes) { - if(locationDescriptor == null || optimisticTypes.isEmpty()) { - return; - } - final File file = ((LocationDescriptor)locationDescriptor).file; - - AccessController.doPrivileged(new PrivilegedAction() { - @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)); - Type.writeTypeMap(optimisticTypes, dout); - dout.flush(); - } catch(final Exception e) { - reportError("write", file, e); - } - } - return null; - } - }); - } - - /** - * Loads the map of optimistic types for a given function. - * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling - * {@link #getLocationDescriptor(Source, int, Type[])}. - * @return the map of optimistic types, or null if persisted type information could not be retrieved. - */ - @SuppressWarnings("resource") - public static Map load(final Object locationDescriptor) { - if (locationDescriptor == null) { - return null; - } - final File file = ((LocationDescriptor)locationDescriptor).file; - return AccessController.doPrivileged(new PrivilegedAction>() { - @Override - public Map run() { - try { - if(!file.isFile()) { - return null; - } - synchronized(getFileLock(file)) { - try (final FileInputStream in = new FileInputStream(file)) { - in.getChannel().lock(0, Long.MAX_VALUE, true); // lock shared - final DataInputStream din = new DataInputStream(new BufferedInputStream(in)); - return Type.readTypeMap(din); - } - } - } catch (final Exception e) { - reportError("read", file, e); - return null; - } - } - }); - } - - private static void reportError(final String msg, final File file, final Exception e) { - final long now = System.currentTimeMillis(); - if(now - lastReportedError > ERROR_REPORT_THRESHOLD) { - reportError(String.format("Failed to %s %s", msg, file), e); - lastReportedError = now; - } - } - - /** - * Logs an error message with warning severity (reasoning being that we're reporting an error that'll disable the - * type info cache, but it's only logged as a warning because that doesn't prevent Nashorn from running, it just - * disables a performance-enhancing cache). - * @param msg the message to log - * @param e the exception that represents the error. - */ - private static void reportError(final String msg, final Exception e) { - getLogger().warning(msg, "\n", exceptionToString(e)); - } - - /** - * A helper that prints an exception stack trace into a string. We have to do this as if we just pass the exception - * to {@link DebugLogger#warning(Object...)}, it will only log the exception message and not the stack, making - * problems harder to diagnose. - * @param e the exception - * @return the string representation of {@link Exception#printStackTrace()} output. - */ - private static String exceptionToString(final Exception e) { - final StringWriter sw = new StringWriter(); - final PrintWriter pw = new PrintWriter(sw, false); - e.printStackTrace(pw); - pw.flush(); - return sw.toString(); - } - - private static File createBaseCacheDir() { - if(MAX_FILES == 0 || Options.getBooleanProperty("nashorn.typeInfo.disabled")) { - return null; - } - try { - return createBaseCacheDirPrivileged(); - } catch(final Exception e) { - reportError("Failed to create cache dir", e); - return null; - } - } - - private static File createBaseCacheDirPrivileged() { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public File run() { - final String explicitDir = System.getProperty("nashorn.typeInfo.cacheDir"); - final File dir; - 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. - 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) { - reportError("Failed to create cache dir", e); - return null; - } - } - - private static File createCacheDirPrivileged(final File baseDir) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public File run() { - final String versionDirName; - try { - versionDirName = getVersionDirName(); - } catch(final Exception e) { - reportError("Failed to calculate version dir name", e); - return null; - } - final File versionDir = new File(baseDir, versionDirName); - if (isSymbolicLink(versionDir)) { - return null; - } - versionDir.mkdirs(); - if (versionDir.isDirectory()) { - //FIXME:Logger is disabled as Context.getContext() always returns null here because global scope object will not be created - //by the time this method gets invoked - getLogger().info("Optimistic type persistence directory is " + versionDir); - return versionDir; - } - getLogger().warning("Could not create optimistic type persistence directory " + versionDir); - return null; - } - }); - } - - /** - * Returns an operating system specific root directory for cache files. - * @return an operating system specific root directory for cache files. - */ - private static File getSystemCacheDir() { - final String os = System.getProperty("os.name", "generic"); - if("Mac OS X".equals(os)) { - // 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. - return new File(System.getProperty("java.io.tmpdir")); - } else { - // In other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache" - return new File(System.getProperty("user.home"), ".cache"); - } - } - - private static final String ANCHOR_PROPS = "anchor.properties"; - - /** - * In order to ensure that changes in Nashorn code don't cause corruption in the data, we'll create a - * per-code-version directory. Normally, this will create the SHA-1 digest of the nashorn.jar. In case the classpath - * for nashorn is local directory (e.g. during development), this will create the string "dev-" followed by the - * timestamp of the most recent .class file. - * - * @return digest of currently running nashorn - * @throws Exception if digest could not be created - */ - public static String getVersionDirName() throws Exception { - // NOTE: getResource("") won't work if the JAR file doesn't have directory entries (and JAR files in JDK distro - // don't, or at least it's a bad idea counting on it). Alternatively, we could've tried - // getResource("OptimisticTypesPersistence.class") but behavior of getResource with regard to its willingness - // to hand out URLs to .class files is also unspecified. Therefore, the most robust way to obtain an URL to our - // package is to have a small non-class anchor file and start out from its URL. - final URL url = OptimisticTypesPersistence.class.getResource(ANCHOR_PROPS); - final String protocol = url.getProtocol(); - if (protocol.equals("jar")) { - // Normal deployment: nashorn.jar - final String jarUrlFile = url.getFile(); - final String filePath = jarUrlFile.substring(0, jarUrlFile.indexOf('!')); - final URL file = new URL(filePath); - try (final InputStream in = file.openStream()) { - final byte[] buf = new byte[128*1024]; - final MessageDigest digest = MessageDigest.getInstance("SHA-1"); - for(;;) { - final int l = in.read(buf); - if(l == -1) { - return Base64.getUrlEncoder().withoutPadding().encodeToString(digest.digest()); - } - digest.update(buf, 0, l); - } - } - } else if(protocol.equals("file")) { - // Development - final String fileStr = url.getFile(); - final String className = OptimisticTypesPersistence.class.getName(); - final int packageNameLen = className.lastIndexOf('.'); - final String dirStr = fileStr.substring(0, fileStr.length() - packageNameLen - 1 - ANCHOR_PROPS.length()); - final File dir = new File(dirStr); - return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile( - dir, 0L))); - } else if(protocol.equals("jrt")) { - return getJrtVersionDirName(); - } else { - throw new AssertionError("unknown protocol"); - } - } - - private static long getLastModifiedClassFile(final File dir, final long max) { - long currentMax = max; - for(final File f: dir.listFiles()) { - if(f.getName().endsWith(".class")) { - final long lastModified = f.lastModified(); - if (lastModified > currentMax) { - currentMax = lastModified; - } - } else if (f.isDirectory()) { - final long lastModified = getLastModifiedClassFile(f, currentMax); - if (lastModified > currentMax) { - currentMax = lastModified; - } - } - } - return currentMax; - } - - /** - * Returns true if the specified file is a symbolic link, and also logs a warning if it is. - * @param file the file - * @return true if file is a symbolic link, false otherwise. - */ - private static boolean isSymbolicLink(final File file) { - if (Files.isSymbolicLink(file.toPath())) { - getLogger().warning("Directory " + file + " is a symlink"); - return true; - } - return false; - } - - private static Object[] createLockArray() { - final Object[] lockArray = new Object[Runtime.getRuntime().availableProcessors() * 2]; - for (int i = 0; i < lockArray.length; ++i) { - lockArray[i] = new Object(); - } - return lockArray; - } - - private static Object getFileLock(final File file) { - return locks[(file.hashCode() & Integer.MAX_VALUE) % locks.length]; - } - - private static DebugLogger getLogger() { - try { - return Context.getContext().getLogger(RecompilableScriptFunctionData.class); - } catch (final NullPointerException e) { - //Don't print stacktrace until we revisit this, NPE is a known issue here - } catch (final Exception e) { - e.printStackTrace(); - } - 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 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 - } - } - - private static Path[] getAllRegularFilesInLastModifiedOrder() throws IOException { - try (final Stream filesStream = Files.walk(baseCacheDir.toPath())) { - // TODO: rewrite below once we can use JDK8 syntactic constructs - return filesStream - .filter(new Predicate() { - @Override - public boolean test(final Path path) { - return !Files.isDirectory(path); - } - }) - .map(new Function() { - @Override - public PathAndTime apply(final Path path) { - return new PathAndTime(path); - } - }) - .sorted() - .map(new Function() { - @Override - public Path apply(final PathAndTime pathAndTime) { - return pathAndTime.path; - } - }) - .toArray(new IntFunction() { // Replace with Path::new - @Override - public Path[] apply(final int length) { - return new Path[length]; - } - }); - } - } - - private static class PathAndTime implements Comparable { - 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 (final 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)); - } - - private static final String JRT_NASHORN_DIR = "/modules/jdk.scripting.nashorn"; - - // version directory name if nashorn is loaded from jrt:/ URL - private static String getJrtVersionDirName() throws Exception { - final FileSystem fs = getJrtFileSystem(); - // consider all .class resources under nashorn module to compute checksum - final Path nashorn = fs.getPath(JRT_NASHORN_DIR); - if (! Files.isDirectory(nashorn)) { - throw new FileNotFoundException("missing " + JRT_NASHORN_DIR + " dir in jrt fs"); - } - final MessageDigest digest = MessageDigest.getInstance("SHA-1"); - Files.walk(nashorn).forEach(new Consumer() { - @Override - public void accept(final Path p) { - // take only the .class resources. - if (Files.isRegularFile(p) && p.toString().endsWith(".class")) { - try { - digest.update(Files.readAllBytes(p)); - } catch (final IOException ioe) { - throw new UncheckedIOException(ioe); - } - } - } - }); - return Base64.getUrlEncoder().withoutPadding().encodeToString(digest.digest()); - } - - // get the default jrt FileSystem instance - private static FileSystem getJrtFileSystem() { - return AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public FileSystem run() { - return FileSystems.getFileSystem(URI.create("jrt:/")); - } - }); - } -}