GlobalConstants.java revision 1423:c13179703f65
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 */
25
26package jdk.nashorn.internal.runtime;
27
28import static jdk.nashorn.internal.codegen.CompilerConstants.staticCall;
29import static jdk.nashorn.internal.codegen.CompilerConstants.virtualCall;
30import static jdk.nashorn.internal.lookup.Lookup.MH;
31import static jdk.nashorn.internal.runtime.UnwarrantedOptimismException.INVALID_PROGRAM_POINT;
32import static jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor.getProgramPoint;
33import static jdk.nashorn.internal.runtime.logging.DebugLogger.quote;
34
35import java.lang.invoke.MethodHandle;
36import java.lang.invoke.MethodHandles;
37import java.lang.invoke.SwitchPoint;
38import java.util.Arrays;
39import java.util.HashMap;
40import java.util.Map;
41import java.util.concurrent.atomic.AtomicBoolean;
42import java.util.logging.Level;
43import jdk.internal.dynalink.CallSiteDescriptor;
44import jdk.internal.dynalink.DynamicLinker;
45import jdk.internal.dynalink.linker.GuardedInvocation;
46import jdk.internal.dynalink.linker.LinkRequest;
47import jdk.nashorn.internal.lookup.Lookup;
48import jdk.nashorn.internal.lookup.MethodHandleFactory;
49import jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor;
50import jdk.nashorn.internal.runtime.logging.DebugLogger;
51import jdk.nashorn.internal.runtime.logging.Loggable;
52import jdk.nashorn.internal.runtime.logging.Logger;
53
54/**
55 * Each context owns one of these. This is basically table of accessors
56 * for global properties. A global constant is evaluated to a MethodHandle.constant
57 * for faster access and to avoid walking to proto chain looking for it.
58 *
59 * We put a switchpoint on the global setter, which invalidates the
60 * method handle constant getters, and reverts to the standard access strategy
61 *
62 * However, there is a twist - while certain globals like "undefined" and "Math"
63 * are usually never reassigned, a global value can be reset once, and never again.
64 * This is a rather common pattern, like:
65 *
66 * x = function(something) { ...
67 *
68 * Thus everything registered as a global constant gets an extra chance. Set once,
69 * reregister the switchpoint. Set twice or more - don't try again forever, or we'd
70 * just end up relinking our way into megamorphism.
71 *
72 * Also it has to be noted that this kind of linking creates a coupling between a Global
73 * and the call sites in compiled code belonging to the Context. For this reason, the
74 * linkage becomes incorrect as soon as the Context has more than one Global. The
75 * {@link #invalidateForever()} is invoked by the Context to invalidate all linkages and
76 * turn off the functionality of this object as soon as the Context's {@link Context#newGlobal()} is invoked
77 * for second time.
78 *
79 * We can extend this to ScriptObjects in general (GLOBAL_ONLY=false), which requires
80 * a receiver guard on the constant getter, but it currently leaks memory and its benefits
81 * have not yet been investigated property.
82 *
83 * As long as all Globals in a Context share the same GlobalConstants instance, we need synchronization
84 * whenever we access it.
85 */
86@Logger(name="const")
87public final class GlobalConstants implements Loggable {
88
89    /**
90     * Should we only try to link globals as constants, and not generic script objects.
91     * Script objects require a receiver guard, which is memory intensive, so this is currently
92     * disabled. We might implement a weak reference based approach to this later.
93     */
94    public static final boolean GLOBAL_ONLY = true;
95
96    private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
97
98    private static final MethodHandle INVALIDATE_SP  = virtualCall(LOOKUP, GlobalConstants.class, "invalidateSwitchPoint", Object.class, Object.class, Access.class).methodHandle();
99    private static final MethodHandle RECEIVER_GUARD = staticCall(LOOKUP, GlobalConstants.class, "receiverGuard", boolean.class, Access.class, Object.class, Object.class).methodHandle();
100
101    /** Logger for constant getters */
102    private final DebugLogger log;
103
104    /**
105     * Access map for this global - associates a symbol name with an Access object, with getter
106     * and invalidation information
107     */
108    private final Map<String, Access> map = new HashMap<>();
109
110    private final AtomicBoolean invalidatedForever = new AtomicBoolean(false);
111
112    /**
113     * Constructor - used only by global
114     * @param log logger, or null if none
115     */
116    public GlobalConstants(final DebugLogger log) {
117        this.log = log == null ? DebugLogger.DISABLED_LOGGER : log;
118    }
119
120    @Override
121    public DebugLogger getLogger() {
122        return log;
123    }
124
125    @Override
126    public DebugLogger initLogger(final Context context) {
127        return DebugLogger.DISABLED_LOGGER;
128    }
129
130    /**
131     * Information about a constant access and its potential invalidations
132     */
133    private static class Access {
134        /** name of symbol */
135        private final String name;
136
137        /** switchpoint that invalidates the getters and setters for this access */
138        private SwitchPoint sp;
139
140        /** invalidation count for this access, i.e. how many times has this property been reset */
141        private int invalidations;
142
143        /** has a guard guarding this property getter failed? */
144        private boolean guardFailed;
145
146        private static final int MAX_RETRIES = 2;
147
148        private Access(final String name, final SwitchPoint sp) {
149            this.name      = name;
150            this.sp        = sp;
151        }
152
153        private boolean hasBeenInvalidated() {
154            return sp.hasBeenInvalidated();
155        }
156
157        private boolean guardFailed() {
158            return guardFailed;
159        }
160
161        private void failGuard() {
162            invalidateOnce();
163            guardFailed = true;
164        }
165
166        private void newSwitchPoint() {
167            assert hasBeenInvalidated();
168            sp = new SwitchPoint();
169        }
170
171        private void invalidate(final int count) {
172            if (!sp.hasBeenInvalidated()) {
173                SwitchPoint.invalidateAll(new SwitchPoint[] { sp });
174                invalidations += count;
175            }
176        }
177
178        /**
179         * Invalidate the access, but do not contribute to the invalidation count
180         */
181        private void invalidateUncounted() {
182            invalidate(0);
183        }
184
185        /**
186         * Invalidate the access, and contribute 1 to the invalidation count
187         */
188        private void invalidateOnce() {
189            invalidate(1);
190        }
191
192        /**
193         * Invalidate the access and make sure that we never try to turn this into
194         * a MethodHandle.constant getter again
195         */
196        private void invalidateForever() {
197            invalidate(MAX_RETRIES);
198        }
199
200        /**
201         * Are we allowed to relink this as constant getter, even though it
202         * it has been reset
203         * @return true if we can relink as constant, one retry is allowed
204         */
205        private boolean mayRetry() {
206            return invalidations < MAX_RETRIES;
207        }
208
209        @Override
210        public String toString() {
211            return "[" + quote(name) + " <id=" + Debug.id(this) + "> inv#=" + invalidations + '/' + MAX_RETRIES + " sp_inv=" + sp.hasBeenInvalidated() + ']';
212        }
213
214        String getName() {
215            return name;
216        }
217
218        SwitchPoint getSwitchPoint() {
219            return sp;
220        }
221    }
222
223    /**
224     * To avoid an expensive global guard "is this the same global", similar to the
225     * receiver guard on the ScriptObject level, we invalidate all getters once
226     * when we switch globals. This is used from the class cache. We _can_ reuse
227     * the same class for a new global, but the builtins and global scoped variables
228     * will have changed.
229     */
230    public void invalidateAll() {
231        if (!invalidatedForever.get()) {
232            log.info("New global created - invalidating all constant callsites without increasing invocation count.");
233            synchronized (this) {
234                for (final Access acc : map.values()) {
235                    acc.invalidateUncounted();
236                }
237            }
238        }
239    }
240
241    /**
242     * To avoid an expensive global guard "is this the same global", similar to the
243     * receiver guard on the ScriptObject level, we invalidate all getters when the
244     * second Global is created by the Context owning this instance. After this
245     * method is invoked, this GlobalConstants instance will both invalidate all the
246     * switch points it produced, and it will stop handing out new method handles
247     * altogether.
248     */
249    public void invalidateForever() {
250        if (invalidatedForever.compareAndSet(false, true)) {
251            log.info("New global created - invalidating all constant callsites.");
252            synchronized (this) {
253                for (final Access acc : map.values()) {
254                    acc.invalidateForever();
255                }
256                map.clear();
257            }
258        }
259    }
260
261    /**
262     * Invalidate the switchpoint of an access - we have written to
263     * the property
264     *
265     * @param obj receiver
266     * @param acc access
267     *
268     * @return receiver, so this can be used as param filter
269     */
270    @SuppressWarnings("unused")
271    private synchronized Object invalidateSwitchPoint(final Object obj, final Access acc) {
272        if (log.isEnabled()) {
273            log.info("*** Invalidating switchpoint " + acc.getSwitchPoint() + " for receiver=" + obj + " access=" + acc);
274        }
275        acc.invalidateOnce();
276        if (acc.mayRetry()) {
277            if (log.isEnabled()) {
278                log.info("Retry is allowed for " + acc + "... Creating a new switchpoint.");
279            }
280            acc.newSwitchPoint();
281        } else {
282            if (log.isEnabled()) {
283                log.info("This was the last time I allowed " + quote(acc.getName()) + " to relink as constant.");
284            }
285        }
286        return obj;
287    }
288
289    private Access getOrCreateSwitchPoint(final String name) {
290        Access acc = map.get(name);
291        if (acc != null) {
292            return acc;
293        }
294        final SwitchPoint sp = new SwitchPoint();
295        map.put(name, acc = new Access(name, sp));
296        return acc;
297    }
298
299    /**
300     * Called from script object on property deletion to erase a property
301     * that might be linked as MethodHandle.constant and force relink
302     * @param name name of property
303     */
304    void delete(final String name) {
305        if (!invalidatedForever.get()) {
306            synchronized (this) {
307                final Access acc = map.get(name);
308                if (acc != null) {
309                    acc.invalidateForever();
310                }
311            }
312        }
313    }
314
315    /**
316     * Receiver guard is used if we extend the global constants to script objects in general.
317     * As the property can have different values in different script objects, while Global is
318     * by definition a singleton, we need this for ScriptObject constants (currently disabled)
319     *
320     * TODO: Note - this seems to cause memory leaks. Use weak references? But what is leaking seems
321     * to be the Access objects, which isn't the case for Globals. Weird.
322     *
323     * @param acc            access
324     * @param boundReceiver  the receiver bound to the callsite
325     * @param receiver       the receiver to check against
326     *
327     * @return true if this receiver is still the one we bound to the callsite
328     */
329    @SuppressWarnings("unused")
330    private static boolean receiverGuard(final Access acc, final Object boundReceiver, final Object receiver) {
331        final boolean id = receiver == boundReceiver;
332        if (!id) {
333            acc.failGuard();
334        }
335        return id;
336    }
337
338    private static boolean isGlobalSetter(final ScriptObject receiver, final FindProperty find) {
339        if (find == null) {
340            return receiver.isScope();
341        }
342        return find.getOwner().isGlobal();
343    }
344
345    /**
346     * Augment a setter with switchpoint for invalidating its getters, should the setter be called
347     *
348     * @param find    property lookup
349     * @param inv     normal guarded invocation for this setter, as computed by the ScriptObject linker
350     * @param desc    callsite descriptor
351     * @param request link request
352     *
353     * @return null if failed to set up constant linkage
354     */
355    GuardedInvocation findSetMethod(final FindProperty find, final ScriptObject receiver, final GuardedInvocation inv, final CallSiteDescriptor desc, final LinkRequest request) {
356        if (invalidatedForever.get() || (GLOBAL_ONLY && !isGlobalSetter(receiver, find))) {
357            return null;
358        }
359
360        final String name = desc.getNameToken(CallSiteDescriptor.NAME_OPERAND);
361
362        synchronized (this) {
363            final Access acc  = getOrCreateSwitchPoint(name);
364
365            if (log.isEnabled()) {
366                log.fine("Trying to link constant SETTER ", acc);
367            }
368
369            if (!acc.mayRetry() || invalidatedForever.get()) {
370                if (log.isEnabled()) {
371                    log.fine("*** SET: Giving up on " + quote(name) + " - retry count has exceeded " + DynamicLinker.getLinkedCallSiteLocation());
372                }
373                return null;
374            }
375
376            if (acc.hasBeenInvalidated()) {
377                log.info("New chance for " + acc);
378                acc.newSwitchPoint();
379            }
380
381            assert !acc.hasBeenInvalidated();
382
383            // if we haven't given up on this symbol, add a switchpoint invalidation filter to the receiver parameter
384            final MethodHandle target           = inv.getInvocation();
385            final Class<?>     receiverType     = target.type().parameterType(0);
386            final MethodHandle boundInvalidator = MH.bindTo(INVALIDATE_SP,  this);
387            final MethodHandle invalidator      = MH.asType(boundInvalidator, boundInvalidator.type().changeParameterType(0, receiverType).changeReturnType(receiverType));
388            final MethodHandle mh               = MH.filterArguments(inv.getInvocation(), 0, MH.insertArguments(invalidator, 1, acc));
389
390            assert inv.getSwitchPoints() == null : Arrays.asList(inv.getSwitchPoints());
391            log.info("Linked setter " + quote(name) + " " + acc.getSwitchPoint());
392            return new GuardedInvocation(mh, inv.getGuard(), acc.getSwitchPoint(), inv.getException());
393        }
394    }
395
396    /**
397     * Try to reuse constant method handles for getters
398     * @param c constant value
399     * @return method handle (with dummy receiver) that returns this constant
400     */
401    public static MethodHandle staticConstantGetter(final Object c) {
402        return MH.dropArguments(JSType.unboxConstant(c), 0, Object.class);
403    }
404
405    private MethodHandle constantGetter(final Object c) {
406        final MethodHandle mh = staticConstantGetter(c);
407        if (log.isEnabled()) {
408            return MethodHandleFactory.addDebugPrintout(log, Level.FINEST, mh, "getting as constant");
409        }
410        return mh;
411    }
412
413    /**
414     * Try to turn a getter into a MethodHandle.constant, if possible
415     *
416     * @param find      property lookup
417     * @param receiver  receiver
418     * @param desc      callsite descriptor
419     *
420     * @return resulting getter, or null if failed to create constant
421     */
422    GuardedInvocation findGetMethod(final FindProperty find, final ScriptObject receiver, final CallSiteDescriptor desc) {
423        // Only use constant getter for fast scope access, because the receiver may change between invocations
424        // for slow-scope and non-scope callsites.
425        // Also return null for user accessor properties as they may have side effects.
426        if (invalidatedForever.get() || !NashornCallSiteDescriptor.isFastScope(desc)
427                || (GLOBAL_ONLY && !find.getOwner().isGlobal())
428                || find.getProperty() instanceof UserAccessorProperty) {
429            return null;
430        }
431
432        final boolean  isOptimistic = NashornCallSiteDescriptor.isOptimistic(desc);
433        final int      programPoint = isOptimistic ? getProgramPoint(desc) : INVALID_PROGRAM_POINT;
434        final Class<?> retType      = desc.getMethodType().returnType();
435        final String   name         = desc.getNameToken(CallSiteDescriptor.NAME_OPERAND);
436
437        synchronized (this) {
438            final Access acc = getOrCreateSwitchPoint(name);
439
440            log.fine("Starting to look up object value " + name);
441            final Object c = find.getObjectValue();
442
443            if (log.isEnabled()) {
444                log.fine("Trying to link constant GETTER " + acc + " value = " + c);
445            }
446
447            if (acc.hasBeenInvalidated() || acc.guardFailed() || invalidatedForever.get()) {
448                if (log.isEnabled()) {
449                    log.info("*** GET: Giving up on " + quote(name) + " - retry count has exceeded " + DynamicLinker.getLinkedCallSiteLocation());
450                }
451                return null;
452            }
453
454            final MethodHandle cmh = constantGetter(c);
455
456            MethodHandle mh;
457            MethodHandle guard;
458
459            if (isOptimistic) {
460                if (JSType.getAccessorTypeIndex(cmh.type().returnType()) <= JSType.getAccessorTypeIndex(retType)) {
461                    //widen return type - this is pessimistic, so it will always work
462                    mh = MH.asType(cmh, cmh.type().changeReturnType(retType));
463                } else {
464                    //immediately invalidate - we asked for a too wide constant as a narrower one
465                    mh = MH.dropArguments(MH.insertArguments(JSType.THROW_UNWARRANTED.methodHandle(), 0, c, programPoint), 0, Object.class);
466                }
467            } else {
468                //pessimistic return type filter
469                mh = Lookup.filterReturnType(cmh, retType);
470            }
471
472            if (find.getOwner().isGlobal()) {
473                guard = null;
474            } else {
475                guard = MH.insertArguments(RECEIVER_GUARD, 0, acc, receiver);
476            }
477
478            if (log.isEnabled()) {
479                log.info("Linked getter " + quote(name) + " as MethodHandle.constant() -> " + c + " " + acc.getSwitchPoint());
480                mh = MethodHandleFactory.addDebugPrintout(log, Level.FINE, mh, "get const " + acc);
481            }
482
483            return new GuardedInvocation(mh, guard, acc.getSwitchPoint(), null);
484        }
485    }
486}
487