diff --git a/IT/src/test/java/com/microsoft/gctoolkit/integration/UnifiedSafepointAggregationTest.java b/IT/src/test/java/com/microsoft/gctoolkit/integration/UnifiedSafepointAggregationTest.java new file mode 100644 index 000000000..a23aeee41 --- /dev/null +++ b/IT/src/test/java/com/microsoft/gctoolkit/integration/UnifiedSafepointAggregationTest.java @@ -0,0 +1,117 @@ +package com.microsoft.gctoolkit.integration; + +import com.microsoft.gctoolkit.GCToolKit; +import com.microsoft.gctoolkit.aggregator.Aggregates; +import com.microsoft.gctoolkit.aggregator.Aggregation; +import com.microsoft.gctoolkit.aggregator.Aggregator; +import com.microsoft.gctoolkit.aggregator.Collates; +import com.microsoft.gctoolkit.aggregator.EventSource; +import com.microsoft.gctoolkit.event.jvm.Safepoint; +import com.microsoft.gctoolkit.integration.io.TestLogFile; +import com.microsoft.gctoolkit.io.GCLogFile; +import com.microsoft.gctoolkit.io.SingleGCLogFile; +import com.microsoft.gctoolkit.jvm.JavaVirtualMachine; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * End to end coverage for the single safepoint line aggregation. + */ +@Tag("modulePath") +public class UnifiedSafepointAggregationTest { + + private SafepointSummary analyze(String logName) { + GCLogFile logFile = new SingleGCLogFile(Path.of(new TestLogFile(logName).getFile().getPath())); + GCToolKit gcToolKit = new GCToolKit(); + gcToolKit.loadAggregation(new SafepointSummary()); + JavaVirtualMachine machine = null; + try { + machine = gcToolKit.analyze(logFile); + } catch (IOException e) { + fail(e.getMessage()); + } + return machine.getAggregation(SafepointSummary.class).orElseGet(() -> { + fail("SAFEPOINT aggregation was not run for " + logName); + return null; + }); + } + + @Test + public void testZGCSafepoints() { + SafepointSummary summary = analyze("zgc/zgc.log"); + assertEquals(3478, summary.count(), "safepoint lines in zgc.log"); + assertTrue(summary.totalTimeToSafepoint() > 0.0d, "ZGC log should report time to safepoint"); + assertTrue(summary.reasons().contains("ZMarkStart"), "expected ZGC VM operations to be reported"); + } + + @Test + public void testG1Safepoints() { + SafepointSummary summary = analyze("g1gc/G1-80-16gbps2.log.0"); + assertEquals(2158, summary.count(), "safepoint lines in G1-80-16gbps2.log.0"); + assertTrue(summary.reasons().contains("G1CollectForAllocation")); + } + + @Test + public void testSerialSafepoints() { + SafepointSummary summary = analyze("serial/factorization-serialgc-tip.log"); + assertEquals(17, summary.count(), "safepoint lines in factorization-serialgc-tip.log"); + assertTrue(summary.reasons().contains("SerialGCCollect")); + } + + @Aggregates(EventSource.SAFEPOINT) + public static class SafepointAggregator extends Aggregator { + + public SafepointAggregator(SafepointSummary aggregation) { + super(aggregation); + register(Safepoint.class, this::process); + } + + private void process(Safepoint event) { + aggregation().record(event); + } + } + + @Collates(SafepointAggregator.class) + public static class SafepointSummary extends Aggregation { + + private final List reasons = new ArrayList<>(); + private double totalTimeToSafepoint = 0.0d; + + public void record(Safepoint event) { + reasons.add(event.getVmOperation()); + if (event.hasReachingSafepointDuration()) + totalTimeToSafepoint += event.getReachingSafepointDuration(); + } + + public int count() { + return reasons.size(); + } + + public List reasons() { + return reasons; + } + + public double totalTimeToSafepoint() { + return totalTimeToSafepoint; + } + + @Override + public boolean hasWarning() { + return false; + } + + @Override + public boolean isEmpty() { + return reasons.isEmpty(); + } + } +} diff --git a/api/src/main/java/com/microsoft/gctoolkit/GCToolKit.java b/api/src/main/java/com/microsoft/gctoolkit/GCToolKit.java index 6c258dcb7..a8bf87c92 100644 --- a/api/src/main/java/com/microsoft/gctoolkit/GCToolKit.java +++ b/api/src/main/java/com/microsoft/gctoolkit/GCToolKit.java @@ -251,7 +251,9 @@ private Set loadDataSourceParsers(Diary diary) { "com.microsoft.gctoolkit.parser.UnifiedGenerationalParser", "com.microsoft.gctoolkit.parser.UnifiedJVMEventParser", "com.microsoft.gctoolkit.parser.UnifiedSurvivorMemoryPoolParser", - "com.microsoft.gctoolkit.parser.ZGCParser" + "com.microsoft.gctoolkit.parser.ZGCParser", + "com.microsoft.gctoolkit.parser.vmops.SafepointParser", + "com.microsoft.gctoolkit.parser.vmops.UnifiedSafepointParser" }; dataSourceParsers = Arrays.stream(parsers) .map(parserName -> { diff --git a/api/src/main/java/com/microsoft/gctoolkit/event/jvm/Safepoint.java b/api/src/main/java/com/microsoft/gctoolkit/event/jvm/Safepoint.java index d74471367..8c97992c1 100644 --- a/api/src/main/java/com/microsoft/gctoolkit/event/jvm/Safepoint.java +++ b/api/src/main/java/com/microsoft/gctoolkit/event/jvm/Safepoint.java @@ -7,6 +7,9 @@ public class Safepoint extends JVMEvent { + private static final double NOT_REPORTED = -1.0d; // negative times.. don't make sense + private static final int NO_THREAD_COUNT = -1; + private final String vmOperation; private int totalNumberOfApplicationThreads; private int initiallyRunning; @@ -20,6 +23,15 @@ public class Safepoint extends JVMEvent { private int pageTrapCount; + //unified log fields + private double timeSinceLastSafepoint = NOT_REPORTED; + private double cleanupPhaseDuration = NOT_REPORTED; + private double reachingSafepointDuration = NOT_REPORTED; + private double atSafepointDuration = NOT_REPORTED; + private double leavingSafepointDuration = NOT_REPORTED; + private int runnableThreads = NO_THREAD_COUNT; + private int totalThreads = NO_THREAD_COUNT; + public Safepoint(String vmOperationName, DateTimeStamp timeStamp, double duration) { super(timeStamp, duration); this.vmOperation = vmOperationName; @@ -46,6 +58,28 @@ public void recordPageTrapCount(int pageTrapCount) { this.pageTrapCount = pageTrapCount; } + /** + * Records the phases that every safepoint line carries. + */ + public void recordPhases(double timeSinceLast, double reachingSafepoint, double atSafepoint) { + this.timeSinceLastSafepoint = timeSinceLast; + this.reachingSafepointDuration = reachingSafepoint; + this.atSafepointDuration = atSafepoint; + } + + public void recordCleanupPhaseDuration(double cleanup) { + this.cleanupPhaseDuration = cleanup; + } + + public void recordLeavingSafepointDuration(double leavingSafepoint) { + this.leavingSafepointDuration = leavingSafepoint; + } + + public void recordThreadsAtSafepoint(int runnable, int total) { + this.runnableThreads = runnable; + this.totalThreads = total; + } + public String getVmOperation() { return vmOperation; } @@ -86,6 +120,62 @@ public int getPageTrapCount() { return pageTrapCount; } + public double getTimeSinceLastSafepoint() { + return timeSinceLastSafepoint; + } + + public double getReachingSafepointDuration() { + return reachingSafepointDuration; + } + + public double getCleanupPhaseDuration() { + return cleanupPhaseDuration; + } + + public double getAtSafepointDuration() { + return atSafepointDuration; + } + + public double getLeavingSafepointDuration() { + return leavingSafepointDuration; + } + + public int getRunnableThreads() { + return runnableThreads; + } + + public int getTotalThreads() { + return totalThreads; + } + + public boolean hasTimeSinceLastSafepoint() { + return timeSinceLastSafepoint != NOT_REPORTED; + } + + public boolean hasReachingSafepointDuration() { + return reachingSafepointDuration != NOT_REPORTED; + } + + public boolean hasCleanupPhaseDuration() { + return cleanupPhaseDuration != NOT_REPORTED; + } + + public boolean hasAtSafepointDuration() { + return atSafepointDuration != NOT_REPORTED; + } + + public boolean hasLeavingSafepointDuration() { + return leavingSafepointDuration != NOT_REPORTED; + } + + public boolean hasRunnableThreads() { + return runnableThreads != NO_THREAD_COUNT; + } + + public boolean hasTotalThreads() { + return totalThreads != NO_THREAD_COUNT; + } + @Override public String toString() { return this.getVmOperation(); diff --git a/parser/src/main/java/com/microsoft/gctoolkit/parser/UnifiedGCLogParser.java b/parser/src/main/java/com/microsoft/gctoolkit/parser/UnifiedGCLogParser.java index ee3e5369e..f89830386 100644 --- a/parser/src/main/java/com/microsoft/gctoolkit/parser/UnifiedGCLogParser.java +++ b/parser/src/main/java/com/microsoft/gctoolkit/parser/UnifiedGCLogParser.java @@ -8,7 +8,7 @@ import java.util.logging.Level; import java.util.logging.Logger; -abstract class UnifiedGCLogParser extends GCLogParser { +public abstract class UnifiedGCLogParser extends GCLogParser { private static final Logger LOGGER = Logger.getLogger(UnifiedGCLogParser.class.getName()); private static final boolean DEBUG = Boolean.getBoolean("microsoft.debug"); diff --git a/parser/src/main/java/com/microsoft/gctoolkit/parser/vmops/UnifiedSafepointParser.java b/parser/src/main/java/com/microsoft/gctoolkit/parser/vmops/UnifiedSafepointParser.java new file mode 100644 index 000000000..aafc197b0 --- /dev/null +++ b/parser/src/main/java/com/microsoft/gctoolkit/parser/vmops/UnifiedSafepointParser.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +package com.microsoft.gctoolkit.parser.vmops; + +import com.microsoft.gctoolkit.aggregator.EventSource; +import com.microsoft.gctoolkit.event.jvm.JVMTermination; +import com.microsoft.gctoolkit.event.jvm.Safepoint; +import com.microsoft.gctoolkit.jvm.Diary; +import com.microsoft.gctoolkit.message.ChannelName; +import com.microsoft.gctoolkit.message.JVMEventChannel; +import com.microsoft.gctoolkit.parser.GCLogTrace; +import com.microsoft.gctoolkit.parser.UnifiedGCLogParser; + +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class UnifiedSafepointParser extends UnifiedGCLogParser implements UnifiedSafepointPatterns { + + private static final Logger LOGGER = Logger.getLogger(UnifiedSafepointParser.class.getName()); + private static final double NANOS_PER_SECOND = 1_000_000_000.0d; + + private static final int VM_OPERATION_GROUP = 1; + private static final int TIME_SINCE_LAST_GROUP = 2; + private static final int REACHING_SAFEPOINT_GROUP = 3; + private static final int CLEANUP_GROUP = 4; + private static final int AT_SAFEPOINT_GROUP = 5; + private static final int LEAVING_SAFEPOINT_GROUP = 6; + private static final int TOTAL_GROUP = 7; + private static final int RUNNABLE_THREADS_GROUP = 8; + private static final int TOTAL_THREADS_GROUP = 9; + + public UnifiedSafepointParser() {} + + @Override + public Set eventsProduced() { + return Set.of(EventSource.SAFEPOINT); + } + + public String getName() { + return "UnifiedSafepointParser"; + } + + @Override + protected void process(String line) { + try { + GCLogTrace trace; + if ((trace = SAFEPOINT.parse(line)) != null) { + super.publish(ChannelName.JVM_EVENT_PARSER_OUTBOX, extractSafepoint(trace)); + } else if (line.equals(END_OF_DATA_SENTINEL)) { + super.publish(ChannelName.JVM_EVENT_PARSER_OUTBOX, new JVMTermination(getClock(), diary.getTimeOfFirstEvent())); + } + } catch (Throwable t) { + LOGGER.log(Level.FINE, "Missed: {0}", line); + } + } + + private Safepoint extractSafepoint(GCLogTrace trace) { + double total = nanosToSeconds(trace, TOTAL_GROUP); + Safepoint safepoint = new Safepoint(trace.getGroup(VM_OPERATION_GROUP), getClock().minus(total), total); + safepoint.recordPhases(nanosToSeconds(trace, TIME_SINCE_LAST_GROUP), + nanosToSeconds(trace, REACHING_SAFEPOINT_GROUP), + nanosToSeconds(trace, AT_SAFEPOINT_GROUP)); + if (trace.groupNotNull(CLEANUP_GROUP)) + safepoint.recordCleanupPhaseDuration(nanosToSeconds(trace, CLEANUP_GROUP)); + if (trace.groupNotNull(LEAVING_SAFEPOINT_GROUP)) + safepoint.recordLeavingSafepointDuration(nanosToSeconds(trace, LEAVING_SAFEPOINT_GROUP)); + if (trace.groupNotNull(RUNNABLE_THREADS_GROUP)) + safepoint.recordThreadsAtSafepoint(trace.getIntegerGroup(RUNNABLE_THREADS_GROUP), trace.getIntegerGroup(TOTAL_THREADS_GROUP)); + return safepoint; + } + + private static double nanosToSeconds(GCLogTrace trace, int group) { + return trace.getLongGroup(group) / NANOS_PER_SECOND; + } + + @Override + public boolean accepts(Diary diary) { + return (diary.isApplicationStoppedTime() || diary.isApplicationRunningTime()) && diary.isUnifiedLogging(); + } + + @Override + public void publishTo(JVMEventChannel bus) { + super.publishTo(bus); + } +} diff --git a/parser/src/main/java/com/microsoft/gctoolkit/parser/vmops/UnifiedSafepointPatterns.java b/parser/src/main/java/com/microsoft/gctoolkit/parser/vmops/UnifiedSafepointPatterns.java new file mode 100644 index 000000000..1d2660c4d --- /dev/null +++ b/parser/src/main/java/com/microsoft/gctoolkit/parser/vmops/UnifiedSafepointPatterns.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +package com.microsoft.gctoolkit.parser.vmops; + + +import com.microsoft.gctoolkit.parser.GCParseRule; +import com.microsoft.gctoolkit.parser.GenericTokens; + +public interface UnifiedSafepointPatterns extends GenericTokens { + //[1.361s][info][safepoint ] Safepoint "G1CollectForAllocation", Time since last: 295590960 ns, Reaching safepoint: 238882 ns, At safepoint: 23888872 ns, Total: 24127754 ns + //[0.557s][info][safepoint ] Safepoint "ICBufferFull", Time since last: 328272185 ns, Reaching safepoint: 4929 ns, Cleanup: 156005 ns, At safepoint: 852 ns, Leaving safepoint: 811 ns, Total: 162597 ns + //[2.803s][info][safepoint ] Safepoint "ZMarkStartYoungAndOld", Time since last: 1367178708 ns, Reaching safepoint: 91483 ns, At safepoint: 33824 ns, Leaving safepoint: 38612 ns, Total: 163919 ns, Threads: 3 runnable, 23 total + GCParseRule SAFEPOINT = new GCParseRule("Unified Safepoint", + "Safepoint " + SAFE_POINT_CAUSE + ", Time since last: (" + INTEGER + ") ns" + ", Reaching safepoint: (" + INTEGER + ") ns" + + ", (?:Cleanup: (" + INTEGER + ") ns, )?" + "At safepoint: (" + INTEGER + ") ns" + ", (?:Leaving safepoint: (" + INTEGER + ") ns, )?" + + "Total: (" + INTEGER + ") ns" + "(?:, Threads: (" + INTEGER + ") runnable, (" + INTEGER + ") total)?"); +} diff --git a/parser/src/main/java/module-info.java b/parser/src/main/java/module-info.java index 4ea91c2a3..9ee1563e9 100644 --- a/parser/src/main/java/module-info.java +++ b/parser/src/main/java/module-info.java @@ -31,6 +31,7 @@ com.microsoft.gctoolkit.parser.JVMEventParser, com.microsoft.gctoolkit.parser.UnifiedJVMEventParser, com.microsoft.gctoolkit.parser.vmops.SafepointParser, + com.microsoft.gctoolkit.parser.vmops.UnifiedSafepointParser, com.microsoft.gctoolkit.parser.SurvivorMemoryPoolParser, com.microsoft.gctoolkit.parser.UnifiedSurvivorMemoryPoolParser, com.microsoft.gctoolkit.parser.CMSTenuredPoolParser, diff --git a/parser/src/test/java/com/microsoft/gctoolkit/parser/vmops/UnifiedSafepointParserTest.java b/parser/src/test/java/com/microsoft/gctoolkit/parser/vmops/UnifiedSafepointParserTest.java new file mode 100644 index 000000000..4d3518cb6 --- /dev/null +++ b/parser/src/test/java/com/microsoft/gctoolkit/parser/vmops/UnifiedSafepointParserTest.java @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +package com.microsoft.gctoolkit.parser.vmops; + +import com.microsoft.gctoolkit.event.jvm.JVMEvent; +import com.microsoft.gctoolkit.event.jvm.Safepoint; +import com.microsoft.gctoolkit.jvm.Diarizer; +import com.microsoft.gctoolkit.parser.GCLogParser; +import com.microsoft.gctoolkit.parser.ParserTest; +import com.microsoft.gctoolkit.parser.jvm.UnifiedDiarizer; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * JDK 14 consolidated safepoint logging onto a single line (JDK-8221507). These tests cover that + * form, which is what every collector emits under -Xlog:safepoint on JDK 14 and later. + */ +public class UnifiedSafepointParserTest extends ParserTest { + + @Override + protected Diarizer diarizer() { + return new UnifiedDiarizer(); + } + + @Override + protected GCLogParser parser() { + return new UnifiedSafepointParser(); + } + + private List safepoints(String... lines) { + List events = feedParser(lines); + return events.stream() + .filter(Safepoint.class::isInstance) + .map(Safepoint.class::cast) + .collect(Collectors.toList()); + } + + private Safepoint parseSingleSafepoint(String line) { + List safepoints = safepoints(line); + assertEquals(1, safepoints.size(), "expected exactly one Safepoint from: " + line); + return safepoints.get(0); + } + + @Test + public void testG1Safepoint() { + Safepoint safepoint = parseSingleSafepoint( + "[1.361s][info][safepoint ] Safepoint \"G1CollectForAllocation\", Time since last: 295590960 ns, Reaching safepoint: 238882 ns, At safepoint: 23888872 ns, Total: 24127754 ns"); + + assertEquals("G1CollectForAllocation", safepoint.getVmOperation()); + assertDoubleEquals(0.024127754d, safepoint.getDuration()); + assertDoubleEquals(0.000238882d, safepoint.getReachingSafepointDuration()); + // The line is written when the safepoint ends, so the event starts total seconds earlier. + // DateTimeStamp rounds to milliseconds, so 1.361 - 0.024127754 lands on 1.337. + assertDoubleEquals(1.337d, safepoint.getDateTimeStamp().getTimeStamp()); + + assertDoubleEquals(0.295590960d, safepoint.getTimeSinceLastSafepoint()); + assertDoubleEquals(0.023888872d, safepoint.getAtSafepointDuration()); + // JDK 14 - 17 report neither of these + assertFalse(safepoint.hasCleanupPhaseDuration()); + assertFalse(safepoint.hasLeavingSafepointDuration()); + assertFalse(safepoint.hasTotalThreads()); + } + + @Test + public void testSerialSafepoint() { + Safepoint safepoint = parseSingleSafepoint( + "[0.773s][info][safepoint ] Safepoint \"SerialGCCollect\", Time since last: 477036488 ns, Reaching safepoint: 34173 ns, At safepoint: 130196669 ns, Total: 130230842 ns"); + + assertDoubleEquals(0.130230842d, safepoint.getDuration()); + assertDoubleEquals(0.000034173d, safepoint.getReachingSafepointDuration()); + assertEquals("SerialGCCollect", safepoint.getVmOperation()); + } + + @Test + public void testZGCSafepoint() { + Safepoint safepoint = parseSingleSafepoint( + "[3.114s][info][safepoint ] Safepoint \"ZMarkStart\", Time since last: 1050386 ns, Reaching safepoint: 197300 ns, At safepoint: 1248300 ns, Total: 1445600 ns"); + + assertDoubleEquals(0.0014456d, safepoint.getDuration()); + assertDoubleEquals(0.0001973d, safepoint.getReachingSafepointDuration()); + assertEquals("ZMarkStart", safepoint.getVmOperation()); + } + + /** + * JDK 21 reports a Cleanup phase between "Reaching safepoint" and "At safepoint". + */ + @Test + public void testSafepointWithCleanupPhase() { + Safepoint safepoint = parseSingleSafepoint( + "[1.136s][info][safepoint ] Safepoint \"XMarkStart\", Time since last: 190106589 ns, Reaching safepoint: 120017 ns, Cleanup: 62407 ns, At safepoint: 121268 ns, Total: 303692 ns"); + + assertDoubleEquals(0.000303692d, safepoint.getDuration()); + assertDoubleEquals(0.000120017d, safepoint.getReachingSafepointDuration()); + assertEquals("XMarkStart", safepoint.getVmOperation()); + + assertDoubleEquals(0.190106589d, safepoint.getTimeSinceLastSafepoint()); + assertDoubleEquals(0.000062407d, safepoint.getCleanupPhaseDuration()); + assertDoubleEquals(0.000121268d, safepoint.getAtSafepointDuration()); + assertFalse(safepoint.hasLeavingSafepointDuration()); + } + + /** + * Later JDK 21 builds add a "Leaving safepoint" phase as well. + */ + @Test + public void testSafepointWithCleanupAndLeavingPhases() { + Safepoint safepoint = parseSingleSafepoint( + "[0.557s][info][safepoint] Safepoint \"ICBufferFull\", Time since last: 328272185 ns, Reaching safepoint: 4929 ns, Cleanup: 156005 ns, At safepoint: 852 ns, Leaving safepoint: 811 ns, Total: 162597 ns"); + + assertDoubleEquals(0.000162597d, safepoint.getDuration()); + assertDoubleEquals(0.000004929d, safepoint.getReachingSafepointDuration()); + assertEquals("ICBufferFull", safepoint.getVmOperation()); + + assertDoubleEquals(0.328272185d, safepoint.getTimeSinceLastSafepoint()); + assertDoubleEquals(0.000156005d, safepoint.getCleanupPhaseDuration()); + assertDoubleEquals(0.000000852d, safepoint.getAtSafepointDuration()); + assertDoubleEquals(0.000000811d, safepoint.getLeavingSafepointDuration()); + assertFalse(safepoint.hasTotalThreads()); + } + + /** + * JDK 25 drops Cleanup, keeps Leaving safepoint, and appends thread counts after Total. + */ + @Test + public void testGenerationalZGCSafepointWithTrailingThreadCounts() { + Safepoint safepoint = parseSingleSafepoint( + "[2.803s][info][safepoint] Safepoint \"ZMarkStartYoungAndOld\", Time since last: 1367178708 ns, Reaching safepoint: 91483 ns, At safepoint: 33824 ns, Leaving safepoint: 38612 ns, Total: 163919 ns, Threads: 3 runnable, 23 total"); + + assertDoubleEquals(0.000163919d, safepoint.getDuration()); + assertDoubleEquals(0.000091483d, safepoint.getReachingSafepointDuration()); + assertEquals("ZMarkStartYoungAndOld", safepoint.getVmOperation()); + + assertDoubleEquals(1.367178708d, safepoint.getTimeSinceLastSafepoint()); + assertFalse(safepoint.hasCleanupPhaseDuration()); + assertDoubleEquals(0.000033824d, safepoint.getAtSafepointDuration()); + assertDoubleEquals(0.000038612d, safepoint.getLeavingSafepointDuration()); + assertTrue(safepoint.hasTotalThreads()); + assertEquals(3, safepoint.getRunnableThreads()); + assertEquals(23, safepoint.getTotalThreads()); + } + + /** + * HotSpot writes the phases such that they account for the whole safepoint. Verified against + * every safepoint line in the logs under gclogs, so it is a cheap check that the capture groups + * are aligned with the fields they are named for. + */ + @Test + public void testPhasesSumToTotal() { + List safepoints = safepoints( + "[0.557s][info][safepoint] Safepoint \"ICBufferFull\", Time since last: 328272185 ns, Reaching safepoint: 4929 ns, Cleanup: 156005 ns, At safepoint: 852 ns, Leaving safepoint: 811 ns, Total: 162597 ns", + "[1.136s][info][safepoint] Safepoint \"XMarkStart\", Time since last: 190106589 ns, Reaching safepoint: 120017 ns, Cleanup: 62407 ns, At safepoint: 121268 ns, Total: 303692 ns", + "[1.361s][info][safepoint] Safepoint \"G1CollectForAllocation\", Time since last: 295590960 ns, Reaching safepoint: 238882 ns, At safepoint: 23888872 ns, Total: 24127754 ns", + "[2.803s][info][safepoint] Safepoint \"ZMarkStartYoungAndOld\", Time since last: 1367178708 ns, Reaching safepoint: 91483 ns, At safepoint: 33824 ns, Leaving safepoint: 38612 ns, Total: 163919 ns, Threads: 3 runnable, 23 total"); + + assertEquals(4, safepoints.size()); + + for (Safepoint safepoint : safepoints) { + double sum = safepoint.getReachingSafepointDuration() + safepoint.getAtSafepointDuration() + + (safepoint.hasCleanupPhaseDuration() ? safepoint.getCleanupPhaseDuration() : 0.0d) + + (safepoint.hasLeavingSafepointDuration() ? safepoint.getLeavingSafepointDuration() : 0.0d); + assertEquals(safepoint.getDuration(), sum, 1.0e-9d, + "phases should account for the total for " + safepoint.getVmOperation()); + } + } + + /** + * Time since last exceeds Integer.MAX_VALUE nanoseconds, so the values must be read as longs. + */ + @Test + public void testLargeNanosecondValues() { + Safepoint safepoint = parseSingleSafepoint( + "[20.947s][info][safepoint ] Safepoint \"SerialCollectForAllocation\", Time since last: 19109967274 ns, Reaching safepoint: 38663 ns, At safepoint: 264693508 ns, Total: 264732171 ns"); + + assertDoubleEquals(19.109967274d, safepoint.getTimeSinceLastSafepoint()); + assertDoubleEquals(0.264732171d, safepoint.getDuration()); + assertDoubleEquals(0.000038663d, safepoint.getReachingSafepointDuration()); + } + + /** + * HotSpot adds and removes VM operations every release, so an unknown name must still yield an + * event carrying the timings. Only the reason is lost. + */ + @Test + public void testUnknownVMOperationStillPublishesTimings() { + Safepoint safepoint = parseSingleSafepoint( + "[1.234s][info][safepoint] Safepoint \"SomeFutureVMOperation\", Time since last: 1000000 ns, Reaching safepoint: 5000 ns, At safepoint: 20000 ns, Total: 25000 ns"); + + assertEquals("SomeFutureVMOperation", safepoint.getVmOperation()); + assertDoubleEquals(0.000025d, safepoint.getDuration()); + assertDoubleEquals(0.000005d, safepoint.getReachingSafepointDuration()); + } + + /** + * Operation names vary across releases and collectors; each must be reported verbatim. + */ + @Test + public void testVMOperationNamesAreReportedVerbatim() { + String[] operations = {"ParallelGCFailedAllocation", "ParallelGCSystemGC", + "ParallelCollectForAllocation", "ParallelGCCollect", "G1PauseRemark", "G1PauseCleanup", + "CollectForMetadataAllocation"}; + + String[] lines = new String[operations.length]; + for (int i = 0; i < operations.length; i++) + lines[i] = "[1.234s][info][safepoint] Safepoint \"" + operations[i] + + "\", Time since last: 1000000 ns, Reaching safepoint: 5000 ns, At safepoint: 20000 ns, Total: 25000 ns"; + + List safepoints = safepoints(lines); + assertEquals(operations.length, safepoints.size()); + + for (int i = 0; i < operations.length; i++) { + assertEquals(operations[i], safepoints.get(i).getVmOperation()); + } + } + + /** + * The JDK 9 - 13 form is handled by UnifiedJVMEventParser, so this parser must leave it alone + * rather than half matching it. + */ + @Test + public void testLegacySafepointFormIsIgnored() { + assertTrue(safepoints( + "[0.648s][info][safepoint ] Entering safepoint region: RevokeBias", + "[0.648s][info][safepoint ] Leaving safepoint region", + "[0.648s][info][safepoint ] Total time for which application threads were stopped: 0.0006115 seconds, Stopping threads took: 0.0003832 seconds" + ).isEmpty()); + } +}