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 
26 package jdk.nashorn.internal.runtime;
27 
28 import static jdk.nashorn.internal.codegen.CompilerConstants.staticCall;
29 import static jdk.nashorn.internal.codegen.CompilerConstants.virtualCall;
30 import static jdk.nashorn.internal.lookup.Lookup.MH;
31 import static jdk.nashorn.internal.runtime.UnwarrantedOptimismException.INVALID_PROGRAM_POINT;
32 import static jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor.getProgramPoint;
33 import static jdk.nashorn.internal.runtime.logging.DebugLogger.quote;
34 
35 import java.lang.invoke.MethodHandle;
36 import java.lang.invoke.MethodHandles;
37 import java.lang.invoke.SwitchPoint;
38 import java.util.Arrays;
39 import java.util.HashMap;
40 import java.util.Map;
41 import java.util.concurrent.atomic.AtomicBoolean;
42 import java.util.logging.Level;
43 import jdk.dynalink.CallSiteDescriptor;
44 import jdk.dynalink.DynamicLinker;
45 import jdk.dynalink.linker.GuardedInvocation;
46 import jdk.dynalink.linker.LinkRequest;
47 import jdk.nashorn.internal.lookup.Lookup;
48 import jdk.nashorn.internal.lookup.MethodHandleFactory;
49 import jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor;
50 import jdk.nashorn.internal.runtime.logging.DebugLogger;
51 import jdk.nashorn.internal.runtime.logging.Loggable;
52 import 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")
87 public 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<Object, 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      */
GlobalConstants(final DebugLogger log)116     public GlobalConstants(final DebugLogger log) {
117         this.log = log == null ? DebugLogger.DISABLED_LOGGER : log;
118     }
119 
120     @Override
getLogger()121     public DebugLogger getLogger() {
122         return log;
123     }
124 
125     @Override
initLogger(final Context context)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 
Access(final String name, final SwitchPoint sp)148         private Access(final String name, final SwitchPoint sp) {
149             this.name      = name;
150             this.sp        = sp;
151         }
152 
hasBeenInvalidated()153         private boolean hasBeenInvalidated() {
154             return sp.hasBeenInvalidated();
155         }
156 
guardFailed()157         private boolean guardFailed() {
158             return guardFailed;
159         }
160 
failGuard()161         private void failGuard() {
162             invalidateOnce();
163             guardFailed = true;
164         }
165 
newSwitchPoint()166         private void newSwitchPoint() {
167             assert hasBeenInvalidated();
168             sp = new SwitchPoint();
169         }
170 
invalidate(final int count)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          */
invalidateUncounted()181         private void invalidateUncounted() {
182             invalidate(0);
183         }
184 
185         /**
186          * Invalidate the access, and contribute 1 to the invalidation count
187          */
invalidateOnce()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          */
invalidateForever()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          */
mayRetry()205         private boolean mayRetry() {
206             return invalidations < MAX_RETRIES;
207         }
208 
209         @Override
toString()210         public String toString() {
211             return "[" + quote(name) + " <id=" + Debug.id(this) + "> inv#=" + invalidations + '/' + MAX_RETRIES + " sp_inv=" + sp.hasBeenInvalidated() + ']';
212         }
213 
getName()214         String getName() {
215             return name;
216         }
217 
getSwitchPoint()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      */
invalidateAll()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      */
invalidateForever()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")
invalidateSwitchPoint(final Object obj, final Access acc)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 
getOrCreateSwitchPoint(final String name)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      */
delete(final Object name)304     void delete(final Object 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")
receiverGuard(final Access acc, final Object boundReceiver, final Object receiver)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 
isGlobalSetter(final ScriptObject receiver, final FindProperty find)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      */
findSetMethod(final FindProperty find, final ScriptObject receiver, final GuardedInvocation inv, final CallSiteDescriptor desc, final LinkRequest request)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 = NashornCallSiteDescriptor.getOperand(desc);
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      */
staticConstantGetter(final Object c)401     public static MethodHandle staticConstantGetter(final Object c) {
402         return MH.dropArguments(JSType.unboxConstant(c), 0, Object.class);
403     }
404 
constantGetter(final Object c)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      */
findGetMethod(final FindProperty find, final ScriptObject receiver, final CallSiteDescriptor desc)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         = NashornCallSiteDescriptor.getOperand(desc);
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