BenchmarkCounters.java revision 13264:48566d838608
1/*
2 * Copyright (c) 2013, 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.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23package org.graalvm.compiler.hotspot.debug;
24
25import java.io.File;
26import java.io.IOException;
27import java.io.OutputStream;
28import java.io.PrintStream;
29import java.util.Iterator;
30import java.util.Locale;
31import java.util.Map;
32import java.util.Map.Entry;
33import java.util.Set;
34import java.util.TreeMap;
35import java.util.TreeSet;
36import java.util.concurrent.ConcurrentHashMap;
37import java.util.concurrent.atomic.AtomicLong;
38
39import org.graalvm.compiler.core.common.SuppressFBWarnings;
40import org.graalvm.compiler.debug.CSVUtil;
41import org.graalvm.compiler.debug.GraalError;
42import org.graalvm.compiler.debug.TTY;
43import org.graalvm.compiler.hotspot.GraalHotSpotVMConfig;
44import org.graalvm.compiler.hotspot.replacements.HotspotSnippetsOptions;
45import org.graalvm.compiler.nodes.debug.DynamicCounterNode;
46import org.graalvm.compiler.options.Option;
47import org.graalvm.compiler.options.OptionKey;
48import org.graalvm.compiler.options.OptionType;
49import org.graalvm.compiler.options.OptionValues;
50
51import jdk.vm.ci.hotspot.HotSpotJVMCIRuntime;
52
53//JaCoCo Exclude
54
55/**
56 * This class contains infrastructure to maintain counters based on {@link DynamicCounterNode}s. The
57 * infrastructure is enabled by specifying either the GenericDynamicCounters or
58 * BenchmarkDynamicCounters option.
59 * <p>
60 *
61 * The counters are kept in a special area allocated for each native JavaThread object, and the
62 * number of counters is configured using {@code -XX:JVMCICounterSize=value}.
63 * {@code -XX:+/-JVMCICountersExcludeCompiler} configures whether to exclude compiler threads
64 * (defaults to true).
65 *
66 * The subsystems that use the logging need to have their own options to turn on the counters, and
67 * insert DynamicCounterNodes when they're enabled.
68 *
69 * Counters will be displayed as a rate (per second) if their group name starts with "~", otherwise
70 * they will be displayed as a total number.
71 *
72 * <h1>Example</h1> In order to create statistics about allocations within the DaCapo pmd benchmark
73 * the following steps are necessary:
74 * <ul>
75 * <li>Set {@code -XX:JVMCICounterSize=value}. The actual required value depends on the granularity
76 * of the profiling, 10000 should be enough for most cases.</li>
77 * <li>Also: {@code -XX:+/-JVMCICountersExcludeCompiler} specifies whether the numbers generated by
78 * compiler threads should be excluded (default: true).</li>
79 * <li>Start the DaCapo pmd benchmark with
80 * {@code "-Dgraal.BenchmarkDynamicCounters=err, starting ====, PASSED in "} and
81 * {@code -Dgraal.ProfileAllocations=true}.</li>
82 * <li>The numbers will only include allocation from compiled code!</li>
83 * <li>The counters can be further configured by modifying the
84 * {@link HotspotSnippetsOptions#ProfileAllocationsContext} flag..</li>
85 * </ul>
86 */
87public class BenchmarkCounters {
88
89    static class Options {
90
91        //@formatter:off
92        @Option(help = "Turn on the benchmark counters, and displays the results on VM shutdown", type = OptionType.Debug)
93        public static final OptionKey<Boolean> GenericDynamicCounters = new OptionKey<>(false);
94        @Option(help = "Turn on the benchmark counters, and displays the results every n milliseconds", type = OptionType.Debug)
95        public static final OptionKey<Integer> TimedDynamicCounters = new OptionKey<>(-1);
96
97        @Option(help = "Turn on the benchmark counters, and listen for specific patterns on System.out/System.err:%n" +
98                       "Format: (err|out),start pattern,end pattern (~ matches multiple digits)%n" +
99                       "Examples:%n" +
100                       "  dacapo = 'err, starting =====, PASSED in'%n" +
101                       "  specjvm2008 = 'out,Iteration ~ (~s) begins:,Iteration ~ (~s) ends:'", type = OptionType.Debug)
102        public static final OptionKey<String> BenchmarkDynamicCounters = new OptionKey<>(null);
103        @Option(help = "Use grouping separators for number printing", type = OptionType.Debug)
104        public static final OptionKey<Boolean> DynamicCountersPrintGroupSeparator = new OptionKey<>(true);
105        @Option(help = "File to which benchmark counters are dumped. A CSV format is used if the file ends with .csv " +
106                       "otherwise a more human readable format is used. The fields in the CSV format are: " +
107                       "category, group, name, value", type = OptionType.Debug)
108        public static final OptionKey<String> BenchmarkCountersFile = new OptionKey<>(null);
109        @Option(help = "Dump dynamic counters", type = OptionType.Debug)
110        public static final OptionKey<Boolean> BenchmarkCountersDumpDynamic = new OptionKey<>(true);
111        @Option(help = "Dump static counters", type = OptionType.Debug)
112        public static final OptionKey<Boolean> BenchmarkCountersDumpStatic = new OptionKey<>(false);
113        //@formatter:on
114    }
115
116    public static boolean enabled = false;
117
118    private static class Counter {
119        public final int index;
120        public final String group;
121        public final AtomicLong staticCounters;
122
123        Counter(int index, String group, AtomicLong staticCounters) {
124            this.index = index;
125            this.group = group;
126            this.staticCounters = staticCounters;
127        }
128    }
129
130    public static final ConcurrentHashMap<String, Counter> counterMap = new ConcurrentHashMap<>();
131    public static long[] delta;
132
133    public static int getIndexConstantIncrement(String name, String group, GraalHotSpotVMConfig config, long increment) {
134        Counter counter = getCounter(name, group, config);
135        counter.staticCounters.addAndGet(increment);
136        return counter.index;
137    }
138
139    public static int getIndex(String name, String group, GraalHotSpotVMConfig config) {
140        Counter counter = getCounter(name, group, config);
141        return counter.index;
142    }
143
144    @SuppressFBWarnings(value = "AT_OPERATION_SEQUENCE_ON_CONCURRENT_ABSTRACTION", justification = "concurrent abstraction calls are in synchronized block")
145    private static Counter getCounter(String name, String group, GraalHotSpotVMConfig config) throws GraalError {
146        if (!enabled) {
147            throw new GraalError("cannot access count index when counters are not enabled: " + group + ", " + name);
148        }
149        String nameGroup = name + "#" + group;
150        Counter counter = counterMap.get(nameGroup);
151        if (counter == null) {
152            synchronized (BenchmarkCounters.class) {
153                counter = counterMap.get(nameGroup);
154                if (counter == null) {
155                    counter = new Counter(counterMap.size(), group, new AtomicLong());
156                    counterMap.put(nameGroup, counter);
157                }
158            }
159        }
160        assert counter.group.equals(group) : "mismatching groups: " + counter.group + " vs. " + group;
161        int countersSize = config.jvmciCountersSize;
162        if (counter.index >= countersSize) {
163            throw new GraalError("too many counters, reduce number of counters or increase -XX:JVMCICounterSize=... (current value: " + countersSize + ")");
164        }
165        return counter;
166    }
167
168    private static synchronized void dump(OptionValues options, PrintStream out, double seconds, long[] counters, int maxRows) {
169        if (!counterMap.isEmpty()) {
170            try (Dumper dumper = Dumper.getDumper(options, out, counterMap.size(), seconds, maxRows)) {
171                TreeSet<String> set = new TreeSet<>();
172                counterMap.forEach((nameGroup, counter) -> set.add(counter.group));
173                for (String group : set) {
174                    if (group != null) {
175                        if (Options.BenchmarkCountersDumpStatic.getValue(options)) {
176                            dumper.dumpCounters(true, group, collectStaticCounters(), counterMap.entrySet(), options);
177                        }
178                        if (Options.BenchmarkCountersDumpDynamic.getValue(options)) {
179                            dumper.dumpCounters(false, group, collectDynamicCounters(counters), counterMap.entrySet(), options);
180                        }
181                    }
182                }
183            }
184
185            clear(counters);
186        }
187    }
188
189    private static synchronized long[] collectDynamicCounters(long[] counters) {
190        long[] array = counters.clone();
191        for (int i = 0; i < array.length; i++) {
192            array[i] -= delta[i];
193        }
194        return array;
195    }
196
197    private static synchronized long[] collectStaticCounters() {
198        long[] array = new long[counterMap.size()];
199        for (Counter counter : counterMap.values()) {
200            array[counter.index] = counter.staticCounters.get();
201        }
202        return array;
203    }
204
205    private static synchronized void clear(long[] counters) {
206        delta = counters;
207    }
208
209    private static boolean shouldDumpComputerReadable(OptionValues options) {
210        String dumpFile = Options.BenchmarkCountersFile.getValue(options);
211        return dumpFile != null && (dumpFile.endsWith(".csv") || dumpFile.endsWith(".CSV"));
212    }
213
214    private abstract static class Dumper implements AutoCloseable {
215        public static Dumper getDumper(OptionValues options, PrintStream out, int counterSize, double second, int maxRows) {
216            Dumper dumper = shouldDumpComputerReadable(options) ? new ComputerReadableDumper(out) : new HumanReadableDumper(out, second, maxRows);
217            dumper.start(counterSize);
218            return dumper;
219        }
220
221        protected final PrintStream out;
222
223        private Dumper(PrintStream out) {
224            this.out = out;
225        }
226
227        protected abstract void start(int size);
228
229        public abstract void dumpCounters(boolean staticCounter, String group, long[] array, Set<Entry<String, Counter>> counterEntrySet, OptionValues options);
230
231        @Override
232        public abstract void close();
233
234    }
235
236    private static String getName(String nameGroup, String group) {
237        return nameGroup.substring(0, nameGroup.length() - group.length() - 1);
238    }
239
240    private static long percentage(long counter, long sum) {
241        return (counter * 200 + 1) / sum / 2;
242    }
243
244    private static class HumanReadableDumper extends Dumper {
245        private final double seconds;
246        private final int maxRows;
247
248        HumanReadableDumper(PrintStream out, double seconds, int maxRows) {
249            super(out);
250            this.seconds = seconds;
251            this.maxRows = maxRows;
252        }
253
254        @Override
255        public void start(int size) {
256            out.println("====== dynamic counters (" + size + " in total) ======");
257        }
258
259        @Override
260        public void close() {
261            out.println("============================");
262        }
263
264        @Override
265        public void dumpCounters(boolean staticCounter, String group, long[] array, Set<Entry<String, Counter>> counterEntrySet, OptionValues options) {
266            // sort the counters by putting them into a sorted map
267            TreeMap<Long, String> sorted = new TreeMap<>();
268            long sum = 0;
269            for (Map.Entry<String, Counter> entry : counterEntrySet) {
270                Counter counter = entry.getValue();
271                int index = counter.index;
272                if (counter.group.equals(group)) {
273                    sum += array[index];
274                    sorted.put(array[index] * array.length + index, getName(entry.getKey(), group));
275                }
276            }
277
278            if (sum > 0) {
279                long cutoff = sorted.size() < 10 ? 1 : Math.max(1, sum / 100);
280                int cnt = sorted.size();
281
282                // remove everything below cutoff and keep at most maxRows
283                Iterator<Map.Entry<Long, String>> iter = sorted.entrySet().iterator();
284                while (iter.hasNext()) {
285                    Map.Entry<Long, String> entry = iter.next();
286                    long counter = entry.getKey() / array.length;
287                    if (counter < cutoff || cnt > maxRows) {
288                        iter.remove();
289                    }
290                    cnt--;
291                }
292
293                String numFmt = Options.DynamicCountersPrintGroupSeparator.getValue(options) ? "%,19d" : "%19d";
294                if (staticCounter) {
295                    out.println("=========== " + group + " (static counters):");
296                    for (Map.Entry<Long, String> entry : sorted.entrySet()) {
297                        long counter = entry.getKey() / array.length;
298                        out.format(Locale.US, numFmt + " %3d%%  %s\n", counter, percentage(counter, sum), entry.getValue());
299                    }
300                    out.format(Locale.US, numFmt + " total\n", sum);
301                } else {
302                    if (group.startsWith("~")) {
303                        out.println("=========== " + group + " (dynamic counters), time = " + seconds + " s:");
304                        for (Map.Entry<Long, String> entry : sorted.entrySet()) {
305                            long counter = entry.getKey() / array.length;
306                            out.format(Locale.US, numFmt + "/s %3d%%  %s\n", (long) (counter / seconds), percentage(counter, sum), entry.getValue());
307                        }
308                        out.format(Locale.US, numFmt + "/s total\n", (long) (sum / seconds));
309                    } else {
310                        out.println("=========== " + group + " (dynamic counters):");
311                        for (Map.Entry<Long, String> entry : sorted.entrySet()) {
312                            long counter = entry.getKey() / array.length;
313                            out.format(Locale.US, numFmt + " %3d%%  %s\n", counter, percentage(counter, sum), entry.getValue());
314                        }
315                        out.format(Locale.US, numFmt + " total\n", sum);
316                    }
317                }
318            }
319        }
320    }
321
322    private static final String CSV_FMT = CSVUtil.buildFormatString("%s", "%s", "%s", "%d");
323
324    private static class ComputerReadableDumper extends Dumper {
325
326        ComputerReadableDumper(PrintStream out) {
327            super(out);
328        }
329
330        @Override
331        public void start(int size) {
332            // do nothing
333        }
334
335        @Override
336        public void close() {
337            // do nothing
338        }
339
340        @Override
341        public void dumpCounters(boolean staticCounter, String group, long[] array, Set<Entry<String, Counter>> counterEntrySet, OptionValues options) {
342            String category = staticCounter ? "static counters" : "dynamic counters";
343            for (Map.Entry<String, Counter> entry : counterEntrySet) {
344                Counter counter = entry.getValue();
345                if (counter.group.equals(group)) {
346                    String name = getName(entry.getKey(), group);
347                    int index = counter.index;
348                    long value = array[index];
349                    CSVUtil.Escape.println(out, CSV_FMT, category, group, name, value);
350                }
351            }
352        }
353    }
354
355    private abstract static class CallbackOutputStream extends OutputStream {
356
357        protected final PrintStream delegate;
358        private final byte[][] patterns;
359        private final int[] positions;
360
361        CallbackOutputStream(PrintStream delegate, String... patterns) {
362            this.delegate = delegate;
363            this.positions = new int[patterns.length];
364            this.patterns = new byte[patterns.length][];
365            for (int i = 0; i < patterns.length; i++) {
366                this.patterns[i] = patterns[i].getBytes();
367            }
368        }
369
370        protected abstract void patternFound(int index);
371
372        @Override
373        public void write(int b) throws IOException {
374            try {
375                delegate.write(b);
376                for (int i = 0; i < patterns.length; i++) {
377                    int j = positions[i];
378                    byte[] cs = patterns[i];
379                    byte patternChar = cs[j];
380                    if (patternChar == '~' && Character.isDigit(b)) {
381                        // nothing to do...
382                    } else {
383                        if (patternChar == '~') {
384                            patternChar = cs[++positions[i]];
385                        }
386                        if (b == patternChar) {
387                            positions[i]++;
388                        } else {
389                            positions[i] = 0;
390                        }
391                    }
392                    if (positions[i] == patterns[i].length) {
393                        positions[i] = 0;
394                        patternFound(i);
395                    }
396                }
397            } catch (RuntimeException e) {
398                e.printStackTrace(delegate);
399                throw e;
400            }
401        }
402    }
403
404    public static void initialize(final HotSpotJVMCIRuntime jvmciRuntime, OptionValues options) {
405        final class BenchmarkCountersOutputStream extends CallbackOutputStream {
406
407            private long startTime;
408            private boolean running;
409            private boolean waitingForEnd;
410
411            private BenchmarkCountersOutputStream(PrintStream delegate, String start, String end) {
412                super(delegate, new String[]{"\n", end, start});
413            }
414
415            @Override
416            protected void patternFound(int index) {
417                switch (index) {
418                    case 2:
419                        startTime = System.nanoTime();
420                        BenchmarkCounters.clear(jvmciRuntime.collectCounters());
421                        running = true;
422                        break;
423                    case 1:
424                        if (running) {
425                            waitingForEnd = true;
426                        }
427                        break;
428                    case 0:
429                        if (waitingForEnd) {
430                            waitingForEnd = false;
431                            running = false;
432                            BenchmarkCounters.dump(options, getPrintStream(options), (System.nanoTime() - startTime) / 1000000000d, jvmciRuntime.collectCounters(), 100);
433                        }
434                        break;
435                }
436            }
437        }
438
439        if (Options.BenchmarkDynamicCounters.getValue(options) != null) {
440            String[] arguments = Options.BenchmarkDynamicCounters.getValue(options).split(",");
441            if (arguments.length == 0 || (arguments.length % 3) != 0) {
442                throw new GraalError("invalid arguments to BenchmarkDynamicCounters: (err|out),start,end,(err|out),start,end,... (~ matches multiple digits)");
443            }
444            for (int i = 0; i < arguments.length; i += 3) {
445                if (arguments[i].equals("err")) {
446                    System.setErr(new PrintStream(new BenchmarkCountersOutputStream(System.err, arguments[i + 1], arguments[i + 2])));
447                } else if (arguments[i].equals("out")) {
448                    System.setOut(new PrintStream(new BenchmarkCountersOutputStream(System.out, arguments[i + 1], arguments[i + 2])));
449                } else {
450                    throw new GraalError("invalid arguments to BenchmarkDynamicCounters: err|out");
451                }
452            }
453            enabled = true;
454        }
455        if (Options.GenericDynamicCounters.getValue(options)) {
456            enabled = true;
457        }
458        if (Options.TimedDynamicCounters.getValue(options) > 0) {
459            Thread thread = new Thread() {
460                long lastTime = System.nanoTime();
461                PrintStream out = getPrintStream(options);
462
463                @Override
464                public void run() {
465                    while (true) {
466                        try {
467                            Thread.sleep(Options.TimedDynamicCounters.getValue(options));
468                        } catch (InterruptedException e) {
469                        }
470                        long time = System.nanoTime();
471                        dump(options, out, (time - lastTime) / 1000000000d, jvmciRuntime.collectCounters(), 10);
472                        lastTime = time;
473                    }
474                }
475            };
476            thread.setDaemon(true);
477            thread.setPriority(Thread.MAX_PRIORITY);
478            thread.start();
479            enabled = true;
480        }
481        if (enabled) {
482            clear(jvmciRuntime.collectCounters());
483        }
484    }
485
486    public static void shutdown(HotSpotJVMCIRuntime jvmciRuntime, OptionValues options, long compilerStartTime) {
487        if (Options.GenericDynamicCounters.getValue(options)) {
488            dump(options, getPrintStream(options), (System.nanoTime() - compilerStartTime) / 1000000000d, jvmciRuntime.collectCounters(), 100);
489        }
490    }
491
492    private static PrintStream getPrintStream(OptionValues options) {
493        if (Options.BenchmarkCountersFile.getValue(options) != null) {
494            try {
495
496                File file = new File(Options.BenchmarkCountersFile.getValue(options));
497                TTY.println("Writing benchmark counters to '%s'", file.getAbsolutePath());
498                return new PrintStream(file);
499            } catch (IOException e) {
500                TTY.out().println(e.getMessage());
501                TTY.out().println("Fallback to default");
502            }
503        }
504        return TTY.out;
505    }
506}
507