OptimisticTypesPersistence.java revision 1036:f0b5e3900a10
1251881Speter/*
2251881Speter * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
3251881Speter * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4251881Speter *
5251881Speter * This code is free software; you can redistribute it and/or modify it
6251881Speter * under the terms of the GNU General Public License version 2 only, as
7251881Speter * published by the Free Software Foundation.  Oracle designates this
8251881Speter * particular file as subject to the "Classpath" exception as provided
9251881Speter * by Oracle in the LICENSE file that accompanied this code.
10251881Speter *
11251881Speter * This code is distributed in the hope that it will be useful, but WITHOUT
12251881Speter * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13251881Speter * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14251881Speter * version 2 for more details (a copy is included in the LICENSE file that
15251881Speter * accompanied this code).
16251881Speter *
17251881Speter * You should have received a copy of the GNU General Public License version
18251881Speter * 2 along with this work; if not, write to the Free Software Foundation,
19251881Speter * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20251881Speter *
21251881Speter * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22251881Speter * or visit www.oracle.com if you need additional information or have any
23251881Speter * questions.
24251881Speter */
25251881Speterpackage jdk.nashorn.internal.codegen;
26251881Speter
27251881Speterimport java.io.BufferedInputStream;
28251881Speterimport java.io.BufferedOutputStream;
29251881Speterimport java.io.DataInputStream;
30251881Speterimport java.io.DataOutputStream;
31251881Speterimport java.io.File;
32251881Speterimport java.io.FileInputStream;
33251881Speterimport java.io.FileOutputStream;
34251881Speterimport java.io.IOException;
35251881Speterimport java.io.InputStream;
36251881Speterimport java.net.URL;
37251881Speterimport java.nio.file.Files;
38251881Speterimport java.nio.file.Path;
39251881Speterimport java.security.AccessController;
40251881Speterimport java.security.MessageDigest;
41251881Speterimport java.security.PrivilegedAction;
42251881Speterimport java.text.SimpleDateFormat;
43251881Speterimport java.util.Base64;
44251881Speterimport java.util.Date;
45251881Speterimport java.util.Map;
46251881Speterimport java.util.Timer;
47251881Speterimport java.util.TimerTask;
48251881Speterimport java.util.concurrent.TimeUnit;
49251881Speterimport java.util.concurrent.atomic.AtomicBoolean;
50251881Speterimport java.util.function.Function;
51251881Speterimport java.util.function.IntFunction;
52251881Speterimport java.util.function.Predicate;
53251881Speterimport java.util.stream.Stream;
54251881Speterimport jdk.nashorn.internal.codegen.types.Type;
55251881Speterimport jdk.nashorn.internal.runtime.Context;
56251881Speterimport jdk.nashorn.internal.runtime.RecompilableScriptFunctionData;
57251881Speterimport jdk.nashorn.internal.runtime.Source;
58251881Speterimport jdk.nashorn.internal.runtime.logging.DebugLogger;
59251881Speterimport jdk.nashorn.internal.runtime.options.Options;
60251881Speter
61251881Speter/**
62251881Speter * Static utility that encapsulates persistence of type information for functions compiled with optimistic
63251881Speter * typing. With this feature enabled, when a JavaScript function is recompiled because it gets deoptimized,
64251881Speter * the type information for deoptimization is stored in a cache file. If the same function is compiled in a
65251881Speter * subsequent JVM invocation, the type information is used for initial compilation, thus allowing the system
66251881Speter * to skip a lot of intermediate recompilations and immediately emit a version of the code that has its
67251881Speter * optimistic types at (or near) the steady state.
68251881Speter * </p><p>
69251881Speter * Normally, the type info persistence feature is disabled. When the {@code nashorn.typeInfo.maxFiles} system
70251881Speter * property is specified with a value greater than 0, it is enabled and operates in an operating-system
71251881Speter * specific per-user cache directory. You can override the directory by specifying it in the
72251881Speter * {@code nashorn.typeInfo.cacheDir} directory. The maximum number of files is softly enforced by a task that
73251881Speter * cleans up the directory periodically on a separate thread. It is run after some delay after a new file is
74251881Speter * added to the cache. The default delay is 20 seconds, and can be set using the
75251881Speter * {@code nashorn.typeInfo.cleanupDelaySeconds} system property. You can also specify the word
76251881Speter * {@code unlimited} as the value for {@code nashorn.typeInfo.maxFiles} in which case the type info cache is
77251881Speter * allowed to grow without limits.
78251881Speter */
79251881Speterpublic final class OptimisticTypesPersistence {
80251881Speter    // Default is 0, for disabling the feature when not specified. A reasonable default when enabled is
81251881Speter    // dependent on the application; setting it to e.g. 20000 is probably good enough for most uses and will
82251881Speter    // usually cap the cache directory to about 80MB presuming a 4kB filesystem allocation unit. There is one
83251881Speter    // file per JavaScript function.
84251881Speter    private static final int DEFAULT_MAX_FILES = 0;
85251881Speter    // Constants for signifying that the cache should not be limited
86251881Speter    private static final int UNLIMITED_FILES = -1;
87251881Speter    // Maximum number of files that should be cached on disk. The maximum will be softly enforced.
88251881Speter    private static final int MAX_FILES = getMaxFiles();
89251881Speter    // Number of seconds to wait between adding a new file to the cache and running a cleanup process
90251881Speter    private static final int DEFAULT_CLEANUP_DELAY = 20;
91251881Speter    private static final int CLEANUP_DELAY = Math.max(0, Options.getIntProperty(
92251881Speter            "nashorn.typeInfo.cleanupDelaySeconds", DEFAULT_CLEANUP_DELAY));
93251881Speter    // The name of the default subdirectory within the system cache directory where we store type info.
94251881Speter    private static final String DEFAULT_CACHE_SUBDIR_NAME = "com.oracle.java.NashornTypeInfo";
95251881Speter    // The directory where we cache type info
96251881Speter    private static final File baseCacheDir = createBaseCacheDir();
97251881Speter    private static final File cacheDir = createCacheDir(baseCacheDir);
98251881Speter    // In-process locks to make sure we don't have a cross-thread race condition manipulating any file.
99251881Speter    private static final Object[] locks = cacheDir == null ? null : createLockArray();
100251881Speter    // Only report one read/write error every minute
101251881Speter    private static final long ERROR_REPORT_THRESHOLD = 60000L;
102251881Speter
103251881Speter    private static volatile long lastReportedError;
104251881Speter    private static final AtomicBoolean scheduledCleanup;
105251881Speter    private static final Timer cleanupTimer;
106251881Speter    static {
107251881Speter        if (baseCacheDir == null || MAX_FILES == UNLIMITED_FILES) {
108251881Speter            scheduledCleanup = null;
109251881Speter            cleanupTimer = null;
110251881Speter        } else {
111251881Speter            scheduledCleanup = new AtomicBoolean();
112251881Speter            cleanupTimer = new Timer(true);
113251881Speter        }
114251881Speter    }
115251881Speter    /**
116251881Speter     * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed
117251881Speter     * to {@link #load(Object)} and {@link #store(Object, Map)} methods.
118251881Speter     * @param source the source where the function comes from
119     * @param functionId the unique ID number of the function within the source
120     * @param paramTypes the types of the function parameters (as persistence is per parameter type
121     * specialization).
122     * @return an opaque descriptor for the persistence location. Can be null if persistence is disabled.
123     */
124    public static Object getLocationDescriptor(final Source source, final int functionId, final Type[] paramTypes) {
125        if(cacheDir == null) {
126            return null;
127        }
128        final StringBuilder b = new StringBuilder(48);
129        // Base64-encode the digest of the source, and append the function id.
130        b.append(source.getDigest()).append('-').append(functionId);
131        // Finally, if this is a parameter-type specialized version of the function, add the parameter types
132        // to the file name.
133        if(paramTypes != null && paramTypes.length > 0) {
134            b.append('-');
135            for(final Type t: paramTypes) {
136                b.append(Type.getShortSignatureDescriptor(t));
137            }
138        }
139        return new LocationDescriptor(new File(cacheDir, b.toString()));
140    }
141
142    private static final class LocationDescriptor {
143        private final File file;
144
145        LocationDescriptor(final File file) {
146            this.file = file;
147        }
148    }
149
150
151    /**
152     * Stores the map of optimistic types for a given function.
153     * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling
154     * {@link #getLocationDescriptor(Source, int, Type[])}.
155     * @param optimisticTypes the map of optimistic types.
156     */
157    @SuppressWarnings("resource")
158    public static void store(final Object locationDescriptor, final Map<Integer, Type> optimisticTypes) {
159        if(locationDescriptor == null || optimisticTypes.isEmpty()) {
160            return;
161        }
162        final File file = ((LocationDescriptor)locationDescriptor).file;
163
164        AccessController.doPrivileged(new PrivilegedAction<Void>() {
165            @Override
166            public Void run() {
167                synchronized(getFileLock(file)) {
168                    if (!file.exists()) {
169                        // If the file already exists, we aren't increasing the number of cached files, so
170                        // don't schedule cleanup.
171                        scheduleCleanup();
172                    }
173                    try (final FileOutputStream out = new FileOutputStream(file)) {
174                        out.getChannel().lock(); // lock exclusive
175                        final DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(out));
176                        Type.writeTypeMap(optimisticTypes, dout);
177                        dout.flush();
178                    } catch(final Exception e) {
179                        reportError("write", file, e);
180                    }
181                }
182                return null;
183            }
184        });
185    }
186
187    /**
188     * Loads the map of optimistic types for a given function.
189     * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling
190     * {@link #getLocationDescriptor(Source, int, Type[])}.
191     * @return the map of optimistic types, or null if persisted type information could not be retrieved.
192     */
193    @SuppressWarnings("resource")
194    public static Map<Integer, Type> load(final Object locationDescriptor) {
195        if (locationDescriptor == null) {
196            return null;
197        }
198        final File file = ((LocationDescriptor)locationDescriptor).file;
199        return AccessController.doPrivileged(new PrivilegedAction<Map<Integer, Type>>() {
200            @Override
201            public Map<Integer, Type> run() {
202                try {
203                    if(!file.isFile()) {
204                        return null;
205                    }
206                    synchronized(getFileLock(file)) {
207                        try (final FileInputStream in = new FileInputStream(file)) {
208                            in.getChannel().lock(0, Long.MAX_VALUE, true); // lock shared
209                            final DataInputStream din = new DataInputStream(new BufferedInputStream(in));
210                            return Type.readTypeMap(din);
211                        }
212                    }
213                } catch (final Exception e) {
214                    reportError("read", file, e);
215                    return null;
216                }
217            }
218        });
219    }
220
221    private static void reportError(final String msg, final File file, final Exception e) {
222        final long now = System.currentTimeMillis();
223        if(now - lastReportedError > ERROR_REPORT_THRESHOLD) {
224            getLogger().warning(String.format("Failed to %s %s", msg, file), e);
225            lastReportedError = now;
226        }
227    }
228
229    private static File createBaseCacheDir() {
230        if(MAX_FILES == 0 || Options.getBooleanProperty("nashorn.typeInfo.disabled")) {
231            return null;
232        }
233        try {
234            return createBaseCacheDirPrivileged();
235        } catch(final Exception e) {
236            getLogger().warning("Failed to create cache dir", e);
237            return null;
238        }
239    }
240
241    private static File createBaseCacheDirPrivileged() {
242        return AccessController.doPrivileged(new PrivilegedAction<File>() {
243            @Override
244            public File run() {
245                final String explicitDir = System.getProperty("nashorn.typeInfo.cacheDir");
246                final File dir;
247                if(explicitDir != null) {
248                    dir = new File(explicitDir);
249                } else {
250                    // When no directory is explicitly specified, get an operating system specific cache
251                    // directory, and create "com.oracle.java.NashornTypeInfo" in it.
252                    final File systemCacheDir = getSystemCacheDir();
253                    dir = new File(systemCacheDir, DEFAULT_CACHE_SUBDIR_NAME);
254                    if (isSymbolicLink(dir)) {
255                        return null;
256                    }
257                }
258                return dir;
259            }
260        });
261    }
262
263    private static File createCacheDir(final File baseDir) {
264        if (baseDir == null) {
265            return null;
266        }
267        try {
268            return createCacheDirPrivileged(baseDir);
269        } catch(final Exception e) {
270            getLogger().warning("Failed to create cache dir", e);
271            return null;
272        }
273    }
274
275    private static File createCacheDirPrivileged(final File baseDir) {
276        return AccessController.doPrivileged(new PrivilegedAction<File>() {
277            @Override
278            public File run() {
279                final String versionDirName;
280                try {
281                    versionDirName = getVersionDirName();
282                } catch(final Exception e) {
283                    getLogger().warning("Failed to calculate version dir name", e);
284                    return null;
285                }
286                final File versionDir = new File(baseDir, versionDirName);
287                if (isSymbolicLink(versionDir)) {
288                    return null;
289                }
290                versionDir.mkdirs();
291                if (versionDir.isDirectory()) {
292                    getLogger().info("Optimistic type persistence directory is " + versionDir);
293                    return versionDir;
294                }
295                getLogger().warning("Could not create optimistic type persistence directory " + versionDir);
296                return null;
297            }
298        });
299    }
300
301    /**
302     * Returns an operating system specific root directory for cache files.
303     * @return an operating system specific root directory for cache files.
304     */
305    private static File getSystemCacheDir() {
306        final String os = System.getProperty("os.name", "generic");
307        if("Mac OS X".equals(os)) {
308            // Mac OS X stores caches in ~/Library/Caches
309            return new File(new File(System.getProperty("user.home"), "Library"), "Caches");
310        } else if(os.startsWith("Windows")) {
311            // On Windows, temp directory is the best approximation of a cache directory, as its contents
312            // persist across reboots and various cleanup utilities know about it. java.io.tmpdir normally
313            // points to a user-specific temp directory, %HOME%\LocalSettings\Temp.
314            return new File(System.getProperty("java.io.tmpdir"));
315        } else {
316            // In other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache"
317            return new File(System.getProperty("user.home"), ".cache");
318        }
319    }
320
321    /**
322     * In order to ensure that changes in Nashorn code don't cause corruption in the data, we'll create a
323     * per-code-version directory. Normally, this will create the SHA-1 digest of the nashorn.jar. In case the classpath
324     * for nashorn is local directory (e.g. during development), this will create the string "dev-" followed by the
325     * timestamp of the most recent .class file.
326     * @return
327     */
328    private static String getVersionDirName() throws Exception {
329        final URL url = OptimisticTypesPersistence.class.getResource("");
330        final String protocol = url.getProtocol();
331        if (protocol.equals("jar")) {
332            // Normal deployment: nashorn.jar
333            final String jarUrlFile = url.getFile();
334            final String filePath = jarUrlFile.substring(0, jarUrlFile.indexOf('!'));
335            final URL file = new URL(filePath);
336            try (final InputStream in = file.openStream()) {
337                final byte[] buf = new byte[128*1024];
338                final MessageDigest digest = MessageDigest.getInstance("SHA-1");
339                for(;;) {
340                    final int l = in.read(buf);
341                    if(l == -1) {
342                        return Base64.getUrlEncoder().withoutPadding().encodeToString(digest.digest());
343                    }
344                    digest.update(buf, 0, l);
345                }
346            }
347        } else if(protocol.equals("file")) {
348            // Development
349            final String fileStr = url.getFile();
350            final String className = OptimisticTypesPersistence.class.getName();
351            final int packageNameLen = className.lastIndexOf('.');
352            final String dirStr = fileStr.substring(0, fileStr.length() - packageNameLen - 1);
353            final File dir = new File(dirStr);
354            return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile(
355                    dir, 0L)));
356        } else {
357            throw new AssertionError();
358        }
359    }
360
361    private static long getLastModifiedClassFile(final File dir, final long max) {
362        long currentMax = max;
363        for(final File f: dir.listFiles()) {
364            if(f.getName().endsWith(".class")) {
365                final long lastModified = f.lastModified();
366                if (lastModified > currentMax) {
367                    currentMax = lastModified;
368                }
369            } else if (f.isDirectory()) {
370                final long lastModified = getLastModifiedClassFile(f, currentMax);
371                if (lastModified > currentMax) {
372                    currentMax = lastModified;
373                }
374            }
375        }
376        return currentMax;
377    }
378
379    /**
380     * Returns true if the specified file is a symbolic link, and also logs a warning if it is.
381     * @param file the file
382     * @return true if file is a symbolic link, false otherwise.
383     */
384    private static boolean isSymbolicLink(final File file) {
385        if (Files.isSymbolicLink(file.toPath())) {
386            getLogger().warning("Directory " + file + " is a symlink");
387            return true;
388        }
389        return false;
390    }
391
392    private static Object[] createLockArray() {
393        final Object[] lockArray = new Object[Runtime.getRuntime().availableProcessors() * 2];
394        for (int i = 0; i < lockArray.length; ++i) {
395            lockArray[i] = new Object();
396        }
397        return lockArray;
398    }
399
400    private static Object getFileLock(final File file) {
401        return locks[(file.hashCode() & Integer.MAX_VALUE) % locks.length];
402    }
403
404    private static DebugLogger getLogger() {
405        try {
406            return Context.getContext().getLogger(RecompilableScriptFunctionData.class);
407        } catch (final Exception e) {
408            e.printStackTrace();
409            return DebugLogger.DISABLED_LOGGER;
410        }
411    }
412
413    private static void scheduleCleanup() {
414        if (MAX_FILES != UNLIMITED_FILES && scheduledCleanup.compareAndSet(false, true)) {
415            cleanupTimer.schedule(new TimerTask() {
416                @Override
417                public void run() {
418                    scheduledCleanup.set(false);
419                    try {
420                        doCleanup();
421                    } catch (final IOException e) {
422                        // Ignore it. While this is unfortunate, we don't have good facility for reporting
423                        // this, as we're running in a thread that has no access to Context, so we can't grab
424                        // a DebugLogger.
425                    }
426                }
427            }, TimeUnit.SECONDS.toMillis(CLEANUP_DELAY));
428        }
429    }
430
431    private static void doCleanup() throws IOException {
432        final Path[] files = getAllRegularFilesInLastModifiedOrder();
433        final int nFiles = files.length;
434        final int filesToDelete = Math.max(0, nFiles - MAX_FILES);
435        int filesDeleted = 0;
436        for (int i = 0; i < nFiles && filesDeleted < filesToDelete; ++i) {
437            try {
438                Files.deleteIfExists(files[i]);
439                // Even if it didn't exist, we increment filesDeleted; it existed a moment earlier; something
440                // else deleted it for us; that's okay with us.
441                filesDeleted++;
442            } catch (final Exception e) {
443                // does not increase filesDeleted
444            }
445            files[i] = null; // gc eligible
446        };
447    }
448
449    private static Path[] getAllRegularFilesInLastModifiedOrder() throws IOException {
450        try (final Stream<Path> filesStream = Files.walk(baseCacheDir.toPath())) {
451            // TODO: rewrite below once we can use JDK8 syntactic constructs
452            return filesStream
453            .filter(new Predicate<Path>() {
454                @Override
455                public boolean test(final Path path) {
456                    return !Files.isDirectory(path);
457                };
458            })
459            .map(new Function<Path, PathAndTime>() {
460                @Override
461                public PathAndTime apply(final Path path) {
462                    return new PathAndTime(path);
463                }
464            })
465            .sorted()
466            .map(new Function<PathAndTime, Path>() {
467                @Override
468                public Path apply(final PathAndTime pathAndTime) {
469                    return pathAndTime.path;
470                }
471            })
472            .toArray(new IntFunction<Path[]>() { // Replace with Path::new
473                @Override
474                public Path[] apply(final int length) {
475                    return new Path[length];
476                }
477            });
478        }
479    }
480
481    private static class PathAndTime implements Comparable<PathAndTime> {
482        private final Path path;
483        private final long time;
484
485        PathAndTime(final Path path) {
486            this.path = path;
487            this.time = getTime(path);
488        }
489
490        @Override
491        public int compareTo(final PathAndTime other) {
492            return Long.compare(time, other.time);
493        }
494
495        private static long getTime(final Path path) {
496            try {
497                return Files.getLastModifiedTime(path).toMillis();
498            } catch (final IOException e) {
499                // All files for which we can't retrieve the last modified date will be considered oldest.
500                return -1L;
501            }
502        }
503    }
504
505    private static int getMaxFiles() {
506        final String str = Options.getStringProperty("nashorn.typeInfo.maxFiles", null);
507        if (str == null) {
508            return DEFAULT_MAX_FILES;
509        } else if ("unlimited".equals(str)) {
510            return UNLIMITED_FILES;
511        }
512        return Math.max(0, Integer.parseInt(str));
513    }
514}
515