001/*
002 * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.
008 *
009 * This code is distributed in the hope that it will be useful, but WITHOUT
010 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
011 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
012 * version 2 for more details (a copy is included in the LICENSE file that
013 * accompanied this code).
014 *
015 * You should have received a copy of the GNU General Public License version
016 * 2 along with this work; if not, write to the Free Software Foundation,
017 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
018 *
019 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
020 * or visit www.oracle.com if you need additional information or have any
021 * questions.
022 */
023package com.oracle.graal.compiler.test;
024
025import static java.lang.Boolean.*;
026import static java.lang.Integer.*;
027import static java.lang.System.*;
028
029import java.io.*;
030import java.lang.reflect.*;
031import java.util.*;
032
033import com.google.monitoring.runtime.instrumentation.*;
034
035/**
036 * Tool for analyzing allocations within a scope using the <a
037 * href="https://code.google.com/p/java-allocation-instrumenter/">Java Allocation Instrumenter</a>.
038 * Allocation records are aggregated per stack trace at an allocation site. The size of the stack
039 * trace is governed by the value of the "AllocSpy.ContextSize" system property (default is 5).
040 * <p>
041 * Using this facility requires using -javaagent on the command line. For example:
042 *
043 * <pre>
044 * mx --vm server unittest -javaagent:lib/java-allocation-instrumenter.jar -dsa -DAllocSpy.ContextSize=6 BC_iadd2
045 * </pre>
046 *
047 * @see #SampleBytes
048 * @see #SampleInstances
049 * @see #HistogramLimit
050 * @see #NameSize
051 * @see #BarSize
052 * @see #NumberSize
053 */
054public final class AllocSpy implements AutoCloseable {
055
056    static ThreadLocal<AllocSpy> current = new ThreadLocal<>();
057
058    private static final boolean ENABLED;
059    static {
060        boolean enabled = false;
061        try {
062            Field field = AllocationRecorder.class.getDeclaredField("instrumentation");
063            field.setAccessible(true);
064            enabled = field.get(null) != null;
065        } catch (Exception e) {
066        }
067        ENABLED = enabled;
068        if (ENABLED) {
069            AllocationRecorder.addSampler(new GraalContextSampler());
070        }
071    }
072
073    public static boolean isEnabled() {
074        return ENABLED;
075    }
076
077    static String prop(String sfx) {
078        return AllocSpy.class.getSimpleName() + "." + sfx;
079    }
080
081    /**
082     * Determines if bytes per allocation site are recorded.
083     */
084    private static final boolean SampleBytes = parseBoolean(getProperty(prop("SampleBytes"), "true"));
085
086    /**
087     * Determines if allocations per allocation site are recorded.
088     */
089    private static final boolean SampleInstances = parseBoolean(getProperty(prop("SampleInstances"), "true"));
090
091    /**
092     * The size of context to record for each allocation site in terms of Graal frames.
093     */
094    private static final int ContextSize = getInteger(prop("ContextSize"), 5);
095
096    /**
097     * Only the {@code HistogramLimit} most frequent values are printed.
098     */
099    private static final int HistogramLimit = getInteger(prop("HistogramLimit"), 40);
100
101    /**
102     * The width of the allocation context column.
103     */
104    private static final int NameSize = getInteger(prop("NameSize"), 50);
105
106    /**
107     * The width of the histogram bar column.
108     */
109    private static final int BarSize = getInteger(prop("BarSize"), 100);
110
111    /**
112     * The width of the frequency column.
113     */
114    private static final int NumberSize = getInteger(prop("NumberSize"), 10);
115
116    final Object name;
117    final AllocSpy parent;
118    final Map<String, CountedValue> bytesPerGraalContext = new HashMap<>();
119    final Map<String, CountedValue> instancesPerGraalContext = new HashMap<>();
120
121    public static AllocSpy open(Object name) {
122        if (ENABLED) {
123            return new AllocSpy(name);
124        }
125        return null;
126    }
127
128    private AllocSpy(Object name) {
129        this.name = name;
130        parent = current.get();
131        current.set(this);
132    }
133
134    public void close() {
135        current.set(parent);
136        PrintStream ps = System.out;
137        ps.println("\n\nAllocation histograms for " + name);
138        if (SampleBytes) {
139            print(ps, bytesPerGraalContext, "BytesPerGraalContext", HistogramLimit, NameSize + 60, BarSize);
140        }
141        if (SampleInstances) {
142            print(ps, instancesPerGraalContext, "InstancesPerGraalContext", HistogramLimit, NameSize + 60, BarSize);
143        }
144    }
145
146    private static void printLine(PrintStream printStream, char c, int lineSize) {
147        char[] charArr = new char[lineSize];
148        Arrays.fill(charArr, c);
149        printStream.printf("%s%n", new String(charArr));
150    }
151
152    private static void print(PrintStream ps, Map<String, CountedValue> map, String name, int limit, int nameSize, int barSize) {
153        if (map.isEmpty()) {
154            return;
155        }
156
157        List<CountedValue> list = new ArrayList<>(map.values());
158        Collections.sort(list);
159
160        // Sum up the total number of elements.
161        int total = 0;
162        for (CountedValue cv : list) {
163            total += cv.getCount();
164        }
165
166        // Print header.
167        ps.printf("%s has %d unique elements and %d total elements:%n", name, list.size(), total);
168
169        int max = list.get(0).getCount();
170        final int lineSize = nameSize + NumberSize + barSize + 10;
171        printLine(ps, '-', lineSize);
172        String formatString = "| %-" + nameSize + "s | %-" + NumberSize + "d | %-" + barSize + "s |\n";
173        for (int i = 0; i < list.size() && i < limit; ++i) {
174            CountedValue cv = list.get(i);
175            int value = cv.getCount();
176            char[] bar = new char[(int) (((double) value / (double) max) * barSize)];
177            Arrays.fill(bar, '=');
178            String[] lines = String.valueOf(cv.getValue()).split("\\n");
179
180            String objectString = lines[0];
181            if (objectString.length() > nameSize) {
182                objectString = objectString.substring(0, nameSize - 3) + "...";
183            }
184            ps.printf(formatString, objectString, value, new String(bar));
185            for (int j = 1; j < lines.length; j++) {
186                String line = lines[j];
187                if (line.length() > nameSize) {
188                    line = line.substring(0, nameSize - 3) + "...";
189                }
190                ps.printf("|   %-" + (nameSize - 2) + "s | %-" + NumberSize + "s | %-" + barSize + "s |%n", line, " ", " ");
191
192            }
193        }
194        printLine(ps, '-', lineSize);
195    }
196
197    CountedValue bytesPerGraalContext(String context) {
198        return getCounter(context, bytesPerGraalContext);
199    }
200
201    CountedValue instancesPerGraalContext(String context) {
202        return getCounter(context, instancesPerGraalContext);
203    }
204
205    protected static CountedValue getCounter(String desc, Map<String, CountedValue> map) {
206        CountedValue count = map.get(desc);
207        if (count == null) {
208            count = new CountedValue(0, desc);
209            map.put(desc, count);
210        }
211        return count;
212    }
213
214    private static final String[] Excluded = {AllocSpy.class.getName(), AllocationRecorder.class.getName()};
215
216    private static boolean excludeFrame(String className) {
217        for (String e : Excluded) {
218            if (className.startsWith(e)) {
219                return true;
220            }
221        }
222        return false;
223    }
224
225    static class GraalContextSampler implements Sampler {
226
227        public void sampleAllocation(int count, String desc, Object newObj, long size) {
228            AllocSpy scope = current.get();
229            if (scope != null) {
230                StringBuilder sb = new StringBuilder(200);
231                Throwable t = new Throwable();
232                int remainingGraalFrames = ContextSize;
233                for (StackTraceElement e : t.getStackTrace()) {
234                    if (remainingGraalFrames < 0) {
235                        break;
236                    }
237                    String className = e.getClassName();
238                    boolean isGraalFrame = className.contains(".graal.");
239                    if (sb.length() != 0) {
240                        append(sb.append('\n'), e);
241                    } else {
242                        if (!excludeFrame(className)) {
243                            sb.append("type=").append(desc);
244                            if (count != -1) {
245                                sb.append('[').append(count).append(']');
246                            }
247                            append(sb.append('\n'), e);
248                        }
249                    }
250                    if (isGraalFrame) {
251                        remainingGraalFrames--;
252                    }
253                }
254                String context = sb.toString();
255                if (SampleBytes) {
256                    scope.bytesPerGraalContext(context).add((int) size);
257                }
258                if (SampleInstances) {
259                    scope.instancesPerGraalContext(context).inc();
260                }
261            }
262        }
263
264        protected StringBuilder append(StringBuilder sb, StackTraceElement e) {
265            String className = e.getClassName();
266            int period = className.lastIndexOf('.');
267            if (period != -1) {
268                sb.append(className, period + 1, className.length());
269            } else {
270                sb.append(className);
271            }
272            sb.append('.').append(e.getMethodName());
273            if (e.isNativeMethod()) {
274                sb.append("(Native Method)");
275            } else if (e.getFileName() != null && e.getLineNumber() >= 0) {
276                sb.append('(').append(e.getFileName()).append(':').append(e.getLineNumber()).append(")");
277            } else {
278                sb.append("(Unknown Source)");
279            }
280            return sb;
281        }
282    }
283
284    /**
285     * A value and a frequency. The ordering imposed by {@link #compareTo(CountedValue)} places
286     * values with higher frequencies first.
287     */
288    static class CountedValue implements Comparable<CountedValue> {
289
290        private int count;
291        private final Object value;
292
293        public CountedValue(int count, Object value) {
294            this.count = count;
295            this.value = value;
296        }
297
298        public int compareTo(CountedValue o) {
299            if (count < o.count) {
300                return 1;
301            } else if (count > o.count) {
302                return -1;
303            }
304            return 0;
305        }
306
307        @Override
308        public String toString() {
309            return count + " -> " + value;
310        }
311
312        public void inc() {
313            count++;
314        }
315
316        public void add(int n) {
317            count += n;
318        }
319
320        public int getCount() {
321            return count;
322        }
323
324        public Object getValue() {
325            return value;
326        }
327    }
328}