diff --git a/make/modules/jdk.jfr/Java.gmk b/make/modules/jdk.jfr/Java.gmk index c39e21a7173..207256fde20 100644 --- a/make/modules/jdk.jfr/Java.gmk +++ b/make/modules/jdk.jfr/Java.gmk @@ -1,5 +1,5 @@ # -# Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -24,5 +24,5 @@ # DISABLED_WARNINGS_java += exports -COPY := .xsd .xml .dtd +COPY := .xsd .xml .dtd .ini JAVAC_FLAGS := -XDstringConcat=inline diff --git a/src/hotspot/share/jfr/dcmd/jfrDcmds.cpp b/src/hotspot/share/jfr/dcmd/jfrDcmds.cpp index 8482583da40..d52bc9e2e5b 100644 --- a/src/hotspot/share/jfr/dcmd/jfrDcmds.cpp +++ b/src/hotspot/share/jfr/dcmd/jfrDcmds.cpp @@ -52,6 +52,9 @@ bool register_jfr_dcmds() { DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl(full_export, true, false)); DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl(full_export, true, false)); DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl(full_export, true, false)); + // JFR.query Uncomment when developing new queries for the JFR.view command + // DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl(full_export, true, true)); + DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl(full_export, true, false)); DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl(full_export, true, false)); return true; } @@ -318,7 +321,7 @@ static DCmdArgumentInfo* create_info(oop argument, TRAPS) { read_string_field(argument, "type", THREAD), read_string_field(argument, "defaultValue", THREAD), read_boolean_field(argument, "mandatory", THREAD), - true, // a DcmdFramework "option" + read_boolean_field(argument, "option", THREAD), read_boolean_field(argument, "allowMultiple", THREAD)); } diff --git a/src/hotspot/share/jfr/dcmd/jfrDcmds.hpp b/src/hotspot/share/jfr/dcmd/jfrDcmds.hpp index 5374c0537a4..e99dc99c8d1 100644 --- a/src/hotspot/share/jfr/dcmd/jfrDcmds.hpp +++ b/src/hotspot/share/jfr/dcmd/jfrDcmds.hpp @@ -145,6 +145,56 @@ class JfrStopFlightRecordingDCmd : public JfrDCmd { } }; +class JfrViewFlightRecordingDCmd : public JfrDCmd { + public: + JfrViewFlightRecordingDCmd(outputStream* output, bool heap) : JfrDCmd(output, heap, num_arguments()) {} + + static const char* name() { + return "JFR.view"; + } + static const char* description() { + return "Display event data in predefined views"; + } + static const char* impact() { + return "Medium"; + } + static const JavaPermission permission() { + JavaPermission p = {"java.lang.management.ManagementPermission", "monitor", NULL}; + return p; + } + virtual const char* javaClass() const { + return "jdk/jfr/internal/dcmd/DCmdView"; + } + static int num_arguments() { + return 7; + } +}; + +class JfrQueryFlightRecordingDCmd : public JfrDCmd { + public: + JfrQueryFlightRecordingDCmd(outputStream* output, bool heap) : JfrDCmd(output, heap, num_arguments()) {} + + static const char* name() { + return "JFR.query"; + } + static const char* description() { + return "Query and display event data in a tabular form"; + } + static const char* impact() { + return "Medium"; + } + static const JavaPermission permission() { + JavaPermission p = {"java.lang.management.ManagementPermission", "monitor", NULL}; + return p; + } + virtual const char* javaClass() const { + return "jdk/jfr/internal/dcmd/DCmdQuery"; + } + static int num_arguments() { + return 5; + } +}; + class JfrConfigureFlightRecorderDCmd : public DCmdWithParser { friend class JfrOptionSet; protected: diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/OldObjectSample.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/OldObjectSample.java index a3461b50324..5ab21309e7e 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/OldObjectSample.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/OldObjectSample.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -79,7 +79,7 @@ public final class OldObjectSample { } } - private static void emit(long ticks) { + public static void emit(long ticks) { boolean emitAll = WhiteBox.getWriteAllObjectSamples(); boolean skipBFS = WhiteBox.getSkipBFS(); JVM.getJVM().emitOldObjectSamples(ticks, emitAll, skipBFS); diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecorder.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecorder.java index 02d9d33e183..a6c0be983de 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecorder.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecorder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -420,7 +420,7 @@ public final class PlatformRecorder { return runningRecordings; } - private List makeChunkList(Instant startTime, Instant endTime) { + public List makeChunkList(Instant startTime, Instant endTime) { Set chunkSet = new HashSet<>(); for (PlatformRecording r : getRecordings()) { chunkSet.addAll(r.getChunks()); @@ -438,7 +438,7 @@ public final class PlatformRecorder { return chunks; } - return Collections.emptyList(); + return new ArrayList<>(); } private void startDiskMonitor() { @@ -454,6 +454,8 @@ public final class PlatformRecorder { r.appendChunk(chunk); } } + // Decrease initial reference count + chunk.release(); FilePurger.purge(); } @@ -659,4 +661,8 @@ public final class PlatformRecorder { rotateDisk(); } } + + public RepositoryChunk getCurrentChunk() { + return currentChunk; + } } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/RepositoryChunk.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/RepositoryChunk.java index 3afc4a806a1..58cb9e53738 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/RepositoryChunk.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/RepositoryChunk.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -33,7 +33,7 @@ import java.util.Comparator; import jdk.jfr.internal.SecuritySupport.SafePath; -final class RepositoryChunk { +public final class RepositoryChunk { static final Comparator END_TIME_COMPARATOR = new Comparator() { @Override @@ -47,7 +47,7 @@ final class RepositoryChunk { private Instant endTime = null; // unfinished private Instant startTime; - private int refCount = 0; + private int refCount = 1; private long size; RepositoryChunk(SafePath path) throws Exception { @@ -166,4 +166,12 @@ final class RepositoryChunk { public SafePath getFile() { return chunkFile; } + + public long getCurrentFileSize() { + try { + return SecuritySupport.getFileSize(chunkFile); + } catch (IOException e) { + return 0L; + } + } } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/Utils.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/Utils.java index 288e0ee9be8..97000e46eba 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/Utils.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/Utils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -55,6 +55,7 @@ import java.util.Objects; import jdk.internal.module.Checks; import jdk.jfr.Event; +import jdk.jfr.EventType; import jdk.jfr.FlightRecorderPermission; import jdk.jfr.Recording; import jdk.jfr.RecordingState; @@ -859,4 +860,12 @@ public final class Utils { } } } + + public static String makeSimpleName(EventType type) { + return makeSimpleName(type.getName()); + } + + public static String makeSimpleName(String qualified) { + return qualified.substring(qualified.lastIndexOf(".") + 1); + } } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/AbstractDCmd.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/AbstractDCmd.java index b7fb23c9402..1335201472a 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/AbstractDCmd.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/AbstractDCmd.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -38,6 +38,8 @@ import java.util.List; import jdk.jfr.FlightRecorder; import jdk.jfr.Recording; import jdk.jfr.internal.JVM; +import jdk.jfr.internal.util.Output.LinePrinter; +import jdk.jfr.internal.util.Output; import jdk.jfr.internal.LogLevel; import jdk.jfr.internal.LogTag; import jdk.jfr.internal.Logger; @@ -50,9 +52,7 @@ import jdk.jfr.internal.Utils; * */ abstract class AbstractDCmd { - - private final StringBuilder currentLine = new StringBuilder(80); - private final List lines = new ArrayList<>(); + private final LinePrinter output = new LinePrinter(); private String source; // Called by native @@ -91,16 +91,19 @@ abstract class AbstractDCmd { DCmdException e = new DCmdException(iae.getMessage()); e.addSuppressed(iae); throw e; - } + } } + protected final Output getOutput() { + return output; + } protected final FlightRecorder getFlightRecorder() { return FlightRecorder.getFlightRecorder(); } protected final String[] getResult() { - return lines.toArray(new String[lines.size()]); + return output.getLines().toArray(new String[0]); } protected void logWarning(String message) { @@ -181,21 +184,19 @@ abstract class AbstractDCmd { } protected final void println() { - lines.add(currentLine.toString()); - currentLine.setLength(0); + output.println(); } protected final void print(String s) { - currentLine.append(s); + output.print(s); } protected final void print(String s, Object... args) { - currentLine.append(args.length > 0 ? String.format(s, args) : s); + output.print(s, args); } protected final void println(String s, Object... args) { - print(s, args); - println(); + output.println(s, args); } protected final void printBytes(long bytes) { @@ -218,6 +219,12 @@ abstract class AbstractDCmd { } } + protected final void printHelpText() { + for (String line : printHelp()) { + println(line); + } + } + protected final void printPath(Path path) { try { println(path.toAbsolutePath().toString()); diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/Argument.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/Argument.java index 7020dbb2fee..fcf8691aa2c 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/Argument.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/Argument.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,6 +29,7 @@ record Argument( String description, String type, boolean mandatory, + boolean option, String defaultValue, boolean allowMultiple ) { } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/ArgumentParser.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/ArgumentParser.java index 1e21566c6fc..312a417128d 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/ArgumentParser.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/ArgumentParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,6 +35,7 @@ import java.util.StringJoiner; final class ArgumentParser { private final Map options = new HashMap<>(); private final Map extendedOptions = new HashMap<>(); + private final List conflictedOptions = new ArrayList<>(); private final StringBuilder builder = new StringBuilder(); private final String text; private final char delimiter; @@ -42,8 +43,7 @@ final class ArgumentParser { private final String valueDelimiter; private final Argument[] arguments; private int position; - - private final List conflictedOptions = new ArrayList<>(); + private int argumentIndex; ArgumentParser(Argument[] arguments, String text, char delimiter) { this.text = text; @@ -60,6 +60,11 @@ final class ArgumentParser { String value = null; if (accept('=')) { value = readText(valueDelimiter); + } else { + if (hasArgumentsLeft()) { + value = key; + key = nextArgument().name(); + } } if (!atEnd() && !accept(delimiter)) { // must be followed by delimiter throw new IllegalArgumentException("Expected delimiter, but found " + currentChar()); @@ -72,6 +77,25 @@ final class ArgumentParser { return options; } + private boolean hasArgumentsLeft() { + for (int index = argumentIndex; index < arguments.length; index++) { + if (!arguments[index].option()) { + return true; + } + } + return false; + } + + private Argument nextArgument() { + while (argumentIndex < arguments.length) { + Argument argument = arguments[argumentIndex++]; + if (!argument.option()) { + return argument; + } + } + return null; + } + protected void checkConflict() { if (conflictedOptions.isEmpty()) { return; @@ -97,14 +121,15 @@ final class ArgumentParser { throw new IllegalArgumentException(sb.toString()); } - private void checkMandatory() { + public boolean checkMandatory() { for (Argument arg : arguments) { if (!options.containsKey(arg.name())) { if (arg.mandatory()) { - throw new IllegalArgumentException("The argument '" + arg.name() + "' is mandatory"); + return false; } } } + return true; } @SuppressWarnings({"unchecked", "rawtypes"}) @@ -194,6 +219,7 @@ final class ArgumentParser { private Object value(String name, String type, String text) { return switch (type) { + case "JULONG" -> parseLong(name, text); case "STRING", "STRING SET" -> text == null ? "" : text; case "BOOLEAN" -> parseBoolean(name, text); case "NANOTIME" -> parseNanotime(name, text); @@ -202,6 +228,22 @@ final class ArgumentParser { }; } + private Long parseLong(String name, String text) { + if (text == null) { + throw new IllegalArgumentException("Parsing error long value: syntax error, value is null"); + } + try { + long value = Long.parseLong(text); + if (value >= 0) { + return value; + } + } catch (NumberFormatException nfe) { + // fall through + } + String msg = "Integer parsing error in command argument '" + name + "'. Could not parse: " + text + "."; + throw new IllegalArgumentException(msg); + } + private Boolean parseBoolean(String name, String text) { if ("true".equals(text)) { return Boolean.TRUE; diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdCheck.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdCheck.java index 888ca4eda2a..7e8d74d9458 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdCheck.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdCheck.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -169,10 +169,10 @@ final class DCmdCheck extends AbstractDCmd { return new Argument[] { new Argument("name", "Recording name, e.g. \\\"My Recording\\\" or omit to see all recordings", - "STRING", false, null, false), + "STRING", false, true, null, false), new Argument("verbose", "Print event settings for the recording(s)","BOOLEAN", - false, "false", false) + false, true, "false", false) }; } } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java index acd3389fc2c..596bed97d7a 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdDump.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -281,24 +281,24 @@ final class DCmdDump extends AbstractDCmd { return new Argument[] { new Argument("name", "Recording name, e.g. \\\"My Recording\\\"", - "STRING", false, null, false), + "STRING", false, true, null, false), new Argument("filename", "Copy recording data to file, e.g. \\\"" + exampleFilename() + "\\\"", - "STRING", false, null, false), + "STRING", false, true, null, false), new Argument("maxage", "Maximum duration to dump, in (s)econds, (m)inutes, (h)ours, or (d)ays, e.g. 60m, or 0 for no limit", - "NANOTIME", false, null, false), + "NANOTIME", false, true, null, false), new Argument("maxsize", "Maximum amount of bytes to dump, in (M)B or (G)B, e.g. 500M, or 0 for no limit", - "MEMORY SIZE", false, "hotspot-pid-xxxxx-id-y-YYYY_MM_dd_HH_mm_ss.jfr", false), + "MEMORY SIZE", false, true, "hotspot-pid-xxxxx-id-y-YYYY_MM_dd_HH_mm_ss.jfr", false), new Argument("begin", "Point in time to dump data from, e.g. 09:00, 21:35:00, 2018-06-03T18:12:56.827Z, 2018-06-03T20:13:46.832, -10m, -3h, or -1d", - "STRING", false, null, false), + "STRING", false, true, null, false), new Argument("end", "Point in time to dump data to, e.g. 09:00, 21:35:00, 2018-06-03T18:12:56.827Z, 2018-06-03T20:13:46.832, -10m, -3h, or -1d", - "STRING", false, null, false), + "STRING", false, true, null, false), new Argument("path-to-gc-roots", "Collect path to GC roots", - "BOOLEAN", false, "false", false) + "BOOLEAN", false, true, "false", false) }; } } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdQuery.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdQuery.java new file mode 100644 index 00000000000..f8e39ea1e2f --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdQuery.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.dcmd; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import jdk.jfr.internal.query.Configuration; +import jdk.jfr.internal.query.QueryPrinter; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; + +/** + * JFR.query + */ +// Instantiated by native +public final class DCmdQuery extends AbstractDCmd { + + protected void execute(ArgumentParser parser) throws DCmdException { + parser.checkUnknownArguments(); + if (!parser.checkMandatory()) { + println("The argument 'query' is mandatory"); + println(); + printHelpText(); + return; + } + + Configuration configuration = new Configuration(); + configuration.output = getOutput(); + configuration.endTime = Instant.now().minusSeconds(1); + Boolean verbose = parser.getOption("verbose"); + if (verbose != null) { + configuration.verboseHeaders = verbose; + } + try (QueryRecording recording = new QueryRecording(configuration, parser)) { + QueryPrinter printer = new QueryPrinter(configuration, recording.getStream()); + String query = parser.getOption("query"); + printer.execute(stripQuotes(query)); + } catch (UserDataException e) { + throw new DCmdException(e.getMessage()); + } catch (UserSyntaxException e) { + throw new DCmdException(e.getMessage()); + } catch (IOException e) { + throw new DCmdException("Could not open repository. " + e.getMessage()); + } catch (IllegalArgumentException e) { + throw new DCmdException(e.getMessage() + ". See help JFR.query"); + } + } + + private String stripQuotes(String text) { + if (text.startsWith("\"")) { + text = text.substring(1); + } + if (text.endsWith("\"")) { + text = text.substring(0, text.length() - 1); + } + return text; + } + + @Override + public String[] printHelp() { + List lines = new ArrayList<>(); + lines.addAll(getOptions().lines().toList()); + lines.add(""); + lines.addAll(QueryPrinter.getGrammarText().lines().toList()); + lines.add(""); + lines.addAll(getExamples().lines().toList()); + return lines.toArray(String[]::new); + } + + private String getExamples() { + // 0123456789001234567890012345678900123456789001234567890012345678900123456789001234567890 + return """ + Example usage: + + $ jcmd JFR.query '"SHOW EVENTS"' + + $ jcmd JFR.query '"SHOW FIELDS ObjectAllocationSample"' + + $ jcmd JFR.query '"SELECT * FROM ObjectAllocationSample"' + verbose=true maxsize=10M + + $ jcmd JFR.query '"SELECT pid, path FROM SystemProcess"' + width=100 + + $ jcmd JFR.query '"SELECT stackTrace.topFrame AS T, SUM(weight) + FROM ObjectAllocationSample GROUP BY T"' + maxage=100s + + $ jcmd JFR.query '"CAPTION 'Method', 'Percentage' + FORMAT default, normalized;width:10 + SELECT stackTrace.topFrame AS T, COUNT(*) AS C + GROUP BY T + FROM ExecutionSample ORDER BY C DESC"' + + $ jcmd JFR.query '"CAPTION 'Start', 'GC ID', 'Heap Before GC', + 'Heap After GC', 'Longest Pause' + SELECT G.startTime, G.gcId, B.heapUsed, + A.heapUsed, longestPause + FROM GarbageCollection AS G, + GCHeapSummary AS B, + GCHeapSummary AS A + WHERE B.when = 'Before GC' AND A.when = 'After GC' + GROUP BY gcId + ORDER BY G.startTime"'"""; + } + + private String getOptions() { + // 0123456789001234567890012345678900123456789001234567890012345678900123456789001234567890 + return """ + Syntax : JFR.query [options] + + Options: + + maxage (Optional) Length of time for the query to span. (INTEGER followed by + 's' for seconds 'm' for minutes or 'h' for hours, no default value) + + maxsize (Optional) Maximum size for the query to span in bytes if one of + the following suffixes is not used: 'm' or 'M' for megabytes OR + 'g' or 'G' for gigabytes. (STRING, no default value) + + (Mandatory) Query, for example '"SELECT * FROM GarbageCollection"' + See below for grammar. (STRING, no default value) + + verbose (Optional) Display additional information about the query execution. + (BOOLEAN, false) + + width (Optional) Maximum number of horizontal characters. (BOOLEAN, false)"""; + } + + @Override + public Argument[] getArgumentInfos() { + return new Argument[] { + new Argument("maxage", + "Length of time for the query to span, in (s)econds, (m)inutes, (h)ours, or (d)ays, e.g. 60m, or 0 for no limit", + "NANOTIME", false, true, "10m", false), + new Argument("maxsize", + "Maximum size for the query to span, in (M)B or (G)B, e.g. 500M, or 0 for no limit", + "MEMORY SIZE", false, true, "100M", false), + new Argument("query", "Query, for example 'SELECT * FROM GarbageCollection'", "STRING", true, false, + null, false), + new Argument("verbose", "Display additional information about the query execution", "BOOLEAN", false, + true, "false", false), + new Argument("width", "Maximum number of horizontal characters", "JULONG", false, true, "100", + false), }; + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStart.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStart.java index a97f8cff747..b8e66d8d3cb 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStart.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStart.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -456,37 +456,37 @@ final class DCmdStart extends AbstractDCmd { return new Argument[] { new Argument("name", "Name that can be used to identify recording, e.g. \\\"My Recording\\\"", - "STRING", false, null, false), + "STRING", false, true, null, false), new Argument("settings", "Settings file(s), e.g. profile or default. See JAVA_HOME/lib/jfr", - "STRING SET", false, "deafult.jfc", true), + "STRING SET", false, true, "default.jfc", true), new Argument("delay", "Delay recording start with (s)econds, (m)inutes), (h)ours), or (d)ays, e.g. 5h.", - "NANOTIME", false, "0s", false), + "NANOTIME", false, true, "0s", false), new Argument("duration", "Duration of recording in (s)econds, (m)inutes, (h)ours, or (d)ays, e.g. 300s.", - "NANOTIME", false, null, false), + "NANOTIME", false, true, null, false), new Argument("disk", "Recording should be persisted to disk", - "BOOLEAN", false, "true", false), + "BOOLEAN", false, true, "true", false), new Argument("filename", "Resulting recording filename, e.g. \\\"" + exampleFilename() + "\\\"", - "STRING", false, "hotspot-pid-xxxxx-id-y-YYYY_MM_dd_HH_mm_ss.jfr", false), + "STRING", false, true, "hotspot-pid-xxxxx-id-y-YYYY_MM_dd_HH_mm_ss.jfr", false), new Argument("maxage", "Maximum time to keep recorded data (on disk) in (s)econds, (m)inutes, (h)ours, or (d)ays, e.g. 60m, or 0 for no limit", - "NANOTIME", false, "0", false), + "NANOTIME", false, true, "0", false), new Argument("maxsize", "Maximum amount of bytes to keep (on disk) in (k)B, (M)B or (G)B, e.g. 500M, or 0 for no limit", - "MEMORY SIZE", false, "250M", false), + "MEMORY SIZE", false, true, "250M", false), new Argument("flush-interval", "Minimum time before flushing buffers, measured in (s)econds, e.g. 4 s, or 0 for flushing when a recording ends", - "NANOTIME", false, "1s", false), + "NANOTIME", false, true, "1s", false), new Argument("dumponexit", "Dump running recording when JVM shuts down", - "BOOLEAN", false, "false", false), + "BOOLEAN", false, true, "false", false), new Argument("path-to-gc-roots", "Collect path to GC roots", - "BOOLEAN", false, "false", false) + "BOOLEAN", false, true, "false", false) }; } } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStop.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStop.java index 5d0abcd9fb6..baa45396a4b 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStop.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStop.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -104,10 +104,10 @@ final class DCmdStop extends AbstractDCmd { return new Argument[] { new Argument("name", "Recording name, e.g. \\\"My Recording\\\"", - "STRING", true, null, false), + "STRING", true, true, null, false), new Argument("filename", "Copy recording data to file, e.g. \\\"" + exampleFilename() + "\\\"", - "STRING", false, null, false) + "STRING", false, true, null, false) }; } } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdView.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdView.java new file mode 100644 index 00000000000..7471457ed82 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdView.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.dcmd; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import jdk.jfr.internal.OldObjectSample; +import jdk.jfr.internal.Utils; +import jdk.jfr.internal.query.Configuration; +import jdk.jfr.internal.query.QueryPrinter; +import jdk.jfr.internal.query.ViewPrinter; +import jdk.jfr.internal.util.Columnizer; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; +/** + * JFR.view + *

+ * The implementation is also used by DCmdQuery since there + * is little difference between JFR.query and JFR.view. + */ +// Instantiated by native +public class DCmdView extends AbstractDCmd { + + protected void execute(ArgumentParser parser) throws DCmdException { + parser.checkUnknownArguments(); + if (!parser.checkMandatory()) { + println("The argument 'view' is mandatory"); + println(); + printHelpText(); + return; + } + Configuration configuration = new Configuration(); + configuration.output = getOutput(); + configuration.endTime = Instant.now().minusSeconds(1); + String view = parser.getOption("view"); + if (view.startsWith("memory-leaks")) { + // Make sure old object sample event is part of data. + OldObjectSample.emit(0); + Utils.waitFlush(10_000); + configuration.endTime = Instant.now(); + } + try (QueryRecording recording = new QueryRecording(configuration, parser)) { + ViewPrinter printer = new ViewPrinter(configuration, recording.getStream()); + printer.execute(view); + } catch (UserDataException e) { + throw new DCmdException(e.getMessage()); + } catch (UserSyntaxException e) { + throw new DCmdException(e.getMessage()); + } catch (IOException e) { + throw new DCmdException("Could not open repository. " + e.getMessage()); + } catch (IllegalArgumentException e) { + throw new DCmdException(e.getMessage() + ". See help JFR.view"); + } + } + + @Override + public String[] printHelp() { + List lines = new ArrayList<>(); + lines.addAll(getOptions().lines().toList()); + lines.add(""); + lines.addAll(ViewPrinter.getAvailableViews()); + lines.add(""); + lines.add(" The parameter can be an event type name. Use the 'JFR.view types'"); + lines.add(" to see a list. To display all views, use 'JFR.view all-views'. To display"); + lines.add(" all events, use 'JFR.view all-events'."); + lines.add(""); + lines.addAll(getExamples().lines().toList()); + return lines.toArray(String[]::new); + } + + public String getOptions() { + // 0123456789001234567890012345678900123456789001234567890012345678900123456789001234567890 + return """ + Options: + + cell-height (Optional) Maximum number of rows in a table cell. (INTEGER, no default value) + + maxage (Optional) Length of time for the view to span. (INTEGER followed by + 's' for seconds 'm' for minutes or 'h' for hours, default value is 10m) + + maxsize (Optional) Maximum size for the view to span in bytes if one of + the following suffixes is not used: 'm' or 'M' for megabytes OR + 'g' or 'G' for gigabytes. (STRING, default value is 32MB) + + truncate (Optional) How to truncate content that exceeds space in a table cell. + Mode can be 'beginning' or 'end'. (STRING, default value 'end') + + verbose (Optional) Displays the query that makes up the view. + (BOOLEAN, default value false) + + (Mandatory) Name of the view or event type to display. + See list below for available views. (STRING, no default value) + + width (Optional) The width of the view in characters + (INTEGER, no default value)"""; + } + + public String getExamples() { + return """ + Example usage: + + $ jcmd JFR.view gc + + $ jcmd JFR.view verbose=true allocation-by-class + + $ jcmd JFR.view contention-by-site + + $ jcmd JFR.view jdk.GarbageCollection + + $ jcmd JFR.view cell-height=5 ThreadStart + + $ jcmd JFR.view truncate=beginning SystemProcess"""; + } + + @Override + public Argument[] getArgumentInfos() { + return new Argument[] { + new Argument("cell-height", + "Maximum heigth of a table cell", + "JULONG", false, true, "1", false), + new Argument("maxage", + "Maximum duration of data to view, in (s)econds, (m)inutes, (h)ours, or (d)ays, e.g. 60m, or 0 for no limit", + "NANOTIME", false, true, "10m", false), + new Argument("maxsize", + "Maximum amount of bytes to view, in (M)B or (G)B, e.g. 500M, or 0 for no limit", + "MEMORY SIZE", false, true, "100M", false), + new Argument("truncate", + "Truncation mode if value doesn't fit in a table cell, valid values are 'beginning' and 'end'", + "STRING", false, true, "end", false), + new Argument("verbose", + "Display additional information about the view, such as the underlying query", + "BOOLEAN", false, true, "false", false), + new Argument("view", + "Name of the view, for example hot-methods", + "STRING", true, false, null, false), + new Argument("width", + "Maximum number of horizontal characters", + "JULONG", false, true, "100", false) + }; + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/QueryRecording.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/QueryRecording.java new file mode 100644 index 00000000000..0128c6da2d6 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/QueryRecording.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.dcmd; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +import jdk.jfr.FlightRecorder; +import jdk.jfr.consumer.EventStream; +import jdk.jfr.internal.PlatformRecorder; +import jdk.jfr.internal.PrivateAccess; +import jdk.jfr.internal.RepositoryChunk; +import jdk.jfr.internal.query.Configuration; +import jdk.jfr.internal.query.QueryPrinter; +import jdk.jfr.internal.query.ViewPrinter; +import jdk.jfr.internal.query.Configuration.Truncate; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; +import jdk.jfr.internal.util.Output; + +/** + * Helper class that holds recording chunks alive during a query. It also helps + * out with configuration shared by DCmdView and DCmdQuery + */ +final class QueryRecording implements AutoCloseable { + private final long DEFAULT_MAX_SIZE = 32 * 1024 * 1024L; + private final long DEFAULT_MAX_AGE = 60 * 10; + + private final PlatformRecorder recorder; + private final List chunks; + private final EventStream eventStream; + private final Instant endTime; + + public QueryRecording(Configuration configuration, ArgumentParser parser) throws IOException, DCmdException { + if (!FlightRecorder.isInitialized()) { + throw new DCmdException("No recording data available. Start a recording with JFR.start"); + } + recorder = PrivateAccess.getInstance().getPlatformRecorder(); + Boolean verbose = parser.getOption("verbose"); + if (verbose != null) { + configuration.verbose = verbose; + } + configuration.truncate = valueOf(parser.getOption("truncate")); + Long width = parser.getOption("width"); + if (width != null) { + configuration.width = (int) Math.min(Integer.MAX_VALUE, width.longValue()); + } + Long height = parser.getOption("cell-height"); + if (height != null) { + if (height < 1) { + throw new DCmdException("Height must be at least 1"); + } + configuration.cellHeight = (int) Math.min(Integer.MAX_VALUE, height.longValue()); + } + Long maxAge = parser.getOption("maxage"); + + Long maxSize = parser.getOption("maxsize"); + if (maxSize == null) { + maxSize = DEFAULT_MAX_SIZE;; + } + Instant startTime = Instant.EPOCH; + endTime = configuration.endTime; + if (maxAge != null) { + startTime = endTime.minus(Duration.ofNanos(maxAge)); + } else { + startTime = endTime.minus(Duration.ofSeconds(DEFAULT_MAX_AGE)); + } + chunks = acquireChunks(startTime); + Instant streamStart = determineStreamStart(maxSize, startTime); + configuration.startTime = streamStart; + eventStream = makeStream(streamStart); + } + + private List acquireChunks(Instant startTime) { + synchronized (recorder) { + List list = recorder.makeChunkList(startTime, endTime); + list.add(currentChunk()); + for (RepositoryChunk r : list) { + r.use(); + } + return list; + } + } + + private RepositoryChunk currentChunk() { + return PrivateAccess.getInstance().getPlatformRecorder().getCurrentChunk(); + } + + private void releaseChunks() { + synchronized (recorder) { + for (RepositoryChunk r : chunks) { + r.release(); + } + } + } + + private EventStream makeStream(Instant startTime) throws IOException { + EventStream es = EventStream.openRepository(); + es.setStartTime(startTime); + es.setEndTime(endTime); + return es; + } + + private Instant determineStreamStart(Long maxSize, Instant startTime) { + ListIterator iterator = chunks.listIterator(chunks.size()); + long size = 0; + while (iterator.hasPrevious()) { + RepositoryChunk r = iterator.previous(); + if (r.isFinished()) { + size += r.getSize(); + if (size > maxSize) { + return r.getStartTime().isAfter(startTime) ? r.getStartTime() : startTime; + } + } else { + size += r.getCurrentFileSize(); + } + } + return startTime; + } + + private Truncate valueOf(String truncate) throws DCmdException { + if (truncate == null || truncate.equals("end")) { + return Truncate.END; + } + if (truncate.equals("beginning")) { + return Truncate.BEGINNING; + } + throw new DCmdException("Truncate must be 'end' or 'beginning"); + } + + public EventStream getStream() { + return eventStream; + } + + @Override + public void close() { + eventStream.close(); + releaseChunks(); + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Aggregator.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Aggregator.java new file mode 100644 index 00000000000..1e02a97a4b9 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Aggregator.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +/** + * Enum describing the different ways values can be aggregated. + */ +enum Aggregator { + /** + * Dummy to indicate no aggregation is being used. + */ + MISSING(" "), + /** + * Calculate the average value of all finite numeric values. + */ + AVERAGE("AVG"), + /** + * Calculate the number of elements, including {@code null}. + */ + COUNT("COUNT"), + /** + * Calculate the difference between the last and first finite numeric value. + */ + DIFFERENCE("DIFF"), + /** + * The first value, including {@code null}. + */ + FIRST("FIRST"), + /** + * The last value, including {@code null}. + */ + LAST("LAST"), + /** + * Aggregate values into a comma-separated list, including {@code null}. + */ + LIST("LIST"), + /** + * The highest numeric value. + */ + MAXIMUM("MAX"), + /** + * The median of all finite numeric values. + */ + MEDIAN("MEDIAN"), + /** + * The lowest numeric value. + */ + MINIMUM("MIN"), + /** + * Calculate the 90th percentile of all finite numeric values. + */ + P90("P90"), + /** + * Calculate the 95th percentile of all finite numeric values. + */ + P95("P95"), + /** + * Calculate the 99th percentile of all finite numeric values. + */ + P99("P99"), + /** + * Calculate the 99.9th percentile of all finite numeric values. + */ + P999("P999"), + /** + * Calculate the standard deviation of all finite numeric values. + */ + STANDARD_DEVIATION("STDEV"), + /** + * Calculate the sum of all finite numeric values. + */ + SUM("SUM"), + /** + * Calculates the number of distinct values determined by invoking Object.equals. + */ + UNIQUE("UNIQUE"), + /** + * The last elements, for an event type, that all share the same end timestamp. + */ + LAST_BATCH("LAST_BATCH"); + + public final String name; + + private Aggregator(String name) { + this.name = name; + } +} \ No newline at end of file diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Configuration.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Configuration.java new file mode 100644 index 00000000000..eb325bc5e2b --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Configuration.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.time.Instant; + +import jdk.jfr.internal.util.Output; + +/** + * Holds information on how a query should be rendered. + */ +public final class Configuration { + public static final int MAX_PREFERRED_WIDTH = 120; + public static final int MIN_PREFERRED_WIDTH = 40; + public static final int PREFERRED_WIDTH = 80; + + public enum Truncate { + BEGINNING, END + } + + /** + * Where the rendered result should be printed. + */ + public Output output; + + /** + * The title of the table or form. + *

+ * {@code null) means no title. + */ + public String title; + + /** + * Truncation mode if text overflows. + *

+ * If truncate is not set, it will be determined by heuristics. + */ + public Truncate truncate; + + /** + * Height of table cells. + *

+ * If cellHeight is not set, it will be determined by heuristics. + */ + public int cellHeight; + + /** + * Width of a table or form. + *

+ * If width is not set, it will be determined by heuristics. + */ + public int width; + + /** + * If additional information should be printed. + */ + public boolean verbose; + + /** + * If symbolic names should be printed for table headers. + */ + public boolean verboseHeaders; + + /** + * If the title of the table or form should be printed. + */ + public boolean verboseTitle; + + /** + * The start time for the query. + *

+ * {@code null) means no start time. + */ + public Instant startTime; + + /** + * The end time for the query. + *

+ * {@code null) means no end time. + */ + public Instant endTime; +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Field.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Field.java new file mode 100644 index 00000000000..794d6370c57 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Field.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.internal.query.Configuration.Truncate; +import jdk.jfr.internal.query.Query.Grouper; +import jdk.jfr.internal.query.Query.OrderElement; + +/** + * Field is the core class of the package. + *

+ * It contains all the information related to how a field in a query should be + * grouped, sorted, formatted and aggregated. + *

+ * Defaults are derived from the metadata available in the event type, for + * example, numeric fields are right-aligned in the output, but can also + * be set using query clauses COLUMN and FORMAT. + *

+ * Settings in {@Configuration} overrides any field setting. + */ +final class Field { + // The fields to use as data sources, for example, when a + // field references multiple event types. First field + // is always the same as this field. + // (It's confusing, but hard to come up with an another + // abstraction that doesn't complicate the implementation.) + final List sourceFields = new ArrayList<>(); + + // Source type + final FilteredType type; + + // Symbolic name + final String name; + + // Index in the fields list + int index; + + // Human readable name + String label; + + // Function to extract a value from an event object + Function valueGetter; + + // Set if the field is part of GROUP BY clause + Grouper grouper; + + // Set if the field is part of ORDER BY clause + OrderElement orderer; + + // Set if the field is part of an aggregation + Aggregator aggregator = Aggregator.MISSING; + + // Height of a table cell + int cellHeight = 1; + + // Truncation mode (beginning or end) + Truncate truncate; + + // If the value visible + boolean visible; + + // Should value be aligned left + boolean alignLeft; + + // Should value be normalized between 0.0 and 1.0 + boolean normalized; + + // Should column be sorted textually + boolean lexicalSort; + + // A percentage value + boolean percentage; + + // A frequency value + boolean frequency; + + // A memory address + boolean memoryAddress; + + // A byte value + boolean bytes; + + // A bits value + boolean bits; + + // A fractional type (double or float) + boolean fractionalType; + + // An integral type (byte, short, int, long) + boolean integralType; + + // A java.time.Duration + boolean timespan; + + // A java.time.Instant + boolean timestamp; + + // Used by LAST_BATCH aggregator + Instant last = Instant.EPOCH; + + // The data type, for example, jdk.types.Frame or java.lang.String + String dataType; + + // Should not be given additional whitespace if available + public boolean fixedWidth; + + // Text to render if value is missing, typically used when value is null + public String missingText = "N/A"; + + public Field(FilteredType type, String name) { + this.type = type; + this.name = name; + } + + @Override + public String toString() { + return type.getName() + "#" + name; + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FieldBuilder.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FieldBuilder.java new file mode 100644 index 00000000000..2105936b6cf --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FieldBuilder.java @@ -0,0 +1,422 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +import jdk.jfr.DataAmount; +import jdk.jfr.EventType; +import jdk.jfr.Frequency; +import jdk.jfr.MemoryAddress; +import jdk.jfr.Percentage; +import jdk.jfr.Timespan; +import jdk.jfr.Timestamp; +import jdk.jfr.ValueDescriptor; +import jdk.jfr.consumer.RecordedClass; +import jdk.jfr.consumer.RecordedClassLoader; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordedFrame; +import jdk.jfr.consumer.RecordedStackTrace; + +/** + * This is a helper class to QueryResolver. It handles the creation of fields + * and their default configuration. + *

+ * The class applies heuristics to decide how values should be formatted, + * and labeled. + */ +final class FieldBuilder { + private static final Set KNOWN_TYPES = createKnownTypes(); + private final List eventTypes; + private final ValueDescriptor descriptor; + private final Field field; + private final String fieldName; + + public FieldBuilder(List eventTypes, FilteredType type, String fieldName) { + this.eventTypes = eventTypes; + this.descriptor = type.getField(fieldName); + this.field = new Field(type, fieldName); + this.fieldName = fieldName; + } + + public List build() { + if (configureSyntheticFields()) { + field.fixedWidth = false; + return List.of(field); + } + + if (descriptor != null) { + field.fixedWidth = !descriptor.getTypeName().equals("java.lang.String"); + field.dataType = descriptor.getTypeName(); + field.label = makeLabel(descriptor, hasDuration()); + field.alignLeft = true; + field.valueGetter = valueGetter(field.name); + + configureNumericTypes(); + configureTime(); + configurePercentage(); + configureDataAmount(); + configureFrequency(); + configureMemoryAddress(); + configureKnownType(); + return List.of(field); + } + return List.of(); + } + + private static Function valueGetter(String name) { + return event -> { + try { + return event.getValue(name); + } catch (NullPointerException e) { + // This can happen when accessing a nested structure + // that is null, for example root.referrer + return null; + } + }; + } + + private boolean hasDuration() { + return field.type.getField("duration") != null; + } + + private boolean configureSyntheticFields() { + if (fieldName.equals("stackTrace.topApplicationFrame")) { + configureTopApplicationFrameField(); + return true; + } + if (fieldName.equals("stackTrace.notInit")) { + configureNotInitFrameField(); + return true; + } + + if (fieldName.equals("stackTrace.topFrame")) { + configureTopFrameField(); + return true; + } + if (fieldName.equals("id") && field.type.getName().equals("jdk.ActiveSetting")) { + configureEventTypeIdField(); + return true; + } + if (fieldName.equals("eventType.label")) { + configureEventType(e -> e.getEventType().getLabel()); + return true; + } + if (fieldName.equals("eventType.name")) { + configureEventType(e -> e.getEventType().getName()); + return true; + } + return false; + } + + private void configureEventTypeIdField() { + Map eventTypes = createEventTypeLookup(); + field.alignLeft = true; + field.label = "Event Type"; + field.dataType = String.class.getName(); + field.valueGetter = event -> eventTypes.get(event.getLong("id")); + field.lexicalSort = true; + field.integralType = false; + } + + private Map createEventTypeLookup() { + Map map = new HashMap<>(); + for (EventType eventType : eventTypes) { + String label = eventType.getLabel(); + if (label == null) { + label = eventType.getName(); + } + map.put(eventType.getId(), label); + } + return map; + } + + private void configureTopFrameField() { + field.alignLeft = true; + field.label = "Method"; + field.dataType = "jdk.types.Method"; + field.valueGetter = e -> { + RecordedStackTrace t = e.getStackTrace(); + return t != null ? t.getFrames().getFirst() : null; + }; + field.lexicalSort = true; + } + + private void configureCustomFrame(Predicate condition) { + field.alignLeft = true; + field.dataType = "jdk.types.Frame"; + field.label = "Method"; + field.lexicalSort = true; + field.valueGetter = e -> { + RecordedStackTrace t = e.getStackTrace(); + if (t != null) { + for (RecordedFrame f : t.getFrames()) { + if (f.isJavaFrame()) { + if (condition.test(f)) { + return f; + } + } + } + } + return null; + }; + } + + private void configureNotInitFrameField() { + configureCustomFrame(frame -> { + return !frame.getMethod().getName().equals(""); + }); + } + + private void configureTopApplicationFrameField() { + configureCustomFrame(frame -> { + RecordedClass cl = frame.getMethod().getType(); + RecordedClassLoader classLoader = cl.getClassLoader(); + return classLoader != null && !"bootstrap".equals(classLoader.getName()); + }); + } + + private void configureEventType(Function retriever) { + field.alignLeft = true; + field.dataType = String.class.getName(); + field.label = "Event Type"; + field.valueGetter = retriever; + field.lexicalSort = true; + } + + private static String makeLabel(ValueDescriptor v, boolean hasDuration) { + String label = v.getLabel(); + if (label == null) { + return v.getName(); + } + String name = v.getName(); + if (name.equals("gcId")) { + return "GC ID"; + } + if (name.equals("compilerId")) { + return "Compiler ID"; + } + if (name.equals("startTime") && !hasDuration) { + return "Time"; + } + return label; + } + + private void configureTime() { + Timestamp timestamp = descriptor.getAnnotation(Timestamp.class); + if (timestamp != null) { + field.alignLeft = true; + field.dataType = Instant.class.getName(); + field.timestamp = true; + field.valueGetter = e -> e.getInstant(fieldName); + } + Timespan timespan = descriptor.getAnnotation(Timespan.class); + if (timespan != null) { + field.alignLeft = false; + field.dataType = Duration.class.getName(); + field.timespan = true; + field.valueGetter = e -> e.getDuration(fieldName); + } + } + + private void configureNumericTypes() { + switch (descriptor.getTypeName()) { + case "int": + case "long": + case "short": + case "byte": + field.integralType = true; + field.alignLeft = false; + break; + case "float": + case "double": + field.fractionalType = true; + field.alignLeft = false; + break; + case "boolean": + field.alignLeft = false; + break; + } + } + + private void configureKnownType() { + String type = descriptor.getTypeName(); + if (KNOWN_TYPES.contains(type)) { + field.lexicalSort = true; + field.fixedWidth = false; + } + } + + private void configureMemoryAddress() { + MemoryAddress memoryAddress = descriptor.getAnnotation(MemoryAddress.class); + if (memoryAddress != null) { + field.memoryAddress = true; + field.alignLeft = true; + } + } + + private void configureFrequency() { + if (descriptor.getAnnotation(Frequency.class) != null) { + field.frequency = true; + } + } + + private void configureDataAmount() { + DataAmount dataAmount = descriptor.getAnnotation(DataAmount.class); + if (dataAmount != null) { + if (DataAmount.BITS.equals(dataAmount.value())) { + field.bits = true; + } + if (DataAmount.BYTES.equals(dataAmount.value())) { + field.bytes = true; + } + } + } + + private void configurePercentage() { + Percentage percentage = descriptor.getAnnotation(Percentage.class); + if (percentage != null) { + field.percentage = true; + } + } + + // Fields created with "SELECT * FROM ..." queries + public static List createWildcardFields(List eventTypes, List types) { + List result = new ArrayList<>(); + for (FilteredType type : types) { + result.addAll(createWildcardFields(eventTypes, type)); + } + return result; + } + + private static List createWildcardFields(List eventTypes, FilteredType type) { + record WildcardElement(String name, String label, ValueDescriptor field) { + } + + var visited = new HashSet(); + var stack = new ArrayDeque(); + var wildcardElements = new ArrayList(); + + for (ValueDescriptor field : type.getFields().reversed()) { + stack.push(new WildcardElement(field.getName(), makeLabel(field, hasDuration(type)), field)); + } + while (!stack.isEmpty()) { + var we = stack.pop(); + if (!visited.contains(we.field)) { + visited.add(we.field); + var subFields = we.field().getFields().reversed(); + if (!subFields.isEmpty() && !KNOWN_TYPES.contains(we.field().getTypeName())) { + for (ValueDescriptor subField : subFields) { + String n = we.name + "." + subField.getName(); + String l = we.label + " : " + makeLabel(subField, false); + if (stack.size() < 2) { // Limit depth to 2 + stack.push(new WildcardElement(n, l, subField)); + } + } + } else { + wildcardElements.add(we); + } + } + } + List result = new ArrayList<>(); + for (WildcardElement we : wildcardElements) { + FieldBuilder fb = new FieldBuilder(eventTypes, type, we.name()); + Field field = fb.build().getFirst(); + field.label = we.label; + field.index = result.size(); + field.visible = true; + field.sourceFields.add(field); + result.add(field); + } + return result; + } + + private static boolean hasDuration(FilteredType type) { + return type.getField("duration") != null; + } + + public static void configureAggregator(Field field) { + Aggregator aggregator = field.aggregator; + if (aggregator == Aggregator.COUNT || aggregator == Aggregator.UNIQUE) { + field.integralType = true; + field.timestamp = false; + field.timespan = false; + field.fractionalType = false; + field.bytes = false; + field.bits = false; + field.frequency = false; + field.memoryAddress = false; + field.percentage = false; + field.alignLeft = false; + field.lexicalSort = false; + } + if (aggregator == Aggregator.LIST) { + field.alignLeft = true; + field.lexicalSort = true; + } + field.label = switch (aggregator) { + case COUNT -> "Count"; + case AVERAGE -> "Avg. " + field.label; + case FIRST, LAST, LAST_BATCH -> field.label; + case MAXIMUM -> "Max. " + field.label; + case MINIMUM -> "Min. " + field.label; + case SUM -> "Total " + field.label; + case UNIQUE -> "Unique Count " + field.label; + case LIST -> field.label + "s"; + case MISSING -> field.label; + case DIFFERENCE -> "Difference " + field.label; + case MEDIAN -> "Median " + field.label; + case P90 -> "P90 " + field.label; + case P95 -> "P95 " + field.label; + case P99 -> "P99 " + field.label; + case P999 -> "P99.9 " + field.label; + case STANDARD_DEVIATION -> "Std. Dev. " + field.label; + }; + } + + private static Set createKnownTypes() { + Set set = new HashSet<>(); + set.add(String.class.getName()); + set.add(Thread.class.getName()); + set.add(Class.class.getName()); + set.add("jdk.types.ThreadGroup"); + set.add("jdk.types.ClassLoader"); + set.add("jdk.types.Method"); + set.add("jdk.types.StackFrame"); + set.add("jdk.types.StackTrace"); + return set; + } +} \ No newline at end of file diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FieldFormatter.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FieldFormatter.java new file mode 100644 index 00000000000..ccb066b2301 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FieldFormatter.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import jdk.jfr.consumer.RecordedClass; +import jdk.jfr.consumer.RecordedClassLoader; +import jdk.jfr.consumer.RecordedFrame; +import jdk.jfr.consumer.RecordedMethod; +import jdk.jfr.consumer.RecordedStackTrace; +import jdk.jfr.consumer.RecordedThread; +import jdk.jfr.consumer.RecordedThreadGroup; +import jdk.jfr.internal.util.ValueFormatter; + +public class FieldFormatter { + + public static String formatCompact(Field field, Object object) { + return format(field, object, true); + } + + public static String format(Field field, Object object) { + return format(field, object, false); + } + + private static String format(Field field, Object object, boolean compact) { + if (object == null) { + return field.missingText; + } + if (object instanceof String s) { + return stripFormatting(s); + } + if (object instanceof Double d) { + if (Double.isNaN(d) || d == Double.NEGATIVE_INFINITY) { + return field.missingText; + } + } + if (object instanceof Float f) { + if (Float.isNaN(f) || f == Float.NEGATIVE_INFINITY) { + return field.missingText; + } + } + if (object instanceof Long l && l == Long.MIN_VALUE) { + return field.missingText; + } + if (object instanceof Integer i && i == Integer.MIN_VALUE) { + return field.missingText; + } + + if (object instanceof RecordedThread t) { + if (t.getJavaThreadId() > 0) { + return t.getJavaName(); + } else { + return t.getOSName(); + } + } + if (object instanceof RecordedClassLoader cl) { + return format(field, cl.getType(), compact); + } + if (object instanceof RecordedStackTrace st) { + return format(field, st.getFrames().getFirst(), compact); + } + if (object instanceof RecordedThreadGroup tg) { + return tg.getName(); + } + if (object instanceof RecordedMethod m) { + return ValueFormatter.formatMethod(m, compact); + } + if (object instanceof RecordedClass clazz) { + String text = ValueFormatter.formatClass(clazz); + if (compact) { + return text.substring(text.lastIndexOf(".") + 1); + } + return text; + } + if (object instanceof RecordedFrame f) { + if (f.isJavaFrame()) { + return format(field, f.getMethod(), compact); + } + return ""; + } + if (object instanceof Duration d) { + if (d.getSeconds() == Long.MIN_VALUE && d.getNano() == 0) { + return field.missingText; + } + if (d.equals(ChronoUnit.FOREVER.getDuration())) { + return "Indefinite"; + } + return ValueFormatter.formatDuration(d); + } + if (object instanceof Instant instant) { + return ValueFormatter.formatTimestamp(instant); + } + if (field.percentage) { + if (object instanceof Number n) { + double d = n.doubleValue(); + return String.format("%.2f", d * 100) + "%"; + } + } + if (field.bits || field.bytes) { + if (object instanceof Number n) { + long amount = n.longValue(); + if (field.frequency) { + if (field.bytes) { + return ValueFormatter.formatBytesPerSecond(amount); + } + if (field.bits) { + return ValueFormatter.formatBitsPerSecond(amount); + } + } else { + if (field.bytes) { + return ValueFormatter.formatBytes(amount); + } + if (field.bits) { + return ValueFormatter.formatBits(amount); + } + } + } + } + if (field.memoryAddress) { + if (object instanceof Number n) { + long d = n.longValue(); + return String.format("0x%08X", d); + } + } + if (field.frequency) { + if (object instanceof Number) { + return object + " Hz"; + } + } + if (object instanceof Number number) { + return ValueFormatter.formatNumber(number); + } + return object.toString(); + } + + private static String stripFormatting(String text) { + if (!hasFormatting(text)) { + return text; // Fast path to avoid allocation + } + StringBuilder sb = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + sb.append(isFormatting(c) ? ' ' : c); + } + return sb.toString(); + } + + private static boolean hasFormatting(String s) { + for (int i = 0; i < s.length(); i++) { + if (isFormatting(s.charAt(i))) { + return true; + } + } + return false; + } + + private static boolean isFormatting(char c) { + return c == '\n' || c == '\r' || c == '\t'; + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FilteredType.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FilteredType.java new file mode 100644 index 00000000000..735417ee4ff --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FilteredType.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import jdk.jfr.EventType; +import jdk.jfr.Experimental; +import jdk.jfr.ValueDescriptor; +import jdk.jfr.internal.Utils; + +/** + * Type referenced in a FROM-clause. + *

+ * If the query has a WHEN clause, the available events for the event type + * is restricted by a list of filter conditions. + */ +final class FilteredType { + public record Filter (Field field, String value) { + + @Override + public int hashCode() { + return field.name.hashCode() + value.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (object instanceof Filter that) { + return this.field.name.equals(that.field.name) && Objects.equals(this.value, that.value); + } + return false; + } + } + + private final List filters = new ArrayList<>(); + private final EventType eventType; + private final String simpleName; + + public FilteredType(EventType type) { + this.eventType = type; + this.simpleName = Utils.makeSimpleName(type); + } + + public boolean isExperimental() { + return eventType.getAnnotation(Experimental.class) != null; + } + + public String getName() { + return eventType.getName(); + } + + public String getLabel() { + return eventType.getLabel(); + } + + public String getSimpleName() { + return simpleName; + } + + public void addFilter(Filter filter) { + filters.add(filter); + } + + public List getFilters() { + return filters; + } + + public ValueDescriptor getField(String name) { + return eventType.getField(name); + } + + public List getFields() { + return eventType.getFields(); + } + + @Override + public int hashCode() { + return Long.hashCode(eventType.getId()) + filters.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (object instanceof FilteredType that) { + return that.eventType.getId() == this.eventType.getId() + && that.filters.equals(this.filters); + } + return false; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getName()); + sb.append(" "); + for (Filter condition : filters) { + sb.append(condition.field()); + sb.append(" = "); + sb.append(condition.value()); + sb.append(" "); + } + return sb.toString(); + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FormRenderer.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FormRenderer.java new file mode 100644 index 00000000000..e29a74c55df --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/FormRenderer.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + + +/** + * Class responsible for printing and formatting the contents of the first row in a table, + * as a form. + */ +import jdk.jfr.internal.util.Output; + +final class FormRenderer { + private static final String LABEL_SUFFIX = ": "; + private final Table table; + private final Output out; + private final Configuration configuration; + private final int width; + + public FormRenderer(Configuration configuration, Table table) { + this.table = table; + this.out = configuration.output; + this.configuration = configuration; + this.width = determineWidth(configuration); + } + + private static int determineWidth(Configuration configuration) { + if (configuration.width == 0) { + return Configuration.PREFERRED_WIDTH; + } else { + return configuration.width; + } + } + + public void render() { + if (table.isEmpty()) { + if (configuration.title != null) { + out.println(); + out.println("No events found for '" + configuration.title +"'."); + } + return; + } + + int maxWidth = 0; + for (Field field : table.getFields()) { + String label = field.label + LABEL_SUFFIX; + maxWidth = Math.max(label.length() + 1, maxWidth); + } + out.println(); + if (maxWidth + 2 > width) { + out.println("Columns are too wide to fit width " + configuration.width + "."); + return; + } + if (configuration.title != null) { + out.println(configuration.title); + out.println("-".repeat(configuration.title.length())); + } + if (table.isEmpty()) { + return; + } + for (Field field : table.getFields()) { + if (field.visible) { + out.println(); + renderField(field); + } + } + } + + private void renderField(Field field) { + Row row = table.getRows().getFirst(); + String label = field.label + LABEL_SUFFIX; + Object value = row.getValue(field.index); + String text = FieldFormatter.format(field, value); + boolean newLine = false; + out.print(label); + long p = width - label.length() - 1; + for (int i = 0; i < text.length(); i++) { + if (newLine) { + out.print(" ".repeat(label.length())); + newLine = false; + } + out.print(text.charAt(i)); + if (i % p == p - 1) { + newLine = true; + out.println(); + } + } + out.println(); + } + + public int getWidth() { + return width; + } +} \ No newline at end of file diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Function.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Function.java new file mode 100644 index 00000000000..8a358385820 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Function.java @@ -0,0 +1,662 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; +import java.util.StringJoiner; + +abstract class Function { + + public abstract void add(Object value); + + public abstract Object result(); + + public static Function create(Field field) { + Aggregator aggregator = field.aggregator; + + if (field.grouper != null || aggregator == Aggregator.MISSING) { + return new FirstNonNull(); + } + if (aggregator == Aggregator.LIST) { + return new List(); + } + + if (aggregator == Aggregator.DIFFERENCE) { + if (field.timestamp) { + return new TimeDifference(); + } else { + return new Difference(); + } + } + + if (aggregator == Aggregator.STANDARD_DEVIATION) { + if (field.timespan) { + return new TimespanFunction(new StandardDeviation()); + } else { + return new StandardDeviation(); + } + } + + if (aggregator == Aggregator.MEDIAN) { + if (field.timespan) { + return new TimespanFunction(new Median()); + } else { + return new Median(); + } + } + + if (aggregator == Aggregator.P90) { + return createPercentile(field, 0.95); + } + + if (aggregator == Aggregator.P95) { + return createPercentile(field, 0.95); + + } + if (aggregator == Aggregator.P99) { + return createPercentile(field, 0.99); + } + if (aggregator == Aggregator.P999) { + return createPercentile(field, 0.9999); + } + if (aggregator == Aggregator.MAXIMUM) { + return new Maximum(); + } + if (aggregator == Aggregator.MINIMUM) { + return new Minimum(); + } + if (aggregator == Aggregator.SUM) { + if (field.timespan) { + return new SumDuration(); + } + if (field.fractionalType) { + return new SumDouble(); + } + if (field.integralType) { + return new SumLong(); + } + } + + if (aggregator == Aggregator.FIRST) { + return new First(); + } + if (aggregator == Aggregator.LAST_BATCH) { + return new LastBatch(field); + } + if (aggregator == Aggregator.LAST) { + return new Last(); + } + if (aggregator == Aggregator.AVERAGE) { + if (field.timespan) { + return new AverageDuration(); + } else { + return new Average(); + } + } + if (aggregator == Aggregator.COUNT) { + return new Count(); + } + if (aggregator == Aggregator.UNIQUE) { + return new Unique(); + } + return new Null(); + } + + // **** AVERAGE **** + + private static final class Average extends Function { + private double total; + private long count; + + @Override + public void add(Object value) { + if (value instanceof Number n && Double.isFinite(n.doubleValue())) { + total += n.doubleValue(); + count++; + } + } + + @Override + public Object result() { + if (count != 0) { + return total / count; + } else { + return null; + } + } + } + + private static final class AverageDuration extends Function { + private long seconds; + private long nanos; + private int count; + + @Override + public void add(Object value) { + if (value instanceof Duration duration) { + seconds += duration.getSeconds(); + nanos += duration.getNano(); + count++; + } + } + + @Override + public Object result() { + if (count != 0) { + long s = seconds / count; + long n = nanos / count; + return Duration.ofSeconds(s, n); + } else { + return null; + } + } + } + + // **** COUNT **** + + private static final class Count extends Function { + private long count = 0; + + @Override + public void add(Object value) { + count++; + } + + @Override + public Object result() { + return count; + } + } + + // **** FIRST **** + + private static final class First extends Function { + private static Object firstObject = new Object(); + private Object first = firstObject; + + @Override + public void add(Object value) { + if (first == firstObject) { + first = value; + } + } + + @Override + public Object result() { + return first == firstObject ? null : first; + } + } + + // **** LAST **** + + private static final class Last extends Function { + private static Object lastObject = new Object(); + private Object last = lastObject; + + @Override + public void add(Object value) { + last = value; + } + + @Override + public Object result() { + return last == lastObject ? null : last; + } + } + + private static final class FirstNonNull extends Function { + private Object first; + + @Override + public void add(Object value) { + if (value == null) { + return; + } + first = value; + } + + @Override + public Object result() { + return first; + } + } + + // **** MAXIMUM **** + + @SuppressWarnings("rawtypes") + private static final class Maximum extends Function { + private Comparable maximum; + + @SuppressWarnings("unchecked") + @Override + public void add(Object value) { + if (value instanceof Comparable comparable) { + if (maximum == null) { + maximum = comparable; + return; + } + if (comparable.compareTo(maximum) > 0) { + maximum = comparable; + } + } + } + + @Override + public Object result() { + if (maximum == null) { + System.out.println("Why"); + } + return maximum; + } + } + + // **** MINIMUM **** + + @SuppressWarnings("rawtypes") + private static final class Minimum extends Function { + private Comparable minimum; + + @SuppressWarnings("unchecked") + @Override + public void add(Object value) { + if (value instanceof Comparable comparable) { + if (minimum == null) { + minimum = comparable; + return; + } + if (comparable.compareTo(minimum) < 0) { + minimum = comparable; + } + } + } + + @Override + public Object result() { + return minimum; + } + } + + // *** NULL **** + + private static final class Null extends Function { + @Override + public void add(Object value) { + } + + @Override + public Object result() { + return null; + } + } + + // **** SUM **** + + private static final class SumDouble extends Function { + private boolean hasValue = false; + private double sum = 0; + + @Override + public void add(Object value) { + if (value instanceof Number n && Double.isFinite(n.doubleValue())) { + sum += n.doubleValue(); + hasValue = true; + } + } + + @Override + public Object result() { + return hasValue ? sum : null; + } + } + + private static final class SumDuration extends Function { + private long seconds; + private long nanos; + private boolean hasValue; + + @Override + public void add(Object value) { + if (value instanceof Duration n) { + seconds += n.getSeconds(); + nanos += n.getNano(); + hasValue = true; + } + } + + @Override + public Object result() { + return hasValue ? Duration.ofSeconds(seconds, nanos) : null; + } + } + + private static final class SumLong extends Function { + private boolean hasValue = false; + private long sum = 0; + + @Override + public void add(Object value) { + if (value instanceof Number n) { + sum += n.longValue(); + hasValue = true; + } + } + + @Override + public Object result() { + return hasValue ? sum : null; + } + } + + // **** UNIQUE **** + + private static final class Unique extends Function { + private final Set unique = new HashSet<>(); + + @Override + public void add(Object value) { + unique.add(value); + } + + @Override + public Object result() { + return unique.size(); + } + } + + // **** LIST **** + + private static final class List extends Function { + private final ArrayList list = new ArrayList<>(); + + @Override + public void add(Object value) { + list.add(value); + } + + @Override + public Object result() { + StringJoiner sj = new StringJoiner(", "); + for (Object object : list) { + sj.add(String.valueOf(object)); + } + return sj.toString(); + } + } + + // **** DIFF **** + + private static final class Difference extends Function { + private Number first; + private Number last; + + @Override + public void add(Object value) { + if (value instanceof Number number && Double.isFinite(number.doubleValue())) { + if (first == null) { + first = number; + } + last = number; + } + } + + @Override + public Object result() { + if (last == null) { + return null; + } + if (isIntegral(first) && isIntegral(last)) { + return last.longValue() - first.longValue(); + } + if (first instanceof Float f && last instanceof Float l) { + return l - f; + } + return last.doubleValue() - first.doubleValue(); + } + + private boolean isIntegral(Number number) { + if ((number instanceof Long) || (number instanceof Integer) || (number instanceof Short) + || (number instanceof Byte)) { + return true; + } + return false; + } + } + private static final class TimeDifference extends Function { + private Instant first; + private Instant last; + + @Override + public void add(Object value) { + if (value instanceof Instant instant) { + if (first == null) { + first = instant; + return; + } + last = instant; + } + } + + @Override + public Object result() { + if (first == null) { + return null; + } + if (last == null) { + return ChronoUnit.FOREVER.getDuration(); + } + return Duration.between(first, last); + } + } + + @SuppressWarnings("rawtypes") + private static final class Median extends Function { + private final java.util.List comparables = new ArrayList<>(); + + @Override + public void add(Object value) { + if (value instanceof Number && value instanceof Comparable c) { + comparables.add(c); + } + } + + @Override + @SuppressWarnings("unchecked") + public Object result() { + if (comparables.isEmpty()) { + return null; + } + if (comparables.size() == 1) { + return comparables.getFirst(); + } + comparables.sort(Comparator.naturalOrder()); + if (comparables.size() % 2 == 1) { + return comparables.get(comparables.size() / 2); + } + Number a = (Number) comparables.get(comparables.size() / 2 - 1); + Number b = (Number) comparables.get(comparables.size() / 2); + return (a.doubleValue() + b.doubleValue()) / 2; + } + } + + // **** PERCENTILE **** + private static Function createPercentile(Field field, double percentile) { + Percentile p = new Percentile(percentile); + if (field.timespan) { + return new TimespanFunction(p); + } else { + return p; + } + } + + private static final class TimespanFunction extends Function { + private final Function function; + + TimespanFunction(Function function) { + this.function = function; + } + + @Override + public void add(Object value) { + if (value instanceof Duration duration) { + long nanos = 1_000_000_000L * duration.getSeconds() + duration.getNano(); + function.add(nanos); + } + } + + @Override + public Object result() { + Object object = function.result(); + if (object instanceof Number nanos) { + return Duration.ofNanos(nanos.longValue()); + } + return null; + } + } + private static final class Percentile extends Function { + private final double percentile; + private final java.util.List numbers = new ArrayList<>(); + + Percentile(double percentile) { + this.percentile = percentile; + } + + @Override + public void add(Object value) { + if (value instanceof Number number) { + if (Double.isFinite(number.doubleValue())) { + numbers.add(number); + } + } + } + + @Override + public Object result() { + if (numbers.isEmpty()) { + return null; + } + if (numbers.size() == 1) { + return numbers.getFirst(); + } + numbers.sort((n1, n2) -> Double.compare(n1.doubleValue(), n2.doubleValue())); + int size = numbers.size(); + // Use size + 1 so range is stretched out for interpolation + // For example with percentile 50% + // size | valueIndex | valueNextindex | fraction + // 2 0 1 0.50 + // 3 1 2 0.0 + // 4 1 2 0.50 + // 5 2 3 0.0 + // 6 2 3 0.50 + double doubleIndex = (size + 1) * percentile; + int valueIndex = (int) doubleIndex - 1; + int valueNextIndex = (int) doubleIndex; + double fraction = doubleIndex - valueIndex; + + if (valueIndex < 0) { + return numbers.getFirst(); + } + if (valueNextIndex >= size) { + return numbers.getLast(); + } + double a = numbers.get(valueIndex).doubleValue(); + double b = numbers.get(valueNextIndex).doubleValue(); + return a + fraction * (b - a); + } + } + + // **** STANDARD DEVIATION **** + + private static final class StandardDeviation extends Function { + private final java.util.List values = new ArrayList<>(); + + @Override + public void add(Object value) { + if (value instanceof Number n && Double.isFinite(n.doubleValue())) { + values.add(n); + } + } + + @Override + public Object result() { + if (values.size() > 0) { + long N = values.size(); + double average = sum() / N; + double sum = 0; + for (Number number : values) { + double diff = number.doubleValue() - average; + sum = sum + (diff * diff); + } + return Math.sqrt(sum / N); + } + return null; + } + + private double sum() { + double sum = 0; + for (Number number : values) { + sum += number.doubleValue(); + } + return sum ; + } + } + + public static final class LastBatch extends Function { + private final Field field; + private final Last last = new Last(); + private Instant timestamp; + + public LastBatch(Field field) { + this.field = field; + } + + @Override + public void add(Object value) { + last.add(value); + } + + @Override + public Object result() { + return last.result(); + } + + public void setTime(Instant timestamp) { + this.timestamp = timestamp; + field.last = timestamp; + } + + public boolean valid() { + if (timestamp != null) { + return timestamp.equals(field.last); + } + return true; + } + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Histogram.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Histogram.java new file mode 100644 index 00000000000..0eca381cca4 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Histogram.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import jdk.jfr.consumer.RecordedClass; +import jdk.jfr.consumer.RecordedClassLoader; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordedFrame; +import jdk.jfr.consumer.RecordedMethod; +import jdk.jfr.consumer.RecordedObject; +import jdk.jfr.consumer.RecordedStackTrace; +import jdk.jfr.consumer.RecordedThread; +import jdk.jfr.consumer.RecordedThreadGroup; +import jdk.jfr.internal.query.Function.LastBatch; + +/** + * Class responsible for aggregating values + */ +final class Histogram { + private static final class LookupKey { + private Object keys; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void add(Object o) { + // One key, fast path + if (keys == null) { + keys = o; + return; + } + // Three or more keys + if (keys instanceof Set set) { + set.add(o); + return; + } + // Two keys + Set set = HashSet.newHashSet(2); + set.add(keys); + set.add(o); + keys = set; + } + + @Override + public int hashCode() { + return Objects.hashCode(keys); + } + + @Override + public boolean equals(Object object) { + if (object instanceof LookupKey that) { + return Objects.deepEquals(that.keys, this.keys); + } + return false; + } + } + + private static final class MethodEquality { + private final String methodName; + private final String descriptor; + private final long classId; + + public MethodEquality(RecordedMethod method) { + methodName = method.getName(); + descriptor = method.getDescriptor(); + classId = method.getType().getId(); + } + + @Override + public int hashCode() { + int hash1 = Long.hashCode(classId); + int hash2 = methodName.hashCode(); + int hash3 = descriptor.hashCode(); + int result = 31 + hash1; + result += 31 * result + hash2; + result += 31 * result + hash3; + return result; + } + + @Override + public boolean equals(Object object) { + if (object instanceof MethodEquality that) { + if ((this.classId != that.classId) || !Objects.equals(this.methodName, that.methodName)) { + return false; + } + return Objects.equals(this.descriptor, that.descriptor); + } + return false; + } + } + + private final Map keyFunctionsMap = new HashMap<>(); + private final List fields = new ArrayList<>(); + + public void addFields(List fields) { + this.fields.addAll(fields); + } + + public void add(RecordedEvent e, FilteredType type, List sourceFields) { + LookupKey lk = new LookupKey(); + final Object[] values = new Object[sourceFields.size()]; + for (int i = 0; i < values.length; i++) { + Field field = sourceFields.get(i); + Object value = field.valueGetter.apply(e); + values[i] = value; + if (field.grouper != null) { + lk.add(makeKey(value)); + } + } + + Function[] fs = keyFunctionsMap.computeIfAbsent(lk, k -> createFunctions()); + for (int i = 0; i < values.length; i++) { + Function function = fs[sourceFields.get(i).index]; + function.add(values[i]); + if (function instanceof LastBatch l) { + l.setTime(e.getEndTime()); + } + } + } + + public List toRows() { + List rows = new ArrayList<>(keyFunctionsMap.size()); + for (Function[] functions : keyFunctionsMap.values()) { + Row row = new Row(fields.size()); + boolean valid = true; + int index = 0; + for (Function f : functions) { + if (f instanceof LastBatch last && !last.valid()) { + valid = false; + } + row.putValue(index++, f.result()); + } + if (valid) { + rows.add(row); + } + } + return rows; + } + + private Function[] createFunctions() { + Function[] functions = new Function[fields.size()]; + for (int i = 0; i < functions.length; i++) { + functions[i] = Function.create(fields.get(i)); + } + return functions; + } + + private static Object makeKey(Object object) { + if (!(object instanceof RecordedObject)) { + return object; + } + if (object instanceof RecordedMethod method) { + return new MethodEquality(method); + } + if (object instanceof RecordedThread thread) { + return thread.getId(); + } + if (object instanceof RecordedClass clazz) { + return clazz.getId(); + } + if (object instanceof RecordedFrame frame) { + if (frame.isJavaFrame()) { + return makeKey(frame.getMethod()); + } + return null; + } + + if (object instanceof RecordedStackTrace stackTrace) { + List recordedFrames = stackTrace.getFrames(); + List frames = new ArrayList<>(recordedFrames.size()); + for (RecordedFrame frame : recordedFrames) { + frames.add(makeKey(frame)); + } + return frames; + } + if (object instanceof RecordedClassLoader classLoader) { + return classLoader.getId(); + } + if (object instanceof RecordedThreadGroup group) { + String name = group.getName(); + String parentName = group.getParent() != null ? group.getParent().getName() : null; + return name + ":" + parentName; + } + return object; + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Query.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Query.java new file mode 100644 index 00000000000..de316aad354 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Query.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.text.ParseException; +import java.util.List; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.function.Consumer; + +final class Query { + enum SortOrder { + NONE, ASCENDING, DESCENDING + } + + record Source(String name, Optional alias) { + } + + record Condition(String field, String value) { + } + + record Where(List conditions) { + } + + record Property(String name, Consumer style) { + } + + record Formatter(List properties) { + } + + record Expression(String name, Optional alias, Aggregator aggregator) { + } + + record Grouper(String field) { + } + + record OrderElement(String name, SortOrder order) { + } + + final List column; + final List format; + final List select; + final List from; + final List where; + final List groupBy; + final List orderBy; + final int limit; + + public Query(String text) throws ParseException { + try (QueryParser qp = new QueryParser(text)) { + column = qp.column(); + format = qp.format(); + select = qp.select(); + from = qp.from(); + where = qp.where(); + groupBy = qp.groupBy(); + orderBy = qp.orderBy(); + limit = qp.limit(); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (!column.isEmpty()) { + StringJoiner sj = new StringJoiner(", "); + for (String c : column) { + sj.add("'" + c + "'"); + } + sb.append("COLUMN ").append(sj).append(" "); + } + if (!format.isEmpty()) { + StringJoiner sj = new StringJoiner(", "); + for (Formatter f : format) { + StringJoiner t = new StringJoiner(";"); + for (Property p : f.properties()) { + t.add(p.name()); + } + sj.add(t.toString()); + } + sb.append("FORMAT ").append(sj).append(" "); + } + StringJoiner t = new StringJoiner(", "); + for (Expression e : select) { + StringBuilder w = new StringBuilder(); + if (e.aggregator() != Aggregator.MISSING) { + w.append(e.aggregator().name()); + w.append("("); + } + w.append(e.name()); + if (e.aggregator() != Aggregator.MISSING) { + w.append(")"); + } + if (e.alias().isPresent()) { + w.append(" AS "); + w.append(e.alias().get()); + } + t.add(w.toString()); + } + sb.append("SELECT ") + .append(select.isEmpty() ? "*" : t.toString()); + StringJoiner u = new StringJoiner(", "); + for (Source e : from) { + String s = e.name(); + if (e.alias().isPresent()) { + s += " AS " + e.alias().get(); + } + u.add(s); + } + sb.append(" FROM ").append(u); + if (!where.isEmpty()) { + StringJoiner sj = new StringJoiner(" AND"); + for (Condition c : where) { + sj.add(" " + c.field() + " = '" + c.value() + "'"); + } + sb.append(" WHERE").append(sj); + } + + if (!groupBy.isEmpty()) { + StringJoiner sj = new StringJoiner(", "); + for (Grouper g : groupBy) { + sj.add(g.field()); + } + sb.append(" GROUP BY ").append(sj); + } + if (!orderBy.isEmpty()) { + StringJoiner sj = new StringJoiner(", "); + for (OrderElement e : orderBy) { + String name = e.name(); + if (e.order() == SortOrder.ASCENDING) { + name += " ASC"; + } + if (e.order() == SortOrder.DESCENDING) { + name += " DESC"; + } + sj.add(name); + } + sb.append(" ORDER BY ").append(sj); + } + if (limit != Integer.MAX_VALUE) { + sb.append(" LIMIT " + limit); + } + return sb.toString(); + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryExecutor.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryExecutor.java new file mode 100644 index 00000000000..51eac7c6a32 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryExecutor.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +import jdk.jfr.EventType; +import jdk.jfr.consumer.EventStream; +import jdk.jfr.consumer.MetadataEvent; + +final class QueryExecutor { + private final List queryRuns = new ArrayList<>(); + private final List eventTypes = new ArrayList<>(); + private final EventStream stream; + + public QueryExecutor(EventStream stream) { + this(stream, List.of()); + } + + public QueryExecutor(EventStream stream, Query query) { + this(stream, List.of(query)); + } + + public QueryExecutor(EventStream stream, List queries) { + this.stream = stream; + for (Query query : queries) { + queryRuns.add(new QueryRun(stream, query)); + } + stream.setReuse(false); + stream.setOrdered(true); + stream.onMetadata(this::onMetadata); + } + + public List run() { + stream.start(); + for (QueryRun run : queryRuns) { + run.complete(); + } + return queryRuns; + } + + private void onMetadata(MetadataEvent e) { + if (eventTypes.isEmpty()) { + eventTypes.addAll(e.getEventTypes()); + } + if (queryRuns.isEmpty()) { + addQueryRuns(); + } + for (QueryRun run : queryRuns) { + run.onMetadata(e); + } + } + + private void addQueryRuns() { + for (EventType type : eventTypes) { + try { + Query query = new Query("SELECT * FROM " + type.getName()); + QueryRun run = new QueryRun(stream, query); + queryRuns.add(run); + } catch (ParseException pe) { + // The event name contained whitespace or similar, ignore. + } + } + } + + public List getEventTypes() { + return eventTypes; + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryParser.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryParser.java new file mode 100644 index 00000000000..dfaeab38b85 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryParser.java @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +import jdk.jfr.internal.query.Configuration.Truncate; +import jdk.jfr.internal.query.Query.Condition; +import jdk.jfr.internal.query.Query.Expression; +import jdk.jfr.internal.query.Query.Formatter; +import jdk.jfr.internal.query.Query.Grouper; +import jdk.jfr.internal.query.Query.OrderElement; +import jdk.jfr.internal.query.Query.Property; +import jdk.jfr.internal.query.Query.SortOrder; +import jdk.jfr.internal.query.Query.Source; +import jdk.jfr.internal.util.Tokenizer; + +final class QueryParser implements AutoCloseable { + static final char[] SEPARATORS = {'=', ',', ';', '(', ')'}; + + private final Tokenizer tokenizer; + + public QueryParser(String text) { + tokenizer = new Tokenizer(text, SEPARATORS); + } + + public List column() throws ParseException { + if (!tokenizer.accept("COLUMN")) { + return List.of(); + } + List texts = new ArrayList<>(); + texts.add(text()); + while (tokenizer.accept(",")) { + texts.add(text()); + } + return texts; + } + + public List format() throws ParseException { + if (tokenizer.accept("FORMAT")) { + List formatters = new ArrayList<>(); + formatters.add(formatter()); + while (tokenizer.accept(",")) { + formatters.add(formatter()); + } + return formatters; + } + return List.of(); + } + + private Formatter formatter() throws ParseException { + List properties = new ArrayList<>(); + properties.add(property()); + while (tokenizer.accept(";")) { + properties.add(property()); + } + return new Formatter(properties); + } + + public List select() throws ParseException { + tokenizer.expect("SELECT"); + if (tokenizer.accept("*")) { + return List.of(); + } + List expressions = new ArrayList<>(); + if (tokenizer.accept("FROM")) { + throw new ParseException("Missing fields in SELECT statement", position()); + } + expressions.add(expression()); + while (tokenizer.accept(",")) { + Expression exp = expression(); + if (exp.name().equalsIgnoreCase("FROM")) { + throw new ParseException("Missing field name in SELECT statement, or qualify field with event type if name is called '" + exp.name() + "'", position()); + } + expressions.add(exp); + } + return expressions; + } + + private Expression expression() throws ParseException { + Expression aggregator = aggregator(); + if (aggregator != null) { + return aggregator; + } else { + return new Expression(eventField(), alias(), Aggregator.MISSING); + } + } + + private Expression aggregator() throws ParseException { + for (Aggregator function : Aggregator.values()) { + if (tokenizer.accept(function.name, "(")) { + String eventField = eventField(); + tokenizer.expect(")"); + return new Expression(eventField, alias(), function); + } + } + return null; + } + + private Optional alias() throws ParseException { + Optional alias = Optional.empty(); + if (tokenizer.accept("AS")) { + alias = Optional.of(symbol()); + } + return alias; + } + + public List from() throws ParseException { + tokenizer.expect("FROM"); + List sources = new ArrayList<>(); + sources.add(source()); + while (tokenizer.accept(",")) { + sources.add(source()); + } + return sources; + } + + private Source source() throws ParseException { + String type = type(); + if (tokenizer.accept("SELECT")) { + throw new ParseException("Subquery is not allowed", position()); + } + if (tokenizer.accept("INNER", "JOIN", "LEFT", "RIGHT", "FULL")) { + throw new ParseException("JOIN is not allowed", position()); + } + return new Source(type, alias()); + } + + private String type() throws ParseException { + return tokenizer.next(); + } + + public List where() throws ParseException { + if (tokenizer.accept("WHERE")) { + List conditions = new ArrayList<>(); + conditions.add(condition()); + while (tokenizer.accept("AND")) { + conditions.add(condition()); + } + return conditions; + } + return List.of(); + } + + private Condition condition() throws ParseException { + String field = eventField(); + if (tokenizer.acceptAny("<", ">", "<>", ">=", "<=", "==", "BETWEEN", "LIKE", "IN")) { + throw new ParseException("The only operator allowed in WHERE clause is '='", position()); + } + tokenizer.expect("="); + String value = text(); + return new Condition(field, value); + } + + public List groupBy() throws ParseException { + if (tokenizer.accept("HAVING")) { + throw new ParseException("HAVING is not allowed", position()); + } + if (tokenizer.accept("GROUP")) { + tokenizer.expect("BY"); + List groupers = new ArrayList<>(); + groupers.add(grouper()); + while (tokenizer.accept(",")) { + groupers.add(grouper()); + } + return groupers; + } + return new ArrayList<>(); // Need to return mutable collection + } + + private Grouper grouper() throws ParseException { + return new Grouper(eventField()); + } + + public List orderBy() throws ParseException { + if (tokenizer.accept("ORDER")) { + tokenizer.expect("BY"); + List fields = new ArrayList<>(); + fields.add(orderer()); + while (tokenizer.accept(",")) { + fields.add(orderer()); + } + return fields; + } + return List.of(); + } + + private OrderElement orderer() throws ParseException { + return new OrderElement(eventField(), sortOrder()); + } + + private SortOrder sortOrder() throws ParseException { + if (tokenizer.accept("ASC")) { + return SortOrder.ASCENDING; + } + if (tokenizer.accept("DESC")) { + return SortOrder.DESCENDING; + } + return SortOrder.NONE; + } + + private String text() throws ParseException { + if (tokenizer.peekChar() != '\'') { + throw new ParseException("Expected text to start with a single quote character", position()); + } + return tokenizer.next(); + } + + private String symbol() throws ParseException { + String s = tokenizer.next(); + for (int index = 0; index < s.length(); index++) { + int cp = s.codePointAt(index); + if (!Character.isLetter(cp)) { + throw new ParseException("Symbol must consist of letters, found '" + s.charAt(index) + "' in '" + s + "'", + position()); + } + } + return s; + } + + private String eventField() throws ParseException { + if (!tokenizer.hasNext()) { + throw new ParseException("Unexpected end when looking for event field", position()); + } + if (tokenizer.peekChar() == '\'') { + throw new ParseException("Expected unquoted symbolic name (not label)", position()); + } + String name = tokenizer.next(); + if (name.equals("*")) { + return name; + } + for (int index = 0; index < name.length(); index++) { + char c = name.charAt(index); + boolean valid = index == 0 ? Character.isJavaIdentifierStart(c) : Character.isJavaIdentifierPart(c); + if (c != '.' && c != '[' && c!= ']' && c != '|' && !valid) { + throw new ParseException("Not a valid field name: " + name, position()); + } + } + return name; + } + + private Property property() throws ParseException { + String text = tokenizer.next(); + Consumer style = switch (text.toLowerCase()) { + case "none" -> field -> {}; + case "missing:" -> field -> {}; + case "normalized" -> field -> field.normalized = field.percentage = true; + case "truncate-beginning" -> field -> field.truncate = Truncate.BEGINNING; + case "truncate-end" -> field -> field.truncate = Truncate.END; + default -> { + if (text.startsWith("missing:")) { + yield missing(text.substring("missing:".length())); + } + if (text.startsWith("cell-height:")) { + yield cellHeight(text.substring("cell-height:".length())); + } + throw new ParseException("Unknown formatter '" + text + "'", position()); + } + }; + return new Property(text, style); + } + + private Consumer missing(String missing) { + if ("whitespace".equals(missing)) { + return field -> field.missingText = ""; + } else { + return field -> field.missingText = missing; + } + } + + private Consumer cellHeight(String height) throws ParseException { + try { + int h = Integer.parseInt(height); + if (h < 1) { + throw new ParseException("Expected 'cell-height:' to be at least 1' ", position()); + } + return field -> field.cellHeight = h; + } catch (NumberFormatException nfe) { + throw new ParseException("Not valid number for 'cell-height:' " + height, position()); + } + } + + public int position() { + return tokenizer.getPosition(); + } + + public int limit() throws ParseException { + if (tokenizer.accept("LIMIT")) { + try { + if (tokenizer.hasNext()) { + String number = tokenizer.next(); + int limit= Integer.parseInt(number); + if (limit < 0) { + throw new ParseException("Expected a positive integer after LIMIT", position()); + } + return limit; + } + } catch (NumberFormatException nfe) { + // Fall through + } + throw new ParseException("Expected an integer after LIMIT", position()); + } + return Integer.MAX_VALUE; + } + + @Override + public void close() throws ParseException { + tokenizer.close(); + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryPrinter.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryPrinter.java new file mode 100644 index 00000000000..ce142e287d6 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryPrinter.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jdk.jfr.AnnotationElement; +import jdk.jfr.EventType; +import jdk.jfr.Experimental; +import jdk.jfr.Relational; +import jdk.jfr.ValueDescriptor; +import jdk.jfr.consumer.EventStream; +import jdk.jfr.internal.Utils; +import jdk.jfr.internal.util.Columnizer; +import jdk.jfr.internal.util.Output; +import jdk.jfr.internal.util.StopWatch; +import jdk.jfr.internal.util.Tokenizer; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; +import jdk.jfr.internal.util.ValueFormatter; + +/** + * Class responsible for executing and displaying the contents of a query. + *

+ * Used by 'jcmd JFR.query' and 'jfr query'. + */ +public final class QueryPrinter { + private final EventStream stream; + private final Configuration configuration; + private final Output out; + private final StopWatch stopWatch; + + /** + * Constructs a query printer. + * + * @param configuration display configuration + * @param stream a non-started stream from where data should be fetched. + */ + public QueryPrinter(Configuration configuration, EventStream stream) { + this.configuration = configuration; + this.out = configuration.output; + this.stopWatch = new StopWatch(); + this.stream = stream; + } + + /** + * Prints the query. + * + * @see getGrammarText(). + * + * @param query the query text + * + * @throws UserDataException if the stream associated with the printer lacks + * event or event metadata + * @throws UserSyntaxException if the syntax of the query is incorrect. + */ + public void execute(String query) throws UserDataException, UserSyntaxException { + if (showEvents(query) || showFields(query)) { + return; + } + showQuery(query); + } + + private void showQuery(String query) throws UserDataException, UserSyntaxException { + try { + stopWatch.beginQueryValidation(); + Query q = new Query(query); + QueryExecutor executor = new QueryExecutor(stream, q); + stopWatch.beginAggregation(); + QueryRun task = executor.run().getFirst(); + if (!task.getSyntaxErrors().isEmpty()) { + throw new UserSyntaxException(task.getSyntaxErrors().getFirst()); + } + if (!task.getMetadataErrors().isEmpty()) { + throw new UserDataException(task.getMetadataErrors().getFirst()); + } + Table table = task.getTable(); + if (configuration.verboseTitle) { + FilteredType type = table.getFields().getFirst().type; + configuration.title = type.getLabel(); + if (type.isExperimental()) { + configuration.title += " (Experimental)"; + } + } + stopWatch.beginFormatting(); + TableRenderer renderer = new TableRenderer(configuration, table, q); + renderer.render(); + stopWatch.finish(); + if (configuration.verbose) { + out.println(); + out.println("Execution: " + stopWatch.toString()); + } + if (configuration.startTime != null) { + String s = ValueFormatter.formatTimestamp(configuration.startTime); + String e = ValueFormatter.formatTimestamp(configuration.endTime); + out.println(); + out.println("Timespan: " + s + " - " + e); + } + } catch (ParseException pe) { + throw new UserSyntaxException(pe.getMessage()); + } + } + + private boolean showFields(String text) { + String eventType = null; + try (Tokenizer t = new Tokenizer(text)) { + t.expect("SHOW"); + t.expect("FIELDS"); + eventType = t.next(); + } catch (ParseException pe) { + return false; + } + Map eventTypes = new HashMap<>(); + stream.onMetadata(e -> { + for (EventType t : e.getAddedEventTypes()) { + eventTypes.put(t.getId(), t); + } + }); + stream.start(); + + List types = new ArrayList<>(eventTypes.values()); + Collections.sort(types, Comparator.comparing(EventType::getName)); + for (EventType type : types) { + String qualifiedName = type.getName(); + String name = Utils.makeSimpleName(qualifiedName); + if (qualifiedName.equals(eventType) || name.equals(eventType)) { + printFields(type, types); + return true; + } + } + return false; + } + + private void printFields(EventType type, List allTypes) { + out.println(); + out.println("" + type.getName() + ":"); + out.println(); + for (ValueDescriptor f : type.getFields()) { + String typeName = Utils.makeSimpleName(f.getTypeName()); + out.println(" " + typeName + " " + f.getName()); + } + List related = new ArrayList<>(); + for (String s : relations(type)) { + out.println(); + String simpleName = Utils.makeSimpleName(s); + out.println("Event types with a " + simpleName + " relation:"); + out.println(); + for (EventType et : allTypes) { + if (et != type) { + List r = relations(et); + if (r.contains(s)) { + related.add(et.getName()); + } + } + } + out.println(new Columnizer(related, 2).toString()); + } + out.println(); + } + + private List relations(EventType type) { + List relations = new ArrayList<>(); + for (ValueDescriptor field : type.getFields()) { + for (AnnotationElement annotation : field.getAnnotationElements()) { + Relational relation = annotation.getAnnotation(Relational.class); + if (relation != null) { + relations.add(annotation.getTypeName()); + } + } + } + return relations; + } + + private boolean showEvents(String queryText) { + try (Tokenizer t = new Tokenizer(queryText)) { + t.expect("SHOW"); + t.expect("EVENTS"); + } catch (ParseException pe) { + return false; + // Ignore + } + out.println("Event Types (number of events):"); + out.println(); + Map eventCount = new HashMap<>(); + Map eventTypes = new HashMap<>(); + stream.onMetadata(e -> { + for (EventType t : e.getAddedEventTypes()) { + eventTypes.put(t.getId(), t); + } + }); + stream.onEvent(event -> { + eventCount.merge(event.getEventType().getId(), 1L, Long::sum); + }); + stream.start(); + List types = new ArrayList<>(); + for (EventType type : eventTypes.values()) { + if (!isExperimental(type)) { + String name = Utils.makeSimpleName(type); + Long count = eventCount.get(type.getId()); + String countText = count == null ? "" : " (" + count + ")"; + types.add(name + countText); + } + } + out.println(new Columnizer(types, 2).toString()); + return true; + } + + private boolean isExperimental(EventType t) { + return t.getAnnotation(Experimental.class) != null; + } + + public static String getGrammarText() { + return """ + Grammar: + + query ::= [column] [format] select from [where] [groupBy] [orderBy] [limit] + column ::= "COLUMN" text ("," text)* + format ::= "FORMAT" formatter ("," formatter)* + formatter ::= property (";" property)* + select ::= "SELECT" "*" | expression ("," expression)* + expression ::= (aggregator | field) [alias] + aggregator ::= function "(" (field | "*") ")" + alias ::= "AS" symbol + from ::= "FROM" source ("," source)* + source ::= type [alias] + where ::= condition ("AND" condition)* + condition ::= field "=" text + groupBy ::= "GROUP BY" field ("," field)* + orderBy ::= "ORDER BY" orderField ("," orderField)* + orderField ::= field [sortOrder] + sortOrder ::= "ASC" | "DESC" + limit ::= "LIMIT" + + - text, characters surrounded by single quotes + - symbol, alphabetic characters + - type, the event type name, for example SystemGC. To avoid ambiguity, + the name may be qualified, for example jdk.SystemGC + - field, the event field name, for example stackTrace. + To avoid ambiguity, the name may be qualified, for example + jdk.SystemGC.stackTrace. A type alias declared in a FROM clause + can be used instead of the type, for example S.eventThread + - function, determines how fields are aggregated when using GROUP BY. + Aggregate functions are: + AVG: The numeric average + COUNT: The number of values + DIFF: The numeric difference between the last and first value + FIRST: The first value + LAST: The last value + LAST_BATCH: The last set of values with the same end timestamp + LIST: All values in a comma-separated list + MAX: The numeric maximum + MEDIAN: The numeric median + MIN: The numeric minimum + P90, P95, P99, P999: The numeric percentile, 90%, 95%, 99% or 99.9% + STDEV: The numeric standard deviation + SUM: The numeric sum + UNIQUE: The unique number of occurrences of a value + Null values are included, but ignored for numeric functions. If no + aggregator function is specified, the first non-null value is used. + - property, any of the following: + cell-height: Maximum height of a table cell + missing:whitespace Replace missing values (N/A) with blank space + normalized Normalize values between 0 and 1.0 for the column + truncate-beginning if value can't fit a table cell, remove the first characters + truncate-end if value can't fit a table cell, remove the last characters + + If no value exist, or a numeric value can't be aggregated, the result is 'N/A', + unless missing:whitespace is used. The top frame of a stack trace can be referred' + to as stackTrace.topFrame. When multiple event types are specified in a FROM clause, + the union of the event types are used (not the cartesian product) + + To see all available events, use the query '"SHOW EVENTS"'. To see all fields for + a particular event type, use the query '"SHOW FIELDS "'."""; + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryResolver.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryResolver.java new file mode 100644 index 00000000000..85dd7e30209 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryResolver.java @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; +import java.util.function.Function; +import java.util.regex.Pattern; + +import jdk.jfr.EventType; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.internal.Utils; +import jdk.jfr.internal.query.FilteredType.Filter; +import jdk.jfr.internal.query.Query.Condition; +import jdk.jfr.internal.query.Query.Expression; +import jdk.jfr.internal.query.Query.Formatter; +import jdk.jfr.internal.query.Query.Grouper; +import jdk.jfr.internal.query.Query.OrderElement; +import jdk.jfr.internal.query.Query.Source; +import jdk.jfr.internal.util.Matcher; +import jdk.jfr.internal.util.SpellChecker; + +/** + * Purpose of this class is to take a query and all available event types and + * check that the query is valid, for example, to see if all fields and types + * referenced in the query exists. The end result is a list of fields + * suitable for grouping, sorting and rendering operations later. + */ +final class QueryResolver { + @SuppressWarnings("serial") + static class QueryException extends Exception { + public QueryException(String message) { + super(message); + } + } + + @SuppressWarnings("serial") + static final class QuerySyntaxException extends QueryException { + public QuerySyntaxException(String message) { + super(message); + } + } + + private final List eventTypes; + private final List fromTypes = new ArrayList<>(); + private final Map typeAliases = new LinkedHashMap<>(); + private final Map fieldAliases = new LinkedHashMap<>(); + private final List resultFields = new ArrayList<>(); + + // For readability take query apart + private final List column; + private final List format; + private final List select; + private final List from; + private final List where; + private final List orderBy; + private final List groupBy; + + public QueryResolver(Query query, List eventTypes) { + this.eventTypes = eventTypes; + this.column = query.column; + this.format = query.format; + this.select = query.select; + this.from = query.from; + this.where = query.where; + this.orderBy = query.orderBy; + this.groupBy = query.groupBy; + } + + public List resolve() throws QueryException { + resolveFrom(); + resolveSelect(); + resolveGroupBy(); + resolveOrderBy(); + resolveWhere(); + applyColumn(); + applyFormat(); + return resultFields; + } + + private void resolveWhere() throws QuerySyntaxException { + for (Condition condition : where) { + List fields = new ArrayList<>(); + String fieldName = condition.field(); + Field aliasedField = fieldAliases.get(fieldName); + if (aliasedField != null) { + fields.add(aliasedField); + } else { + fields.addAll(resolveFields(fieldName, fromTypes)); + } + for (Field field : fields) { + field.type.addFilter(new Filter(field, condition.value())); + } + } + } + + private void resolveFrom() throws QueryException { + for (Source source : from) { + List eventTypes = resolveEventType(source.name()); + if (!source.alias().isEmpty() && eventTypes.size() > 1) { + throw new QueryException("Alias can only refer to a single event type"); + } + for (EventType eventType : eventTypes) { + FilteredType type = new FilteredType(eventType); + fromTypes.add(type); + source.alias().ifPresent(alias -> typeAliases.put(alias, type)); + } + } + } + + private void resolveSelect() throws QueryException { + if (select.isEmpty()) { // SELECT * + resultFields.addAll(FieldBuilder.createWildcardFields(eventTypes, fromTypes)); + return; + } + for (Expression expression : select) { + Field field = addField(expression.name(), fromTypes); + field.visible = true; + field.aggregator = expression.aggregator(); + FieldBuilder.configureAggregator(field); + expression.alias().ifPresent(alias -> fieldAliases.put(alias, field)); + if (field.name.equals("*") && field.aggregator != Aggregator.COUNT) { + throw new QuerySyntaxException("Wildcard ('*') can only be used with aggregator function COUNT"); + } + } + } + + private void resolveGroupBy() throws QueryException { + if (groupBy.isEmpty()) { + // Queries on the form "SELECT SUM(a), b, c FROM D" should group all rows implicitly + var f = select.stream().filter(e -> e.aggregator() != Aggregator.MISSING).findFirst(); + if (f.isPresent()) { + Grouper grouper = new Grouper("startTime"); + for (var fr : fromTypes) { + Field implicit = addField("startTime", List.of(fr)); + implicit.valueGetter = e -> 1; + implicit.grouper = grouper; + } + groupBy.add(grouper); + return; + } + } + + for (Grouper grouper : groupBy) { + for (FilteredType type : fromTypes) { + String fieldName = grouper.field(); + // Check if alias exists, e.g. "SELECT field AS K FROM ... GROUP BY K" + Field field= fieldAliases.get(fieldName); + if (field != null) { + fieldName = field.name; + if (field.aggregator != Aggregator.MISSING) { + throw new QueryException("Aggregate funtion can't be used together with an alias"); + } + } + field = addField(fieldName, List.of(type)); + field.grouper = grouper; + } + } + } + + private void resolveOrderBy() throws QueryException { + for (OrderElement orderer : orderBy) { + Field field = fieldAliases.get(orderer.name()); + if (field == null) { + field = addField(orderer.name(), fromTypes); + } + field.orderer = orderer; + } + } + + private void applyColumn() throws QueryException { + if (column.isEmpty()) { + return; + } + if (column.size() != select.size()) { + throw new QuerySyntaxException("Number of fields in COLUMN clause doesn't match SELECT"); + } + + for (Field field : resultFields) { + if (field.visible) { + field.label = column.get(field.index); + } + } + } + + private void applyFormat() throws QueryException { + if (format.isEmpty()) { + return; + } + if (format.size() != select.size()) { + throw new QueryException("Number of fields in FORMAT doesn't match SELECT"); + } + for (Field field : resultFields) { + if (field.visible) { + for (var formatter : format.get(field.index).properties()) { + formatter.style().accept(field); + } + } + } + } + + private Field addField(String name, List types) throws QueryException { + List fields = resolveFields(name, types); + if (fields.isEmpty()) { + throw new QueryException(unknownField(name, types)); + } + + Field primary = fields.getFirst(); + boolean mixedTypes = false; + for (Field f : fields) { + if (!f.dataType.equals(primary.dataType)) { + mixedTypes = true; + } + } + for (Field field: fields) { + field.index = resultFields.size(); + primary.sourceFields.add(field); + // Convert to String if field data types mismatch + if (mixedTypes) { + final Function valueGetter = field.valueGetter; + field.valueGetter = event -> { + return FieldFormatter.format(field, valueGetter.apply(event)); + }; + field.lexicalSort = true; + field.dataType = String.class.getName(); + field.alignLeft = true; + } + } + resultFields.add(primary); + return primary; + } + + private List resolveFields(String name, List types) { + List fields = new ArrayList<>(); + + if (name.equals("*")) { + // Used with COUNT(*) and UNIQUE(*) + // All events should have a start time + name = "startTime"; + } + if (name.startsWith("[")) { + int index = name.indexOf("]"); + if (index != -1) { + String typeNames = name.substring(1, index); + String suffix = name.substring(index + 1); + for (String typeName : typeNames.split(Pattern.quote("|"))) { + fields.addAll(resolveFields(typeName + suffix, types)); + } + return fields; + } + } + + // Match "namespace.Event.field" and "Event.field" + if (name.contains(".")) { + for (FilteredType et : types) { + String fullEventType = et.getName() + "."; + if (name.startsWith(fullEventType)) { + String fieldName = name.substring(fullEventType.length()); + FieldBuilder fb = new FieldBuilder(eventTypes, et, fieldName); + fields.addAll(fb.build()); + } + String eventType = et.getSimpleName() + "."; + if (name.startsWith(eventType)) { + String fieldName = name.substring(eventType.length()); + FieldBuilder fb = new FieldBuilder(eventTypes, et, fieldName); + fields.addAll(fb.build()); + } + } + } + // Match "ALIAS.field" where ALIAS can be "namespace.Event" or "Event" + for (var entry : typeAliases.entrySet()) { + String alias = entry.getKey() + "."; + FilteredType s = entry.getValue(); + if (name.startsWith(alias)) { + int index = name.indexOf("."); + String unaliased = s.getName() + "." + name.substring(index + 1); + fields.addAll(resolveFields(unaliased, List.of(s))); + } + } + + // Match without namespace + for (FilteredType eventType : types) { + FieldBuilder fb = new FieldBuilder(eventTypes, eventType, name); + fields.addAll(fb.build()); + } + return fields; + } + + private List resolveEventType(String name) throws QueryException { + List types = new ArrayList<>(); + // Match fully qualified name first + for (EventType eventType : eventTypes) { + if (Matcher.match(eventType.getName(),name)) { + types.add(eventType); + } + } + // Match less qualified name + for (EventType eventType : eventTypes) { + if (eventType.getName().endsWith("." + name)) { + types.add(eventType); + break; + } + } + if (types.isEmpty()) { + throw new QueryException(unknownEventType(eventTypes, name)); + } + return types; + } + + private static String unknownField(String name, List types) { + List alternatives = new ArrayList<>(); + StringJoiner sj = new StringJoiner(", "); + for (FilteredType t : types) { + for (var v : t.getFields()) { + alternatives.add(v.getName()); + alternatives.add(t.getName() + "." + v.getName()); + alternatives.add(t.getSimpleName() + "." + v.getName()); + } + sj.add(t.getName()); + } + String message = "Can't find field named '" + name + "' in " + sj; + String alternative = SpellChecker.check(name, alternatives); + if (alternative != null) { + return message + ".\nDid you mean '" + alternative + "'?"; + } else { + return message + ".\nUse 'SHOW FIELDS " + types.getFirst().getSimpleName() + "' to list available fields."; + } + } + + private static String unknownEventType(List eventTypes, String name) { + List alternatives = new ArrayList<>(); + for (EventType type : eventTypes) { + alternatives.add(Utils.makeSimpleName(type)); + } + String alternative = SpellChecker.check(name, alternatives); + String message = "Can't find event type named '" + name + "'."; + if (alternative != null) { + return message + " Did you mean '" + alternative + "'?"; + } else { + return message + " 'SHOW EVENTS' will list available event types."; + } + } + + public List getFromTypes() { + return fromTypes; + } +} \ No newline at end of file diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryRun.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryRun.java new file mode 100644 index 00000000000..5f53363db3c --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryRun.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import jdk.jfr.consumer.EventStream; +import jdk.jfr.consumer.MetadataEvent; +import jdk.jfr.internal.query.QueryResolver.QueryException; +import jdk.jfr.internal.query.QueryResolver.QuerySyntaxException; + +final class QueryRun { + private final Histogram histogram = new Histogram(); + private final Table table = new Table(); + private final List syntaxErrors = new ArrayList<>(); + private final List metadataErrors = new ArrayList<>(); + private final Query query; + private final EventStream stream; + + public QueryRun(EventStream stream, Query query) { + this.stream = stream; + this.query = query; + } + + void onMetadata(MetadataEvent e) { + if (table.getFields().isEmpty()) { + // Only use first metadata event for now + try { + QueryResolver resolver = new QueryResolver(query, e.getEventTypes()); + List fields = resolver.resolve(); + table.addFields(fields); + histogram.addFields(fields); + addEventListeners(); + } catch (QuerySyntaxException qe) { + syntaxErrors.add(qe.getMessage()); + } catch (QueryException qe) { + metadataErrors.add(qe.getMessage()); + } + } + } + + public void complete() { + if (!query.groupBy.isEmpty()) { + table.addRows(histogram.toRows()); + } + } + + private void addEventListeners() { + for (var entry : groupByTypeDescriptor().entrySet()) { + FilteredType type = entry.getKey(); + List sourceFields = entry.getValue(); + stream.onEvent(type.getName(), e -> { + for (var filter : type.getFilters()) { + Object object = filter.field().valueGetter.apply(e); + String text = FieldFormatter.format(filter.field(), object); + if (!text.equals(filter.value())) { + return; + } + } + if (query.groupBy.isEmpty()) { + table.add(e, sourceFields); + } else { + histogram.add(e, type, sourceFields); + } + }); + } + } + + private LinkedHashMap> groupByTypeDescriptor() { + var multiMap = new LinkedHashMap>(); + for (Field field : table.getFields()) { + for (Field sourceFields : field.sourceFields) { + multiMap.computeIfAbsent(sourceFields.type, k -> new ArrayList<>()).add(field); + } + } + return multiMap; + } + + public List getSyntaxErrors() { + return syntaxErrors; + } + + public List getMetadataErrors() { + return metadataErrors; + } + + public Query getQuery() { + return query; + } + + public Table getTable() { + return table; + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Row.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Row.java new file mode 100644 index 00000000000..da32f86c171 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Row.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.util.Arrays; + +final class Row { + private final Object[] values; + private final String[] texts; + + public Row(int size) { + values = new Object[size]; + texts = new String[size]; + } + + public Object getValue(int index) { + return values[index]; + } + + public void putValue(int index, Object o) { + values[index] = o; + } + + public String getText(int index) { + return texts[index]; + } + + public void putText(int index, String text) { + texts[index] = text; + } + + @Override + public String toString() { + return Arrays.asList(values).toString(); + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Table.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Table.java new file mode 100644 index 00000000000..19842751907 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Table.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.util.ArrayList; +import java.util.List; + +import jdk.jfr.consumer.RecordedEvent; + +/** + * Class responsible for holding rows, their values and textual + * representation. + */ +final class Table { + private final List rows = new ArrayList<>(); + private final List fields = new ArrayList<>(); + + boolean isEmpty() { + return rows.isEmpty(); + } + + void addRows(List rows) { + this.rows.addAll(rows); + } + + List getRows() { + return rows; + } + + void addFields(List fields) { + for (int index = 0; index getFields() { + return fields; + } + + public void add(RecordedEvent event, List sourceFields) { + Row row = new Row(fields.size()); + for (Field field : sourceFields) { + row.putValue(field.index, field.valueGetter.apply(event)); + } + rows.add(row); + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/TableCell.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/TableCell.java new file mode 100644 index 00000000000..1099aa866f4 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/TableCell.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.util.ArrayList; +import java.util.List; +import jdk.jfr.internal.query.Configuration.Truncate; + +final class TableCell { + static final String ELLIPSIS = "..."; + static final String COLUMN_SEPARATOR = " "; + static final int MINIMAL_CELL_WIDTH = 1 + COLUMN_SEPARATOR.length(); + static final int ELLIPSIS_LENGTH = ELLIPSIS.length(); + + private final List lines = new ArrayList<>(); + private final Truncate truncate; + private int preferredWidth; + + final int cellHeight; + final Field field; + int width; + + public TableCell(Field field, int cellHeight, Truncate truncate) { + this.field = field; + this.cellHeight = cellHeight; + this.truncate = truncate; + } + + public int getContentWidth() { + return width - COLUMN_SEPARATOR.length(); + } + + public int getHeight() { + return lines.size(); + } + + public String getText(int rowIndex) { + return lines.get(rowIndex); + } + + public void setPreferredWidth(int width) { + preferredWidth = width; + } + + public int getPreferredWidth() { + return preferredWidth; + } + public void addLine(String text) { + int contentWidth = getContentWidth(); + if (text.length() >= contentWidth) { + add(truncate(text, contentWidth)); + } else { + addAligned(text); + } + } + + public void setContent(String text) { + clear(); + int contentSize = getContentSize(); + // Bail out early to prevent ellipsis when size is the same + if (text.length() == contentSize) { + add(text); + return; + } + // Text is larger than size of the cell, truncate + if (text.length() >= contentSize) { + add(truncate(text, contentSize)); + return; + } + // Text fits on one line, pad left or right depending on alignment + int contentWidth = getContentWidth(); + if (text.length() < contentWidth) { + addAligned(text); + return; + } + // Multiple lines and text fits cell + add(text); + } + + private void addAligned(String text) { + String padding = " ".repeat(getContentWidth() - text.length()); + if (field.alignLeft) { + add(text + padding); + } else { + add(padding + text); + } + } + + public int getContentSize() { + return cellHeight * getContentWidth(); + } + + public List getLines() { + return lines; + } + + private String truncate(String text, int size) { + if (size < ELLIPSIS_LENGTH) { + return ELLIPSIS.substring(0, ELLIPSIS_LENGTH - size); + } + int textSize = size - ELLIPSIS_LENGTH; + if (truncate == Truncate.BEGINNING) { + return ELLIPSIS + text.substring(text.length() - textSize); + } else { + return text.substring(0, textSize) + ELLIPSIS; + } + } + + private void add(String text) { + int contentWidth = getContentWidth(); + int contentSize = getContentSize(); + for (int index = 0; index < contentSize; index += contentWidth) { + int end = index + contentWidth; + if (end >= text.length()) { + String content = text.substring(index); + content += " ".repeat(contentWidth - content.length()); + lines.add(content); + return; + } + lines.add(text.substring(index, end)); + } + } + + public void clear() { + lines.clear(); + } +} \ No newline at end of file diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/TableRenderer.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/TableRenderer.java new file mode 100644 index 00000000000..b494d6b6dc8 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/TableRenderer.java @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import static jdk.jfr.internal.query.Configuration.MAX_PREFERRED_WIDTH; +import static jdk.jfr.internal.query.Configuration.MIN_PREFERRED_WIDTH; +import static jdk.jfr.internal.query.Configuration.PREFERRED_WIDTH; + +import java.util.List; +import java.util.function.Predicate; + +import jdk.jfr.consumer.RecordedFrame; +import jdk.jfr.consumer.RecordedMethod; +import jdk.jfr.consumer.RecordedStackTrace; +import jdk.jfr.internal.query.Configuration.Truncate; +import jdk.jfr.internal.util.Output; + +/** + * Class responsible for printing and formatting the contents of a table. + */ +final class TableRenderer { + private final Configuration configuration; + private final List tableCells; + private final Table table; + private final Query query; + private final Output out; + private int width; + private int preferredWidth; + + public TableRenderer(Configuration configuration, Table table, Query query) { + this.configuration = configuration; + this.tableCells = createTableCells(table); + this.table = table; + this.query = query; + this.out = configuration.output; + } + + private List createTableCells(Table table) { + return table.getFields().stream().filter(f -> f.visible).map(f -> createTableCell(f)).toList(); + } + + private TableCell createTableCell(Field field) { + Truncate truncate = configuration.truncate; + if (truncate == null) { + truncate = field.truncate; + } + if (configuration.cellHeight != 0) { + return new TableCell(field, configuration.cellHeight, truncate); + } else { + return new TableCell(field, field.cellHeight, truncate); + } + } + + public void render() { + if (isEmpty()) { + if (configuration.title != null) { + out.println(); + out.println("No events found for '" + configuration.title +"'."); + } + return; + } + if (tooManyColumns()) { + out.println(); + out.println("Too many columns to fit width."); + return; + } + + formatRow(); + sortRows(); + setColumnWidths(); + printTitle(); + printHeaderRow(); + printHeaderRowSeparators(); + printRows(); + } + + private boolean isEmpty() { + return tableCells.isEmpty() || table.getRows().isEmpty(); + } + + private boolean tooManyColumns() { + int minWidth = tableCells.size() * TableCell.MINIMAL_CELL_WIDTH; + if (configuration.width != 0) { + return minWidth > configuration.width; + } + return minWidth > MAX_PREFERRED_WIDTH; + } + + private void formatRow() { + double[] max = calculateNormalization(); + for (Row row : table.getRows()) { + for (Field field : table.getFields()) { + int index = field.index; + Object object = row.getValue(index); + if (field.normalized && object instanceof Number number) { + object = number.doubleValue() / max[index]; + } + String text = FieldFormatter.format(field, object); + row.putText(index, text); + if (index < tableCells.size()) { + TableCell cell = tableCells.get(index); + int width = text.length() + TableCell.COLUMN_SEPARATOR.length(); + if (width > cell.getPreferredWidth()) { + cell.setPreferredWidth(width); + } + } + } + } + } + + private double[] calculateNormalization() { + double[] max = new double[tableCells.size()]; + int index = 0; + for (TableCell cell : tableCells) { + if (cell.field.normalized) { + for (Row row : table.getRows()) { + if (row.getValue(index) instanceof Number number) { + max[index] += number.doubleValue(); + } + } + } + index++; + } + return max; + } + + private void sortRows() { + TableSorter sorter = new TableSorter(table, query); + sorter.sort(); + } + + private void setColumnWidths() { + setRowWidths(); + setPreferredHeaderWidths(); + if (configuration.width == 0) { + preferredWidth= determineTableWidth(); + } else { + preferredWidth = configuration.width; + } + // Set minimum table cell width + distribute(cell -> cell.width < TableCell.MINIMAL_CELL_WIDTH); + // Fill with preferred width + distribute(cell -> cell.width < cell.getPreferredWidth()); + // Distribute additional width to table cells with a non-fixed size + distribute(cell -> !cell.field.fixedWidth); + // If all table cells are fixed size, distribute to any of them + distribute(cell -> true); + } + + private void setRowWidths() { + int rowCount = 0; + for (Row row : table.getRows()) { + if (rowCount == query.limit) { + return; + } + int columnIndex = 0; + for (TableCell cell : tableCells) { + String text = row.getText(columnIndex); + int width = text.length() + TableCell.COLUMN_SEPARATOR.length(); + if (width > cell.getPreferredWidth()) { + cell.setPreferredWidth(width); + } + columnIndex++; + } + rowCount++; + } + } + + private void setPreferredHeaderWidths() { + for (TableCell cell : tableCells) { + int headerWidth = cell.field.label.length(); + if (configuration.verboseHeaders) { + headerWidth = Math.max(fieldName(cell.field).length(), headerWidth); + } + headerWidth += TableCell.COLUMN_SEPARATOR.length(); + if (headerWidth > cell.getPreferredWidth()) { + cell.setPreferredWidth(headerWidth); + } + } + } + + private int determineTableWidth() { + int preferred = 0; + for (TableCell cell : tableCells) { + preferred += cell.getPreferredWidth(); + } + // Avoid a very large table. + if (preferred > MAX_PREFERRED_WIDTH) { + return MAX_PREFERRED_WIDTH; + } + // Avoid a very small width, but not preferred width if there a few columns + if (preferred < MIN_PREFERRED_WIDTH && tableCells.size() < 3) { + return MIN_PREFERRED_WIDTH; + } + // Expand to preferred width + if (preferred < PREFERRED_WIDTH) { + return PREFERRED_WIDTH; + } + return preferred; + } + + private void distribute(Predicate condition) { + long amountLeft = preferredWidth - width; + long last = -1; + while (amountLeft > 0 && amountLeft != last) { + last = amountLeft; + for (TableCell cell : tableCells) { + if (condition.test(cell)) { + cell.width++; + width++; + amountLeft--; + } + } + } + } + + private void printTitle() { + String title = configuration.title; + if (title != null) { + if (isExperimental()) { + title += " (Experimental)"; + } + int pos = width - title.length(); + pos = Math.max(0, pos); + pos = pos / 2; + out.println(); + out.println(" ".repeat(pos) + title); + out.println(); + } + } + + private boolean isExperimental() { + return tableCells.stream().flatMap(c -> c.field.sourceFields.stream()).anyMatch(f -> f.type.isExperimental()); + } + + private void printHeaderRow() { + printRow(cell -> cell.field.label); + if (configuration.verboseHeaders) { + printRow(cell -> fieldName(cell.field)); + } + } + + private void printHeaderRowSeparators() { + printRow(cell -> "-".repeat(cell.getContentWidth())); + } + + private void printRow(java.util.function.Function action) { + for (TableCell cell : tableCells) { + cell.setContent(action.apply(cell)); + } + printRow(); + } + + private void printRows() { + int rowCount = 0; + for (Row row : table.getRows()) { + if (rowCount == query.limit) { + return; + } + int columnIndex = 0; + for (TableCell cell : tableCells) { + setCellContent(cell, row, columnIndex++); + } + printRow(); + rowCount++; + } + } + + private void setCellContent(TableCell cell, Row row, int columnIndex) { + String text = row.getText(columnIndex); + if (cell.cellHeight > 1) { + Object o = row.getValue(columnIndex); + if (o instanceof RecordedStackTrace s) { + setStackTrace(cell, s); + return; + } + } + + if (text.length() > cell.getContentSize()) { + Object o = row.getValue(columnIndex); + + cell.setContent(FieldFormatter.formatCompact(cell.field, o)); + return; + } + cell.setContent(text); + } + + private void setStackTrace(TableCell cell, RecordedStackTrace s) { + int row = 0; + cell.clear(); + for(RecordedFrame f : s.getFrames()) { + if (row == cell.cellHeight) { + return; + } + if (f.isJavaFrame()) { + RecordedMethod method = f.getMethod(); + String text = FieldFormatter.format(cell.field, method); + if (text.length() > cell.getContentWidth()) { + text = FieldFormatter.formatCompact(cell.field, method); + } + cell.addLine(text); + } + row++; + } + } + + private void printRow() { + long maxHeight = 0; + for (TableCell cell : tableCells) { + maxHeight = Math.max(cell.getHeight(), maxHeight); + } + TableCell lastCell = tableCells.get(tableCells.size() - 1); + for (int rowIndex = 0; rowIndex < maxHeight; rowIndex++) { + for (TableCell cell : tableCells) { + if (rowIndex < cell.getHeight()) { + out.print(cell.getText(rowIndex)); + } else { + out.print(" ".repeat(cell.getContentWidth())); + } + if (cell != lastCell) { + out.print(TableCell.COLUMN_SEPARATOR); + } + } + out.println(); + } + } + + private String fieldName(Field field) { + return "(" + field.name + ")"; + } + + public long getWidth() { + return width; + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/TableSorter.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/TableSorter.java new file mode 100644 index 00000000000..65f664018ba --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/TableSorter.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; + +import jdk.jfr.internal.query.Query.OrderElement; +import jdk.jfr.internal.query.Query.SortOrder; +/** + * Class responsible for sorting a table according to an ORDER BY statement or + * a heuristics. + */ +final class TableSorter { + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static class ColumnComparator implements Comparator { + private final int factor; + private final int index; + private final boolean lexical; + + public ColumnComparator(Field field, SortOrder order) { + this.factor = sortOrderToFactor(determineSortOrder(field, order)); + this.index = field.index; + this.lexical = field.lexicalSort; + } + + private SortOrder determineSortOrder(Field field, SortOrder order) { + if (order != SortOrder.NONE) { + return order; + } + if (field.timespan || field.percentage) { + return SortOrder.DESCENDING; + } + return SortOrder.ASCENDING; + } + + int sortOrderToFactor(SortOrder order) { + return order == SortOrder.DESCENDING ? -1 : 1; + } + + @Override + public int compare(Row rowA, Row rowB) { + if (lexical) { + return compareObjects(rowA.getText(index), rowB.getText(index)); + } else { + return compareObjects(rowA.getValue(index), rowB.getValue(index)); + } + } + + private int compareObjects(Object a, Object b) { + if (a instanceof Comparable c1 && b instanceof Comparable c2) { + return factor * c1.compareTo(c2); + } + return factor; + } + } + + private final Table table; + private final Query query; + + public TableSorter(Table table, Query query) { + this.table = table; + this.query = query; + } + + public void sort() { + if (table.getFields().isEmpty()) { + return; + } + if (query.orderBy.isEmpty()) { + sortDefault(); + return; + } + sortOrderBy(); + } + + private void sortDefault() { + if (sortAggregators()) { + return; + } + if (sortGroupBy()) { + return; + } + sortLeftMost(); + } + + private boolean sortAggregators() { + return sortPredicate(field -> field.aggregator != Aggregator.MISSING); + } + + private boolean sortGroupBy() { + return sortPredicate(field -> query.groupBy.contains(field.grouper)); + } + + private void sortOrderBy() { + for (OrderElement orderer : query.orderBy.reversed()) { + sortPredicate(field -> field.orderer == orderer); + } + } + + private boolean sortPredicate(Predicate predicate) { + boolean sorted = false; + for (Field field : table.getFields()) { + if (predicate.test(field)) { + sort(field, determineSortOrder(field)); + sorted = true; + } + } + return sorted; + } + + private SortOrder determineSortOrder(Field field) { + if (field.orderer == null) { + return SortOrder.NONE; + } else { + return field.orderer.order(); + } + } + + private void sortLeftMost() { + sort(table.getFields().getFirst(), SortOrder.NONE); + } + + private void sort(Field field, SortOrder order) { + table.getRows().sort(new ColumnComparator(field, order)); + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/ViewFile.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/ViewFile.java new file mode 100644 index 00000000000..dc8aa75e490 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/ViewFile.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import jdk.jfr.internal.util.Tokenizer; + +/** + * Represents a configuration file that holds a set up queries and their + * associated metadata, such as labels and descriptions. + */ +final class ViewFile { + record ViewConfiguration(String name, String category, Map properties) { + public String query() { + String form = get("form"); + if (form != null) { + return form; + } + String table = get("table"); + if (table != null) { + return table; + } + throw new IllegalStateException("Expected section to have form or table attribute"); + } + + public String getLabel() { + return get("label"); + } + + public String getForm() { + return get("form"); + } + + public String getTable() { + return get("table"); + } + + private String get(String key) { + return properties.get(key); + } + } + + private final List configurations; + + public ViewFile(String text) throws ParseException { + this.configurations = parse(text); + } + + public static ViewFile getDefault() { + try { + var is = ViewFile.class.getResourceAsStream("/jdk/jfr/internal/query/view.ini"); + byte[] bytes = is.readAllBytes(); + String query = new String(bytes, Charset.forName("UTF-8")); + return new ViewFile(query); + } catch (ParseException e) { + throw new InternalError("Internal error, invalid view.ini", e); + } catch (IOException e) { + throw new InternalError("Internal error, could not read view.ini", e); + } + } + + public List getViewConfigurations() { + return configurations; + } + + private List parse(String text) throws ParseException { + List views = new ArrayList<>(); + try (Tokenizer tokenizer = new Tokenizer(text, '[', ']', ';')) { + while (tokenizer.hasNext()) { + while (tokenizer.accept(";")) { + tokenizer.skipLine(); + } + if (tokenizer.accept("[")) { + String fullName = tokenizer.next(); + tokenizer.expect("]"); + views.add(createView(fullName)); + } + if (views.isEmpty()) { + throw new ParseException("Expected view file to begin with a section", tokenizer.getPosition()); + } + String key = tokenizer.next(); + tokenizer.expect("="); + String value = tokenizer.next(); + ViewConfiguration view = views.get(views.size() - 1); + view.properties().put(key, value); + } + } + return views; + } + + private ViewConfiguration createView(String fullName) { + int index = fullName.lastIndexOf("."); + if (index == -1) { + throw new InternalError("Missing name space for " + fullName); + } + String category = fullName.substring(0, index); + String name = fullName.substring(index + 1); + return new ViewConfiguration(name, category, new LinkedHashMap<>()); + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/ViewPrinter.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/ViewPrinter.java new file mode 100644 index 00000000000..57c38544b83 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/ViewPrinter.java @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.query; + +import java.io.Closeable; +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +import jdk.jfr.EventType; +import jdk.jfr.consumer.EventStream; +import jdk.jfr.internal.query.QueryResolver.QueryException; +import jdk.jfr.internal.query.ViewFile.ViewConfiguration; +import jdk.jfr.internal.util.Columnizer; +import jdk.jfr.internal.util.Output; +import jdk.jfr.internal.util.StopWatch; +import jdk.jfr.internal.util.Tokenizer; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; +import jdk.jfr.internal.util.ValueFormatter; +/** + * Class responsible for executing and displaying the contents of a query. + *

+ * Used by 'jcmd JFR.view' and 'jfr view'. + *

+ * Views are defined in jdk/jfr/internal/query/view.ini + */ +public final class ViewPrinter { + private final Configuration configuration; + private final EventStream stream; + private final Output out; + private final StopWatch stopWatch; + + /** + * Constructs a view printer object. + * + * @param configuration display configuration + * @param stream a non-started stream from where data should be fetched. + */ + public ViewPrinter(Configuration configuration, EventStream stream) { + this.configuration = configuration; + this.out = configuration.output; + this.stopWatch = new StopWatch(); + this.stream = stream; + } + + /** + * Prints the view. + * + * @param text the view or event type to display + * + * @throws UserDataException if the stream associated with the printer lacks + * event or event metadata + * @throws UserSyntaxException if the syntax of the query is incorrect. + */ + public void execute(String text) throws UserDataException, UserSyntaxException { + try { + if (showViews(text) || showEventType(text)) { + return; + } + } catch (ParseException pe) { + throw new InternalError("Internal error, view.ini file is invalid", pe); + } + throw new UserDataException("Could not find a view or an event type named " + text); + } + + private boolean showEventType(String eventType) { + try { + QueryPrinter printer = new QueryPrinter(configuration, stream); + configuration.verboseTitle = true; + printer.execute("SELECT * FROM " + eventType); + return true; + } catch (UserDataException | UserSyntaxException e) { + return false; + } + } + + private boolean showViews(String text) throws UserDataException, ParseException, UserSyntaxException { + if (configuration.verbose) { + configuration.verboseHeaders = true; + } + if (text.equalsIgnoreCase("all-events")) { + QueryExecutor executor = new QueryExecutor(stream); + stopWatch.beginAggregation(); + List runs = executor.run(); + stopWatch.beginFormatting(); + for (QueryRun task : runs) { + Table table = task.getTable(); + FilteredType type = table.getFields().getFirst().type; + configuration.title = type.getLabel(); + TableRenderer renderer = new TableRenderer(configuration, table , task.getQuery()); + renderer.render(); + out.println(); + } + stopWatch.finish(); + if (configuration.verbose) { + out.println(); + out.println("Execution: " + stopWatch.toString()); + } + printTimespan(); + return true; + } + if (text.equals("types")) { + QueryPrinter qp = new QueryPrinter(configuration, stream); + qp.execute("SHOW EVENTS"); + return true; + } + List views = ViewFile.getDefault().getViewConfigurations(); + if (text.equalsIgnoreCase("all-views")) { + stopWatch.beginQueryValidation(); + List queries = new ArrayList<>(); + for (ViewConfiguration view : views) { + queries.add(new Query(view.query())); + } + QueryExecutor executor = new QueryExecutor(stream, queries); + int index = 0; + stopWatch.beginAggregation(); + List runs = executor.run(); + stopWatch.beginFormatting(); + for (QueryRun run : runs) { + printView(views.get(index++), run); + } + stopWatch.finish(); + if (configuration.verbose) { + out.println(); + out.println("Execution: " + stopWatch.toString()); + } + printTimespan(); + printViewTypeRelation(views, executor.getEventTypes()); + return true; + } + for (ViewConfiguration view : views) { + if (view.name().equalsIgnoreCase(text)) { + stopWatch.beginQueryValidation(); + Query q = new Query(view.query()); + QueryExecutor executor = new QueryExecutor(stream, q); + stopWatch.beginAggregation(); + QueryRun run = executor.run().getFirst(); + stopWatch.beginFormatting(); + printView(view, run); + stopWatch.finish(); + if (configuration.verbose) { + out.println(); + out.println("Execution: " + stopWatch.toString()); + out.println(); + } + printTimespan(); + return true; + } + } + return false; + } + + void printViewTypeRelation(List views, List eventTypes) throws ParseException { + if (!configuration.verbose) { + return; + } + out.println(); + out.println("Event types and views"); + out.println(); + Map> viewMap = new HashMap<>(); + for (EventType type : eventTypes) { + viewMap.put(type.getName(), new LinkedHashSet<>()); + } + for (ViewConfiguration view : views) { + Query query = new Query(view.query()); + if (query.from.getFirst().name().equals("*")) { + continue; + } + QueryResolver resolver = new QueryResolver(query, eventTypes); + try { + resolver.resolve(); + } catch (QueryException e) { + throw new InternalError(e); + } + for (FilteredType ft: resolver.getFromTypes()) { + Set list = viewMap.get(ft.getName()); + list.add(view.name()); + } + } + List names = new ArrayList<>(viewMap.keySet()); + Collections.sort(names); + for (String name : names) { + Set vs = viewMap.get(name); + StringJoiner sj = new StringJoiner(", "); + vs.stream().forEach(sj::add); + out.println(String.format("%-35s %s", name, sj.toString())); + } + } + + private void printTimespan() { + if (configuration.startTime != null) { + String start = ValueFormatter.formatTimestamp(configuration.startTime); + String end = ValueFormatter.formatTimestamp(configuration.endTime); + out.println(); + out.println("Timespan: " + start + " - " + end); + } + } + + private void printView(ViewConfiguration section, QueryRun queryRun) + throws UserDataException, ParseException, UserSyntaxException { + if (!queryRun.getSyntaxErrors().isEmpty()) { + throw new UserSyntaxException(queryRun.getSyntaxErrors().getFirst()); + } + if (!queryRun.getMetadataErrors().isEmpty()) { + // Recording doesn't have the event, + out.println(queryRun.getMetadataErrors().getFirst()); + out.println("Missing event found for " + section.name()); + return; + } + Table table = queryRun.getTable(); + configuration.title = section.getLabel(); + long width = 0; + if (section.getForm() != null) { + FormRenderer renderer = new FormRenderer(configuration, table); + renderer.render(); + width = renderer.getWidth(); + } + if (section.getTable() != null) { + Query query = queryRun.getQuery(); + TableRenderer renderer = new TableRenderer(configuration, table, query); + renderer.render(); + width = renderer.getWidth(); + } + if (width != 0 && configuration.verbose && !queryRun.getTable().isEmpty()) { + out.println(); + Query query = queryRun.getQuery(); + printQuery(new LineBuilder(out, width), query.toString()); + } + } + + private void printQuery(LineBuilder lb, String query) { + char[] separators = {'=', ','}; + try (Tokenizer tokenizer = new Tokenizer(query, separators)) { + while (tokenizer.hasNext()) { + lb.append(nextText(tokenizer)); + } + lb.out.println(); + } catch (ParseException pe) { + throw new InternalError("Could not format already parsed query", pe); + } + } + + private String nextText(Tokenizer tokenizer) throws ParseException { + if (tokenizer.peekChar() == '\'') { + return "'" + tokenizer.next() + "'"; + } else { + return tokenizer.next(); + } + } + + // Helper class for line breaking + private static class LineBuilder implements Closeable { + private final Output out; + private final long width; + private int position; + LineBuilder(Output out, long width) { + this.out = out; + this.width = width; + } + + public void append(String text) { + String original = text; + if (!text.equals(",") && !text.equals(";") && position != 0) { + text = " " + text; + } + if (text.length() > width) { + print(text); + return; + } + + if (text.length() + position > width) { + out.println(); + position = 0; + text = original; + } + out.print(text); + position += text.length(); + } + + private void print(String s) { + for (int i = 0; i < s.length(); i++) { + if (position % width == 0 && position != 0) { + out.println(); + } + out.print(s.charAt(i)); + position++; + } + } + + @Override + public void close() throws IOException { + out.println(); + } + } + + public static List getAvailableViews() { + List list = new ArrayList<>(); + list.add("Java virtual machine views:"); + list.add(new Columnizer(getViewList("jvm"), 3).toString()); + list.add(""); + list.add("Environment views:"); + list.add(new Columnizer(getViewList("environment"), 3).toString()); + list.add(""); + list.add("Application views:"); + list.add(new Columnizer(getViewList("application"), 3).toString()); + list.add(""); + return list; + } + + private static List getViewList(String selection) { + List names = new ArrayList<>(); + for (var view : ViewFile.getDefault().getViewConfigurations()) { + String category = view.category(); + if (selection.equals(category)) { + names.add(view.name()); + } + } + return names; + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini new file mode 100644 index 00000000000..615588a88ca --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini @@ -0,0 +1,576 @@ +; +; Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. +; DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +; +; This code is free software; you can redistribute it and/or modify it +; under the terms of the GNU General Public License version 2 only, as +; published by the Free Software Foundation. Oracle designates this +; particular file as subject to the "Classpath" exception as provided +; by Oracle in the LICENSE file that accompanied this code. +; +; This code is distributed in the hope that it will be useful, but WITHOUT +; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +; version 2 for more details (a copy is included in the LICENSE file that +; accompanied this code). +; +; You should have received a copy of the GNU General Public License version +; 2 along with this work; if not, write to the Free Software Foundation, +; Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +; +; Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +; or visit www.oracle.com if you need additional information or have any +; questions. +; + +[environment.active-recordings] +label = "Active Recordings" +table = "COLUMN 'Start', 'Duration', 'Name', + 'Destination', 'Max Age', 'Max Size' + FORMAT none, none, none, + cell-height:5, none, none + SELECT LAST(recordingStart), LAST(recordingDuration), LAST(name), + LAST(destination), LAST(maxAge), LAST(maxSize) + FROM ActiveRecording + GROUP BY id" + +[environment.active-settings] +label = "Active Settings" +table = "COLUMN 'Event Type', 'Enabled', 'Threshold', + 'Stack Trace','Period','Cutoff', 'Throttle' + FORMAT none, missing:whitespace, missing:whitespace, missing:whitespace, + missing:whitespace, missing:whitespace, missing:whitespace + SELECT E.id, LAST_BATCH(E.value), LAST_BATCH(T.value), + LAST_BATCH(S.value), LAST_BATCH(P.value), + LAST_BATCH(C.value), LAST_BATCH(U.value) + FROM + ActiveSetting AS E, + ActiveSetting AS T, + ActiveSetting AS S, + ActiveSetting AS P, + ActiveSetting AS C, + ActiveSetting AS U + WHERE + E.name = 'enabled' AND + T.name = 'threshold' AND + S.name = 'stackTrace' AND + P.name = 'period' AND + C.name = 'cutoff' AND + U.name = 'throttle' + GROUP BY + id + ORDER BY + E.id" + +[application.allocation-by-class] +label = "Allocation by Class" +table = "COLUMN 'Object Type', 'Allocation Pressure' + FORMAT none, normalized + SELECT objectClass AS O, SUM(weight) AS W + FROM ObjectAllocationSample GROUP BY O ORDER BY W DESC LIMIT 25" + +[application.allocation-by-thread] +label = "Allocation by Thread" +table = "COLUMN 'Thread', 'Allocation Pressure' + FORMAT none, normalized + SELECT eventThread AS T, SUM(weight) AS W + FROM ObjectAllocationSample GROUP BY T ORDER BY W DESC LIMIT 25" + +[application.allocation-by-site] +label = "Allocation by Site" +table = "COLUMN 'Method', 'Allocation Pressure' + FORMAT none, normalized + SELECT stackTrace.topFrame AS S, SUM(weight) AS W + FROM ObjectAllocationSample + GROUP BY S + ORDER BY W DESC LIMIT 25" + +[application.class-loaders] +label = "Class Loaders" +table = "FORMAT missing:null-bootstrap, none, none + SELECT classLoader, LAST(hiddenClassCount), + LAST(classCount) AS C + FROM ClassLoaderStatistics + GROUP BY classLoader ORDER BY C DESC"; + +[jvm.class-modifications] +label = "Class Modifications" +table = "COLUMN 'Time', 'Requested By', 'Operation', 'Classes' + SELECT duration, stackTrace.topApplicationFrame, eventType.label, classCount + FROM RetransformClasses, RedefineClasses + GROUP BY redefinitionId + ORDER BY duration DESC" + +[jvm.compiler-configuration] +label = "Compiler Configuration" +form = "SELECT LAST(threadCount), LAST(dynamicCompilerThreadCount), LAST(tieredCompilation) + FROM CompilerConfiguration" + +[jvm.compiler-statistics] +label = "Compiler Statistics" +form = "SELECT LAST(compileCount), LAST(peakTimeSpent), LAST(totalTimeSpent), + LAST(bailoutCount), LAST(osrCompileCount), + LAST(standardCompileCount), LAST(osrBytesCompiled), + LAST(standardBytesCompiled), LAST(nmethodsSize), + LAST(nmethodCodeSize) FROM CompilerStatistics" + +[jvm.compiler-phases] +label = "Concurrent Compiler Phases" +table = "COLUMN 'Level', 'Phase', 'Average', + 'P95', 'Longest', 'Count', + 'Total' + SELECT phaseLevel AS L, phase AS P, AVG(duration), + P95(duration), MAX(duration), COUNT(*), + SUM(duration) AS S + FROM CompilerPhase + GROUP BY P ORDER BY L ASC, S DESC" + +[environment.container-configuration] +label = "Container Configuration" +form = "SELECT LAST(containerType), LAST(cpuSlicePeriod), LAST(cpuQuota), LAST(cpuShares), + LAST(effectiveCpuCount), LAST(memorySoftLimit), LAST(memoryLimit), + LAST(swapMemoryLimit), LAST(hostTotalMemory) + FROM ContainerConfiguration" + +[environment.container-cpu-usage] +label = "Container CPU Usage" +form = "SELECT LAST(cpuTime), LAST(cpuUserTime), LAST(cpuSystemTime) FROM ContainerCPUUsage" + +[environment.container-memory-usage] +label = "Container Memory Usage" +form = "SELECT LAST(memoryFailCount), LAST(memoryUsage), LAST(swapMemoryUsage) FROM ContainerMemoryUsage" + +[environment.container-io-usage] +label = "Container I/O Usage" +form = "SELECT LAST(serviceRequests), LAST(dataTransferred) FROM ContainerIOUsage" + +[environment.container-cpu-throttling] +label = "Container CPU Throttling" +form = "SELECT LAST(cpuElapsedSlices), LAST(cpuThrottledSlices), LAST(cpuThrottledTime) FROM ContainerCPUThrottling" + +[application.contention-by-thread] +label = "Contention by Thread" +table = "COLUMN 'Thread', 'Count', 'Avg', 'P90', 'Max.' + SELECT eventThread, COUNT(*), AVG(duration), P90(duration), MAX(duration) AS M + FROM JavaMonitorEnter GROUP BY eventThread ORDER BY M" + +[application.contention-by-class] +label = "Contention by Lock Class" +table = "COLUMN 'Lock Class', 'Count', 'Avg.', 'P90', 'Max.' + SELECT monitorClass, COUNT(*), AVG(duration), P90(duration), MAX(duration) AS M + FROM JavaMonitorEnter GROUP BY monitorClass ORDER BY M" + +[application.contention-by-site] +label = "Contention by Site" +table = "COLUMN 'StackTrace', 'Count', 'Avg.', 'Max.' + SELECT stackTrace AS S, COUNT(*), AVG(duration), MAX(duration) AS M + FROM JavaMonitorEnter GROUP BY S ORDER BY M" + +[application.contention-by-address] +label = "Contention by Monitor Address" +table = "COLUMN 'Monitor Address', 'Class', 'Threads', 'Max Duration' + SELECT address, FIRST(monitorClass), UNIQUE(*), MAX(duration) AS M + FROM JavaMonitorEnter + GROUP BY monitorClass ORDER BY M" + +[environment.cpu-information] +label ="CPU Information" +form = "SELECT cpu, sockets, cores, hwThreads, description FROM CPUInformation" + +[environment.cpu-load] +label = "CPU Load Statistics" +form = "COLUMN + 'JVM User (Minimum)', + 'JVM User (Average)', + 'JVM User (Maximum)', + 'JVM System (Minimum)', + 'JVM System (Average)', + 'JVM System (Maximum)', + 'Machine Total (Minimum)', + 'Machine Total (Average)', + 'Machine Total (Maximum)' + SELECT MIN(jvmUser), AVG(jvmUser), MAX(jvmUser), + MIN(jvmSystem), AVG(jvmSystem), MAX(jvmSystem), + MIN(machineTotal), AVG(machineTotal), MAX(machineTotal) + FROM CPULoad" + +[environment.cpu-load-samples] +label = "CPU Load" +table = "SELECT startTime, jvmUser, jvmSystem, machineTotal FROM CPULoad" + +[environment.cpu-tsc] +label ="CPU Time Stamp Counter" +form = "SELECT LAST(fastTimeAutoEnabled), LAST(fastTimeEnabled), + LAST(fastTimeFrequency), LAST(osFrequency) + FROM CPUTimeStampCounter" + +[jvm.deoptimizations-by-reason] +label = "Deoptimization by Reason" +table = "SELECT reason, COUNT(reason) AS C + FROM Deoptimization GROUP BY reason ORDER BY C DESC" + +[jvm.deoptimizations-by-site] +label = "Deoptimization by Site" +table = "SELECT method, lineNumber, bci, COUNT(reason) AS C + FROM Deoptimization GROUP BY method ORDER BY C DESC" + +[environment.events-by-count] +label = "Event Types by Count" +table = "SELECT eventType.label AS E, COUNT(*) AS C FROM * GROUP BY E ORDER BY C DESC" + +[environment.events-by-name] +label = "Event Types by Name" +table = "SELECT eventType.label AS E, COUNT(*) AS C FROM * GROUP BY E ORDER BY E ASC" + +[environment.environment-variables] +label = "Environment Variables" +table = "FORMAT none, cell-height:20 + SELECT LAST(key) AS K, LAST(value) + FROM InitialEnvironmentVariable GROUP BY key ORDER BY K" + +[application.exception-count] +label ="Exception Statistics" +form = "COLUMN 'Exceptions Thrown' SELECT DIFF(throwables) FROM ExceptionStatistics" + +[application.exception-by-type] +label ="Exceptions by Type" +table = "COLUMN 'Class', 'Count' + SELECT thrownClass AS T, COUNT(thrownClass) AS C + FROM JavaErrorThrow, JavaExceptionThrow GROUP BY T ORDER BY C DESC" + +[application.exception-by-message] +label ="Exceptions by Message" +table = "COLUMN 'Message', 'Count' + SELECT message AS M, COUNT(message) AS C + FROM JavaErrorThrow, JavaExceptionThrow GROUP BY M ORDER BY C DESC" + +[application.exception-by-site] +label ="Exceptions by Site" +table = "COLUMN 'Method', 'Count' + SELECT stackTrace.notInit AS S, COUNT(startTime) AS C + FROM JavaErrorThrow, JavaExceptionThrow GROUP BY S ORDER BY C DESC" + +[application.file-reads-by-path] +label = "File Reads by Path" +table = "COLUMN 'Path', 'Reads', 'Total Read' + FORMAT cell-height:5, none, none + SELECT path, COUNT(*), SUM(bytesRead) AS S FROM FileRead + GROUP BY path ORDER BY S DESC" + +[application.file-writes-by-path] +label = "File Writes by Path" +table = "COLUMN 'Path', 'Writes', 'Total Written' + FORMAT cell-height:5, none, none + SELECT path, COUNT(bytesWritten), SUM(bytesWritten) AS S FROM FileWrite + GROUP BY path ORDER BY S DESC" + +[application.finalizers] +label = "Finalizers" +table = "SELECT finalizableClass, LAST_BATCH(objects) AS O, LAST_BATCH(totalFinalizersRun) + FROM FinalizerStatistics GROUP BY finalizableClass ORDER BY O DESC" + +[jvm.gc] +label = "Garbage Collections" +table = "COLUMN 'Start', 'GC ID', 'Type', 'Heap Before GC', 'Heap After GC', 'Longest Pause' + FORMAT none, none, missing:Unknown, none, none, none + SELECT G.startTime, gcId, [Y|O].eventType.label, + B.heapUsed, A.heapUsed, longestPause + FROM + GarbageCollection AS G, + GCHeapSummary AS B, + GCHeapSummary AS A, + OldGarbageCollection AS O, + YoungGarbageCollection AS Y + WHERE B.when = 'Before GC' AND A.when = 'After GC' + GROUP BY gcId ORDER BY G.startTime" + +[jvm.gc-concurrent-phases] +label = "Concurrent GC Phases" +table = "COLUMN 'Name', 'Average', 'P95', + 'Longest', 'Count', 'Total' + SELECT name, AVG(duration), P95(duration), + MAX(duration), COUNT(*), SUM(duration) AS S + FROM GCPhaseConcurrent, GCPhaseConcurrentLevel1 + GROUP BY name ORDER BY S" + +[jvm.gc-configuration] +label = 'GC Configuration' +form = "COLUMN 'Young GC', 'Old GC', + 'Parallel GC Threads','Concurrent GC Threads', + 'Dynamic GC Threads', 'Concurrent Explicit GC', + 'Disable Explicit GC', 'Pause Target', + 'GC Time Ratio' + SELECT LAST(youngCollector), LAST(oldCollector), + LAST(parallelGCThreads), LAST(concurrentGCThreads), + LAST(usesDynamicGCThreads), LAST(isExplicitGCConcurrent), + LAST(isExplicitGCDisabled), LAST(pauseTarget), + LAST(gcTimeRatio) + FROM GCConfiguration" + +[jvm.gc-references] +label = "GC References" +table = "COLUMN 'Time', 'GC ID', 'Soft Ref.', 'Weak Ref.', 'Phantom Ref.', 'Final Ref.', 'Total Count' + SELECT G.startTime, G.gcId, S.count, W.count, P.count, F.count, SUM(G.count) + FROM GCReferenceStatistics AS S, + GCReferenceStatistics AS W, + GCReferenceStatistics AS P, + GCReferenceStatistics AS F, + GCReferenceStatistics AS G + WHERE S.type = 'Soft reference' AND + W.type = 'Weak reference' AND + P.type = 'Phantom reference' AND + F.type = 'Final reference' + GROUP BY gcId ORDER By G.gcId ASC" + +[jvm.gc-pause-phases] +label = "GC Pause Phases" +table = "COLUMN 'Type', 'Name', 'Average', + 'P95', 'Longest', 'Count', 'Total' + SELECT eventType.label AS T, name, AVG(duration), + P95(duration), MAX(duration), COUNT(*), SUM(duration) AS S + FROM GCPhasePause, GCPhasePauseLevel1, GCPhasePauseLevel2, + GCPhasePauseLevel3, GCPhasePauseLevel4 GROUP BY name + ORDER BY T ASC, S" + +[jvm.gc-pauses] +label = "GC Pauses" +form = "COLUMN 'Total Pause Time','Number of Pauses', 'Minimum Pause Time', + 'Median Pause Time', 'Average Pause Time', 'P90 Pause Time', + 'P95 Pause Time', 'P99 Pause Time', 'P99.9% Pause Time', + 'Maximum Pause Time' + SELECT SUM(duration), COUNT(duration), MIN(duration), + MEDIAN(duration), AVG(duration), P90(duration), + P95(duration), P99(duration), P999(duration), + MAX(duration) + FROM GCPhasePause" + +[jvm.gc-allocation-trigger] +label = "GC Allocation Trigger" +table = "COLUMN 'Trigger Method (Non-JDK)', 'Count', 'Total Requested' + SELECT stackTrace.topApplicationFrame AS S, COUNT(*), SUM(size) + FROM AllocationRequiringGC GROUP BY S" + +[jvm.gc-cpu-time] +label = "GC CPU Time" +form = "COLUMN 'GC User Time', 'GC System Time', + 'GC Wall Clock Time', 'Total Time', + 'GC Count' + SELECT SUM(userTime), SUM(systemTime), + SUM(realTime), DIFF(startTime), COUNT(*) + FROM GCCPUTime" + +[jvm.heap-configuration] +label = "Heap Configuration" +form = "SELECT LAST(initialSize), LAST(minSize), LAST(maxSize), + LAST(usesCompressedOops), LAST(compressedOopsMode) + FROM GCHeapConfiguration" + +[application.hot-methods] +label = "Java Methods that Executes the Most" +table = "COLUMN 'Method', 'Samples', 'Percent' + FORMAT none, none, normalized + SELECT stackTrace.topFrame AS T, COUNT(*), COUNT(*) + FROM ExecutionSample GROUP BY T LIMIT 25" + +[environment.jvm-flags] +label = "Command Line Flags" +table = "SELECT name AS N, LAST(value) + FROM IntFlag, UnsignedIntFlag, BooleanFlag, + LongFlag, UnsignedLongFlag, + DoubleFlag, StringFlag, + IntFlagChanged, UnsignedIntFlagChanged, BooleanFlagChanged, + LongFlagChanged, UnsignedLongFlagChanged, + DoubleFlagChanged, StringFlagChanged + GROUP BY name ORDER BY name ASC" + +[jvm.jvm-information] +label = "JVM Information" +form = "COLUMN + 'PID', 'VM Start', 'Name', 'Version', + 'VM Arguments', 'Program Arguments' + SELECT LAST(pid), LAST(jvmStartTime), LAST(jvmName), LAST(jvmVersion), + LAST(jvmArguments), LAST(javaArguments) FROM JVMInformation" + +[application.latencies-by-type] +label = "Latencies by Type" +table = "COLUMN 'Event Type', 'Count', 'Average', 'P 99', 'Longest', 'Total' + SELECT eventType.label AS T, COUNT(*), AVG(duration), P99(duration), MAX(duration), SUM(duration) + FROM JavaMonitorWait, JavaMonitorEnter, ThreadPark, ThreadSleep, + SocketRead, SocketWrite, FileWrite, FileRead GROUP BY T" + +[application.memory-leaks-by-class] +label = "Memory Leak Candidates by Class" +table = "COLUMN 'Alloc. Time', 'Object Class', 'Object Age', 'Heap Usage' + SELECT LAST_BATCH(allocationTime), LAST_BATCH(object.type), LAST_BATCH(objectAge), + LAST_BATCH(lastKnownHeapUsage) FROM OldObjectSample GROUP BY object.type ORDER BY allocationTime" + +[application.memory-leaks-by-site] +label = "Memory Leak Candidates by Site" +table = "COLUMN 'Alloc. Time', 'Application Method', 'Object Age', 'Heap Usage' + SELECT LAST_BATCH(allocationTime), LAST_BATCH(stackTrace.topApplicationFrame), LAST_BATCH(objectAge), + LAST_BATCH(lastKnownHeapUsage) FROM OldObjectSample GROUP BY stackTrace.topApplicationFrame ORDER BY allocationTime" + +[application.modules] +label = "Modules" +table = "SELECT LAST(source.name) AS S FROM ModuleRequire GROUP BY source.name ORDER BY S" + +[application.monitor-inflation] +label = "Monitor Inflation" +table = "SELECT stackTrace, monitorClass, COUNT(*), SUM(duration) AS S + FROM jdk.JavaMonitorInflate GROUP BY stackTrace, monitorClass ORDER BY S" + +[environment.native-libraries] +label = "Native Libraries" +table = "FORMAT cell-height:2, none, none + SELECT name AS N, baseAddress, topAddress FROM NativeLibrary GROUP BY name ORDER BY N" + +[jvm.native-memory-committed] +label = "Native Memory Committed" +table = "COLUMN 'Memory Type', 'First Observed', 'Average', 'Last Observed', 'Maximum' + SELECT type, FIRST(committed), AVG(committed), LAST(committed), MAX(committed) AS M + FROM NativeMemoryUsage GROUP BY type ORDER BY M DESC" + +[jvm.native-memory-reserved] +label = "Native Memory Reserved" +table = "COLUMN 'Memory Type', 'First Observed', 'Average', 'Last Observed', 'Maximum' + SELECT type, FIRST(reserved), AVG(reserved), LAST(reserved), MAX(reserved) AS M + FROM NativeMemoryUsage GROUP BY type ORDER BY M DESC" + +[application.native-methods] +label = "Waiting or Executing Native Methods" +table = "COLUMN 'Method', 'Samples', 'Percent' + FORMAT none, none, normalized + SELECT stackTrace.topFrame AS T, COUNT(*), COUNT(*) + FROM NativeMethodSample GROUP BY T" + +[environment.network-utilization] +label = "Network Utilization" +table = "SELECT networkInterface, AVG(readRate), MAX(readRate), AVG(writeRate), MAX(writeRate) + FROM NetworkUtilization GROUP BY networkInterface" + +[application.object-statistics] +label = "Objects Occupying More than 1%" +table = "COLUMN 'Class', 'Count', 'Heap Space', 'Increase' + SELECT + LAST_BATCH(objectClass), LAST_BATCH(count), + LAST_BATCH(totalSize), DIFF(totalSize) + FROM ObjectCountAfterGC, ObjectCount + GROUP BY objectClass + ORDER BY totalSize DESC" + +[application.pinned-threads] +label = "Pinned Virtual Threads" +table = "COLUMN 'Method', 'Pinned Count', 'Longest Pinning', 'Total Time Pinned' + SELECT stackTrace.topApplicationFrame AS S, COUNT(*), + MAX(duration), SUM(duration) AS T FROM VirtualThreadPinned + GROUP BY S + ORDER BY T DESC" + +[application.thread-count] +label ="Java Thread Statistics" +table = "SELECT * FROM JavaThreadStatistics" + +[environment.recording] +label = "Recording Information" +form = "COLUMN 'Event Count', 'First Recorded Event', 'Last Recorded Event', + 'Length of Recorded Events', 'Dump Reason' + SELECT COUNT(startTime), FIRST(startTime), LAST(startTime), + DIFF(startTime), LAST(jdk.Shutdown.reason) + FROM *" + +[jvm.safepoints] +label = "Safepoints" +table = "COLUMN 'Start Time', 'Duration', + 'State Syncronization', 'Cleanup', + 'JNI Critical Threads', 'Total Threads' + SELECT B.startTime, DIFF([B|E].startTime), + S.duration, C.duration, + jniCriticalThreadCount, totalThreadCount + FROM SafepointBegin AS B, SafepointEnd AS E, + SafepointCleanup AS C, SafepointStateSynchronization AS S + GROUP BY safepointId ORDER BY B.startTime" + +[jvm.longest-compilations] +label = "Longest Compilations" +table = "SELECT startTime, duration AS D, method, compileLevel, succeded + FROM Compilation ORDER BY D LIMIT 25" + +[application.longest-class-loading] +label = "Longest Class Loading" +table = "COLUMN 'Time', 'Loaded Class', 'Load Time' + SELECT startTime,loadedClass, duration AS D + FROM ClassLoad ORDER BY D DESC LIMIT 25" + +[environment.system-properties] +label = "System Properties at Startup" +table = "FORMAT none, cell-height:25 + SELECT key AS K, value FROM InitialSystemProperty GROUP BY key ORDER by K" + +[application.socket-writes-by-host] +label = "Socket Writes by Host" +table = "COLUMN 'Host', 'Writes', 'Total Written' + FORMAT cell-height:2, none, none + SELECT host, COUNT(*), SUM(bytesWritten) AS S FROM SocketWrite + GROUP BY host ORDER BY S DESC" + +[application.socket-reads-by-host] +label = "Socket Reads by Host" +table = "COLUMN 'Host', 'Reads', 'Total Read' + FORMAT cell-height:2, none, none + SELECT host, COUNT(*), SUM(bytesRead) AS S FROM SocketRead + GROUP BY host ORDER BY S DESC" + +[environment.system-information] +label = "System Information" +form = "COLUMN 'Total Physical Memory Size', 'OS Version', 'CPU Type', + 'Number of Cores', 'Number of Hardware Threads', + 'Number of Sockets', 'CPU Description' + SELECT LAST(totalSize), LAST(osVersion), LAST(cpu), + LAST(cores), LAST(hwThreads), + LAST(sockets), LAST(description) + FROM CPUInformation, PhysicalMemory, OSInformation" + +[environment.system-processes] +label = "System Processes" +table = "COLUMN 'First Observed', 'Last Observed', 'PID', 'Command Line' + SELECT FIRST(startTime), LAST(startTime), + FIRST(pid), FIRST(commandLine) + FROM SystemProcess GROUP BY pid" + +[jvm.tlabs] +label = "Thread Local Allocation Buffers" +form = "COLUMN 'Inside TLAB Count', 'Inside TLAB Minimum Size', 'Inside TLAB Average Size', + 'Inside TLAB Maximum Size', 'Inside TLAB Total Allocation', + 'Outside TLAB Count', 'OutSide TLAB Minimum Size', 'Outside TLAB Average Size', + 'Outside TLAB Maximum Size', 'Outside TLAB Total Allocation' + SELECT COUNT(I.tlabSize), MIN(I.tlabSize), AVG(I.tlabSize), + MAX(I.tlabSize), SUM(I.tlabSize), + COUNT(O.allocationSize), MIN(O.allocationSize), AVG(O.allocationSize), + MAX(O.allocationSize), SUM(O.allocationSize) + FROM ObjectAllocationInNewTLAB AS I, ObjectAllocationOutsideTLAB AS O" + +[application.thread-allocation] +label = "Thread Allocation Statistics" +table = "COLUMN 'Thread', 'Allocated', 'Percentage' + FORMAT none, none, normalized + SELECT thread, LAST(allocated), LAST(allocated) AS A FROM ThreadAllocationStatistics + GROUP BY thread ORDER BY A DESC" + +[application.thread-cpu-load] +label = "Thread CPU Load" +table = "COLUMN 'Thread', 'System', 'User' + SELECT eventThread AS E, LAST(system), LAST(user) AS U + FROM ThreadCPULoad GROUP BY E ORDER BY U DESC" + +[application.thread-start] +label = "Platform Thread Start by Method" +table = "COLUMN 'Start Time','Stack Trace', 'Thread', 'Duration' + SELECT S.startTime, S.stackTrace, eventThread, DIFF(startTime) AS D + FROM ThreadStart AS S, ThreadEnd AS E GROUP + by eventThread ORDER BY D DESC" + +[jvm.vm-operations] +label = "VM Operations" +table = "COLUMN 'VM Operation', 'Average Duration', 'Longest Duration', 'Count' , 'Total Duration' + SELECT operation, AVG(duration), MAX(duration), COUNT(*), SUM(duration) + FROM jdk.ExecuteVMOperation GROUP BY operation" diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Assemble.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Assemble.java index f07eff246bc..75169e816a9 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Assemble.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Assemble.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -38,6 +38,9 @@ import java.util.Collections; import java.util.Deque; import java.util.List; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; + final class Assemble extends Command { @Override diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Command.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Command.java index fc3b46ced54..032e4d1982b 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Command.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Command.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -40,6 +40,9 @@ import java.util.Collections; import java.util.Deque; import java.util.List; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; + abstract class Command { public static final String title = "Tool for working with Flight Recorder files"; private static final Command HELP = new Help(); @@ -48,6 +51,9 @@ abstract class Command { private static List createCommands() { List commands = new ArrayList<>(); commands.add(new Print()); + // Uncomment when developing new queries for the view command + // commands.add(new Query()); + commands.add(new View()); commands.add(new Configure()); commands.add(new Metadata()); commands.add(new Scrub()); @@ -170,6 +176,18 @@ abstract class Command { return false; } + protected int acceptInt(Deque options, String text) throws UserSyntaxException { + if (options.size() < 1) { + throw new UserSyntaxException("missing integer value"); + } + String t = options.remove(); + try { + return Integer.parseInt(t); + } catch (NumberFormatException nfe) { + throw new UserSyntaxException("could not parse integer value " + t); + } + } + protected void warnForWildcardExpansion(String option, String filter) throws UserDataException { // Users should quote their wildcards to avoid expansion by the shell try { diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Configure.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Configure.java index 30012bf94d9..63b2d8fe74f 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Configure.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Configure.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -46,6 +46,8 @@ import jdk.jfr.internal.jfc.model.JFCModelException; import jdk.jfr.internal.jfc.model.SettingsLog; import jdk.jfr.internal.jfc.model.UserInterface; import jdk.jfr.internal.jfc.model.XmlInput; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; final class Configure extends Command { private final List inputFiles = new ArrayList<>(); diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Disassemble.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Disassemble.java index aaa11b64c40..edcaf228d78 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Disassemble.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Disassemble.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -43,6 +43,8 @@ import java.util.List; import jdk.jfr.internal.consumer.ChunkHeader; import jdk.jfr.internal.consumer.FileAccess; import jdk.jfr.internal.consumer.RecordingInput; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; final class Disassemble extends Command { diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Filters.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Filters.java index 716b221a06c..8627c31e56a 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Filters.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Filters.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -33,6 +33,7 @@ import java.util.function.Predicate; import jdk.jfr.EventType; import jdk.jfr.consumer.RecordedThread; +import jdk.jfr.internal.util.UserSyntaxException; import jdk.jfr.consumer.RecordedEvent; /** diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Help.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Help.java index ef485b49855..cc316fabe6e 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Help.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Help.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,6 +29,9 @@ import java.io.PrintStream; import java.util.Deque; import java.util.List; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; + final class Help extends Command { @Override diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Main.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Main.java index da2f625ac52..79f25df9a39 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Main.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,6 +29,9 @@ import java.util.ArrayDeque; import java.util.Arrays; import java.util.Deque; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; + /** * Launcher class for the JDK_HOME\bin\jfr tool * @@ -49,7 +52,7 @@ public final class Main { System.out.println(); System.out.println(" java -XX:StartFlightRecording:filename=recording.jfr,duration=30s ... "); System.out.println(); - System.out.println("A recording can also be started on already running Java Virtual Machine:"); + System.out.println("A recording can also be started on an already running Java Virtual Machine:"); System.out.println(); System.out.println(" jcmd (to list available pids)"); System.out.println(" jcmd JFR.start"); @@ -71,11 +74,13 @@ public final class Main { System.out.println(); System.out.println(" jfr print --events " + q + "jdk.*" + q + " --stack-depth 64 recording.jfr"); System.out.println(); + System.out.println(" jfr view gc recording.jfr"); + System.out.println(); + System.out.println(" jfr view allocation-by-site recording.jfr"); + System.out.println(); System.out.println(" jfr summary recording.jfr"); System.out.println(); - System.out.println(" jfr metadata recording.jfr"); - System.out.println(); - System.out.println(" jfr metadata --categories GC,Detailed"); + System.out.println(" jfr metadata"); System.out.println(); System.out.println("For more information about available commands, use 'jfr help'"); System.exit(EXIT_OK); diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Metadata.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Metadata.java index 975cd86ae84..9d9d14c7e6a 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Metadata.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Metadata.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -44,6 +44,8 @@ import jdk.jfr.internal.PrivateAccess; import jdk.jfr.internal.Type; import jdk.jfr.internal.MetadataRepository; import jdk.jfr.internal.consumer.JdkJfrConsumer; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Print.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Print.java index 8157add0e91..569a7602500 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Print.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Print.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,6 +35,8 @@ import java.util.List; import java.util.function.Predicate; import jdk.jfr.EventType; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Query.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Query.java new file mode 100644 index 00000000000..cd4496825c5 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Query.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.internal.tool; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +import jdk.jfr.consumer.EventStream; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.Output.BufferedPrinter; +import jdk.jfr.internal.util.UserSyntaxException; +import jdk.jfr.internal.query.QueryPrinter; +import jdk.jfr.internal.query.Configuration.Truncate; +import jdk.jfr.internal.query.Configuration; + +final class Query extends Command { + @Override + public String getName() { + return "query"; + } + + @Override + public String getDescription() { + return "Display event values in a recording file (.jfr) in a tabular format"; + } + + @Override + public void displayOptionUsage(PrintStream p) { + // 0123456789001234567890012345678900123456789001234567890012345678900123456789001234567890 + p.println(" --verbose Displays the symbolic column names"); + p.println(); + p.println(" --width The width of the table. Default value depends on the query"); + p.println(); + p.println(" Query, for example \"SELECT * FROM GarbageCollection\""); + p.println(" See below for grammar."); + p.println(); + p.println(" Location of the recording file (.jfr)"); + p.println(); + p.println(QueryPrinter.getGrammarText()); + p.println(); + p.println("Example usage:"); + p.println(); + p.println(" $ jfr query \"SHOW EVENTS\" recording.jfr"); + p.println(); + p.println(" $ jfr query \"SHOW FIELDS ObjectAllocationSample\" recording.jfr"); + p.println(); + p.println(" $ jfr query --verbose \"SELECT * FROM ObjectAllocationSample\" recording.jfr"); + p.println(); + p.println(" $ jfr query --width 160 \"SELECT pid, path FROM SystemProcess\" recording.jfr"); + p.println(); + p.println(" $ jfr query \"SELECT stackTrace.topFrame AS T, SUM(weight)"); + p.println(" FROM ObjectAllocationSample GROUP BY T\" recording.jfr"); + p.println(); + p.println("$ jfr JFR.query \"COLUMN 'Method', 'Percentage'"); + p.println(" FORMAT default, normalized;width:10"); + p.println(" SELECT stackTrace.topFrame AS T, COUNT(*) AS C"); + p.println(" GROUP BY T"); + p.println(" FROM ExecutionSample ORDER BY C DESC\" recording.jfr"); + p.println(); + p.println("$ jcmd JFR.query \"COLUMN 'Start', 'GC ID', 'Heap Before GC',"); + p.println(" 'Heap After GC', 'Longest Pause'"); + p.println(" SELECT G.startTime, G.gcId, B.heapUsed,"); + p.println(" A.heapUsed, longestPause"); + p.println(" FROM GarbageCollection AS G,"); + p.println(" GCHeapSummary AS B,"); + p.println(" GCHeapSummary AS A"); + p.println(" WHERE B.when = 'Before GC' AND A.when = 'After GC'"); + p.println(" GROUP BY gcId"); + p.println(" ORDER BY G.startTime\" recording.jfr"); + } + + @Override + public List getOptionSyntax() { + List list = new ArrayList<>(); + list.add("[--verbose] [--width ] "); + return list; + } + + @Override + public void execute(Deque options) throws UserSyntaxException, UserDataException { + Path file = getJFRInputFile(options); + int optionCount = options.size(); + var configuration = new Configuration(); + BufferedPrinter printer = new BufferedPrinter(System.out); + configuration.output = printer; + while (optionCount > 0) { + if (acceptSwitch(options, "--verbose")) { + configuration.verbose = true; + configuration.verboseHeaders = true; + } + if (acceptOption(options, "--truncate")) { + String mode = options.remove(); + try { + configuration.truncate = Truncate.valueOf(mode.toUpperCase()); + } catch (IllegalArgumentException iae) { + throw new UserSyntaxException("truncate must be 'beginning' or 'end'"); + } + } + if (acceptOption(options, "--cell-height")) { + configuration.cellHeight = acceptInt(options, "cell-height"); + } + if (acceptOption(options, "--width")) { + configuration.width = acceptInt(options, "width"); + } + if (optionCount == 1) { + String query = options.pop(); + try (EventStream stream = EventStream.openFile(file)) { + QueryPrinter qp = new QueryPrinter(configuration, stream); + qp.execute(query); + printer.flush(); + } catch (IOException ioe) { + couldNotReadError(file, ioe); + } + return; + } + if (optionCount == options.size()) { + throw new UserSyntaxException("unknown option " + options.peek()); + } + optionCount = options.size(); + } + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Scrub.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Scrub.java index ac257f14d06..a17662af88d 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Scrub.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Scrub.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -37,6 +37,8 @@ import java.util.function.Predicate; import jdk.jfr.EventType; import jdk.jfr.consumer.RecordedEvent; import jdk.jfr.consumer.RecordingFile; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; final class Scrub extends Command { diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Summary.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Summary.java index 9d8ab000dad..a092cf69094 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Summary.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Summary.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -43,6 +43,8 @@ import jdk.jfr.internal.Type; import jdk.jfr.internal.consumer.ChunkHeader; import jdk.jfr.internal.consumer.FileAccess; import jdk.jfr.internal.consumer.RecordingInput; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; final class Summary extends Command { private final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withLocale(Locale.UK).withZone(ZoneOffset.UTC); diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/View.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/View.java new file mode 100644 index 00000000000..31f14e35d64 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/View.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.internal.tool; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +import jdk.jfr.consumer.EventStream; +import jdk.jfr.internal.util.Columnizer; +import jdk.jfr.internal.query.ViewPrinter; +import jdk.jfr.internal.query.Configuration; +import jdk.jfr.internal.query.Configuration.Truncate; +import jdk.jfr.internal.util.UserDataException; +import jdk.jfr.internal.util.UserSyntaxException; +import jdk.jfr.internal.util.Output.BufferedPrinter; + +public final class View extends Command { + @Override + public String getName() { + return "view"; + } + + @Override + protected String getTitle() { + return "Display event values in a recording file (.jfr) in predefined views"; + } + + @Override + public String getDescription() { + return "Display events in a tabular format. See 'jfr help view' for details."; + } + + @Override + public void displayOptionUsage(PrintStream stream) { + stream.println(" --verbose Displays the query that makes up the view"); + stream.println(""); + stream.println(" --width The width of the view in characters. Default value depends on the view"); + stream.println(""); + stream.println(" --truncate How to truncate content that exceeds space in a table cell."); + stream.println(" Mode can be 'beginning' or 'end'. Default value is 'end'"); + stream.println(""); + stream.println(" --cell-height Maximum number of rows in a table cell. Default value depends on the view"); + stream.println(""); + stream.println(" Name of the view or event type to display. See list below for"); + stream.println(" available views"); + stream.println(""); + stream.println(" Location of the recording file (.jfr)"); + stream.println(); + for (String line : ViewPrinter.getAvailableViews()) { + stream.println(line); + } + stream.println(" The parameter can be an event type name. Use the 'jfr view types '"); + stream.println(" to see a list. To display all views, use 'jfr view all-views '. To display"); + stream.println(" all events, use 'jfr view all-events '."); + stream.println(); + stream.println("Example usage:"); + stream.println(); + stream.println(" jfr view gc recording.jfr"); + stream.println(); + stream.println(" jfr view --width 160 hot-methods recording.jfr"); + stream.println(); + stream.println(" jfr view --verbose allocation-by-class recording.jfr"); + stream.println(); + stream.println(" jfr view contention-by-site recording.jfr"); + stream.println(); + stream.println(" jfr view jdk.GarbageCollection recording.jfr"); + stream.println(); + stream.println(" jfr view --cell-height 10 ThreadStart recording.jfr"); + stream.println(); + stream.println(" jfr view --truncate beginning SystemProcess recording.jfr"); + stream.println(); + } + + @Override + public List getOptionSyntax() { + List list = new ArrayList<>(); + list.add("[--verbose]"); + list.add("[--width "); + list.add("[--truncate ]"); + list.add("[--cell-height ]"); + list.add(""); + list.add(""); + return list; + } + + @Override + public void execute(Deque options) throws UserSyntaxException, UserDataException { + Path file = getJFRInputFile(options); + int optionCount = options.size(); + if (optionCount < 1) { + throw new UserSyntaxException("must specify a view or event type"); + } + Configuration configuration = new Configuration(); + BufferedPrinter printer = new BufferedPrinter(System.out); + configuration.output = printer; + while (true) { + if (acceptSwitch(options, "--verbose")) { + configuration.verbose = true; + } + if (acceptOption(options, "--truncate")) { + String mode = options.remove(); + try { + configuration.truncate = Truncate.valueOf(mode.toUpperCase()); + } catch (IllegalArgumentException iae) { + throw new UserSyntaxException("truncate must be 'beginning' or 'end'"); + } + } + if (acceptOption(options, "--cell-height")) { + configuration.cellHeight = acceptInt(options, "cell-height"); + } + if (acceptOption(options, "--width")) { + configuration.width = acceptInt(options, "width"); + } + if (options.size() == 1) { + String view = options.pop(); + try (EventStream stream = EventStream.openFile(file)) { + ViewPrinter vp = new ViewPrinter(configuration, stream); + vp.execute(view); + printer.flush(); + return; + } catch (IOException ioe) { + couldNotReadError(file, ioe); + } + } + System.out.println("count:" + optionCount); + System.out.println("size:" + options.size()); + if (optionCount == options.size()) { + String peek = options.peek(); + if (peek == null) { + throw new UserSyntaxException("must specify option "); + } + throw new UserSyntaxException("unknown option " + peek); + } + optionCount = options.size(); + } + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Columnizer.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Columnizer.java new file mode 100644 index 00000000000..91da118eb5c --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Columnizer.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Class that creates a column-sorted list. + *

+ * For example, the list: "Bison", "Dog", "Frog", Goldfish", "Kangaroo", "Ant", + * "Jaguar", "Cat", "Elephant", "Ibex" becomes: + *

+ *  Ant   Elephant Jaguar
+ *  Bison Frog     Kangaroo
+ *  Cat   Goldfish
+ *  Dog   Ibex"
+ * 
+ */ +public final class Columnizer { + private static final class Column { + int maxWidth; + List entries = new ArrayList<>(); + public void add(String text) { + entries.add(text); + maxWidth = Math.max(maxWidth, text.length()); + } + } + private final List columns = new ArrayList<>(); + + public Columnizer(List texts, int columnCount) { + List list = new ArrayList<>(texts); + Collections.sort(list); + int columnHeight = (list.size() + columnCount - 1) / columnCount; + int index = 0; + Column column = null; + for (String text : list) { + if (index % columnHeight == 0) { + column = new Column(); + columns.add(column); + } + column.add(text); + index++; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + int index = 0; + while (true) { + for (Column column : columns) { + if (index == column.entries.size()) { + return sb.toString(); + } + if (index != 0 && columns.getFirst() == column) { + sb.append(System.lineSeparator()); + } + String text = column.entries.get(index); + sb.append(" "); + sb.append(text); + sb.append(" ".repeat(column.maxWidth - text.length())); + } + index++; + } + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Matcher.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Matcher.java new file mode 100644 index 00000000000..6ebb29e80d3 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Matcher.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.util; + +public final class Matcher { + + /** + * Returns true if text matches pattern of characters, '*' and '?' + */ + public static boolean match(String text, String pattern) { + if (pattern.length() == 0) { + // empty filter string matches if string is empty + return text.length() == 0; + } + if (pattern.charAt(0) == '*') { // recursive check + pattern = pattern.substring(1); + for (int n = 0; n <= text.length(); n++) { + if (match(text.substring(n), pattern)) + return true; + } + } else if (text.length() == 0) { + // empty string and non-empty filter does not match + return false; + } else if (pattern.charAt(0) == '?') { + // eat any char and move on + return match(text.substring(1), pattern.substring(1)); + } else if (pattern.charAt(0) == text.charAt(0)) { + // eat chars and move on + return match(text.substring(1), pattern.substring(1)); + } + return false; + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Output.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Output.java new file mode 100644 index 00000000000..6391ea9f1ae --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Output.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.util; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +public interface Output { + + public void println(); + + public void print(String s); + + public void print(String s, Object... args); + + default public void println(String s, Object... args) { + print(s, args); + println(); + } + + public void print(char c); + + public static final class LinePrinter implements Output { + private final StringBuilder currentLine = new StringBuilder(80); + private final List lines = new ArrayList<>(); + + @Override + public void println() { + lines.add(currentLine.toString()); + currentLine.setLength(0); + } + + @Override + public void print(String s) { + currentLine.append(s); + } + + @Override + public void print(String s, Object... args) { + currentLine.append(args.length > 0 ? String.format(s, args) : s); + } + + @Override + public void print(char c) { + currentLine.append(c); + } + + public List getLines() { + return lines; + } + } + + public static final class BufferedPrinter implements Output { + private final StringBuilder buffer = new StringBuilder(100_000); + private final PrintStream out; + + public BufferedPrinter(PrintStream out) { + this.out = out; + } + + @Override + public void println() { + buffer.append(System.lineSeparator()); + flushCheck(); + } + + @Override + public void print(String s) { + buffer.append(s); + flushCheck(); + } + + @Override + public void print(String s, Object... args) { + if (args.length > 0) { + buffer.append(String.format(s, args)); + } else { + buffer.append(s); + } + flushCheck(); + } + + @Override + public void print(char c) { + buffer.append(c); + flush(); + } + + public void flush() { + out.print(buffer.toString()); + buffer.setLength(0); + } + + private void flushCheck() { + if (buffer.length() > 99_000) { + flush(); + } + } + } +} \ No newline at end of file diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/SpellChecker.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/SpellChecker.java new file mode 100644 index 00000000000..0b16f6c5950 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/SpellChecker.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.util; + +import java.util.List; + +public final class SpellChecker { + public static String check(String name, List alternatives) { + for (String expected : alternatives) { + String s = name.toLowerCase(); + int lengthDifference = expected.length() - s.length(); + boolean spellingError = false; + if (lengthDifference == 0) { + if (expected.equals(s)) { + spellingError = true; // incorrect case, or we wouldn't be here + } else { + if (s.length() < 6) { + spellingError = diff(expected, s) < 2; // one incorrect letter + } else { + spellingError = diff(expected, s) < 3; // two incorrect letter + } + } + } + if (lengthDifference == 1) { + spellingError = inSequence(expected, s); // missing letter + } + if (lengthDifference == -1) { + spellingError = inSequence(s, expected); // additional letter + } + if (spellingError) { + return expected; + } + } + return null; + } + + private static int diff(String a, String b) { + int count = a.length(); + for (int i = 0; i < a.length(); i++) { + if (a.charAt(i) == b.charAt(i)) { + count--; + } + } + return count; + } + + private static boolean inSequence(String longer, String shorter) { + int l = 0; + int s = 0; + while (l < longer.length() && s < shorter.length()) { + if (longer.charAt(l) == shorter.charAt(s)) { + s++; + } + l++; + } + return shorter.length() == s; // if 0, all letters in longer found in shorter + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/StopWatch.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/StopWatch.java new file mode 100644 index 00000000000..24bf3d16b01 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/StopWatch.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.util; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +public final class StopWatch { + private record Timing(String name, Instant start) { + } + + private final List timings = new ArrayList<>(); + + public void beginQueryValidation() { + beginTask("query-validation"); + } + + public void beginAggregation() { + beginTask("aggregation"); + } + + public void beginFormatting() { + beginTask("formatting"); + } + + public void beginTask(String name) { + timings.add(new Timing(name, Instant.now())); + } + + public void finish() { + beginTask("end"); + } + + @Override + public String toString() { + StringJoiner sb = new StringJoiner(", "); + for (int i = 0; i < timings.size() - 1; i++) { + Timing current = timings.get(i); + Timing next = timings.get(i + 1); + Duration d = Duration.between(current.start(), next.start()); + sb.add(current.name() + "=" + ValueFormatter.formatDuration(d)); + } + return sb.toString(); + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Tokenizer.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Tokenizer.java new file mode 100644 index 00000000000..460635be61a --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Tokenizer.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.util; + +import java.text.ParseException; + +public final class Tokenizer implements AutoCloseable { + private final String text; + private final char[] separators; + private int index; + + /** + * Constructs a Tokenizer. + * + * @param text text to tokenize + * @param separators separator, for example ',' or ';' + */ + public Tokenizer(String text, char... separators) { + this.text = text; + this.separators = separators; + } + + /** + * If the next token matches a string, it is consumed and {@code true} returned, + * {@code false} otherwise. + */ + public boolean accept(String match) { + skipWhitespace(); + int t = 0; + while (index + t < text.length() && t < match.length()) { + char c = Character.toLowerCase(text.charAt(index + t)); + char d = Character.toLowerCase(match.charAt(t)); + if (d != c) { + return false; + } + t++; + if (isSeparator(c)) { + break; + } + } + if (t == match.length()) { + index += match.length(); + return true; + } + return false; + } + + /** + * Similar to accept(String), but requires several tokens to match. + */ + public boolean accept(String... matches) { + int position = index; + for (String s : matches) { + if (!accept(s)) { + index = position; + return false; + + } + } + return true; + } + + /** + * Similar to accept(String...), but sufficient if one token matches. + * + * @param matches + * @return + */ + public boolean acceptAny(String... matches) { + for (String match : matches) { + if (accept(match)) { + return true; + } + } + return false; + } + + /** + * Return {@code true} if there are more tokens. + */ + public boolean hasNext() { + int k = index; + while (k < text.length()) { + char c = text.charAt(k); + if (!Character.isWhitespace(c)) { + return true; + } + k++; + } + return false; + } + + /** + * Throws exception if the next token doesn't match. + */ + public void expect(String expected) throws ParseException { + if (!accept(expected)) { + throw new ParseException("Expected " + expected, index); + } + } + + /** + * Return the next character without consuming it, or throw exception if + * {@code EOF} is reached. + */ + public char peekChar() throws ParseException { + skipWhitespace(); + if (index < text.length()) { + return text.charAt(index); + } + throw new ParseException("Unexpected EOF reached", index); + } + + /** + * Return the next token, or throw exception if {@code EOF} is reached. + */ + public String next() throws ParseException { + skipWhitespace(); + StringBuilder sb = new StringBuilder(); + while (index < text.length()) { + char c = text.charAt(index); + if (isQuoteCharacter(c)) { + int p = findNext(c); + String t = text.substring(index + 1, p); + sb.append(t); + index = p; + } else { + if (isSeparator(c)) { + if (sb.isEmpty()) { + index++; + return String.valueOf(c); // Inte helt optimalt + } else { + return sb.toString(); + } + } + if (Character.isWhitespace(c)) { + return sb.toString(); + } + sb.append(c); + } + index++; + } + if (sb.isEmpty()) { + throw new ParseException("Unexpected EOF reached", index); + } + return sb.toString(); + } + + /** + * Skips all character until {@code '\n'} + */ + public void skipLine() { + while (index < text.length()) { + char c = text.charAt(index++); + if (c == '\n') { + return; + } + } + } + + /** + * Returns the current position in the text. + */ + public int getPosition() { + return index; + } + + private void skipWhitespace() { + while (index < text.length()) { + char c = text.charAt(index); + if (!Character.isWhitespace(c)) { + return; + } + index++; + } + } + + private boolean isSeparator(char c) { + for (char separator : separators) { + if (c == separator) { + return true; + } + } + return false; + } + + private int findNext(char c) throws ParseException { + for (int p = index + 1; p < text.length(); p++) { + if (c == text.charAt(p)) { + return p; + } + } + throw new ParseException("Could not find match " + c, index); + } + + private boolean isQuoteCharacter(char c) { + return c == '\'' || c == '"'; + } + + /** + * Closes the Tokenizer. + *

+ * Requires that all the tokens have been consumed. + * + * @throws ParseException if there are tokens left + */ + @Override + public void close() throws ParseException { + skipWhitespace(); + if (index != text.length()) { + throw new ParseException("Unexpected token '" + next() + "' found", getPosition()); + } + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/UserDataException.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/UserDataException.java new file mode 100644 index 00000000000..c1764aa1bef --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/UserDataException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.internal.util; + +/** + * Exception that is thrown if there is something wrong with the input, for instance + * a file that can't be read or a numerical value that is out of range. + *

+ * When this exception is thrown, a user will typically not want to see the + * command line syntax, but instead information about what was wrong with the + * input. + */ +public final class UserDataException extends Exception { + private static final long serialVersionUID = 6656457380115167810L; + /** + * The error message. + * + * The first letter should not be capitalized, so a context can be printed prior + * to the error message. + * + * @param errorMessage + */ + public UserDataException(String errorMessage) { + super(errorMessage); + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/UserSyntaxException.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/UserSyntaxException.java new file mode 100644 index 00000000000..5339173e1c4 --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/UserSyntaxException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.internal.util; + +/** + * Exception that is thrown if options don't follow the syntax of the command. + */ +public final class UserSyntaxException extends Exception { + private static final long serialVersionUID = 3437009454344160933L; + + /** + * The error message. + * + * The first letter should not be capitalized, so a context can be printed prior + * to the error message. + * + * @param errorMessage message + */ + public UserSyntaxException(String errorMessage) { + super(errorMessage); + } +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/ValueFormatter.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/ValueFormatter.java new file mode 100644 index 00000000000..46e40816a4b --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/ValueFormatter.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.internal.util; + +import java.text.NumberFormat; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +import jdk.jfr.consumer.RecordedClass; +import jdk.jfr.consumer.RecordedMethod; + +public final class ValueFormatter { + private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance(); + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss"); + private static final Duration MICRO_SECOND = Duration.ofNanos(1_000); + private static final Duration SECOND = Duration.ofSeconds(1); + private static final Duration MINUTE = Duration.ofMinutes(1); + private static final Duration HOUR = Duration.ofHours(1); + private static final Duration DAY = Duration.ofDays(1); + private static final int NANO_SIGNIFICANT_FIGURES = 9; + private static final int MILL_SIGNIFICANT_FIGURES = 3; + private static final int DISPLAY_NANO_DIGIT = 3; + private static final int BASE = 10; + + public static String formatNumber(Number n) { + return NUMBER_FORMAT.format(n); + } + + public static String formatDuration(Duration d) { + Duration roundedDuration = roundDuration(d); + if (roundedDuration.equals(Duration.ZERO)) { + return "0 s"; + } else if (roundedDuration.isNegative()) { + return "-" + formatPositiveDuration(roundedDuration.abs()); + } else { + return formatPositiveDuration(roundedDuration); + } + } + + private static String formatPositiveDuration(Duration d){ + if (d.compareTo(MICRO_SECOND) < 0) { + // 0.000001 ms - 0.000999 ms + double outputMs = (double) d.toNanosPart() / 1_000_000; + return String.format("%.6f ms", outputMs); + } else if (d.compareTo(SECOND) < 0) { + // 0.001 ms - 999 ms + int valueLength = countLength(d.toNanosPart()); + int outputDigit = NANO_SIGNIFICANT_FIGURES - valueLength; + double outputMs = (double) d.toNanosPart() / 1_000_000; + return String.format("%." + outputDigit + "f ms", outputMs); + } else if (d.compareTo(MINUTE) < 0) { + // 1.00 s - 59.9 s + int valueLength = countLength(d.toSecondsPart()); + int outputDigit = MILL_SIGNIFICANT_FIGURES - valueLength; + double outputSecond = d.toSecondsPart() + (double) d.toMillisPart() / 1_000; + return String.format("%." + outputDigit + "f s", outputSecond); + } else if (d.compareTo(HOUR) < 0) { + // 1 m 0 s - 59 m 59 s + return String.format("%d m %d s", d.toMinutesPart(), d.toSecondsPart()); + } else if (d.compareTo(DAY) < 0) { + // 1 h 0 m - 23 h 59 m + return String.format("%d h %d m", d.toHoursPart(), d.toMinutesPart()); + } else { + // 1 d 0 h - + return String.format("%d d %d h", d.toDaysPart(), d.toHoursPart()); + } + } + + private static int countLength(long value){ + return (int) Math.log10(value) + 1; + } + + private static Duration roundDuration(Duration d) { + if (d.equals(Duration.ZERO)) { + return d; + } else if(d.isNegative()) { + Duration roundedPositiveDuration = roundPositiveDuration(d.abs()); + return roundedPositiveDuration.negated(); + } else { + return roundPositiveDuration(d); + } + } + + private static Duration roundPositiveDuration(Duration d){ + if (d.compareTo(MICRO_SECOND) < 0) { + // No round + return d; + } else if (d.compareTo(SECOND) < 0) { + // Round significant figures to three digits + int valueLength = countLength(d.toNanosPart()); + int roundValue = (int) Math.pow(BASE, valueLength - DISPLAY_NANO_DIGIT); + long roundedNanos = Math.round((double) d.toNanosPart() / roundValue) * roundValue; + return d.truncatedTo(ChronoUnit.SECONDS).plusNanos(roundedNanos); + } else if (d.compareTo(MINUTE) < 0) { + // Round significant figures to three digits + int valueLength = countLength(d.toSecondsPart()); + int roundValue = (int) Math.pow(BASE, valueLength); + long roundedMills = Math.round((double) d.toMillisPart() / roundValue) * roundValue; + return d.truncatedTo(ChronoUnit.SECONDS).plusMillis(roundedMills); + } else if (d.compareTo(HOUR) < 0) { + // Round for more than 500 ms or less + return d.plusMillis(SECOND.dividedBy(2).toMillisPart()).truncatedTo(ChronoUnit.SECONDS); + } else if (d.compareTo(DAY) < 0) { + // Round for more than 30 seconds or less + return d.plusSeconds(MINUTE.dividedBy(2).toSecondsPart()).truncatedTo(ChronoUnit.MINUTES); + } else { + // Round for more than 30 minutes or less + return d.plusMinutes(HOUR.dividedBy(2).toMinutesPart()).truncatedTo(ChronoUnit.HOURS); + } + } + + public static String formatClass(RecordedClass clazz) { + String name = clazz.getName(); + if (name.startsWith("[")) { + return decodeDescriptors(name, "").getFirst(); + } + return name; + } + + // Tjis method can't handle Long.MIN_VALUE because absolute value is negative + private static String formatDataAmount(String formatter, long amount) { + int exp = (int) (Math.log(Math.abs(amount)) / Math.log(1024)); + char unitPrefix = "kMGTPE".charAt(exp - 1); + return String.format(formatter, amount / Math.pow(1024, exp), unitPrefix); + } + + public static String formatBytesCompact(long bytes) { + if (bytes < 1024) { + return String.valueOf(bytes); + } + return formatDataAmount("%.1f%cB", bytes); + } + + public static String formatBits(long bits) { + if (bits == 1 || bits == -1) { + return bits + " bit"; + } + if (bits < 1024 && bits > -1024) { + return bits + " bits"; + } + return formatDataAmount("%.1f %cbit", bits); + } + + public static String formatBytes(long bytes) { + if (bytes == 1 || bytes == -1) { + return bytes + " byte"; + } + if (bytes < 1024 && bytes > -1024) { + return bytes + " bytes"; + } + return formatDataAmount("%.1f %cB", bytes); + } + + public static String formatBytesPerSecond(long bytes) { + if (bytes < 1024 && bytes > -1024) { + return bytes + " byte/s"; + } + return formatDataAmount("%.1f %cB/s", bytes); + } + + public static String formatBitsPerSecond(long bits) { + if (bits < 1024 && bits > -1024) { + return bits + " bps"; + } + return formatDataAmount("%.1f %cbps", bits); + } + + public static String formatMethod(RecordedMethod m, boolean compact) { + StringBuilder sb = new StringBuilder(); + sb.append(m.getType().getName()); + sb.append("."); + sb.append(m.getName()); + sb.append("("); + StringJoiner sj = new StringJoiner(", "); + String md = m.getDescriptor().replace("/", "."); + String parameter = md.substring(1, md.lastIndexOf(")")); + List parameters = decodeDescriptors(parameter, ""); + if (!compact) { + for (String qualifiedName :parameters) { + String typeName = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1); + sj.add(typeName); + } + sb.append(sj.toString()); + } else { + if (!parameters.isEmpty()) { + sb.append("..."); + } + } + sb.append(")"); + + return sb.toString(); + } + + private static List decodeDescriptors(String descriptor, String arraySize) { + List descriptors = new ArrayList<>(); + for (int index = 0; index < descriptor.length(); index++) { + String arrayBrackets = ""; + while (descriptor.charAt(index) == '[') { + arrayBrackets = arrayBrackets + "[" + arraySize + "]"; + arraySize = ""; + index++; + } + char c = descriptor.charAt(index); + String type; + switch (c) { + case 'L': + int endIndex = descriptor.indexOf(';', index); + type = descriptor.substring(index + 1, endIndex); + index = endIndex; + break; + case 'I': + type = "int"; + break; + case 'J': + type = "long"; + break; + case 'Z': + type = "boolean"; + break; + case 'D': + type = "double"; + break; + case 'F': + type = "float"; + break; + case 'S': + type = "short"; + break; + case 'C': + type = "char"; + break; + case 'B': + type = "byte"; + break; + default: + type = ""; + } + descriptors.add(type + arrayBrackets); + } + return descriptors; + } + + public static String formatTimestamp(Instant instant) { + return LocalTime.ofInstant(instant, ZoneId.systemDefault()).format(DATE_FORMAT); + } +} diff --git a/test/jdk/jdk/jfr/jcmd/TestJcmdView.java b/test/jdk/jdk/jfr/jcmd/TestJcmdView.java new file mode 100644 index 00000000000..f6857b759a8 --- /dev/null +++ b/test/jdk/jdk/jfr/jcmd/TestJcmdView.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.jcmd; + +import java.util.concurrent.CountDownLatch; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordingStream; +import jdk.test.lib.process.OutputAnalyzer; +/** + * @test + * @summary The test verifies JFR.view command + * @key jfr + * @requires vm.hasJFR + * @requires (vm.gc == "G1" | vm.gc == null) + * & vm.opt.ExplicitGCInvokesConcurrent != false + * @library /test/lib /test/jdk + * @run main/othervm -XX:-ExplicitGCInvokesConcurrent -XX:-DisableExplicitGC + * -XX:+UseG1GC jdk.jfr.jcmd.TestJcmdView + */ +public class TestJcmdView { + + public static void main(String... args) throws Throwable { + CountDownLatch jvmInformation = new CountDownLatch(1); + CountDownLatch systemGC = new CountDownLatch(1); + CountDownLatch gcHeapSummary = new CountDownLatch(1); + CountDownLatch oldCollection = new CountDownLatch(1); + CountDownLatch garbageCollection = new CountDownLatch(1); + + try (RecordingStream rs = new RecordingStream()) { + // Make sure chunks are not released after consumption + rs.setMaxSize(Long.MAX_VALUE); + rs.enable("jdk.JVMInformation").with("period", "beginChunk"); + rs.enable("jdk.SystemGC"); + rs.enable("jdk.GCHeapSummary"); + rs.enable("jdk.GarbageCollection"); + rs.enable("jdk.OldGarbageCollection"); + rs.enable("jdk.YoungGarbageCollection"); + rs.onEvent("jdk.JVMInformation", e -> { + jvmInformation.countDown(); + System.out.println(e); + }); + rs.onEvent("jdk.SystemGC", e -> { + systemGC.countDown(); + System.out.println(e); + }); + rs.onEvent("jdk.GCHeapSummary", e -> { + gcHeapSummary.countDown(); + System.out.println(e); + }); + rs.onEvent("jdk.OldGarbageCollection", e -> { + oldCollection.countDown(); + System.out.println(e); + }); + rs.onEvent("jdk.GarbageCollection", e-> { + garbageCollection.countDown(); + System.out.println(e); + }); + rs.startAsync(); + // Emit some GC events + System.gc(); + System.gc(); + System.gc(); + // Wait for them being in the repository + jvmInformation.await(); + systemGC.await(); + gcHeapSummary.await(); + oldCollection.countDown(); + // Test events that are in the current chunk + testEventType(); + testFormView(); + testTableView(); + rs.disable("jdk.JVMInformation"); + // Force chunk rotation + rotate(); + // Test events that are NOT in current chunk + testEventType(); + testFormView(); + testTableView(); + } + } + + private static void rotate() { + try (Recording r = new Recording()) { + r.start(); + } + } + + private static void testFormView() throws Throwable { + OutputAnalyzer output = JcmdHelper.jcmd("JFR.view", "jvm-information"); + // Verify title + output.shouldContain("JVM Information"); + // Verify field label + output.shouldContain("VM Arguments:"); + // Verify field value + long pid = ProcessHandle.current().pid(); + String lastThreeDigits = String.valueOf(pid % 1000); + output.shouldContain(lastThreeDigits); + } + + private static void testTableView() throws Throwable { + OutputAnalyzer output = JcmdHelper.jcmd("JFR.view", "verbose=true", "gc"); + // Verify heading + output.shouldContain("Longest Pause"); + // Verify verbose heading + output.shouldContain("(longestPause)"); + // Verify row contents + output.shouldContain("Old Garbage Collection"); + // Verify verbose query + output.shouldContain("SELECT"); + } + + private static void testEventType() throws Throwable { + OutputAnalyzer output = JcmdHelper.jcmd( + "JFR.view", "verbose=true", "width=300", "cell-height=100", "SystemGC"); + // Verify title + output.shouldContain("System GC"); + // Verify headings + output.shouldContain("Invoked Concurrent"); + // Verify verbose headings + output.shouldContain("invokedConcurrent"); + // Verify thread value + output.shouldContain(Thread.currentThread().getName()); + // Verify stack frame + output.shouldContain("TestJcmdView.main"); + } +} diff --git a/test/jdk/jdk/jfr/tool/TestView.java b/test/jdk/jdk/jfr/tool/TestView.java new file mode 100644 index 00000000000..bf9cdce8449 --- /dev/null +++ b/test/jdk/jdk/jfr/tool/TestView.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.tool; + +import java.io.FileWriter; +import java.nio.file.Files; +import java.nio.file.Path; + +import jdk.test.lib.Utils; +import jdk.test.lib.process.OutputAnalyzer; +/** + * @test + * @summary Test jfr view + * @key jfr + * @requires vm.hasJFR + * @requires (vm.gc == "G1" | vm.gc == null) + * & vm.opt.ExplicitGCInvokesConcurrent != false + * @library /test/lib /test/jdk + * @run main/othervm -XX:-ExplicitGCInvokesConcurrent -XX:-DisableExplicitGC + * -XX:+UseG1GC jdk.jfr.jcmd.TestJcmdView + */ +public class TestView { + + public static void main(String... args) throws Throwable { + testIncorrectUsage(); + String recordingFile = ExecuteHelper.createProfilingRecording().toAbsolutePath().toString(); + testEventType(recordingFile); + testFormView(recordingFile); + testTableView(recordingFile); + } + + private static void testIncorrectUsage() throws Throwable { + OutputAnalyzer output = ExecuteHelper.jfr("view"); + output.shouldContain("missing file"); + + output = ExecuteHelper.jfr("view", "missing.jfr"); + output.shouldContain("could not open file "); + + Path file = Utils.createTempFile("faked-file", ".jfr"); + FileWriter fw = new FileWriter(file.toFile()); + fw.write('d'); + fw.close(); + output = ExecuteHelper.jfr("view", "--wrongOption", file.toAbsolutePath().toString()); + output.shouldContain("unknown option"); + Files.delete(file); + } + + private static void testFormView(String recording) throws Throwable { + OutputAnalyzer output = ExecuteHelper.jfr("view", "jvm-information", recording); + // Verify title + output.shouldContain("JVM Information"); + // Verify field label + output.shouldContain("VM Arguments:"); + // Verify field value + long pid = ProcessHandle.current().pid(); + String lastThreeDigits = String.valueOf(pid % 1000); + output.shouldContain(lastThreeDigits); + } + + private static void testTableView(String recording) throws Throwable { + OutputAnalyzer output = ExecuteHelper.jfr("view", "--verbose", "gc", recording); + // Verify heading + output.shouldContain("Longest Pause"); + // Verify verbose heading + output.shouldContain("(longestPause)"); + // Verify row contents + output.shouldContain("Old Garbage Collection"); + // Verify verbose query + output.shouldContain("SELECT"); + } + + private static void testEventType(String recording) throws Throwable { + OutputAnalyzer output = ExecuteHelper.jfr( + "view", "--verbose", "--width", "300", "--cell-height", "100", "SystemGC", recording); + // Verify title + output.shouldContain("System GC"); + // Verify headings + output.shouldContain("Invoked Concurrent"); + // Verify verbose headings + output.shouldContain("invokedConcurrent"); + // Verify thread value + output.shouldContain(Thread.currentThread().getName()); + // Verify stack frame + output.shouldContain("ExecuteHelper.createProfilingRecording"); + } +}