diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/PrettyWriter.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/PrettyWriter.java index 77b3f4d1e8c41..a03c2630dd856 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/PrettyWriter.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/PrettyWriter.java @@ -59,13 +59,20 @@ */ public final class PrettyWriter extends EventPrintWriter { private static final String TYPE_OLD_OBJECT = Type.TYPES_PREFIX + "OldObject"; + private static final DateTimeFormatter TIME_FORMAT_EXACT = DateTimeFormatter.ofPattern("HH:mm:ss.SSSSSSSSS (yyyy-MM-dd)"); private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss.SSS (yyyy-MM-dd)"); private static final Long ZERO = 0L; + private final boolean showExact; private boolean showIds; private RecordedEvent currentEvent; - public PrettyWriter(PrintWriter destination) { + public PrettyWriter(PrintWriter destination, boolean showExact) { super(destination); + this.showExact = showExact; + } + + public PrettyWriter(PrintWriter destination) { + this(destination, false); } @Override @@ -508,7 +515,11 @@ private boolean printFormatted(ValueDescriptor field, Object value) { println("Forever"); return true; } - println(ValueFormatter.formatDuration(d)); + if (showExact) { + println(String.format("%.9f s", (double) d.toNanos() / 1_000_000_000)); + } else { + println(ValueFormatter.formatDuration(d)); + } return true; } if (value instanceof OffsetDateTime odt) { @@ -516,40 +527,34 @@ private boolean printFormatted(ValueDescriptor field, Object value) { println("N/A"); return true; } - println(TIME_FORMAT.format(odt)); + if (showExact) { + println(TIME_FORMAT_EXACT.format(odt)); + } else { + println(TIME_FORMAT.format(odt)); + } return true; } Percentage percentage = field.getAnnotation(Percentage.class); if (percentage != null) { if (value instanceof Number n) { - double d = n.doubleValue(); - println(String.format("%.2f", d * 100) + "%"); + double p = 100 * n.doubleValue(); + if (showExact) { + println(String.format("%.9f%%", p)); + } else { + println(String.format("%.2f%%", p)); + } return true; } } DataAmount dataAmount = field.getAnnotation(DataAmount.class); - if (dataAmount != null) { - if (value instanceof Number n) { - long amount = n.longValue(); - if (field.getAnnotation(Frequency.class) != null) { - if (dataAmount.value().equals(DataAmount.BYTES)) { - println(ValueFormatter.formatBytesPerSecond(amount)); - return true; - } - if (dataAmount.value().equals(DataAmount.BITS)) { - println(ValueFormatter.formatBitsPerSecond(amount)); - return true; - } - } else { - if (dataAmount.value().equals(DataAmount.BYTES)) { - println(ValueFormatter.formatBytes(amount)); - return true; - } - if (dataAmount.value().equals(DataAmount.BITS)) { - println(ValueFormatter.formatBits(amount)); - return true; - } - } + if (dataAmount != null && value instanceof Number number) { + boolean frequency = field.getAnnotation(Frequency.class) != null; + String unit = dataAmount.value(); + boolean bits = unit.equals(DataAmount.BITS); + boolean bytes = unit.equals(DataAmount.BYTES); + if (bits || bytes) { + formatMemory(number.longValue(), bytes, frequency); + return true; } } MemoryAddress memoryAddress = field.getAnnotation(MemoryAddress.class); @@ -571,6 +576,35 @@ private boolean printFormatted(ValueDescriptor field, Object value) { return false; } + private void formatMemory(long value, boolean bytesUnit, boolean frequency) { + if (showExact) { + StringBuilder sb = new StringBuilder(); + sb.append(value); + sb.append(bytesUnit ? " byte" : " bit"); + if (value > 1) { + sb.append("s"); + } + if (frequency) { + sb.append("/s"); + } + println(sb.toString()); + return; + } + if (frequency) { + if (bytesUnit) { + println(ValueFormatter.formatBytesPerSecond(value)); + } else { + println(ValueFormatter.formatBitsPerSecond(value)); + } + return; + } + if (bytesUnit) { + println(ValueFormatter.formatBytes(value)); + } else { + println(ValueFormatter.formatBits(value)); + } + } + public void setShowIds(boolean showIds) { this.showIds = showIds; } 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 569a76025005f..79dd04c379fa5 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 @@ -49,7 +49,7 @@ public String getName() { @Override public List getOptionSyntax() { List list = new ArrayList<>(); - list.add("[--xml|--json]"); + list.add("[--xml|--json|--exact]"); list.add("[--categories ]"); list.add("[--events ]"); list.add("[--stack-depth ]"); @@ -73,6 +73,8 @@ public void displayOptionUsage(PrintStream stream) { stream.println(); stream.println(" --json Print recording in JSON format"); stream.println(); + stream.println(" --exact Pretty-print numbers and timestamps with full precision."); + stream.println(); stream.println(" --categories Select events matching a category name."); stream.println(" The filter is a comma-separated list of names,"); stream.println(" simple and/or qualified, and/or quoted glob patterns"); @@ -95,7 +97,7 @@ public void displayOptionUsage(PrintStream stream) { char q = quoteCharacter(); stream.println(" jfr print --categories " + q + "GC,JVM,Java*" + q + " recording.jfr"); stream.println(); - stream.println(" jfr print --events "+ q + "jdk.*" + q +" --stack-depth 64 recording.jfr"); + stream.println(" jfr print --exact --events "+ q + "jdk.*" + q +" --stack-depth 64 recording.jfr"); stream.println(); stream.println(" jfr print --json --events CPULoad recording.jfr"); } @@ -140,6 +142,9 @@ public void execute(Deque options) throws UserSyntaxException, UserDataE throw new UserSyntaxException("not a valid value for --stack-depth"); } } + if (acceptFormatterOption(options, eventWriter, "--exact")) { + eventWriter = new PrettyWriter(pw, true);; + } if (acceptFormatterOption(options, eventWriter, "--json")) { eventWriter = new JSONWriter(pw); } @@ -155,7 +160,7 @@ public void execute(Deque options) throws UserSyntaxException, UserDataE optionCount = options.size(); } if (eventWriter == null) { - eventWriter = new PrettyWriter(pw); // default to pretty printer + eventWriter = new PrettyWriter(pw, false); // default to pretty printer } eventWriter.setStackDepth(stackDepth); if (!eventFilters.isEmpty()) { diff --git a/src/jdk.jfr/share/man/jfr.md b/src/jdk.jfr/share/man/jfr.md index b516c590084ef..f7ff1b627755b 100644 --- a/src/jdk.jfr/share/man/jfr.md +++ b/src/jdk.jfr/share/man/jfr.md @@ -106,7 +106,7 @@ Use `jfr print` to print the contents of a flight recording file to standard out The syntax is: -`jfr print` \[`--xml`|`--json`\] +`jfr print` \[`--xml`|`--json`|`--exact`\] \[`--categories` <*filters*>\] \[`--events` <*filters*>\] \[`--stack-depth` <*depth*>\] @@ -120,6 +120,9 @@ where: `--json` : Print the recording in JSON format. +`--exact` +: Pretty-print numbers and timestamps with full precision. + `--categories` <*filters*> : Select events matching a category name. The filter is a comma-separated list of names, diff --git a/test/jdk/jdk/jfr/tool/TestPrint.java b/test/jdk/jdk/jfr/tool/TestPrint.java index fcedca3bcb9de..997c73cf8b24c 100644 --- a/test/jdk/jdk/jfr/tool/TestPrint.java +++ b/test/jdk/jdk/jfr/tool/TestPrint.java @@ -24,9 +24,19 @@ package jdk.jfr.tool; import java.io.FileWriter; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import jdk.jfr.Recording; +import jdk.jfr.Event; +import jdk.jfr.Percentage; +import jdk.jfr.Timestamp; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.Timespan; +import jdk.jfr.DataAmount; +import jdk.jfr.Frequency; import jdk.test.lib.Utils; import jdk.test.lib.process.OutputAnalyzer; @@ -40,20 +50,105 @@ */ public class TestPrint { + static class ExactEvent extends Event { + @DataAmount(DataAmount.BITS) + long oneBit; + + @DataAmount(DataAmount.BITS) + long bits; + + @Frequency + @DataAmount(DataAmount.BITS) + long oneBitPerSecond; + + @Frequency + @DataAmount(DataAmount.BITS) + long bitsPerSecond; + + @DataAmount(DataAmount.BYTES) + long oneByte; + + @DataAmount(DataAmount.BYTES) + long bytes; + + @Frequency + @DataAmount(DataAmount.BYTES) + long oneBytePerSecond; + + @Frequency + @DataAmount(DataAmount.BYTES) + long bytesPerSecond; + + @Percentage + double percentage; + + @Timestamp(Timestamp.MILLISECONDS_SINCE_EPOCH) + long timestamp; + + @Timespan(Timespan.NANOSECONDS) + long timespan; + } + public static void main(String[] args) throws Throwable { + testNoFile(); + testMissingFile(); + testIncorrectOption(); + testExact(); + } + private static void testNoFile() throws Throwable { OutputAnalyzer output = ExecuteHelper.jfr("print"); output.shouldContain("missing file"); + } - output = ExecuteHelper.jfr("print", "missing.jfr"); + private static void testMissingFile() throws Throwable { + OutputAnalyzer output = ExecuteHelper.jfr("print", "missing.jfr"); output.shouldContain("could not open file "); + } - Path file = Utils.createTempFile("faked-print-file", ".jfr"); + private static void testIncorrectOption() throws Throwable { + Path file = Utils.createTempFile("faked-print-file", ".jfr"); FileWriter fw = new FileWriter(file.toFile()); fw.write('d'); fw.close(); - output = ExecuteHelper.jfr("print", "--wrongOption", file.toAbsolutePath().toString()); + OutputAnalyzer output = ExecuteHelper.jfr("print", "--wrongOption", file.toAbsolutePath().toString()); output.shouldContain("unknown option"); Files.delete(file); } + + private static void testExact() throws Throwable{ + try (Recording r = new Recording()) { + r.start(); + ExactEvent e = new ExactEvent(); + e.begin(); + e.oneBit = 1L; + e.bits = 222_222_222L; + e.oneBitPerSecond = 1L; + e.bitsPerSecond = 333_333_333L; + e.oneByte = 1L; + e.bytes = 444_444_444L; + e.oneBytePerSecond = 1L; + e.bytesPerSecond = 555_555_555L; + e.percentage = 0.666_666_666_66; + e.timestamp = 777; + e.timespan = 888_888_888L; + e.commit(); + r.stop(); + Path file = Path.of("exact.jfr"); + r.dump(file); + OutputAnalyzer output = ExecuteHelper.jfr("print", "--exact", file.toAbsolutePath().toString()); + output.shouldContain("oneBit = 1 bit"); + output.shouldContain("bits = 222222222 bits"); + output.shouldContain("oneBitPerSecond = 1 bit/s"); + output.shouldContain("bitsPerSecond = 333333333 bits/s"); + output.shouldContain("oneByte = 1 byte"); + output.shouldContain("bytes = 444444444 bytes"); + output.shouldContain("oneBytePerSecond = 1 byte/s"); + output.shouldContain("bytesPerSecond = 555555555 bytes/s"); + output.shouldContain(String.valueOf(100 * e.percentage) + "%"); + output.shouldContain("00.777000000 (19"); + output.shouldContain(String.valueOf(e.timespan) + " s"); + Files.delete(file); + } + } }