diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index a5022d550531..60cbbe6d353a 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -6,6 +6,7 @@ This changelog summarizes major changes to GraalVM Native Image. * (GR-48304) Red Hat added support for the JFR event ThreadAllocationStatistics. * (GR-48343) Red Hat added support for the JFR events AllocationRequiringGC and SystemGC. * (GR-48612) Enable `--strict-image-heap` by default. The option is now deprecated and can be removed from your argument list. A blog post with more information will follow shortly. +* (GR-47109) JFR event throttling support was added, along with the jdk.ObjectAllocationSample event. * (GR-48354) Remove native-image-agent legacy `build`-option * (GR-49221) Support for thread dumps can now be enabled with `--enable-monitoring=threaddump`. The option `-H:±DumpThreadStacksOnSignal` is deprecated and marked for removal. * (GR-48579) Options ParseOnce, ParseOnceJIT, and InlineBeforeAnalysis are deprecated and no longer have any effect. diff --git a/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/ThreadLocalAllocation.java b/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/ThreadLocalAllocation.java index dda62aa072cb..1265b7a7fbd2 100644 --- a/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/ThreadLocalAllocation.java +++ b/substratevm/src/com.oracle.svm.core.genscavenge/src/com/oracle/svm/core/genscavenge/ThreadLocalAllocation.java @@ -62,7 +62,7 @@ import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.hub.LayoutEncoding; import com.oracle.svm.core.jfr.JfrTicks; -import com.oracle.svm.core.jfr.events.ObjectAllocationInNewTLABEvent; +import com.oracle.svm.core.jfr.events.JfrAllocationEvents; import com.oracle.svm.core.log.Log; import com.oracle.svm.core.snippets.KnownIntrinsics; import com.oracle.svm.core.snippets.SubstrateForeignCallTarget; @@ -238,7 +238,7 @@ private static Object slowPathNewInstanceWithoutAllocating(DynamicHub hub) { AlignedHeader newTlab = HeapImpl.getChunkProvider().produceAlignedChunk(); return allocateInstanceInNewTlab(hub, size, newTlab); } finally { - ObjectAllocationInNewTLABEvent.emit(startTicks, hub, size, HeapParameters.getAlignedHeapChunkSize()); + JfrAllocationEvents.emit(startTicks, hub, size, HeapParameters.getAlignedHeapChunkSize()); DeoptTester.enableDeoptTesting(); } } @@ -319,7 +319,7 @@ private static Object slowPathNewArrayLikeObject0(DynamicHub hub, int length, Un } return array; } finally { - ObjectAllocationInNewTLABEvent.emit(startTicks, hub, size, tlabSize); + JfrAllocationEvents.emit(startTicks, hub, size, tlabSize); DeoptTester.enableDeoptTesting(); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java index 361d6ae7f91e..8f23d8387632 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/UninterruptibleUtils.java @@ -438,6 +438,17 @@ public static int abs(int a) { public static long abs(long a) { return (a < 0) ? -a : a; } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static long floor(double a) { + return (long) a; + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static long ceil(double a) { + long floor = floor(a); + return a > floor ? floor + 1 : floor; + } } public static class Byte { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEvent.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEvent.java index eddc156d657c..b54d41eb8e10 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEvent.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrEvent.java @@ -30,57 +30,75 @@ import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.thread.JavaThreads; +import java.util.ArrayList; +import java.util.List; + /** * This file contains the VM-level events that Native Image supports on all JDK versions. The event * IDs depend on the JDK version (see metadata.xml file) and are computed at image build time. */ public final class JfrEvent { - public static final JfrEvent ThreadStart = create("jdk.ThreadStart", false); - public static final JfrEvent ThreadEnd = create("jdk.ThreadEnd", false); - public static final JfrEvent ThreadCPULoad = create("jdk.ThreadCPULoad", false); - public static final JfrEvent DataLoss = create("jdk.DataLoss", false); - public static final JfrEvent ClassLoadingStatistics = create("jdk.ClassLoadingStatistics", false); - public static final JfrEvent InitialEnvironmentVariable = create("jdk.InitialEnvironmentVariable", false); - public static final JfrEvent InitialSystemProperty = create("jdk.InitialSystemProperty", false); - public static final JfrEvent JavaThreadStatistics = create("jdk.JavaThreadStatistics", false); - public static final JfrEvent JVMInformation = create("jdk.JVMInformation", false); - public static final JfrEvent OSInformation = create("jdk.OSInformation", false); - public static final JfrEvent PhysicalMemory = create("jdk.PhysicalMemory", false); - public static final JfrEvent ExecutionSample = create("jdk.ExecutionSample", false); - public static final JfrEvent NativeMethodSample = create("jdk.NativeMethodSample", false); - public static final JfrEvent GarbageCollection = create("jdk.GarbageCollection", true); - public static final JfrEvent GCPhasePause = create("jdk.GCPhasePause", true); - public static final JfrEvent GCPhasePauseLevel1 = create("jdk.GCPhasePauseLevel1", true); - public static final JfrEvent GCPhasePauseLevel2 = create("jdk.GCPhasePauseLevel2", true); - public static final JfrEvent GCPhasePauseLevel3 = create("jdk.GCPhasePauseLevel3", true); - public static final JfrEvent GCPhasePauseLevel4 = create("jdk.GCPhasePauseLevel4", true); - public static final JfrEvent SafepointBegin = create("jdk.SafepointBegin", true); - public static final JfrEvent SafepointEnd = create("jdk.SafepointEnd", true); - public static final JfrEvent ExecuteVMOperation = create("jdk.ExecuteVMOperation", true); - public static final JfrEvent JavaMonitorEnter = create("jdk.JavaMonitorEnter", true); - public static final JfrEvent ThreadPark = create("jdk.ThreadPark", true); - public static final JfrEvent JavaMonitorWait = create("jdk.JavaMonitorWait", true); - public static final JfrEvent JavaMonitorInflate = create("jdk.JavaMonitorInflate", true); - public static final JfrEvent ObjectAllocationInNewTLAB = create("jdk.ObjectAllocationInNewTLAB", false); - public static final JfrEvent GCHeapSummary = create("jdk.GCHeapSummary", false); - public static final JfrEvent ThreadAllocationStatistics = create("jdk.ThreadAllocationStatistics", false); - public static final JfrEvent SystemGC = create("jdk.SystemGC", true); - public static final JfrEvent AllocationRequiringGC = create("jdk.AllocationRequiringGC", false); - + private static final List events = new ArrayList<>(); + public static final JfrEvent ThreadStart = create("jdk.ThreadStart", false, false); + public static final JfrEvent ThreadEnd = create("jdk.ThreadEnd", false, false); + public static final JfrEvent ThreadCPULoad = create("jdk.ThreadCPULoad", false, false); + public static final JfrEvent DataLoss = create("jdk.DataLoss", false, false); + public static final JfrEvent ClassLoadingStatistics = create("jdk.ClassLoadingStatistics", false, false); + public static final JfrEvent InitialEnvironmentVariable = create("jdk.InitialEnvironmentVariable", false, false); + public static final JfrEvent InitialSystemProperty = create("jdk.InitialSystemProperty", false, false); + public static final JfrEvent JavaThreadStatistics = create("jdk.JavaThreadStatistics", false, false); + public static final JfrEvent JVMInformation = create("jdk.JVMInformation", false, false); + public static final JfrEvent OSInformation = create("jdk.OSInformation", false, false); + public static final JfrEvent PhysicalMemory = create("jdk.PhysicalMemory", false, false); + public static final JfrEvent ExecutionSample = create("jdk.ExecutionSample", false, false); + public static final JfrEvent NativeMethodSample = create("jdk.NativeMethodSample", false, false); + public static final JfrEvent GarbageCollection = create("jdk.GarbageCollection", true, false); + public static final JfrEvent GCPhasePause = create("jdk.GCPhasePause", true, false); + public static final JfrEvent GCPhasePauseLevel1 = create("jdk.GCPhasePauseLevel1", true, false); + public static final JfrEvent GCPhasePauseLevel2 = create("jdk.GCPhasePauseLevel2", true, false); + public static final JfrEvent GCPhasePauseLevel3 = create("jdk.GCPhasePauseLevel3", true, false); + public static final JfrEvent GCPhasePauseLevel4 = create("jdk.GCPhasePauseLevel4", true, false); + public static final JfrEvent SafepointBegin = create("jdk.SafepointBegin", true, false); + public static final JfrEvent SafepointEnd = create("jdk.SafepointEnd", true, false); + public static final JfrEvent ExecuteVMOperation = create("jdk.ExecuteVMOperation", true, false); + public static final JfrEvent JavaMonitorEnter = create("jdk.JavaMonitorEnter", true, false); + public static final JfrEvent ThreadPark = create("jdk.ThreadPark", true, false); + public static final JfrEvent JavaMonitorWait = create("jdk.JavaMonitorWait", true, false); + public static final JfrEvent JavaMonitorInflate = create("jdk.JavaMonitorInflate", true, false); + public static final JfrEvent ObjectAllocationInNewTLAB = create("jdk.ObjectAllocationInNewTLAB", false, false); + public static final JfrEvent GCHeapSummary = create("jdk.GCHeapSummary", false, false); + public static final JfrEvent ThreadAllocationStatistics = create("jdk.ThreadAllocationStatistics", false, false); + public static final JfrEvent SystemGC = create("jdk.SystemGC", true, false); + public static final JfrEvent AllocationRequiringGC = create("jdk.AllocationRequiringGC", false, false); + public static final JfrEvent ObjectAllocationSample = create("jdk.ObjectAllocationSample", false, true); private final long id; private final String name; private final boolean hasDuration; + private JfrThrottler throttler; @Platforms(Platform.HOSTED_ONLY.class) - public static JfrEvent create(String name, boolean hasDuration) { - return new JfrEvent(name, hasDuration); + public static JfrEvent create(String name, boolean hasDuration, boolean hasThrottling) { + return new JfrEvent(name, hasDuration, hasThrottling); } @Platforms(Platform.HOSTED_ONLY.class) - private JfrEvent(String name, boolean hasDuration) { + private JfrEvent(String name, boolean hasDuration, boolean hasThrottling) { this.id = JfrMetadataTypeLibrary.lookupPlatformEvent(name); this.name = name; this.hasDuration = hasDuration; + if (hasThrottling) { + throttler = new JfrThrottler(); + } + events.add(this); + } + + public static List getEvents() { + return events; + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public JfrThrottler getThrottler() { + return throttler; } @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) @@ -113,6 +131,6 @@ public boolean shouldEmit(long durationTicks) { @Uninterruptible(reason = "Prevent races with VM operations that start/stop recording.", callerMustBe = true) private boolean shouldEmit0() { - return SubstrateJVM.get().isRecording() && SubstrateJVM.get().isEnabled(this); + return SubstrateJVM.get().shouldCommit(this) && SubstrateJVM.get().isRecording() && SubstrateJVM.get().isEnabled(this); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java index f77545e4f5e8..f4c239fb3831 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrManager.java @@ -86,6 +86,7 @@ public RuntimeSupport.Hook startupHook() { return isFirstIsolate -> { parseFlightRecorderLogging(SubstrateOptions.FlightRecorderLogging.getValue()); periodicEventSetup(); + SubstrateJVM.getJfrRandom().resetSeed(); if (isJFREnabled()) { initRecording(); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThrottler.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThrottler.java new file mode 100644 index 000000000000..d67327f2187d --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThrottler.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2023, Red Hat Inc. 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 com.oracle.svm.core.jfr; + +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.util.TimeUtils; +import com.oracle.svm.core.jfr.utils.JfrReadWriteLock; + +/** + * Each event that allows throttling should have its own throttler instance. Multiple threads may + * use the same throttler instance when emitting a particular JFR event type. The throttler uses a + * rotating window scheme where each window represents a time slice. The data from the current + * window is used to set the parameters of the next window. The active window is guaranteed not to + * change while there are threads using it for sampling. + * + * This class is based on JfrAdaptiveSampler in hotspot/share/jfr/support/jfrAdaptiveSampler.cpp and + * hotspot/share/jfr/support/jfrAdaptiveSampler.hpp. Commit + * hash:1100dbc6b2a1f2d5c431c6f5c6eb0b9092aee817. Openjdk version "22-internal". + */ +public class JfrThrottler { + private final JfrReadWriteLock rwlock; + private volatile boolean disabled; + protected volatile JfrThrottlerWindow activeWindow; + + // The following are set to match the values in OpenJDK + protected static final int WINDOW_DIVISOR = 5; + protected static final int LOW_RATE_UPPER_BOUND = 9; + protected JfrThrottlerWindow window0; + protected JfrThrottlerWindow window1; + + // The following fields are only be accessed by threads holding the writer lock + private double ewmaPopulationSizeAlpha = 0; + private boolean reconfigure; + private long accumulatedDebtCarryLimit; + private long accumulatedDebtCarryCount; + protected long periodNs; + protected long eventSampleSize; + protected double avgPopulationSize = 0; + + public JfrThrottler() { + reconfigure = false; + disabled = true; + window0 = new JfrThrottlerWindow(); + window1 = new JfrThrottlerWindow(); + activeWindow = window0; + rwlock = new JfrReadWriteLock(); + } + + /** + * Convert rate to samples/second if possible. Want to avoid a long period with large windows + * with a large number of samples per window in favor of many smaller windows. This is in the + * critical section because setting the sample size and period must be done together atomically. + * Otherwise, we risk a window's params being set with only one of the two updated. + */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private void normalize(long samplesPerPeriod, double periodMs) { + assert rwlock.isCurrentThreadWriteOwner(); + // Do we want more than 10samples/s ? If so convert to samples/s + double periodsPerSecond = 1000.0 / periodMs; + double samplesPerSecond = samplesPerPeriod * periodsPerSecond; + if (samplesPerSecond > LOW_RATE_UPPER_BOUND && periodMs > TimeUtils.millisPerSecond) { + this.periodNs = TimeUtils.nanosPerSecond; + this.eventSampleSize = (long) samplesPerSecond; + return; + } + + this.eventSampleSize = samplesPerPeriod; + this.periodNs = TimeUtils.millisToNanos((long) periodMs); + } + + @Uninterruptible(reason = "Avoid deadlock due to locking without transition.") + public void setThrottle(long eventSampleSize, long periodMs) { + if (eventSampleSize == Target_jdk_jfr_internal_settings_ThrottleSetting.OFF) { + disabled = true; + return; + } else if (eventSampleSize < 0 || periodMs < 0) { + return; + } + + // Blocking lock because new settings MUST be applied. + rwlock.writeLockNoTransition(); + try { + normalize(eventSampleSize, periodMs); + reconfigure = true; + rotateWindow(); + } finally { + rwlock.unlock(); + } + disabled = false; + } + + /** + * Acquiring the reader lock before using the active window prevents the active window from + * changing while sampling is in progress. If we encounter an expired window, there's no point + * in sampling, so the reader lock can be returned. An expired window should be rotated. The + * writer lock must be acquired before attempting to rotate. Once an expired window is detected, + * it is likely, but not guaranteed, to be rotated before any NEW threads (readers) are allowed + * to begin sampling. + */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public boolean sample() { + if (disabled) { + return true; + } + // New readers will block here if there is a writer waiting for the lock. + rwlock.readLockNoTransition(); + try { + boolean expired = activeWindow.isExpired(); + if (expired) { + rwlock.unlock(); + rwlock.writeLockNoTransition(); + /* + * Once in the critical section, ensure the active window is still expired. Another + * thread may have already handled the expired window, or new settings may have + * already triggered a rotation. + */ + if (activeWindow.isExpired()) { + rotateWindow(); + } + return false; + } + return activeWindow.sample(); + } finally { + rwlock.unlock(); + } + } + + /** + * Only one thread should be rotating at once. If rotating due to an expired window, then other + * threads that try to rotate due to expiry, will simply return false. If there's a race with a + * thread updating settings, then one will just have to wait for the other to finish. Order + * doesn't really matter as long as they are not interrupted. + */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private void rotateWindow() { + assert rwlock.isCurrentThreadWriteOwner(); + configure(); + installNextWindow(); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private void installNextWindow() { + assert rwlock.isCurrentThreadWriteOwner(); + activeWindow = getNextWindow(); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private JfrThrottlerWindow getNextWindow() { + assert rwlock.isCurrentThreadWriteOwner(); + if (window0 == activeWindow) { + return window1; + } + return window0; + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private long computeAccumulatedDebtCarryLimit(long windowDurationNs) { + assert rwlock.isCurrentThreadWriteOwner(); + if (periodNs == 0 || windowDurationNs >= TimeUtils.nanosPerSecond) { + return 1; + } + return TimeUtils.nanosPerSecond / windowDurationNs; + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private long amortizeDebt(JfrThrottlerWindow lastWindow) { + assert rwlock.isCurrentThreadWriteOwner(); + if (accumulatedDebtCarryCount == accumulatedDebtCarryLimit) { + accumulatedDebtCarryCount = 1; + return 0; // reset because new settings have been applied + } + accumulatedDebtCarryCount++; + // Did we sample less than we were supposed to? + return lastWindow.samplesExpected() - lastWindow.samplesTaken(); + } + + /** + * Handles the case where the sampling rate is very low. + */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private void setSamplePointsAndWindowDuration() { + assert rwlock.isCurrentThreadWriteOwner(); + assert reconfigure; + JfrThrottlerWindow next = getNextWindow(); + long samplesPerWindow = eventSampleSize / WINDOW_DIVISOR; + long windowDurationNs = periodNs / WINDOW_DIVISOR; + /* + * If period isn't 1s, then we're effectively taking under 10 samples/s because the values + * have already undergone normalization. + */ + if (eventSampleSize <= LOW_RATE_UPPER_BOUND || periodNs > TimeUtils.nanosPerSecond) { + samplesPerWindow = eventSampleSize; + windowDurationNs = periodNs; + } + activeWindow.samplesPerWindow = samplesPerWindow; + activeWindow.windowDurationNs = windowDurationNs; + next.samplesPerWindow = samplesPerWindow; + next.windowDurationNs = windowDurationNs; + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private void configure() { + assert rwlock.isCurrentThreadWriteOwner(); + JfrThrottlerWindow next = getNextWindow(); + + // Store updated parameters to both windows. + if (reconfigure) { + setSamplePointsAndWindowDuration(); + accumulatedDebtCarryLimit = computeAccumulatedDebtCarryLimit(next.windowDurationNs); + // This effectively means we reset the debt count upon reconfiguration + accumulatedDebtCarryCount = accumulatedDebtCarryLimit; + avgPopulationSize = 0; + ewmaPopulationSizeAlpha = 1.0 / windowLookback(next); + reconfigure = false; + } + + next.configure(amortizeDebt(activeWindow), projectPopulationSize(activeWindow.measuredPopSize.get())); + } + + /** + * The lookback values are set to match the values in OpenJDK. + */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected static double windowLookback(JfrThrottlerWindow window) { + if (window.windowDurationNs <= TimeUtils.nanosPerSecond) { + return 25.0; + } else if (window.windowDurationNs <= TimeUtils.nanosPerSecond * 60L) { + return 5.0; + } else { + return 1.0; + } + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private double projectPopulationSize(long lastWindowMeasuredPop) { + assert rwlock.isCurrentThreadWriteOwner(); + avgPopulationSize = exponentiallyWeightedMovingAverage(lastWindowMeasuredPop, ewmaPopulationSizeAlpha, avgPopulationSize); + return avgPopulationSize; + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private static double exponentiallyWeightedMovingAverage(double currentMeasurement, double alpha, double prevEwma) { + return alpha * currentMeasurement + (1 - alpha) * prevEwma; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThrottlerWindow.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThrottlerWindow.java new file mode 100644 index 000000000000..ecaa01e4195c --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrThrottlerWindow.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2023, Red Hat Inc. 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 com.oracle.svm.core.jfr; + +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.jdk.UninterruptibleUtils; + +import static java.lang.Math.log; + +/** + * This class is based on JfrSamplerWindow in hotspot/share/jfr/support/jfrAdaptiveSampler.cpp and + * hotspot/share/jfr/support/jfrAdaptiveSampler.hpp. Commit hash: + * 1100dbc6b2a1f2d5c431c6f5c6eb0b9092aee817. Openjdk version "22-internal". + */ +public class JfrThrottlerWindow { + // reset every rotation + public UninterruptibleUtils.AtomicLong measuredPopSize; + public UninterruptibleUtils.AtomicLong endTicks; + + // Calculated every rotation based on params set by user and results from previous windows + public volatile long samplingInterval; + public volatile double maxSampleablePopulation; + + // Set by user + public long samplesPerWindow; + public long windowDurationNs; + public long debt; + + public JfrThrottlerWindow() { + windowDurationNs = 0; + samplesPerWindow = 0; + maxSampleablePopulation = 0; + measuredPopSize = new UninterruptibleUtils.AtomicLong(0); + endTicks = new UninterruptibleUtils.AtomicLong(JfrTicks.currentTimeNanos() + windowDurationNs); + samplingInterval = 1; + debt = 0; + } + + /** + * The reader lock must be acquired here. The active window will not change while in this + * method. + */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public boolean sample() { + // Guarantees only one thread can record the last event of the window + long prevMeasuredPopSize = measuredPopSize.getAndIncrement(); + + // Stop sampling if we're already over maxSampleablePopulation and we're over the expected + // samples per window. + return prevMeasuredPopSize % samplingInterval == 0 && + (prevMeasuredPopSize < maxSampleablePopulation); + } + + /** Thread's calling this method should have acquired the JftThrottler writer lcok. */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public long samplesTaken() { + if (measuredPopSize.get() > maxSampleablePopulation) { + return samplesExpected(); + } + return measuredPopSize.get() / samplingInterval; + } + + /** Thread's calling this method should have acquired the JftThrottler writer lock. */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public long samplesExpected() { + return samplesPerWindow + debt; + } + + /** Thread's calling this method should have acquired the JftThrottler writer lock. */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void configure(long newDebt, double projectedPopSize) { + this.debt = newDebt; + if (projectedPopSize <= samplesExpected()) { + samplingInterval = 1; + } else { + + double projectedProbability = samplesExpected() / projectedPopSize; + samplingInterval = nextGeometric(projectedProbability, SubstrateJVM.getNextRandomUniform()); + } + + this.maxSampleablePopulation = samplesExpected() * samplingInterval; + + // reset + measuredPopSize.set(0); + advanceEndTicks(); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected void advanceEndTicks() { + endTicks.set(System.nanoTime() + windowDurationNs); + } + + /** + * This method is essentially the same as jfrAdaptiveSampler::next_geometric(double, double) in + * the OpenJDK. + */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private static long nextGeometric(double probability, double u) { + double randomVar = u; + if (randomVar == 0.0) { + randomVar = 0.01; + } + // Inverse CDF for the geometric distribution. + return UninterruptibleUtils.Math.ceil(log(1.0 - randomVar) / log(1.0 - probability)); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public boolean isExpired() { + if (System.nanoTime() >= endTicks.get()) { + return true; + } + return false; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java index 8b4975136958..4a6f2c1b07bb 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java @@ -37,8 +37,10 @@ import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.heap.VMOperationInfos; import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.jfr.events.JfrAllocationEvents; import com.oracle.svm.core.jfr.logging.JfrLogging; import com.oracle.svm.core.jfr.sampler.JfrExecutionSampler; +import com.oracle.svm.core.jfr.utils.JfrRandom; import com.oracle.svm.core.sampler.SamplerBufferPool; import com.oracle.svm.core.sampler.SubstrateSigprofHandler; import com.oracle.svm.core.thread.JavaThreads; @@ -80,8 +82,8 @@ public class SubstrateJVM { private final SamplerBufferPool samplerBufferPool; private final JfrUnlockedChunkWriter unlockedChunkWriter; private final JfrRecorderThread recorderThread; - private final JfrLogging jfrLogging; + private final JfrRandom jfrRandom; private boolean initialized; /* @@ -117,6 +119,7 @@ public SubstrateJVM(List configurations, boolean writeFile) { recorderThread = new JfrRecorderThread(globalMemory, unlockedChunkWriter); jfrLogging = new JfrLogging(); + jfrRandom = new JfrRandom(); initialized = false; recording = false; @@ -187,8 +190,17 @@ public static JfrLogging getJfrLogging() { return get().jfrLogging; } + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static double getNextRandomUniform() { + return get().jfrRandom.nextUniform(); + } + + public static JfrRandom getJfrRandom() { + return get().jfrRandom; + } + @Uninterruptible(reason = "Prevent races with VM operations that start/stop recording.", callerMustBe = true) - protected boolean isRecording() { + public boolean isRecording() { return recording; } @@ -647,6 +659,28 @@ public boolean isEnabled(JfrEvent event) { return eventSettings[(int) event.getId()].isEnabled(); } + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public boolean shouldCommit(JfrEvent event) { + JfrThrottler throttler = event.getThrottler(); + if (throttler != null) { + return throttler.sample(); + } + return true; + } + + public boolean setThrottle(long eventTypeId, long eventSampleSize, long periodMs) { + for (JfrEvent event : JfrEvent.getEvents()) { + if (eventTypeId == event.getId()) { + JfrThrottler throttler = event.getThrottler(); + if (throttler != null) { + throttler.setThrottle(eventSampleSize, periodMs); + break; + } + } + } + return true; + } + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public void setLarge(JfrEvent event, boolean large) { eventSettings[(int) event.getId()].setLarge(large); @@ -694,6 +728,10 @@ private static class JfrBeginRecordingOperation extends JavaVMOperation { @Override protected void operate() { + for (IsolateThread isolateThread = VMThreads.firstThread(); isolateThread.isNonNull(); isolateThread = VMThreads.nextThread(isolateThread)) { + JfrAllocationEvents.resetLastAllocationSize(isolateThread); + } + SubstrateJVM.get().recording = true; /* Recording is enabled, so JFR events can be triggered at any time. */ SubstrateJVM.getThreadRepo().registerRunningThreads(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM.java index 15db4a59c588..5b7dcc2a5096 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM.java @@ -414,8 +414,7 @@ public static boolean setCutoff(long eventTypeId, long cutoffTicks) { @Substitute @TargetElement(onlyWith = JDK22OrLater.class) public static boolean setThrottle(long eventTypeId, long eventSampleSize, long periodMs) { - // Not supported but this method is called during JFR startup, so we can't throw an error. - return true; + return SubstrateJVM.get().setThrottle(eventTypeId, eventSampleSize, periodMs); } /** See {@link JVM#emitOldObjectSamples}. */ diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM_JDK21.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM_JDK21.java index 6b80d7613597..364cdfeccab6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM_JDK21.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_JVM_JDK21.java @@ -288,8 +288,7 @@ public boolean setCutoff(long eventTypeId, long cutoffTicks) { @Substitute public boolean setThrottle(long eventTypeId, long eventSampleSize, long periodMs) { - // Not supported but this method is called during JFR startup, so we can't throw an error. - return true; + return SubstrateJVM.get().setThrottle(eventTypeId, eventSampleSize, periodMs); } /** See {@link JVM#emitOldObjectSamples}. */ diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_settings_ThrottleSetting.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_settings_ThrottleSetting.java new file mode 100644 index 000000000000..fed5c41150a3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/Target_jdk_jfr_internal_settings_ThrottleSetting.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2023, Red Hat Inc. 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 com.oracle.svm.core.jfr; + +import com.oracle.svm.core.annotate.Alias; + +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "jdk.jfr.internal.settings.ThrottleSetting", onlyWith = HasJfrSupport.class) +final class Target_jdk_jfr_internal_settings_ThrottleSetting { + @Alias static long OFF; +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/ObjectAllocationInNewTLABEvent.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/JfrAllocationEvents.java similarity index 54% rename from substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/ObjectAllocationInNewTLABEvent.java rename to substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/JfrAllocationEvents.java index dcccd614a7b4..47e6855b6ced 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/ObjectAllocationInNewTLABEvent.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/events/JfrAllocationEvents.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 2022, 2022, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2022, 2022, Red Hat Inc. All rights reserved. + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2023, Red Hat Inc. 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 @@ -26,6 +26,7 @@ package com.oracle.svm.core.jfr.events; +import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.StackValue; import org.graalvm.word.UnsignedWord; @@ -37,16 +38,28 @@ import com.oracle.svm.core.jfr.JfrNativeEventWriterData; import com.oracle.svm.core.jfr.JfrNativeEventWriterDataAccess; import com.oracle.svm.core.jfr.SubstrateJVM; +import com.oracle.svm.core.threadlocal.FastThreadLocalFactory; +import com.oracle.svm.core.threadlocal.FastThreadLocalLong; +import com.oracle.svm.core.thread.PlatformThreads; +import com.oracle.svm.core.thread.JavaThreads; + +public class JfrAllocationEvents { + private static final FastThreadLocalLong lastAllocationSize = FastThreadLocalFactory.createLong("ObjectAllocationSampleEvent.lastAllocationSize"); + + public static void resetLastAllocationSize(IsolateThread thread) { + lastAllocationSize.set(thread, 0); + } -public class ObjectAllocationInNewTLABEvent { public static void emit(long startTicks, DynamicHub hub, UnsignedWord allocationSize, UnsignedWord tlabSize) { if (HasJfrSupport.get()) { - emit0(startTicks, hub, allocationSize, tlabSize); + Class clazz = DynamicHub.toClass(hub); + emitObjectAllocationInNewTLAB(startTicks, clazz, allocationSize, tlabSize); + emitObjectAllocationSample(startTicks, clazz); } } @Uninterruptible(reason = "Accesses a JFR buffer.") - private static void emit0(long startTicks, DynamicHub hub, UnsignedWord allocationSize, UnsignedWord tlabSize) { + private static void emitObjectAllocationInNewTLAB(long startTicks, Class clazz, UnsignedWord allocationSize, UnsignedWord tlabSize) { if (JfrEvent.ObjectAllocationInNewTLAB.shouldEmit()) { JfrNativeEventWriterData data = StackValue.get(JfrNativeEventWriterData.class); JfrNativeEventWriterDataAccess.initializeThreadLocalNativeBuffer(data); @@ -55,10 +68,28 @@ private static void emit0(long startTicks, DynamicHub hub, UnsignedWord allocati JfrNativeEventWriter.putLong(data, startTicks); JfrNativeEventWriter.putEventThread(data); JfrNativeEventWriter.putLong(data, SubstrateJVM.get().getStackTraceId(JfrEvent.ObjectAllocationInNewTLAB, 0)); - JfrNativeEventWriter.putClass(data, DynamicHub.toClass(hub)); + JfrNativeEventWriter.putClass(data, clazz); JfrNativeEventWriter.putLong(data, allocationSize.rawValue()); JfrNativeEventWriter.putLong(data, tlabSize.rawValue()); JfrNativeEventWriter.endSmallEvent(data); } } + + @Uninterruptible(reason = "Accesses a JFR buffer.") + private static void emitObjectAllocationSample(long startTicks, Class clazz) { + if (JfrEvent.ObjectAllocationSample.shouldEmit()) { + long currentAllocationSize = PlatformThreads.getThreadAllocatedBytes(JavaThreads.getCurrentThreadId()); + long weight = currentAllocationSize - lastAllocationSize.get(); + JfrNativeEventWriterData data = StackValue.get(JfrNativeEventWriterData.class); + JfrNativeEventWriterDataAccess.initializeThreadLocalNativeBuffer(data); + JfrNativeEventWriter.beginSmallEvent(data, JfrEvent.ObjectAllocationSample); + JfrNativeEventWriter.putLong(data, startTicks); + JfrNativeEventWriter.putEventThread(data); + JfrNativeEventWriter.putLong(data, SubstrateJVM.get().getStackTraceId(JfrEvent.ObjectAllocationSample, 0)); + JfrNativeEventWriter.putClass(data, clazz); + JfrNativeEventWriter.putLong(data, weight); + JfrNativeEventWriter.endSmallEvent(data); + lastAllocationSize.set(currentAllocationSize); + } + } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/utils/JfrRandom.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/utils/JfrRandom.java new file mode 100644 index 000000000000..b07a3ea38487 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/utils/JfrRandom.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2023, Red Hat Inc. 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 com.oracle.svm.core.jfr.utils; + +import org.graalvm.nativeimage.Platforms; +import com.oracle.svm.core.Uninterruptible; +import org.graalvm.nativeimage.Platform; +import com.oracle.svm.core.locks.VMMutex; + +/** + * This class is essentially the same as JfrPRNG in + * jdk/src/hotspot/shar/jfr/utilities/jfrRandom.inline.hpp in the OpenJDK. Commit hash: + * 1100dbc6b2a1f2d5c431c6f5c6eb0b9092aee817. + */ +public class JfrRandom { + private static final long prngMult = 25214903917L; + private static final long prngAdd = 11; + private static final long prngModPower = 48; + private static final long modMask = (1L << prngModPower) - 1; + private volatile long random = 0; + + private VMMutex mutex; + + @Platforms(Platform.HOSTED_ONLY.class) + public JfrRandom() { + mutex = new VMMutex("JfrRandom"); + } + + /** + * This is the formula for RAND48 used in unix systems (linear congruential generator). This is + * also what JFR in hotspot uses. + */ + @Uninterruptible(reason = "Locking with no transition.") + private long nextRandom() { + // Should be atomic to avoid repeated values + mutex.lockNoTransition(); + try { + if (random == 0) { + random = System.currentTimeMillis(); + } + long next = (prngMult * random + prngAdd) & modMask; + random = next; + assert random > 0; + return next; + } finally { + mutex.unlock(); + } + } + + public void resetSeed() { + mutex.lock(); + try { + random = 0; + } finally { + mutex.unlock(); + } + } + + /** This logic is essentially copied from JfrPRNG in Hotspot. */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public double nextUniform() { + long next = nextRandom(); + // Take the top 26 bits + long masked = next >> (prngModPower - 26); + // Normalize between 0 and 1 + return masked / (double) (1L << 26); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/utils/JfrReadWriteLock.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/utils/JfrReadWriteLock.java new file mode 100644 index 000000000000..69b33d751f18 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/utils/JfrReadWriteLock.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2023, Red Hat Inc. 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 com.oracle.svm.core.jfr.utils; + +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.jdk.UninterruptibleUtils; +import jdk.graal.compiler.nodes.PauseNode; +import com.oracle.svm.core.thread.VMThreads; +import com.oracle.svm.core.jfr.SubstrateJVM; + +/** An uninterruptible read-write lock implementation using atomics with writer preference. */ +public class JfrReadWriteLock { + private static final long CURRENTLY_WRITING = Long.MAX_VALUE; + private final UninterruptibleUtils.AtomicLong ownerCount; + private final UninterruptibleUtils.AtomicLong waitingWriters; + private volatile long writeOwnerTid; // If this is set, then a writer owns the lock. Otherwise + // -1. + + public JfrReadWriteLock() { + ownerCount = new UninterruptibleUtils.AtomicLong(0); + waitingWriters = new UninterruptibleUtils.AtomicLong(0); + writeOwnerTid = -1; + } + + @Uninterruptible(reason = "This method does not do a transition, so the whole critical section must be uninterruptible.", callerMustBe = true) + public void readLockNoTransition() { + readTryLock(Integer.MAX_VALUE); + } + + @Uninterruptible(reason = "This method does not do a transition, so the whole critical section must be uninterruptible.", callerMustBe = true) + public void writeLockNoTransition() { + writeTryLock(Integer.MAX_VALUE); + } + + /** + * The bias towards writers does NOT ensure that there are no waiting writers at the time a + * reader enters the critical section. Readers only make a best-effort check there are no + * waiting writers before they attempt to acquire the lock to prevent writer starvation. + */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void readTryLock(int retries) { + int yields = 0; + for (int i = 0; i < retries; i++) { + long readers = ownerCount.get(); + // Only begin the attempt to enter the critical section if no writers are waiting or + // writes are in-progress. + if (waitingWriters.get() > 0 || readers == CURRENTLY_WRITING) { + yields = maybeYield(i, yields); + } else { + // Attempt to take the lock. + if (ownerCount.compareAndSet(readers, readers + 1)) { + return; + } + } + } + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void writeTryLock(int retries) { + // Increment the writer count to signal our intent to acquire the lock. + waitingWriters.incrementAndGet(); + try { + int yields = 0; + for (int i = 0; i < retries; i++) { + long readers = ownerCount.get(); + // Only enter the critical section if all in-progress readers have finished. + if (readers != 0) { + yields = maybeYield(i, yields); + } else { + // Attempt to acquire the lock. + if (ownerCount.compareAndSet(0, CURRENTLY_WRITING)) { + writeOwnerTid = SubstrateJVM.getCurrentThreadId(); + return; + } + } + } + } finally { + // Regardless of whether we eventually acquired the lock, signal we are done waiting. + long waiters = waitingWriters.decrementAndGet(); + assert waiters >= 0; + } + } + + /** + * This is essentially the same logic as in + * {@link com.oracle.svm.core.thread.JavaSpinLockUtils#tryLock(Object, long, int)}. + */ + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + private static int maybeYield(int retryCount, int yields) { + if ((retryCount & 0xff) == 0 && VMThreads.singleton().supportsNativeYieldAndSleep()) { + if (yields > 5) { + VMThreads.singleton().nativeSleep(1); + } else { + VMThreads.singleton().yield(); + return yields + 1; + } + } else { + PauseNode.pause(); + } + return yields; + } + + @Uninterruptible(reason = "Used in locking without transition, so the whole critical section must be uninterruptible.", callerMustBe = true) + public void unlock() { + if (writeOwnerTid < 0) { + // Readers own the lock. + long readerCount = ownerCount.decrementAndGet(); + assert readerCount >= 0; + return; + } + // A writer owns the lock. + assert isCurrentThreadWriteOwner(); + writeOwnerTid = -1; + ownerCount.set(0); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public boolean isCurrentThreadWriteOwner() { + return writeOwnerTid == SubstrateJVM.getCurrentThreadId(); + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/AbstractJfrTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/AbstractJfrTest.java index 14d0608e6051..c947177eae49 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/AbstractJfrTest.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/AbstractJfrTest.java @@ -97,7 +97,7 @@ protected static void checkRecording(EventValidator validator, Path path, JfrRec } } - private static List getEvents(Path path, String[] testedEvents, boolean validateTestedEventsOnly) throws IOException { + protected static List getEvents(Path path, String[] testedEvents, boolean validateTestedEventsOnly) throws IOException { /* Only return events that are in the list of tested events. */ ArrayList result = new ArrayList<>(); for (RecordedEvent event : RecordingFile.readAllEvents(path)) { diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestThrottler.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestThrottler.java new file mode 100644 index 000000000000..c2ff45da8f9f --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/TestThrottler.java @@ -0,0 +1,474 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2023, Red Hat Inc. 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 com.oracle.svm.test.jfr; + +import com.oracle.svm.core.NeverInline; +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.genscavenge.HeapParameters; +import com.oracle.svm.core.jfr.JfrEvent; +import com.oracle.svm.core.jfr.JfrThrottler; +import com.oracle.svm.core.jfr.JfrThrottlerWindow; +import com.oracle.svm.core.util.TimeUtils; +import com.oracle.svm.core.util.UnsignedUtils; +import jdk.jfr.Recording; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TestThrottler extends JfrRecordingTest { + + // Based on the hardcoded value in the throttler class. + private static final long WINDOWS_PER_PERIOD = 5; + // Arbitrary + private static final long WINDOW_DURATION_MS = 200; + private static final long SAMPLES_PER_WINDOW = 10; + private static final long SECOND_IN_MS = 1000; + + /** + * This is the simplest test that ensures that sampling stops after the cap is hit. Single + * thread. All sampling is done within the first window. No rotations. + */ + @Test + public void testCapSingleThread() { + // Doesn't rotate after starting sampling + JfrThrottler throttler = new JfrTestThrottler(); + throttler.setThrottle(SAMPLES_PER_WINDOW * WINDOWS_PER_PERIOD, WINDOW_DURATION_MS * WINDOWS_PER_PERIOD); + for (int i = 0; i < SAMPLES_PER_WINDOW * WINDOWS_PER_PERIOD; i++) { + boolean sample = throttler.sample(); + assertFalse("failed! should take sample if under window limit", i < SAMPLES_PER_WINDOW && !sample); + assertFalse("failed! should not take sample if over window limit", i >= SAMPLES_PER_WINDOW && sample); + } + } + + /** + * This test ensures that sampling stops after the cap is hit, even when multiple threads are + * doing sampling. + */ + @Test + public void testCapConcurrent() throws InterruptedException { + final long samplesPerWindow = 100000; + final int testingThreadCount = 10; + final AtomicInteger count = new AtomicInteger(); + List testingThreads = new ArrayList<>(); + JfrTestThrottler throttler = new JfrTestThrottler(); + throttler.beginTest(samplesPerWindow * WINDOWS_PER_PERIOD, WINDOW_DURATION_MS * WINDOWS_PER_PERIOD); + Runnable doSampling = () -> { + for (int i = 0; i < samplesPerWindow; i++) { + boolean sample = throttler.sample(); + if (sample) { + count.incrementAndGet(); + } + } + }; + count.set(0); + + for (int i = 0; i < testingThreadCount; i++) { + Thread worker = new Thread(doSampling); + worker.start(); + testingThreads.add(worker); + } + for (Thread thread : testingThreads) { + thread.join(); + } + + assertFalse("failed! Too many samples taken! " + count.get(), count.get() > samplesPerWindow); + // Previous measured population should be 3*samplesPerWindow + // Force window rotation and repeat. + count.set(0); + expireAndRotate(throttler); + for (int i = 0; i < testingThreadCount; i++) { + Thread worker = new Thread(doSampling); + worker.start(); + testingThreads.add(worker); + } + for (Thread thread : testingThreads) { + thread.join(); + } + + assertFalse("failed! Too many samples taken (after rotation)! " + count.get(), count.get() > samplesPerWindow); + } + + /** + * This test ensures that sampling stops after the cap is hit. Then sampling resumes once the + * window rotates. + */ + @Test + public void testExpiry() { + final long samplesPerWindow = 10; + JfrTestThrottler throttler = new JfrTestThrottler(); + throttler.beginTest(samplesPerWindow * WINDOWS_PER_PERIOD, WINDOW_DURATION_MS * WINDOWS_PER_PERIOD); + int count = 0; + + for (int i = 0; i < samplesPerWindow * 10; i++) { + boolean sample = throttler.sample(); + if (sample) { + count++; + } + } + + assertTrue("Should have taken maximum possible samples: " + samplesPerWindow + " but took:" + count, samplesPerWindow == count); + + // rotate window by advancing time forward + expireAndRotate(throttler); + + assertTrue("After window rotation, it should be possible to take more samples", throttler.sample()); + } + + /** + * This test checks the projected population after a window rotation. This is a test of the EWMA + * calculation. Window lookback is 25 and windowDuration is un-normalized because the period is + * not greater than 1s. + */ + @Test + public void testEWMA() { + // Results in 50 samples per second + final long samplesPerWindow = 10; + JfrTestThrottler throttler = new JfrTestThrottler(); + throttler.beginTest(samplesPerWindow * WINDOWS_PER_PERIOD, SECOND_IN_MS); + assertTrue(throttler.getWindowLookback() == 25.0); + // Arbitrarily chosen + int[] population = {310, 410, 610, 310, 910, 420, 770, 290, 880, 640, 220, 110, 330, 590}; + // actualProjections are the expected EWMA values + int[] actualProjections = {12, 28, 51, 61, 95, 108, 135, 141, 170, 189, 190, 187, 193, 209}; + for (int p = 0; p < population.length; p++) { + for (int i = 0; i < population[p]; i++) { + throttler.sample(); + } + expireAndRotate(throttler); + double projectedPopulation = throttler.getActiveWindowProjectedPopulationSize(); + assertTrue(actualProjections[p] == (int) projectedPopulation); + } + } + + /** + * Ensure debt is being calculated as expected. + */ + @Test + public void testDebt() { + final long samplesPerWindow = 10; + final long populationPerWindow = 50; + JfrTestThrottler throttler = new JfrTestThrottler(); + throttler.beginTest(samplesPerWindow * WINDOWS_PER_PERIOD, WINDOWS_PER_PERIOD * WINDOW_DURATION_MS); + + for (int p = 0; p < 50; p++) { + for (int i = 0; i < populationPerWindow; i++) { + throttler.sample(); + } + expireAndRotate(throttler); + } + + // Do not sample for this window. Rotate. + expireAndRotate(throttler); + + // Debt should be at least 10 because we took no samples last window. + long debt = throttler.getActiveWindowDebt(); + assertTrue("Should have debt from under sampling.", debt >= 10); + + // Limit max potential samples to half samplesPerWindow. This means debt must increase by at + // least samplesPerWindow/2. + for (int i = 0; i < samplesPerWindow / 2; i++) { + throttler.sample(); + } + expireAndRotate(throttler); + assertTrue("Should have debt from under sampling.", throttler.getActiveWindowDebt() >= debt + samplesPerWindow / 2); + + // Window lookback is 25. Do not sample for 25 windows. + for (int i = 0; i < 25; i++) { + expireAndRotate(throttler); + } + + // At this point sampling interval must be 1 because the projected population must be 0. + for (int i = 0; i < (samplesPerWindow + samplesPerWindow * WINDOWS_PER_PERIOD); i++) { + throttler.sample(); + } + + assertFalse(throttler.sample()); + + expireAndRotate(throttler); + assertTrue("Should not have any debt remaining.", throttler.getActiveWindowDebt() == 0); + } + + /** + * Tests normalization of sample size and period. + */ + @Test + public void testNormalization() { + long sampleSize = 10 * 600; + long periodMs = 60 * SECOND_IN_MS; + JfrTestThrottler throttler = new JfrTestThrottler(); + throttler.beginTest(sampleSize, periodMs); + assertTrue(throttler.getPeriodNs() + " " + throttler.getEventSampleSize(), + throttler.getEventSampleSize() == sampleSize / 60 && throttler.getPeriodNs() == 1000000 * SECOND_IN_MS); + + sampleSize = 10 * 3600; + periodMs = 3600 * SECOND_IN_MS; + throttler.setThrottle(sampleSize, periodMs); + assertTrue(throttler.getPeriodNs() + " " + throttler.getEventSampleSize(), + throttler.getEventSampleSize() == sampleSize / 3600 && throttler.getPeriodNs() == 1000000 * SECOND_IN_MS); + } + + /** + * Checks that no ObjectAllocationSample events are emitted when the sampling rate is 0. + */ + @Test + public void testZeroRate() throws Throwable { + // Test throttler in isolation + JfrTestThrottler throttler = new JfrTestThrottler(); + throttler.setThrottle(0, 2 * SECOND_IN_MS); + assertFalse(throttler.sample()); + throttler.setThrottle(10, 2 * SECOND_IN_MS); + assertTrue(throttler.sample()); + + // Test applying throttling settings to an actual recording + Recording recording = new Recording(); + recording.setDestination(createTempJfrFile()); + recording.enable(JfrEvent.ObjectAllocationSample.getName()).with("throttle", "0/s"); + recording.start(); + + final int alignedHeapChunkSize = UnsignedUtils.safeToInt(HeapParameters.getAlignedHeapChunkSize()); + allocateCharArray(alignedHeapChunkSize); + + recording.stop(); + recording.close(); + + // Call getEvents directly because we expect zero events (which ordinarily would result in + // failure). + assertTrue(getEvents(recording.getDestination(), new String[]{JfrEvent.ObjectAllocationSample.getName()}, true).size() == 0); + } + + @NeverInline("Prevent escape analysis.") + private static char[] allocateCharArray(int length) { + return new char[length]; + } + + @Test + public void testDistributionUniform() { + final int maxPopPerWindow = 2000; + final int minPopPerWindow = 2; + final int expectedSamplesPerWindow = 50; + testDistribution(() -> ThreadLocalRandom.current().nextInt(minPopPerWindow, maxPopPerWindow + 1), expectedSamplesPerWindow, 0.05); + } + + @Test + public void testDistributionHighRate() { + final int maxPopPerWindow = 2000; + final int expectedSamplesPerWindow = 50; + testDistribution(() -> maxPopPerWindow, expectedSamplesPerWindow, 0.02); + } + + @Test + public void testDistributionLowRate() { + final int minPopPerWindow = 2; + testDistribution(() -> minPopPerWindow, minPopPerWindow, 0.05); + } + + @Test + public void testDistributionEarlyBurst() { + final int maxPopPerWindow = 2000; + final int expectedSamplesPerWindow = 50; + final int accumulatedDebtCarryLimit = 10; // 1000 / windowDurationMs + AtomicInteger count = new AtomicInteger(1); + testDistribution(() -> count.getAndIncrement() % accumulatedDebtCarryLimit == 1 ? maxPopPerWindow : 0, expectedSamplesPerWindow, 0.9); + } + + @Test + public void testDistributionMidBurst() { + final int maxPopPerWindow = 2000; + final int expectedSamplesPerWindow = 50; + final int accumulatedDebtCarryLimit = 10; // 1000 / windowDurationMs + AtomicInteger count = new AtomicInteger(1); + testDistribution(() -> count.getAndIncrement() % accumulatedDebtCarryLimit == 5 ? maxPopPerWindow : 0, expectedSamplesPerWindow, 0.5); + } + + @Test + public void testDistributionLateBurst() { + final int maxPopPerWindow = 2000; + final int expectedSamplesPerWindow = 50; + final int accumulatedDebtCarryLimit = 10; // 1000 / windowDurationMs + AtomicInteger count = new AtomicInteger(1); + testDistribution(() -> count.getAndIncrement() % accumulatedDebtCarryLimit == 0 ? maxPopPerWindow : 0, expectedSamplesPerWindow, 0.0); + } + + /** + * This is a more involved test that checks the sample distribution. It has been adapted from + * JfrGTestAdaptiveSampling in the OpenJDK. + */ + private static void testDistribution(IncomingPopulation incomingPopulation, int samplePointsPerWindow, double errorFactor) { + final int distributionSlots = 100; + final int windowDurationMs = 100; + final int windowCount = 10000; + final int expectedSamplesPerWindow = 50; + final int expectedSamples = expectedSamplesPerWindow * windowCount; + + JfrTestThrottler throttler = new JfrTestThrottler(); + throttler.beginTest(expectedSamplesPerWindow * WINDOWS_PER_PERIOD, windowDurationMs * WINDOWS_PER_PERIOD); + + int[] population = new int[distributionSlots]; + int[] sample = new int[distributionSlots]; + + int populationSize = 0; + int sampleSize = 0; + for (int t = 0; t < windowCount; t++) { + int windowPop = incomingPopulation.getWindowPopulation(); + for (int i = 0; i < windowPop; i++) { + populationSize++; + int index = ThreadLocalRandom.current().nextInt(0, 100); + population[index] += 1; + if (throttler.sample()) { + sampleSize++; + sample[index] += 1; + } + } + expireAndRotate(throttler); + } + int targetSampleSize = samplePointsPerWindow * windowCount; + expectNear(targetSampleSize, sampleSize, expectedSamples * errorFactor); + assertDistributionProperties(distributionSlots, population, sample, populationSize, sampleSize); + } + + private static void expectNear(double value1, double value2, double error) { + assertTrue(Math.abs(value1 - value2) <= error); + } + + private static void assertDistributionProperties(int distributionSlots, int[] population, int[] sample, int populationSize, int sampleSize) { + int populationSum = 0; + int sampleSum = 0; + for (int i = 0; i < distributionSlots; i++) { + populationSum += i * population[i]; + sampleSum += i * sample[i]; + } + + double populationMean = populationSum / (double) populationSize; + double sampleMean = sampleSum / (double) sampleSize; + + double populationVariance = 0; + double sampleVariance = 0; + for (int i = 0; i < distributionSlots; i++) { + double populationDiff = i - populationMean; + populationVariance += population[i] * populationDiff * populationDiff; + + double sampleDiff = i - sampleMean; + sampleVariance += sample[i] * sampleDiff * sampleDiff; + } + populationVariance = populationVariance / (populationSize - 1); + sampleVariance = sampleVariance / (sampleSize - 1); + double populationStdev = Math.sqrt(populationVariance); + double sampleStdev = Math.sqrt(sampleVariance); + expectNear(populationStdev, sampleStdev, 0.5); // 0.5 value to match Hotspot test + expectNear(populationMean, sampleMean, populationStdev); + } + + interface IncomingPopulation { + int getWindowPopulation(); + } + + /** + * Helper method that expires and rotates a throttler's active window. + */ + private static void expireAndRotate(JfrTestThrottler throttler) { + throttler.expireActiveWindow(); + assertTrue("should be expired", throttler.isActiveWindowExpired()); + assertFalse("Should have rotated not sampled!", throttler.sample()); + } + + private static class JfrTestThrottler extends JfrThrottler { + public void beginTest(long eventSampleSize, long periodMs) { + window0 = new JfrTestThrottlerWindow(); + window1 = new JfrTestThrottlerWindow(); + activeWindow = window0; + window0().currentTestNanos = 0; + window1().currentTestNanos = 0; + setThrottle(eventSampleSize, periodMs); + } + + public double getActiveWindowProjectedPopulationSize() { + return avgPopulationSize; + } + + public long getActiveWindowDebt() { + return activeWindow.debt; + } + + public double getWindowLookback() { + return windowLookback(activeWindow); + } + + public boolean isActiveWindowExpired() { + return activeWindow.isExpired(); + } + + public long getPeriodNs() { + return periodNs; + } + + public long getEventSampleSize() { + return eventSampleSize; + } + + public void expireActiveWindow() { + if (eventSampleSize <= LOW_RATE_UPPER_BOUND || periodNs > TimeUtils.nanosPerSecond) { + window0().currentTestNanos += periodNs; + window1().currentTestNanos += periodNs; + } + window0().currentTestNanos += periodNs / WINDOW_DIVISOR; + window1().currentTestNanos += periodNs / WINDOW_DIVISOR; + } + + private JfrTestThrottlerWindow window0() { + return (JfrTestThrottlerWindow) window0; + } + + private JfrTestThrottlerWindow window1() { + return (JfrTestThrottlerWindow) window1; + } + } + + private static class JfrTestThrottlerWindow extends JfrThrottlerWindow { + public volatile long currentTestNanos = 0; + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public boolean isExpired() { + if (currentTestNanos >= endTicks.get()) { + return true; + } + return false; + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + protected void advanceEndTicks() { + endTicks.set(currentTestNanos + windowDurationNs); + } + } +}