OptimisticTypesPersistence.java revision 1099:fd2181c811c4
1/*
2 * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25package jdk.nashorn.internal.codegen;
26
27import java.io.BufferedInputStream;
28import java.io.BufferedOutputStream;
29import java.io.DataInputStream;
30import java.io.DataOutputStream;
31import java.io.File;
32import java.io.FileInputStream;
33import java.io.FileOutputStream;
34import java.io.IOException;
35import java.io.InputStream;
36import java.io.PrintWriter;
37import java.io.StringWriter;
38import java.net.URL;
39import java.nio.file.Files;
40import java.nio.file.Path;
41import java.security.AccessController;
42import java.security.MessageDigest;
43import java.security.PrivilegedAction;
44import java.text.SimpleDateFormat;
45import java.util.Base64;
46import java.util.Date;
47import java.util.Map;
48import java.util.Timer;
49import java.util.TimerTask;
50import java.util.concurrent.TimeUnit;
51import java.util.concurrent.atomic.AtomicBoolean;
52import java.util.function.Function;
53import java.util.function.IntFunction;
54import java.util.function.Predicate;
55import java.util.stream.Stream;
56import jdk.nashorn.internal.codegen.types.Type;
57import jdk.nashorn.internal.runtime.Context;
58import jdk.nashorn.internal.runtime.RecompilableScriptFunctionData;
59import jdk.nashorn.internal.runtime.Source;
60import jdk.nashorn.internal.runtime.logging.DebugLogger;
61import jdk.nashorn.internal.runtime.options.Options;
62
63/**
64 * Static utility that encapsulates persistence of type information for functions compiled with optimistic
65 * typing. With this feature enabled, when a JavaScript function is recompiled because it gets deoptimized,
66 * the type information for deoptimization is stored in a cache file. If the same function is compiled in a
67 * subsequent JVM invocation, the type information is used for initial compilation, thus allowing the system
68 * to skip a lot of intermediate recompilations and immediately emit a version of the code that has its
69 * optimistic types at (or near) the steady state.
70 * </p><p>
71 * Normally, the type info persistence feature is disabled. When the {@code nashorn.typeInfo.maxFiles} system
72 * property is specified with a value greater than 0, it is enabled and operates in an operating-system
73 * specific per-user cache directory. You can override the directory by specifying it in the
74 * {@code nashorn.typeInfo.cacheDir} directory. The maximum number of files is softly enforced by a task that
75 * cleans up the directory periodically on a separate thread. It is run after some delay after a new file is
76 * added to the cache. The default delay is 20 seconds, and can be set using the
77 * {@code nashorn.typeInfo.cleanupDelaySeconds} system property. You can also specify the word
78 * {@code unlimited} as the value for {@code nashorn.typeInfo.maxFiles} in which case the type info cache is
79 * allowed to grow without limits.
80 */
81public final class OptimisticTypesPersistence {
82    // Default is 0, for disabling the feature when not specified. A reasonable default when enabled is
83    // dependent on the application; setting it to e.g. 20000 is probably good enough for most uses and will
84    // usually cap the cache directory to about 80MB presuming a 4kB filesystem allocation unit. There is one
85    // file per JavaScript function.
86    private static final int DEFAULT_MAX_FILES = 0;
87    // Constants for signifying that the cache should not be limited
88    private static final int UNLIMITED_FILES = -1;
89    // Maximum number of files that should be cached on disk. The maximum will be softly enforced.
90    private static final int MAX_FILES = getMaxFiles();
91    // Number of seconds to wait between adding a new file to the cache and running a cleanup process
92    private static final int DEFAULT_CLEANUP_DELAY = 20;
93    private static final int CLEANUP_DELAY = Math.max(0, Options.getIntProperty(
94            "nashorn.typeInfo.cleanupDelaySeconds", DEFAULT_CLEANUP_DELAY));
95    // The name of the default subdirectory within the system cache directory where we store type info.
96    private static final String DEFAULT_CACHE_SUBDIR_NAME = "com.oracle.java.NashornTypeInfo";
97    // The directory where we cache type info
98    private static final File baseCacheDir = createBaseCacheDir();
99    private static final File cacheDir = createCacheDir(baseCacheDir);
100    // In-process locks to make sure we don't have a cross-thread race condition manipulating any file.
101    private static final Object[] locks = cacheDir == null ? null : createLockArray();
102    // Only report one read/write error every minute
103    private static final long ERROR_REPORT_THRESHOLD = 60000L;
104
105    private static volatile long lastReportedError;
106    private static final AtomicBoolean scheduledCleanup;
107    private static final Timer cleanupTimer;
108    static {
109        if (baseCacheDir == null || MAX_FILES == UNLIMITED_FILES) {
110            scheduledCleanup = null;
111            cleanupTimer = null;
112        } else {
113            scheduledCleanup = new AtomicBoolean();
114            cleanupTimer = new Timer(true);
115        }
116    }
117    /**
118     * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed
119     * to {@link #load(Object)} and {@link #store(Object, Map)} methods.
120     * @param source the source where the function comes from
121     * @param functionId the unique ID number of the function within the source
122     * @param paramTypes the types of the function parameters (as persistence is per parameter type
123     * specialization).
124     * @return an opaque descriptor for the persistence location. Can be null if persistence is disabled.
125     */
126    public static Object getLocationDescriptor(final Source source, final int functionId, final Type[] paramTypes) {
127        if(cacheDir == null) {
128            return null;
129        }
130        final StringBuilder b = new StringBuilder(48);
131        // Base64-encode the digest of the source, and append the function id.
132        b.append(source.getDigest()).append('-').append(functionId);
133        // Finally, if this is a parameter-type specialized version of the function, add the parameter types
134        // to the file name.
135        if(paramTypes != null && paramTypes.length > 0) {
136            b.append('-');
137            for(final Type t: paramTypes) {
138                b.append(Type.getShortSignatureDescriptor(t));
139            }
140        }
141        return new LocationDescriptor(new File(cacheDir, b.toString()));
142    }
143
144    private static final class LocationDescriptor {
145        private final File file;
146
147        LocationDescriptor(final File file) {
148            this.file = file;
149        }
150    }
151
152
153    /**
154     * Stores the map of optimistic types for a given function.
155     * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling
156     * {@link #getLocationDescriptor(Source, int, Type[])}.
157     * @param optimisticTypes the map of optimistic types.
158     */
159    @SuppressWarnings("resource")
160    public static void store(final Object locationDescriptor, final Map<Integer, Type> optimisticTypes) {
161        if(locationDescriptor == null || optimisticTypes.isEmpty()) {
162            return;
163        }
164        final File file = ((LocationDescriptor)locationDescriptor).file;
165
166        AccessController.doPrivileged(new PrivilegedAction<Void>() {
167            @Override
168            public Void run() {
169                synchronized(getFileLock(file)) {
170                    if (!file.exists()) {
171                        // If the file already exists, we aren't increasing the number of cached files, so
172                        // don't schedule cleanup.
173                        scheduleCleanup();
174                    }
175                    try (final FileOutputStream out = new FileOutputStream(file)) {
176                        out.getChannel().lock(); // lock exclusive
177                        final DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(out));
178                        Type.writeTypeMap(optimisticTypes, dout);
179                        dout.flush();
180                    } catch(final Exception e) {
181                        reportError("write", file, e);
182                    }
183                }
184                return null;
185            }
186        });
187    }
188
189    /**
190     * Loads the map of optimistic types for a given function.
191     * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling
192     * {@link #getLocationDescriptor(Source, int, Type[])}.
193     * @return the map of optimistic types, or null if persisted type information could not be retrieved.
194     */
195    @SuppressWarnings("resource")
196    public static Map<Integer, Type> load(final Object locationDescriptor) {
197        if (locationDescriptor == null) {
198            return null;
199        }
200        final File file = ((LocationDescriptor)locationDescriptor).file;
201        return AccessController.doPrivileged(new PrivilegedAction<Map<Integer, Type>>() {
202            @Override
203            public Map<Integer, Type> run() {
204                try {
205                    if(!file.isFile()) {
206                        return null;
207                    }
208                    synchronized(getFileLock(file)) {
209                        try (final FileInputStream in = new FileInputStream(file)) {
210                            in.getChannel().lock(0, Long.MAX_VALUE, true); // lock shared
211                            final DataInputStream din = new DataInputStream(new BufferedInputStream(in));
212                            return Type.readTypeMap(din);
213                        }
214                    }
215                } catch (final Exception e) {
216                    reportError("read", file, e);
217                    return null;
218                }
219            }
220        });
221    }
222
223    private static void reportError(final String msg, final File file, final Exception e) {
224        final long now = System.currentTimeMillis();
225        if(now - lastReportedError > ERROR_REPORT_THRESHOLD) {
226            reportError(String.format("Failed to %s %s", msg, file), e);
227            lastReportedError = now;
228        }
229    }
230
231    /**
232     * Logs an error message with warning severity (reasoning being that we're reporting an error that'll disable the
233     * type info cache, but it's only logged as a warning because that doesn't prevent Nashorn from running, it just
234     * disables a performance-enhancing cache).
235     * @param msg the message to log
236     * @param e the exception that represents the error.
237     */
238    private static void reportError(final String msg, final Exception e) {
239        getLogger().warning(msg, "\n", exceptionToString(e));
240    }
241
242    /**
243     * A helper that prints an exception stack trace into a string. We have to do this as if we just pass the exception
244     * to {@link DebugLogger#warning(Object...)}, it will only log the exception message and not the stack, making
245     * problems harder to diagnose.
246     * @param e the exception
247     * @return the string representation of {@link Exception#printStackTrace()} output.
248     */
249    private static String exceptionToString(final Exception e) {
250        final StringWriter sw = new StringWriter();
251        final PrintWriter pw = new PrintWriter(sw, false);
252        e.printStackTrace(pw);
253        pw.flush();
254        return sw.toString();
255    }
256
257    private static File createBaseCacheDir() {
258        if(MAX_FILES == 0 || Options.getBooleanProperty("nashorn.typeInfo.disabled")) {
259            return null;
260        }
261        try {
262            return createBaseCacheDirPrivileged();
263        } catch(final Exception e) {
264            reportError("Failed to create cache dir", e);
265            return null;
266        }
267    }
268
269    private static File createBaseCacheDirPrivileged() {
270        return AccessController.doPrivileged(new PrivilegedAction<File>() {
271            @Override
272            public File run() {
273                final String explicitDir = System.getProperty("nashorn.typeInfo.cacheDir");
274                final File dir;
275                if(explicitDir != null) {
276                    dir = new File(explicitDir);
277                } else {
278                    // When no directory is explicitly specified, get an operating system specific cache
279                    // directory, and create "com.oracle.java.NashornTypeInfo" in it.
280                    final File systemCacheDir = getSystemCacheDir();
281                    dir = new File(systemCacheDir, DEFAULT_CACHE_SUBDIR_NAME);
282                    if (isSymbolicLink(dir)) {
283                        return null;
284                    }
285                }
286                return dir;
287            }
288        });
289    }
290
291    private static File createCacheDir(final File baseDir) {
292        if (baseDir == null) {
293            return null;
294        }
295        try {
296            return createCacheDirPrivileged(baseDir);
297        } catch(final Exception e) {
298            reportError("Failed to create cache dir", e);
299            return null;
300        }
301    }
302
303    private static File createCacheDirPrivileged(final File baseDir) {
304        return AccessController.doPrivileged(new PrivilegedAction<File>() {
305            @Override
306            public File run() {
307                final String versionDirName;
308                try {
309                    versionDirName = getVersionDirName();
310                } catch(final Exception e) {
311                    reportError("Failed to calculate version dir name", e);
312                    return null;
313                }
314                final File versionDir = new File(baseDir, versionDirName);
315                if (isSymbolicLink(versionDir)) {
316                    return null;
317                }
318                versionDir.mkdirs();
319                if (versionDir.isDirectory()) {
320                    getLogger().info("Optimistic type persistence directory is " + versionDir);
321                    return versionDir;
322                }
323                getLogger().warning("Could not create optimistic type persistence directory " + versionDir);
324                return null;
325            }
326        });
327    }
328
329    /**
330     * Returns an operating system specific root directory for cache files.
331     * @return an operating system specific root directory for cache files.
332     */
333    private static File getSystemCacheDir() {
334        final String os = System.getProperty("os.name", "generic");
335        if("Mac OS X".equals(os)) {
336            // Mac OS X stores caches in ~/Library/Caches
337            return new File(new File(System.getProperty("user.home"), "Library"), "Caches");
338        } else if(os.startsWith("Windows")) {
339            // On Windows, temp directory is the best approximation of a cache directory, as its contents
340            // persist across reboots and various cleanup utilities know about it. java.io.tmpdir normally
341            // points to a user-specific temp directory, %HOME%\LocalSettings\Temp.
342            return new File(System.getProperty("java.io.tmpdir"));
343        } else {
344            // In other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache"
345            return new File(System.getProperty("user.home"), ".cache");
346        }
347    }
348
349    /**
350     * In order to ensure that changes in Nashorn code don't cause corruption in the data, we'll create a
351     * per-code-version directory. Normally, this will create the SHA-1 digest of the nashorn.jar. In case the classpath
352     * for nashorn is local directory (e.g. during development), this will create the string "dev-" followed by the
353     * timestamp of the most recent .class file.
354     *
355     * @return digest of currently running nashorn
356     * @throws Exception if digest could not be created
357     */
358    public static String getVersionDirName() throws Exception {
359        // NOTE: getResource("") won't work if the JAR file doesn't have directory entries (and JAR files in JDK distro
360        // don't, or at least it's a bad idea counting on it). Alternatively, we could've tried
361        // getResource("OptimisticTypesPersistence.class") but behavior of getResource with regard to its willingness
362        // to hand out URLs to .class files is also unspecified. Therefore, the most robust way to obtain an URL to our
363        // package is to have a small non-class anchor file and start out from its URL.
364        final URL url = OptimisticTypesPersistence.class.getResource("anchor.properties");
365        final String protocol = url.getProtocol();
366        if (protocol.equals("jar")) {
367            // Normal deployment: nashorn.jar
368            final String jarUrlFile = url.getFile();
369            final String filePath = jarUrlFile.substring(0, jarUrlFile.indexOf('!'));
370            final URL file = new URL(filePath);
371            try (final InputStream in = file.openStream()) {
372                final byte[] buf = new byte[128*1024];
373                final MessageDigest digest = MessageDigest.getInstance("SHA-1");
374                for(;;) {
375                    final int l = in.read(buf);
376                    if(l == -1) {
377                        return Base64.getUrlEncoder().withoutPadding().encodeToString(digest.digest());
378                    }
379                    digest.update(buf, 0, l);
380                }
381            }
382        } else if(protocol.equals("file")) {
383            // Development
384            final String fileStr = url.getFile();
385            final String className = OptimisticTypesPersistence.class.getName();
386            final int packageNameLen = className.lastIndexOf('.');
387            final String dirStr = fileStr.substring(0, fileStr.length() - packageNameLen - 1);
388            final File dir = new File(dirStr);
389            return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile(
390                    dir, 0L)));
391        } else {
392            throw new AssertionError();
393        }
394    }
395
396    private static long getLastModifiedClassFile(final File dir, final long max) {
397        long currentMax = max;
398        for(final File f: dir.listFiles()) {
399            if(f.getName().endsWith(".class")) {
400                final long lastModified = f.lastModified();
401                if (lastModified > currentMax) {
402                    currentMax = lastModified;
403                }
404            } else if (f.isDirectory()) {
405                final long lastModified = getLastModifiedClassFile(f, currentMax);
406                if (lastModified > currentMax) {
407                    currentMax = lastModified;
408                }
409            }
410        }
411        return currentMax;
412    }
413
414    /**
415     * Returns true if the specified file is a symbolic link, and also logs a warning if it is.
416     * @param file the file
417     * @return true if file is a symbolic link, false otherwise.
418     */
419    private static boolean isSymbolicLink(final File file) {
420        if (Files.isSymbolicLink(file.toPath())) {
421            getLogger().warning("Directory " + file + " is a symlink");
422            return true;
423        }
424        return false;
425    }
426
427    private static Object[] createLockArray() {
428        final Object[] lockArray = new Object[Runtime.getRuntime().availableProcessors() * 2];
429        for (int i = 0; i < lockArray.length; ++i) {
430            lockArray[i] = new Object();
431        }
432        return lockArray;
433    }
434
435    private static Object getFileLock(final File file) {
436        return locks[(file.hashCode() & Integer.MAX_VALUE) % locks.length];
437    }
438
439    private static DebugLogger getLogger() {
440        try {
441            return Context.getContext().getLogger(RecompilableScriptFunctionData.class);
442        } catch (final Exception e) {
443            e.printStackTrace();
444            return DebugLogger.DISABLED_LOGGER;
445        }
446    }
447
448    private static void scheduleCleanup() {
449        if (MAX_FILES != UNLIMITED_FILES && scheduledCleanup.compareAndSet(false, true)) {
450            cleanupTimer.schedule(new TimerTask() {
451                @Override
452                public void run() {
453                    scheduledCleanup.set(false);
454                    try {
455                        doCleanup();
456                    } catch (final IOException e) {
457                        // Ignore it. While this is unfortunate, we don't have good facility for reporting
458                        // this, as we're running in a thread that has no access to Context, so we can't grab
459                        // a DebugLogger.
460                    }
461                }
462            }, TimeUnit.SECONDS.toMillis(CLEANUP_DELAY));
463        }
464    }
465
466    private static void doCleanup() throws IOException {
467        final Path[] files = getAllRegularFilesInLastModifiedOrder();
468        final int nFiles = files.length;
469        final int filesToDelete = Math.max(0, nFiles - MAX_FILES);
470        int filesDeleted = 0;
471        for (int i = 0; i < nFiles && filesDeleted < filesToDelete; ++i) {
472            try {
473                Files.deleteIfExists(files[i]);
474                // Even if it didn't exist, we increment filesDeleted; it existed a moment earlier; something
475                // else deleted it for us; that's okay with us.
476                filesDeleted++;
477            } catch (final Exception e) {
478                // does not increase filesDeleted
479            }
480            files[i] = null; // gc eligible
481        }
482    }
483
484    private static Path[] getAllRegularFilesInLastModifiedOrder() throws IOException {
485        try (final Stream<Path> filesStream = Files.walk(baseCacheDir.toPath())) {
486            // TODO: rewrite below once we can use JDK8 syntactic constructs
487            return filesStream
488            .filter(new Predicate<Path>() {
489                @Override
490                public boolean test(final Path path) {
491                    return !Files.isDirectory(path);
492                }
493            })
494            .map(new Function<Path, PathAndTime>() {
495                @Override
496                public PathAndTime apply(final Path path) {
497                    return new PathAndTime(path);
498                }
499            })
500            .sorted()
501            .map(new Function<PathAndTime, Path>() {
502                @Override
503                public Path apply(final PathAndTime pathAndTime) {
504                    return pathAndTime.path;
505                }
506            })
507            .toArray(new IntFunction<Path[]>() { // Replace with Path::new
508                @Override
509                public Path[] apply(final int length) {
510                    return new Path[length];
511                }
512            });
513        }
514    }
515
516    private static class PathAndTime implements Comparable<PathAndTime> {
517        private final Path path;
518        private final long time;
519
520        PathAndTime(final Path path) {
521            this.path = path;
522            this.time = getTime(path);
523        }
524
525        @Override
526        public int compareTo(final PathAndTime other) {
527            return Long.compare(time, other.time);
528        }
529
530        private static long getTime(final Path path) {
531            try {
532                return Files.getLastModifiedTime(path).toMillis();
533            } catch (final IOException e) {
534                // All files for which we can't retrieve the last modified date will be considered oldest.
535                return -1L;
536            }
537        }
538    }
539
540    private static int getMaxFiles() {
541        final String str = Options.getStringProperty("nashorn.typeInfo.maxFiles", null);
542        if (str == null) {
543            return DEFAULT_MAX_FILES;
544        } else if ("unlimited".equals(str)) {
545            return UNLIMITED_FILES;
546        }
547        return Math.max(0, Integer.parseInt(str));
548    }
549}
550