blob: 9eb23bd29fb5fb8a09ecacac71019f022fb6af08 [file] [log] [blame]
/*
* Copyright (c) 2010, 2014, 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.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
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;
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.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;
/**
* <p>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.
* </p>
*/
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<Integer, Type> optimisticTypes) {
if(locationDescriptor == null || optimisticTypes.isEmpty()) {
return;
}
final File file = ((LocationDescriptor)locationDescriptor).file;
AccessController.doPrivileged(new PrivilegedAction<Void>() {
@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<Integer, Type> load(final Object locationDescriptor) {
if (locationDescriptor == null) {
return null;
}
final File file = ((LocationDescriptor)locationDescriptor).file;
return AccessController.doPrivileged(new PrivilegedAction<Map<Integer, Type>>() {
@Override
public Map<Integer, Type> 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<File>() {
@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<File>() {
@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()) {
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");
}
}
/**
* 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.properties");
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);
final File dir = new File(dirStr);
return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile(
dir, 0L)));
} else {
throw new AssertionError();
}
}
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 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<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 (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));
}
}