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