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