1 /*
2  * Copyright (c) 2010, 2017, 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.linker;
27 
28 import static jdk.nashorn.internal.lookup.Lookup.MH;
29 
30 import java.io.FileNotFoundException;
31 import java.io.FileOutputStream;
32 import java.io.PrintWriter;
33 import java.lang.invoke.MethodHandle;
34 import java.lang.invoke.MethodHandles;
35 import java.lang.invoke.MethodType;
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.Comparator;
39 import java.util.HashMap;
40 import java.util.LinkedList;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 import java.util.Random;
44 import java.util.Set;
45 import java.util.concurrent.atomic.AtomicInteger;
46 import java.util.concurrent.atomic.LongAdder;
47 import jdk.dynalink.DynamicLinker;
48 import jdk.dynalink.linker.GuardedInvocation;
49 import jdk.dynalink.support.ChainedCallSite;
50 import jdk.nashorn.internal.runtime.Context;
51 import jdk.nashorn.internal.runtime.Debug;
52 import jdk.nashorn.internal.runtime.ScriptObject;
53 import jdk.nashorn.internal.runtime.ScriptRuntime;
54 import jdk.nashorn.internal.runtime.options.Options;
55 
56 
57 /**
58  * Relinkable form of call site.
59  */
60 public class LinkerCallSite extends ChainedCallSite {
61     /** Maximum number of arguments passed directly. */
62     public static final int ARGLIMIT = 125;
63 
64     private static final String PROFILEFILE = Options.getStringProperty("nashorn.profilefile", "NashornProfile.txt");
65 
66     private static final MethodHandle INCREASE_MISS_COUNTER = MH.findStatic(MethodHandles.lookup(), LinkerCallSite.class, "increaseMissCount", MH.type(Object.class, String.class, Object.class));
67 
LinkerCallSite(final NashornCallSiteDescriptor descriptor)68     LinkerCallSite(final NashornCallSiteDescriptor descriptor) {
69         super(descriptor);
70         if (Context.DEBUG) {
71             LinkerCallSite.count.increment();
72         }
73     }
74 
75     /**
76      * Construct a new linker call site.
77      * @param name     Name of method.
78      * @param type     Method type.
79      * @param flags    Call site specific flags.
80      * @return New LinkerCallSite.
81      */
newLinkerCallSite(final MethodHandles.Lookup lookup, final String name, final MethodType type, final int flags)82     static LinkerCallSite newLinkerCallSite(final MethodHandles.Lookup lookup, final String name, final MethodType type, final int flags) {
83         final NashornCallSiteDescriptor desc = NashornCallSiteDescriptor.get(lookup, name, type, flags);
84 
85         if (desc.isProfile()) {
86             return ProfilingLinkerCallSite.newProfilingLinkerCallSite(desc);
87         }
88 
89         if (desc.isTrace()) {
90             return new TracingLinkerCallSite(desc);
91         }
92 
93         return new LinkerCallSite(desc);
94     }
95 
96     @Override
toString()97     public String toString() {
98         return getDescriptor().toString();
99     }
100 
101     /**
102      * Get the descriptor for this callsite
103      * @return a {@link NashornCallSiteDescriptor}
104      */
getNashornDescriptor()105     public NashornCallSiteDescriptor getNashornDescriptor() {
106         return (NashornCallSiteDescriptor)getDescriptor();
107     }
108 
109     @Override
relink(final GuardedInvocation invocation, final MethodHandle relink)110     public void relink(final GuardedInvocation invocation, final MethodHandle relink) {
111         super.relink(invocation, getDebuggingRelink(relink));
112     }
113 
114     @Override
resetAndRelink(final GuardedInvocation invocation, final MethodHandle relink)115     public void resetAndRelink(final GuardedInvocation invocation, final MethodHandle relink) {
116         super.resetAndRelink(invocation, getDebuggingRelink(relink));
117     }
118 
getDebuggingRelink(final MethodHandle relink)119     private MethodHandle getDebuggingRelink(final MethodHandle relink) {
120         if (Context.DEBUG) {
121             return MH.filterArguments(relink, 0, getIncreaseMissCounter(relink.type().parameterType(0)));
122         }
123         return relink;
124     }
125 
getIncreaseMissCounter(final Class<?> type)126     private MethodHandle getIncreaseMissCounter(final Class<?> type) {
127         final MethodHandle missCounterWithDesc = MH.bindTo(INCREASE_MISS_COUNTER, getDescriptor().getOperation() + " @ " + getScriptLocation());
128         if (type == Object.class) {
129             return missCounterWithDesc;
130         }
131         return MH.asType(missCounterWithDesc, missCounterWithDesc.type().changeParameterType(0, type).changeReturnType(type));
132     }
133 
getScriptLocation()134     private static String getScriptLocation() {
135         final StackTraceElement caller = DynamicLinker.getLinkedCallSiteLocation();
136         return caller == null ? "unknown location" : (caller.getFileName() + ":" + caller.getLineNumber());
137     }
138 
139     /**
140      * Instrumentation - increase the miss count when a callsite misses. Used as filter
141      * @param desc descriptor for table entry
142      * @param self self reference
143      * @return self reference
144      */
increaseMissCount(final String desc, final Object self)145     public static Object increaseMissCount(final String desc, final Object self) {
146         missCount.increment();
147         if (r.nextInt(100) < missSamplingPercentage) {
148             final AtomicInteger i = missCounts.get(desc);
149             if (i == null) {
150                 missCounts.put(desc, new AtomicInteger(1));
151             } else {
152                 i.incrementAndGet();
153             }
154         }
155         return self;
156     }
157 
158     /*
159      * Debugging call sites.
160      */
161 
162     private static class ProfilingLinkerCallSite extends LinkerCallSite {
163         /** List of all profiled call sites. */
164         private static LinkedList<ProfilingLinkerCallSite> profileCallSites = null;
165 
166         /** Start time when entered at zero depth. */
167         private long startTime;
168 
169         /** Depth of nested calls. */
170         private int depth;
171 
172         /** Total time spent in this call site. */
173         private long totalTime;
174 
175         /** Total number of times call site entered. */
176         private long hitCount;
177 
178         private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
179 
180         private static final MethodHandle PROFILEENTRY    = MH.findVirtual(LOOKUP, ProfilingLinkerCallSite.class, "profileEntry",    MH.type(Object.class, Object.class));
181         private static final MethodHandle PROFILEEXIT     = MH.findVirtual(LOOKUP, ProfilingLinkerCallSite.class, "profileExit",     MH.type(Object.class, Object.class));
182         private static final MethodHandle PROFILEVOIDEXIT = MH.findVirtual(LOOKUP, ProfilingLinkerCallSite.class, "profileVoidExit", MH.type(void.class));
183 
184         /*
185          * Constructor
186          */
187 
ProfilingLinkerCallSite(final NashornCallSiteDescriptor desc)188         ProfilingLinkerCallSite(final NashornCallSiteDescriptor desc) {
189            super(desc);
190         }
191 
newProfilingLinkerCallSite(final NashornCallSiteDescriptor desc)192         public static ProfilingLinkerCallSite newProfilingLinkerCallSite(final NashornCallSiteDescriptor desc) {
193             if (profileCallSites == null) {
194                 profileCallSites = new LinkedList<>();
195 
196                 final Thread profileDumperThread = new Thread(new ProfileDumper());
197                 Runtime.getRuntime().addShutdownHook(profileDumperThread);
198             }
199 
200             final ProfilingLinkerCallSite callSite = new ProfilingLinkerCallSite(desc);
201             profileCallSites.add(callSite);
202 
203             return callSite;
204         }
205 
206         @Override
setTarget(final MethodHandle newTarget)207         public void setTarget(final MethodHandle newTarget) {
208             final MethodType type   = type();
209             final boolean    isVoid = type.returnType() == void.class;
210             final Class<?> newSelfType = newTarget.type().parameterType(0);
211 
212             MethodHandle selfFilter = MH.bindTo(PROFILEENTRY, this);
213             if (newSelfType != Object.class) {
214                 // new target uses a more precise 'self' type than Object.class. We need to
215                 // convert the filter type. Note that the profileEntry method returns "self"
216                 // argument "as is" and so the cast introduced will succeed for any type.
217                 final MethodType selfFilterType = MethodType.methodType(newSelfType, newSelfType);
218                 selfFilter = selfFilter.asType(selfFilterType);
219             }
220 
221             MethodHandle methodHandle = MH.filterArguments(newTarget, 0, selfFilter);
222 
223             if (isVoid) {
224                 methodHandle = MH.filterReturnValue(methodHandle, MH.bindTo(PROFILEVOIDEXIT, this));
225             } else {
226                 final MethodType filter = MH.type(type.returnType(), type.returnType());
227                 methodHandle = MH.filterReturnValue(methodHandle, MH.asType(MH.bindTo(PROFILEEXIT, this), filter));
228             }
229 
230             super.setTarget(methodHandle);
231         }
232 
233         /**
234          * Start the clock for a profile entry and increase depth
235          * @param self argument to filter
236          * @return preserved argument
237          */
238         @SuppressWarnings("unused")
profileEntry(final Object self)239         public Object profileEntry(final Object self) {
240             if (depth == 0) {
241                 startTime = System.nanoTime();
242             }
243 
244             depth++;
245             hitCount++;
246 
247             return self;
248         }
249 
250         /**
251          * Decrease depth and stop the clock for a profile entry
252          * @param result return value to filter
253          * @return preserved argument
254          */
255         @SuppressWarnings("unused")
profileExit(final Object result)256         public Object profileExit(final Object result) {
257             depth--;
258 
259             if (depth == 0) {
260                 totalTime += System.nanoTime() - startTime;
261             }
262 
263             return result;
264         }
265 
266         /**
267          * Decrease depth without return value filter
268          */
269         @SuppressWarnings("unused")
profileVoidExit()270         public void profileVoidExit() {
271             depth--;
272 
273             if (depth == 0) {
274                 totalTime += System.nanoTime() - startTime;
275             }
276         }
277 
278         static class ProfileDumper implements Runnable {
279             @Override
run()280             public void run() {
281                 PrintWriter out    = null;
282                 boolean fileOutput = false;
283 
284                 try {
285                     try {
286                         out = new PrintWriter(new FileOutputStream(PROFILEFILE));
287                         fileOutput = true;
288                     } catch (final FileNotFoundException e) {
289                         out = Context.getCurrentErr();
290                     }
291 
292                     dump(out);
293                 } finally {
294                     if (out != null && fileOutput) {
295                         out.close();
296                     }
297                 }
298             }
299 
dump(final PrintWriter out)300             private static void dump(final PrintWriter out) {
301                 int index = 0;
302                 for (final ProfilingLinkerCallSite callSite : profileCallSites) {
303                    out.println("" + (index++) + '\t' +
304                                   callSite.getDescriptor().getOperation() + '\t' +
305                                   callSite.totalTime + '\t' +
306                                   callSite.hitCount);
307                 }
308             }
309         }
310     }
311 
312     /**
313      * Debug subclass for LinkerCallSite that allows tracing
314      */
315     private static class TracingLinkerCallSite extends LinkerCallSite {
316         private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
317 
318         private static final MethodHandle TRACEOBJECT = MH.findVirtual(LOOKUP, TracingLinkerCallSite.class, "traceObject", MH.type(Object.class, MethodHandle.class, Object[].class));
319         private static final MethodHandle TRACEVOID   = MH.findVirtual(LOOKUP, TracingLinkerCallSite.class, "traceVoid", MH.type(void.class, MethodHandle.class, Object[].class));
320         private static final MethodHandle TRACEMISS   = MH.findVirtual(LOOKUP, TracingLinkerCallSite.class, "traceMiss", MH.type(void.class, String.class, Object[].class));
321 
TracingLinkerCallSite(final NashornCallSiteDescriptor desc)322         TracingLinkerCallSite(final NashornCallSiteDescriptor desc) {
323            super(desc);
324         }
325 
326         @Override
setTarget(final MethodHandle newTarget)327         public void setTarget(final MethodHandle newTarget) {
328             if (!getNashornDescriptor().isTraceEnterExit()) {
329                 super.setTarget(newTarget);
330                 return;
331             }
332 
333             final MethodType type = type();
334             final boolean isVoid = type.returnType() == void.class;
335 
336             MethodHandle traceMethodHandle = isVoid ? TRACEVOID : TRACEOBJECT;
337             traceMethodHandle = MH.bindTo(traceMethodHandle, this);
338             traceMethodHandle = MH.bindTo(traceMethodHandle, newTarget);
339             traceMethodHandle = MH.asCollector(traceMethodHandle, Object[].class, type.parameterCount());
340             traceMethodHandle = MH.asType(traceMethodHandle, type);
341 
342             super.setTarget(traceMethodHandle);
343         }
344 
345         @Override
initialize(final MethodHandle relinkAndInvoke)346         public void initialize(final MethodHandle relinkAndInvoke) {
347             super.initialize(getFallbackLoggingRelink(relinkAndInvoke));
348         }
349 
350         @Override
relink(final GuardedInvocation invocation, final MethodHandle relink)351         public void relink(final GuardedInvocation invocation, final MethodHandle relink) {
352             super.relink(invocation, getFallbackLoggingRelink(relink));
353         }
354 
355         @Override
resetAndRelink(final GuardedInvocation invocation, final MethodHandle relink)356         public void resetAndRelink(final GuardedInvocation invocation, final MethodHandle relink) {
357             super.resetAndRelink(invocation, getFallbackLoggingRelink(relink));
358         }
359 
getFallbackLoggingRelink(final MethodHandle relink)360         private MethodHandle getFallbackLoggingRelink(final MethodHandle relink) {
361             if (!getNashornDescriptor().isTraceMisses()) {
362                 // If we aren't tracing misses, just return relink as-is
363                 return relink;
364             }
365             final MethodType type = relink.type();
366             return MH.foldArguments(relink, MH.asType(MH.asCollector(MH.insertArguments(TRACEMISS, 0, this, "MISS " + getScriptLocation() + " "), Object[].class, type.parameterCount()), type.changeReturnType(void.class)));
367         }
368 
printObject(final PrintWriter out, final Object arg)369         private void printObject(final PrintWriter out, final Object arg) {
370             if (!getNashornDescriptor().isTraceObjects()) {
371                 out.print((arg instanceof ScriptObject) ? "ScriptObject" : arg);
372                 return;
373             }
374 
375             if (arg instanceof ScriptObject) {
376                 final ScriptObject object = (ScriptObject)arg;
377 
378                 boolean isFirst = true;
379                 final Set<Object> keySet = object.keySet();
380 
381                 if (keySet.isEmpty()) {
382                     out.print(ScriptRuntime.safeToString(arg));
383                 } else {
384                     out.print("{ ");
385 
386                     for (final Object key : keySet) {
387                         if (!isFirst) {
388                             out.print(", ");
389                         }
390 
391                         out.print(key);
392                         out.print(":");
393 
394                         final Object value = object.get(key);
395 
396                         if (value instanceof ScriptObject) {
397                             out.print("...");
398                         } else {
399                             printObject(out, value);
400                         }
401 
402                         isFirst = false;
403                     }
404 
405                     out.print(" }");
406                 }
407              } else {
408                 out.print(ScriptRuntime.safeToString(arg));
409             }
410         }
411 
tracePrint(final PrintWriter out, final String tag, final Object[] args, final Object result)412         private void tracePrint(final PrintWriter out, final String tag, final Object[] args, final Object result) {
413             //boolean isVoid = type().returnType() == void.class;
414             out.print(Debug.id(this) + " TAG " + tag);
415             out.print(getDescriptor().getOperation() + "(");
416 
417             if (args.length > 0) {
418                 printObject(out, args[0]);
419                 for (int i = 1; i < args.length; i++) {
420                     final Object arg = args[i];
421                     out.print(", ");
422 
423                     if (!(arg instanceof ScriptObject && ((ScriptObject)arg).isScope())) {
424                         printObject(out, arg);
425                     } else {
426                         out.print("SCOPE");
427                     }
428                 }
429             }
430 
431             out.print(")");
432 
433             if (tag.equals("EXIT  ")) {
434                 out.print(" --> ");
435                 printObject(out, result);
436             }
437 
438             out.println();
439         }
440 
441         /**
442          * Trace event. Wrap an invocation with a return value
443          *
444          * @param mh     invocation handle
445          * @param args   arguments to call
446          *
447          * @return return value from invocation
448          *
449          * @throws Throwable if invocation fails or throws exception/error
450          */
451         @SuppressWarnings("unused")
traceObject(final MethodHandle mh, final Object... args)452         public Object traceObject(final MethodHandle mh, final Object... args) throws Throwable {
453             final PrintWriter out = Context.getCurrentErr();
454             tracePrint(out, "ENTER ", args, null);
455             final Object result = mh.invokeWithArguments(args);
456             tracePrint(out, "EXIT  ", args, result);
457 
458             return result;
459         }
460 
461         /**
462          * Trace event. Wrap an invocation that returns void
463          *
464          * @param mh     invocation handle
465          * @param args   arguments to call
466          *
467          * @throws Throwable if invocation fails or throws exception/error
468          */
469         @SuppressWarnings("unused")
traceVoid(final MethodHandle mh, final Object... args)470         public void traceVoid(final MethodHandle mh, final Object... args) throws Throwable {
471             final PrintWriter out = Context.getCurrentErr();
472             tracePrint(out, "ENTER ", args, null);
473             mh.invokeWithArguments(args);
474             tracePrint(out, "EXIT  ", args, null);
475         }
476 
477         /**
478          * Tracer function that logs a callsite miss
479          *
480          * @param desc callsite descriptor string
481          * @param args arguments to function
482          *
483          * @throws Throwable if invocation fails or throws exception/error
484          */
485         @SuppressWarnings("unused")
traceMiss(final String desc, final Object... args)486         public void traceMiss(final String desc, final Object... args) throws Throwable {
487             tracePrint(Context.getCurrentErr(), desc, args, null);
488         }
489     }
490 
491     // counters updated in debug mode
492     private static LongAdder count;
493     private static final HashMap<String, AtomicInteger> missCounts = new HashMap<>();
494     private static LongAdder missCount;
495     private static final Random r = new Random();
496     private static final int missSamplingPercentage = Options.getIntProperty("nashorn.tcs.miss.samplePercent", 1);
497 
498     static {
499         if (Context.DEBUG) {
500             count = new LongAdder();
501             missCount = new LongAdder();
502         }
503     }
504 
505     @Override
getMaxChainLength()506     protected int getMaxChainLength() {
507         return 8;
508     }
509 
510     /**
511      * Get the callsite count
512      * @return the count
513      */
getCount()514     public static long getCount() {
515         return count.longValue();
516     }
517 
518     /**
519      * Get the callsite miss count
520      * @return the missCount
521      */
getMissCount()522     public static long getMissCount() {
523         return missCount.longValue();
524     }
525 
526     /**
527      * Get given miss sampling percentage for sampler. Default is 1%. Specified with -Dnashorn.tcs.miss.samplePercent=x
528      * @return miss sampling percentage
529      */
getMissSamplingPercentage()530     public static int getMissSamplingPercentage() {
531         return missSamplingPercentage;
532     }
533 
534     /**
535      * Dump the miss counts collected so far to a given output stream
536      * @param out print stream
537      */
getMissCounts(final PrintWriter out)538     public static void getMissCounts(final PrintWriter out) {
539         final ArrayList<Entry<String, AtomicInteger>> entries = new ArrayList<>(missCounts.entrySet());
540 
541         Collections.sort(entries, new Comparator<Map.Entry<String, AtomicInteger>>() {
542             @Override
543             public int compare(final Entry<String, AtomicInteger> o1, final Entry<String, AtomicInteger> o2) {
544                 return o2.getValue().get() - o1.getValue().get();
545             }
546         });
547 
548         for (final Entry<String, AtomicInteger> entry : entries) {
549             out.println("  " + entry.getKey() + "\t" + entry.getValue().get());
550         }
551     }
552 
553 }
554