diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixSubstrateSigprofHandler.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixSubstrateSigprofHandler.java index b114dd354677..d06fbd06b514 100644 --- a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixSubstrateSigprofHandler.java +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/PosixSubstrateSigprofHandler.java @@ -44,12 +44,14 @@ import com.oracle.svm.core.IsolateListenerSupport; import com.oracle.svm.core.IsolateListenerSupportFeature; import com.oracle.svm.core.RegisterDumper; +import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.c.function.CEntryPointOptions; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.graal.stackvalue.UnsafeStackValue; import com.oracle.svm.core.heap.RestrictHeapAccess; +import com.oracle.svm.core.jfr.JfrExecutionSamplerSupported; import com.oracle.svm.core.jfr.JfrFeature; import com.oracle.svm.core.jfr.sampler.JfrExecutionSampler; import com.oracle.svm.core.option.HostedOptionKey; @@ -58,6 +60,7 @@ import com.oracle.svm.core.posix.headers.Time; import com.oracle.svm.core.sampler.SubstrateSigprofHandler; import com.oracle.svm.core.thread.ThreadListenerSupport; +import com.oracle.svm.core.thread.ThreadListenerSupportFeature; import com.oracle.svm.core.util.TimeUtils; import com.oracle.svm.core.util.UserError; @@ -128,7 +131,7 @@ static boolean isSignalHandlerBasedExecutionSamplerEnabled() { } private static boolean isPlatformSupported() { - return Platform.includedIn(Platform.LINUX.class); + return Platform.includedIn(Platform.LINUX.class) && SubstrateOptions.EnableSignalHandling.getValue(); } private static void validateSamplerOption(HostedOptionKey isSamplerEnabled) { @@ -149,12 +152,12 @@ static class Options { class PosixSubstrateSigProfHandlerFeature implements InternalFeature { @Override public List> getRequiredFeatures() { - return List.of(IsolateListenerSupportFeature.class, JfrFeature.class); + return List.of(ThreadListenerSupportFeature.class, IsolateListenerSupportFeature.class, JfrFeature.class); } @Override public void afterRegistration(AfterRegistrationAccess access) { - if (JfrFeature.isExecutionSamplerSupported() && isSignalHandlerBasedExecutionSamplerEnabled()) { + if (JfrExecutionSamplerSupported.isSupported() && isSignalHandlerBasedExecutionSamplerEnabled()) { SubstrateSigprofHandler sampler = new PosixSubstrateSigprofHandler(); ImageSingletons.add(JfrExecutionSampler.class, sampler); ImageSingletons.add(SubstrateSigprofHandler.class, sampler); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java new file mode 100644 index 000000000000..fbf40373822d --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java @@ -0,0 +1,691 @@ +/* + * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.jfr; + +import static com.oracle.svm.core.jfr.JfrThreadLocal.getJavaBufferList; +import static com.oracle.svm.core.jfr.JfrThreadLocal.getNativeBufferList; + +import java.nio.charset.StandardCharsets; + +import org.graalvm.nativeimage.IsolateThread; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.word.UnsignedWord; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.heap.VMOperationInfos; +import com.oracle.svm.core.jfr.sampler.JfrExecutionSampler; +import com.oracle.svm.core.jfr.sampler.JfrRecurringCallbackExecutionSampler; +import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; +import com.oracle.svm.core.locks.VMMutex; +import com.oracle.svm.core.os.RawFileOperationSupport; +import com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode; +import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; +import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; +import com.oracle.svm.core.sampler.SamplerBuffersAccess; +import com.oracle.svm.core.thread.JavaVMOperation; +import com.oracle.svm.core.thread.ThreadingSupportImpl; +import com.oracle.svm.core.thread.VMOperation; +import com.oracle.svm.core.thread.VMOperationControl; +import com.oracle.svm.core.thread.VMThreads; + +import jdk.graal.compiler.api.replacements.Fold; +import jdk.graal.compiler.core.common.NumUtil; + +/** + * This class is used when writing the in-memory JFR data to a file. For all operations, except + * those listed in {@link JfrUnlockedChunkWriter}, it is necessary to acquire the {@link #lock} + * before invoking the operation. + * + *

+ * If an operation needs both a safepoint and the lock, then it is necessary to acquire the lock + * outside of the safepoint. Otherwise, this will result in deadlocks as other threads may hold the + * lock while they are paused at a safepoint. + *

+ */ +public final class JfrChunkFileWriter implements JfrChunkWriter { + public static final byte[] FILE_MAGIC = {'F', 'L', 'R', '\0'}; + public static final short JFR_VERSION_MAJOR = 2; + public static final short JFR_VERSION_MINOR = 1; + private static final int CHUNK_SIZE_OFFSET = 8; + private static final int FILE_STATE_OFFSET = 64; + private static final byte COMPLETE = 0; + private static final short FLAG_COMPRESSED_INTS = 0b01; + private static final short FLAG_CHUNK_FINAL = 0b10; + + private final VMMutex lock; + private final JfrGlobalMemory globalMemory; + private final JfrMetadata metadata; + private final JfrRepository[] flushCheckpointRepos; + private final JfrRepository[] threadCheckpointRepos; + private final boolean compressedInts; + + private long notificationThreshold; + + private String filename; + private RawFileDescriptor fd; + private long chunkStartTicks; + private long chunkStartNanos; + private byte nextGeneration; + private boolean newChunk; + private boolean isFinal; + private long lastMetadataId; + private long metadataPosition; + private long lastCheckpointOffset; + + @Platforms(Platform.HOSTED_ONLY.class) + public JfrChunkFileWriter(JfrGlobalMemory globalMemory, JfrStackTraceRepository stackTraceRepo, JfrMethodRepository methodRepo, JfrTypeRepository typeRepo, JfrSymbolRepository symbolRepo, + JfrThreadRepository threadRepo) { + this.lock = new VMMutex("jfrChunkWriter"); + this.globalMemory = globalMemory; + this.metadata = new JfrMetadata(null); + this.compressedInts = true; + + /* + * Repositories earlier in the write order may reference entries of repositories later in + * the write order. This ordering is required to prevent races during flushing without + * changing epoch. + */ + this.flushCheckpointRepos = new JfrRepository[]{stackTraceRepo, methodRepo, typeRepo, symbolRepo}; + this.threadCheckpointRepos = new JfrRepository[]{threadRepo}; + } + + @Override + public void initialize(long maxChunkSize) { + this.notificationThreshold = maxChunkSize; + } + + @Override + public JfrChunkWriter lock() { + assert !VMOperation.isInProgressAtSafepoint() : "could cause deadlocks"; + lock.lock(); + return this; + } + + @Override + public void unlock() { + lock.unlock(); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + @Override + public boolean hasOpenFile() { + return getFileSupport().isValid(fd); + } + + @Override + public long getChunkStartNanos() { + return chunkStartNanos; + } + + @Override + public void setFilename(String filename) { + assert lock.isOwner(); + this.filename = filename; + } + + @Override + public void maybeOpenFile() { + assert lock.isOwner(); + if (filename != null) { + openFile(filename); + } + } + + @Override + public void openFile(String outputFile) { + assert lock.isOwner(); + filename = outputFile; + fd = getFileSupport().create(filename, FileCreationMode.CREATE_OR_REPLACE, FileAccessMode.READ_WRITE); + + chunkStartTicks = JfrTicks.elapsedTicks(); + chunkStartNanos = JfrTicks.currentTimeNanos(); + nextGeneration = 1; + newChunk = true; + isFinal = false; + lastMetadataId = -1; + metadataPosition = -1; + lastCheckpointOffset = -1; + + writeFileHeader(); + } + + @Override + @Uninterruptible(reason = "Prevent safepoints as those could change the flushed position.") + public void write(JfrBuffer buffer) { + assert lock.isOwner(); + assert buffer.isNonNull(); + assert buffer.getBufferType() == JfrBufferType.C_HEAP || VMOperation.isInProgressAtSafepoint() || JfrBufferNodeAccess.isLockedByCurrentThread(buffer.getNode()); + + UnsignedWord unflushedSize = JfrBufferAccess.getUnflushedSize(buffer); + if (unflushedSize.equal(0)) { + return; + } + + boolean success = getFileSupport().write(fd, JfrBufferAccess.getFlushedPos(buffer), unflushedSize); + if (success) { + JfrBufferAccess.increaseFlushedPos(buffer, unflushedSize); + } + } + + @Override + public void flush() { + assert lock.isOwner(); + + flushStorage(true); + + writeThreadCheckpoint(true); + writeFlushCheckpoint(true); + writeMetadataEvent(); + patchFileHeader(true); + + newChunk = false; + } + + @Override + public void markChunkFinal() { + assert lock.isOwner(); + isFinal = true; + } + + /** + * Write all the in-memory data to the file. + */ + @Override + public void closeFile() { + assert lock.isOwner(); + /* + * Switch to a new epoch. This is done at a safepoint to ensure that we end up with + * consistent data, even if multiple threads have JFR events in progress. + */ + JfrChangeEpochOperation op = new JfrChangeEpochOperation(); + op.enqueue(); + + /* + * After changing the epoch, all subsequently triggered JFR events will be recorded into the + * data structures of the new epoch. This guarantees that the data in the old epoch can be + * persisted to a file without a safepoint. + */ + + writeThreadCheckpoint(false); + writeFlushCheckpoint(false); + writeMetadataEvent(); + patchFileHeader(false); + + getFileSupport().close(fd); + filename = null; + fd = WordFactory.nullPointer(); + } + + private void writeFileHeader() { + /* Write the header - some data gets patched later on. */ + getFileSupport().write(fd, FILE_MAGIC); + getFileSupport().writeShort(fd, JFR_VERSION_MAJOR); + getFileSupport().writeShort(fd, JFR_VERSION_MINOR); + assert getFileSupport().position(fd) == CHUNK_SIZE_OFFSET; + getFileSupport().writeLong(fd, 0L); // chunk size + getFileSupport().writeLong(fd, 0L); // last checkpoint offset + getFileSupport().writeLong(fd, 0L); // metadata position + getFileSupport().writeLong(fd, chunkStartNanos); + getFileSupport().writeLong(fd, 0L); // durationNanos + getFileSupport().writeLong(fd, chunkStartTicks); + getFileSupport().writeLong(fd, JfrTicks.getTicksFrequency()); + assert getFileSupport().position(fd) == FILE_STATE_OFFSET; + getFileSupport().writeByte(fd, getAndIncrementGeneration()); + getFileSupport().writeByte(fd, (byte) 0); // padding + getFileSupport().writeShort(fd, computeHeaderFlags()); + } + + private void patchFileHeader(boolean flushpoint) { + assert lock.isOwner(); + assert metadataPosition > 0; + assert lastCheckpointOffset > 0; + + byte generation = flushpoint ? getAndIncrementGeneration() : COMPLETE; + long currentPos = getFileSupport().position(fd); + long durationNanos = JfrTicks.currentTimeNanos() - chunkStartNanos; + + getFileSupport().seek(fd, CHUNK_SIZE_OFFSET); + getFileSupport().writeLong(fd, currentPos); + getFileSupport().writeLong(fd, lastCheckpointOffset); + getFileSupport().writeLong(fd, metadataPosition); + getFileSupport().writeLong(fd, chunkStartNanos); + getFileSupport().writeLong(fd, durationNanos); + + getFileSupport().seek(fd, FILE_STATE_OFFSET); + getFileSupport().writeByte(fd, generation); + getFileSupport().writeByte(fd, (byte) 0); + getFileSupport().writeShort(fd, computeHeaderFlags()); + + /* Move pointer back to correct position for next write. */ + getFileSupport().seek(fd, currentPos); + } + + private short computeHeaderFlags() { + short flags = 0; + if (compressedInts) { + flags |= FLAG_COMPRESSED_INTS; + } + if (isFinal) { + flags |= FLAG_CHUNK_FINAL; + } + return flags; + } + + private byte getAndIncrementGeneration() { + if (nextGeneration == Byte.MAX_VALUE) { + // Restart counter if required. + nextGeneration = 1; + return Byte.MAX_VALUE; + } + return nextGeneration++; + } + + private void writeFlushCheckpoint(boolean flushpoint) { + writeCheckpointEvent(JfrCheckpointType.Flush, flushCheckpointRepos, newChunk, flushpoint); + } + + private void writeThreadCheckpoint(boolean flushpoint) { + assert threadCheckpointRepos.length == 1 && threadCheckpointRepos[0] == SubstrateJVM.getThreadRepo(); + /* The code below is only atomic enough because the epoch can't change while flushing. */ + if (SubstrateJVM.getThreadRepo().hasUnflushedData()) { + writeCheckpointEvent(JfrCheckpointType.Threads, threadCheckpointRepos, false, flushpoint); + } else if (!flushpoint) { + /* After an epoch change, the previous epoch data must be completely clear. */ + SubstrateJVM.getThreadRepo().clearPreviousEpoch(); + } + } + + private void writeCheckpointEvent(JfrCheckpointType type, JfrRepository[] repositories, boolean writeSerializers, boolean flushpoint) { + assert lock.isOwner(); + + long start = beginEvent(); + writeCompressedLong(JfrReservedEvent.CHECKPOINT.getId()); + writeCompressedLong(JfrTicks.elapsedTicks()); + writeCompressedLong(0); // duration + writeCompressedLong(getDeltaToLastCheckpoint(start)); + writeByte(type.getId()); + + long poolCountPos = getFileSupport().position(fd); + getFileSupport().writeInt(fd, 0); // pool count (patched below) + + int poolCount = writeSerializers ? writeSerializers() : 0; + poolCount += writeConstantPools(repositories, flushpoint); + + long currentPos = getFileSupport().position(fd); + getFileSupport().seek(fd, poolCountPos); + writePaddedInt(poolCount); + + getFileSupport().seek(fd, currentPos); + endEvent(start); + + lastCheckpointOffset = start; + } + + private long getDeltaToLastCheckpoint(long startOfNewCheckpoint) { + if (lastCheckpointOffset < 0) { + return 0L; + } + return lastCheckpointOffset - startOfNewCheckpoint; + } + + private int writeSerializers() { + JfrSerializer[] serializers = JfrSerializerSupport.get().getSerializers(); + for (JfrSerializer serializer : serializers) { + serializer.write(this); + } + return serializers.length; + } + + private int writeConstantPools(JfrRepository[] repositories, boolean flushpoint) { + int poolCount = 0; + for (JfrRepository repo : repositories) { + poolCount += repo.write(this, flushpoint); + } + return poolCount; + } + + @Override + public void setMetadata(byte[] bytes) { + metadata.setDescriptor(bytes); + } + + private void writeMetadataEvent() { + assert lock.isOwner(); + + /* Only write the metadata if this is a new chunk or if it changed in the meanwhile. */ + long currentMetadataId = metadata.getCurrentMetadataId(); + if (lastMetadataId == currentMetadataId) { + return; + } + + long start = beginEvent(); + writeCompressedLong(JfrReservedEvent.METADATA.getId()); + writeCompressedLong(JfrTicks.elapsedTicks()); + writeCompressedLong(0); // duration + writeCompressedLong(currentMetadataId); + writeBytes(metadata.getDescriptor()); // payload + endEvent(start); + + metadataPosition = start; + lastMetadataId = currentMetadataId; + } + + @Override + public boolean shouldRotateDisk() { + assert lock.isOwner(); + return getFileSupport().isValid(fd) && getFileSupport().size(fd) > notificationThreshold; + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public long beginEvent() { + long start = getFileSupport().position(fd); + // Write a placeholder for the size. Will be patched by endEvent, + getFileSupport().writeInt(fd, 0); + return start; + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void endEvent(long start) { + long end = getFileSupport().position(fd); + long writtenBytes = end - start; + assert (int) writtenBytes == writtenBytes; + + getFileSupport().seek(fd, start); + writePaddedInt(writtenBytes); + getFileSupport().seek(fd, end); + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void writeBoolean(boolean value) { + assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); + writeByte((byte) (value ? 1 : 0)); + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void writeByte(byte value) { + assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); + getFileSupport().writeByte(fd, value); + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void writeBytes(byte[] values) { + assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); + getFileSupport().write(fd, values); + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void writeCompressedInt(int value) { + assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); + writeCompressedLong(value & 0xFFFFFFFFL); + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void writePaddedInt(long value) { + assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); + assert (int) value == value; + getFileSupport().writeInt(fd, JfrNativeEventWriter.makePaddedInt((int) value)); + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void writeCompressedLong(long value) { + assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); + long v = value; + if ((v & ~0x7FL) == 0L) { + getFileSupport().writeByte(fd, (byte) v); // 0-6 + return; + } + getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 0-6 + v >>>= 7; + if ((v & ~0x7FL) == 0L) { + getFileSupport().writeByte(fd, (byte) v); // 7-13 + return; + } + getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 7-13 + v >>>= 7; + if ((v & ~0x7FL) == 0L) { + getFileSupport().writeByte(fd, (byte) v); // 14-20 + return; + } + getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 14-20 + v >>>= 7; + if ((v & ~0x7FL) == 0L) { + getFileSupport().writeByte(fd, (byte) v); // 21-27 + return; + } + getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 21-27 + v >>>= 7; + if ((v & ~0x7FL) == 0L) { + getFileSupport().writeByte(fd, (byte) v); // 28-34 + return; + } + getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 28-34 + v >>>= 7; + if ((v & ~0x7FL) == 0L) { + getFileSupport().writeByte(fd, (byte) v); // 35-41 + return; + } + getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 35-41 + v >>>= 7; + if ((v & ~0x7FL) == 0L) { + getFileSupport().writeByte(fd, (byte) v); // 42-48 + return; + } + getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 42-48 + v >>>= 7; + + if ((v & ~0x7FL) == 0L) { + getFileSupport().writeByte(fd, (byte) v); // 49-55 + return; + } + getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 49-55 + getFileSupport().writeByte(fd, (byte) (v >>> 7)); // 56-63, last byte as is. + } + + @Fold + static RawFileOperationSupport getFileSupport() { + return RawFileOperationSupport.bigEndian(); + } + + @Override + public void writeString(String str) { + if (str.isEmpty()) { + getFileSupport().writeByte(fd, StringEncoding.EMPTY_STRING.getValue()); + } else { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + getFileSupport().writeByte(fd, StringEncoding.UTF8_BYTE_ARRAY.getValue()); + writeCompressedInt(bytes.length); + getFileSupport().write(fd, bytes); + } + } + + @Uninterruptible(reason = "Prevent pollution of the current thread's thread local JFR buffer.") + private void flushStorage(boolean flushpoint) { + traverseThreadLocalBuffers(getJavaBufferList(), flushpoint); + traverseThreadLocalBuffers(getNativeBufferList(), flushpoint); + + flushGlobalMemory(flushpoint); + } + + @Uninterruptible(reason = "Locking without transition requires that the whole critical section is uninterruptible.") + private void traverseThreadLocalBuffers(JfrBufferList list, boolean flushpoint) { + JfrBufferNode node = list.getHead(); + JfrBufferNode prev = WordFactory.nullPointer(); + + while (node.isNonNull()) { + JfrBufferNode next = node.getNext(); + boolean lockAcquired = JfrBufferNodeAccess.tryLock(node); + if (lockAcquired) { + JfrBuffer buffer = JfrBufferNodeAccess.getBuffer(node); + if (buffer.isNull()) { + list.removeNode(node, prev); + JfrBufferNodeAccess.free(node); + node = next; + continue; + } + + try { + if (flushpoint) { + /* + * I/O operations may be slow, so this flushes to the global buffers instead + * of writing to disk directly. This mitigates the risk of acquiring the + * thread-local buffers for too long. + */ + SubstrateJVM.getGlobalMemory().write(buffer, true); + } else { + write(buffer); + } + /* + * The flushed position is modified in the calls above. We do *not* reinitialize + * the thread-local buffers as the individual threads will handle space + * reclamation on their own time. + */ + } finally { + JfrBufferNodeAccess.unlock(node); + } + } + + assert lockAcquired || flushpoint; + prev = node; + node = next; + } + } + + @Uninterruptible(reason = "Locking without transition requires that the whole critical section is uninterruptible.") + private void flushGlobalMemory(boolean flushpoint) { + JfrBufferList buffers = globalMemory.getBuffers(); + JfrBufferNode node = buffers.getHead(); + while (node.isNonNull()) { + boolean lockAcquired = JfrBufferNodeAccess.tryLock(node); + if (lockAcquired) { + try { + JfrBuffer buffer = JfrBufferNodeAccess.getBuffer(node); + write(buffer); + JfrBufferAccess.reinitialize(buffer); + } finally { + JfrBufferNodeAccess.unlock(node); + } + } + assert lockAcquired || flushpoint; + node = node.getNext(); + } + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public boolean isLockedByCurrentThread() { + return lock.isOwner(); + } + + public enum StringEncoding { + NULL(0), + EMPTY_STRING(1), + CONSTANT_POOL(2), + UTF8_BYTE_ARRAY(3), + CHAR_ARRAY(4), + LATIN1_BYTE_ARRAY(5); + + private final byte value; + + StringEncoding(int value) { + this.value = NumUtil.safeToByte(value); + } + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public byte getValue() { + return value; + } + } + + private class JfrChangeEpochOperation extends JavaVMOperation { + protected JfrChangeEpochOperation() { + super(VMOperationInfos.get(JfrChangeEpochOperation.class, "JFR change epoch", SystemEffect.SAFEPOINT)); + } + + @Override + protected void operate() { + changeEpoch(); + } + + /** + * We need to ensure that all JFR events that are triggered by the current thread are + * recorded for the next epoch. Otherwise, those JFR events could pollute the data that we + * currently try to persist. To ensure that, we must uninterruptedly flush all data that is + * currently in-flight. + */ + @Uninterruptible(reason = "Prevent pollution of the current thread's thread local JFR buffer.") + private void changeEpoch() { + processSamplerBuffers(); + flushStorage(false); + + /* Notify all event writers that the epoch changed. */ + for (IsolateThread thread = VMThreads.firstThread(); thread.isNonNull(); thread = VMThreads.nextThread(thread)) { + JfrThreadLocal.notifyEventWriter(thread); + } + + JfrTraceIdEpoch.getInstance().changeEpoch(); + + // Now that the epoch changed, re-register all running threads for the new epoch. + SubstrateJVM.getThreadRepo().registerRunningThreads(); + } + + /** + * The VM is at a safepoint, so all other threads have a native state. However, execution + * sampling could still be executed. For the {@link JfrRecurringCallbackExecutionSampler}, + * it is sufficient to mark this method as uninterruptible to prevent execution of the + * recurring callbacks. If the SIGPROF-based sampler is used, the signal handler may still + * be executed at any time for any thread (including the current thread). To prevent races, + * we need to ensure that there are no threads that execute the SIGPROF handler while we are + * accessing the currently active buffers of other threads. + */ + @Uninterruptible(reason = "Prevent JFR recording.") + private static void processSamplerBuffers() { + assert VMOperation.isInProgressAtSafepoint(); + assert ThreadingSupportImpl.isRecurringCallbackPaused(); + + JfrExecutionSampler.singleton().disallowThreadsInSamplerCode(); + try { + processSamplerBuffers0(); + } finally { + JfrExecutionSampler.singleton().allowThreadsInSamplerCode(); + } + } + + @Uninterruptible(reason = "Prevent JFR recording.") + private static void processSamplerBuffers0() { + SamplerBuffersAccess.processActiveBuffers(); + SamplerBuffersAccess.processFullBuffers(false); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkNoWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkNoWriter.java new file mode 100644 index 000000000000..89b0de50c3b4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkNoWriter.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jfr; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.util.VMError; + +/** + * Dummy implementation of a {@link JfrChunkWriter} that does not perform any file system + * operations. + */ +public final class JfrChunkNoWriter implements JfrChunkWriter { + + private static final String ERROR_MESSAGE = "JfrChunkWriter does not permit write operations, " + + "so reaching this method during runtime indicates a semantic error in the code."; + + @Platforms(Platform.HOSTED_ONLY.class) + public JfrChunkNoWriter() { + } + + @Override + public void initialize(long maxChunkSize) { + /* Nothing to do. */ + } + + @Override + public JfrChunkWriter lock() { + /* Nothing to do. */ + return this; + } + + @Override + public boolean isLockedByCurrentThread() { + /* Nothing to do. */ + return false; + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public boolean hasOpenFile() { + /* Nothing to do. */ + return false; + } + + @Override + public void unlock() { + /* Nothing to do. */ + } + + @Override + public long getChunkStartNanos() { + /* Nothing to do. */ + return -1L; + } + + @Override + public void setFilename(String filename) { + /* Nothing to do. */ + } + + @Override + public void maybeOpenFile() { + /* Nothing to do. */ + } + + @Override + public void openFile(String outputFile) { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public void write(JfrBuffer buffer) { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void flush() { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void markChunkFinal() { + /* Nothing to do. */ + } + + @Override + public void closeFile() { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void setMetadata(byte[] bytes) { + /* Nothing to do. */ + } + + @Override + public boolean shouldRotateDisk() { + /* Nothing to do. */ + return false; + } + + @Override + public long beginEvent() { + throw VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void endEvent(long start) { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void writeBoolean(boolean value) { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void writeByte(byte value) { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void writeBytes(byte[] values) { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void writeCompressedInt(int value) { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void writePaddedInt(long value) { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void writeCompressedLong(long value) { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } + + @Override + public void writeString(String str) { + VMError.shouldNotReachHere(ERROR_MESSAGE); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java index 219bdb4c5b7c..3412f99eac85 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,645 +24,45 @@ */ package com.oracle.svm.core.jfr; -import static com.oracle.svm.core.jfr.JfrThreadLocal.getJavaBufferList; -import static com.oracle.svm.core.jfr.JfrThreadLocal.getNativeBufferList; +public interface JfrChunkWriter extends JfrUnlockedChunkWriter { -import java.nio.charset.StandardCharsets; + void unlock(); -import jdk.graal.compiler.api.replacements.Fold; -import jdk.graal.compiler.core.common.NumUtil; -import org.graalvm.nativeimage.IsolateThread; -import org.graalvm.nativeimage.Platform; -import org.graalvm.nativeimage.Platforms; -import org.graalvm.word.UnsignedWord; -import org.graalvm.word.WordFactory; + long getChunkStartNanos(); -import com.oracle.svm.core.Uninterruptible; -import com.oracle.svm.core.heap.VMOperationInfos; -import com.oracle.svm.core.jfr.sampler.JfrExecutionSampler; -import com.oracle.svm.core.jfr.sampler.JfrRecurringCallbackExecutionSampler; -import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; -import com.oracle.svm.core.locks.VMMutex; -import com.oracle.svm.core.os.RawFileOperationSupport; -import com.oracle.svm.core.os.RawFileOperationSupport.FileAccessMode; -import com.oracle.svm.core.os.RawFileOperationSupport.FileCreationMode; -import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor; -import com.oracle.svm.core.sampler.SamplerBuffersAccess; -import com.oracle.svm.core.thread.JavaVMOperation; -import com.oracle.svm.core.thread.ThreadingSupportImpl; -import com.oracle.svm.core.thread.VMOperation; -import com.oracle.svm.core.thread.VMOperationControl; -import com.oracle.svm.core.thread.VMThreads; + void setFilename(String filename); -/** - * This class is used when writing the in-memory JFR data to a file. For all operations, except - * those listed in {@link JfrUnlockedChunkWriter}, it is necessary to acquire the {@link #lock} - * before invoking the operation. - * - * If an operation needs both a safepoint and the lock, then it is necessary to acquire the lock - * outside of the safepoint. Otherwise, this will result in deadlocks as other threads may hold the - * lock while they are paused at a safepoint. - */ -public final class JfrChunkWriter implements JfrUnlockedChunkWriter { - public static final byte[] FILE_MAGIC = {'F', 'L', 'R', '\0'}; - public static final short JFR_VERSION_MAJOR = 2; - public static final short JFR_VERSION_MINOR = 1; - private static final int CHUNK_SIZE_OFFSET = 8; - private static final int FILE_STATE_OFFSET = 64; - private static final byte COMPLETE = 0; - private static final short FLAG_COMPRESSED_INTS = 0b01; - private static final short FLAG_CHUNK_FINAL = 0b10; - - private final VMMutex lock; - private final JfrGlobalMemory globalMemory; - private final JfrMetadata metadata; - private final JfrRepository[] flushCheckpointRepos; - private final JfrRepository[] threadCheckpointRepos; - private final boolean compressedInts; - - private long notificationThreshold; - - private String filename; - private RawFileDescriptor fd; - private long chunkStartTicks; - private long chunkStartNanos; - private byte nextGeneration; - private boolean newChunk; - private boolean isFinal; - private long lastMetadataId; - private long metadataPosition; - private long lastCheckpointOffset; - - @Platforms(Platform.HOSTED_ONLY.class) - public JfrChunkWriter(JfrGlobalMemory globalMemory, JfrStackTraceRepository stackTraceRepo, JfrMethodRepository methodRepo, JfrTypeRepository typeRepo, JfrSymbolRepository symbolRepo, - JfrThreadRepository threadRepo) { - this.lock = new VMMutex("jfrChunkWriter"); - this.globalMemory = globalMemory; - this.metadata = new JfrMetadata(null); - this.compressedInts = true; - - /* - * Repositories earlier in the write order may reference entries of repositories later in - * the write order. This ordering is required to prevent races during flushing without - * changing epoch. - */ - this.flushCheckpointRepos = new JfrRepository[]{stackTraceRepo, methodRepo, typeRepo, symbolRepo}; - this.threadCheckpointRepos = new JfrRepository[]{threadRepo}; - } - - @Override - public void initialize(long maxChunkSize) { - this.notificationThreshold = maxChunkSize; - } - - @Override - public JfrChunkWriter lock() { - assert !VMOperation.isInProgressAtSafepoint() : "could cause deadlocks"; - lock.lock(); - return this; - } - - public void unlock() { - lock.unlock(); - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - @Override - public boolean hasOpenFile() { - return getFileSupport().isValid(fd); - } - - public long getChunkStartNanos() { - return chunkStartNanos; - } - - public void setFilename(String filename) { - assert lock.isOwner(); - this.filename = filename; - } - - public void maybeOpenFile() { - assert lock.isOwner(); - if (filename != null) { - openFile(filename); - } - } - - public void openFile(String outputFile) { - assert lock.isOwner(); - filename = outputFile; - fd = getFileSupport().create(filename, FileCreationMode.CREATE_OR_REPLACE, FileAccessMode.READ_WRITE); - - chunkStartTicks = JfrTicks.elapsedTicks(); - chunkStartNanos = JfrTicks.currentTimeNanos(); - nextGeneration = 1; - newChunk = true; - isFinal = false; - lastMetadataId = -1; - metadataPosition = -1; - lastCheckpointOffset = -1; - - writeFileHeader(); - } - - @Uninterruptible(reason = "Prevent safepoints as those could change the flushed position.") - public void write(JfrBuffer buffer) { - assert lock.isOwner(); - assert buffer.isNonNull(); - assert buffer.getBufferType() == JfrBufferType.C_HEAP || VMOperation.isInProgressAtSafepoint() || JfrBufferNodeAccess.isLockedByCurrentThread(buffer.getNode()); - - UnsignedWord unflushedSize = JfrBufferAccess.getUnflushedSize(buffer); - if (unflushedSize.equal(0)) { - return; - } - - boolean success = getFileSupport().write(fd, JfrBufferAccess.getFlushedPos(buffer), unflushedSize); - if (success) { - JfrBufferAccess.increaseFlushedPos(buffer, unflushedSize); - } - } - - public void flush() { - assert lock.isOwner(); - - flushStorage(true); - - writeThreadCheckpoint(true); - writeFlushCheckpoint(true); - writeMetadataEvent(); - patchFileHeader(true); - - newChunk = false; - } - - public void markChunkFinal() { - assert lock.isOwner(); - isFinal = true; - } - - /** - * Write all the in-memory data to the file. - */ - public void closeFile() { - assert lock.isOwner(); - /* - * Switch to a new epoch. This is done at a safepoint to ensure that we end up with - * consistent data, even if multiple threads have JFR events in progress. - */ - JfrChangeEpochOperation op = new JfrChangeEpochOperation(); - op.enqueue(); - - /* - * After changing the epoch, all subsequently triggered JFR events will be recorded into the - * data structures of the new epoch. This guarantees that the data in the old epoch can be - * persisted to a file without a safepoint. - */ - - writeThreadCheckpoint(false); - writeFlushCheckpoint(false); - writeMetadataEvent(); - patchFileHeader(false); - - getFileSupport().close(fd); - filename = null; - fd = WordFactory.nullPointer(); - } - - private void writeFileHeader() { - /* Write the header - some data gets patched later on. */ - getFileSupport().write(fd, FILE_MAGIC); - getFileSupport().writeShort(fd, JFR_VERSION_MAJOR); - getFileSupport().writeShort(fd, JFR_VERSION_MINOR); - assert getFileSupport().position(fd) == CHUNK_SIZE_OFFSET; - getFileSupport().writeLong(fd, 0L); // chunk size - getFileSupport().writeLong(fd, 0L); // last checkpoint offset - getFileSupport().writeLong(fd, 0L); // metadata position - getFileSupport().writeLong(fd, chunkStartNanos); - getFileSupport().writeLong(fd, 0L); // durationNanos - getFileSupport().writeLong(fd, chunkStartTicks); - getFileSupport().writeLong(fd, JfrTicks.getTicksFrequency()); - assert getFileSupport().position(fd) == FILE_STATE_OFFSET; - getFileSupport().writeByte(fd, getAndIncrementGeneration()); - getFileSupport().writeByte(fd, (byte) 0); // padding - getFileSupport().writeShort(fd, computeHeaderFlags()); - } - - private void patchFileHeader(boolean flushpoint) { - assert lock.isOwner(); - assert metadataPosition > 0; - assert lastCheckpointOffset > 0; - - byte generation = flushpoint ? getAndIncrementGeneration() : COMPLETE; - long currentPos = getFileSupport().position(fd); - long durationNanos = JfrTicks.currentTimeNanos() - chunkStartNanos; - - getFileSupport().seek(fd, CHUNK_SIZE_OFFSET); - getFileSupport().writeLong(fd, currentPos); - getFileSupport().writeLong(fd, lastCheckpointOffset); - getFileSupport().writeLong(fd, metadataPosition); - getFileSupport().writeLong(fd, chunkStartNanos); - getFileSupport().writeLong(fd, durationNanos); - - getFileSupport().seek(fd, FILE_STATE_OFFSET); - getFileSupport().writeByte(fd, generation); - getFileSupport().writeByte(fd, (byte) 0); - getFileSupport().writeShort(fd, computeHeaderFlags()); - - /* Move pointer back to correct position for next write. */ - getFileSupport().seek(fd, currentPos); - } - - private short computeHeaderFlags() { - short flags = 0; - if (compressedInts) { - flags |= FLAG_COMPRESSED_INTS; - } - if (isFinal) { - flags |= FLAG_CHUNK_FINAL; - } - return flags; - } - - private byte getAndIncrementGeneration() { - if (nextGeneration == Byte.MAX_VALUE) { - // Restart counter if required. - nextGeneration = 1; - return Byte.MAX_VALUE; - } - return nextGeneration++; - } - - private void writeFlushCheckpoint(boolean flushpoint) { - writeCheckpointEvent(JfrCheckpointType.Flush, flushCheckpointRepos, newChunk, flushpoint); - } - - private void writeThreadCheckpoint(boolean flushpoint) { - assert threadCheckpointRepos.length == 1 && threadCheckpointRepos[0] == SubstrateJVM.getThreadRepo(); - /* The code below is only atomic enough because the epoch can't change while flushing. */ - if (SubstrateJVM.getThreadRepo().hasUnflushedData()) { - writeCheckpointEvent(JfrCheckpointType.Threads, threadCheckpointRepos, false, flushpoint); - } else if (!flushpoint) { - /* After an epoch change, the previous epoch data must be completely clear. */ - SubstrateJVM.getThreadRepo().clearPreviousEpoch(); - } - } - - private void writeCheckpointEvent(JfrCheckpointType type, JfrRepository[] repositories, boolean writeSerializers, boolean flushpoint) { - assert lock.isOwner(); - - long start = beginEvent(); - writeCompressedLong(JfrReservedEvent.CHECKPOINT.getId()); - writeCompressedLong(JfrTicks.elapsedTicks()); - writeCompressedLong(0); // duration - writeCompressedLong(getDeltaToLastCheckpoint(start)); - writeByte(type.getId()); - - long poolCountPos = getFileSupport().position(fd); - getFileSupport().writeInt(fd, 0); // pool count (patched below) - - int poolCount = writeSerializers ? writeSerializers() : 0; - poolCount += writeConstantPools(repositories, flushpoint); - - long currentPos = getFileSupport().position(fd); - getFileSupport().seek(fd, poolCountPos); - writePaddedInt(poolCount); - - getFileSupport().seek(fd, currentPos); - endEvent(start); - - lastCheckpointOffset = start; - } - - private long getDeltaToLastCheckpoint(long startOfNewCheckpoint) { - if (lastCheckpointOffset < 0) { - return 0L; - } - return lastCheckpointOffset - startOfNewCheckpoint; - } - - private int writeSerializers() { - JfrSerializer[] serializers = JfrSerializerSupport.get().getSerializers(); - for (JfrSerializer serializer : serializers) { - serializer.write(this); - } - return serializers.length; - } - - private int writeConstantPools(JfrRepository[] repositories, boolean flushpoint) { - int poolCount = 0; - for (JfrRepository repo : repositories) { - poolCount += repo.write(this, flushpoint); - } - return poolCount; - } - - public void setMetadata(byte[] bytes) { - metadata.setDescriptor(bytes); - } - - private void writeMetadataEvent() { - assert lock.isOwner(); - - /* Only write the metadata if this is a new chunk or if it changed in the meanwhile. */ - long currentMetadataId = metadata.getCurrentMetadataId(); - if (lastMetadataId == currentMetadataId) { - return; - } - - long start = beginEvent(); - writeCompressedLong(JfrReservedEvent.METADATA.getId()); - writeCompressedLong(JfrTicks.elapsedTicks()); - writeCompressedLong(0); // duration - writeCompressedLong(currentMetadataId); - writeBytes(metadata.getDescriptor()); // payload - endEvent(start); - - metadataPosition = start; - lastMetadataId = currentMetadataId; - } - - public boolean shouldRotateDisk() { - assert lock.isOwner(); - return getFileSupport().isValid(fd) && getFileSupport().size(fd) > notificationThreshold; - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public long beginEvent() { - long start = getFileSupport().position(fd); - // Write a placeholder for the size. Will be patched by endEvent, - getFileSupport().writeInt(fd, 0); - return start; - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public void endEvent(long start) { - long end = getFileSupport().position(fd); - long writtenBytes = end - start; - assert (int) writtenBytes == writtenBytes; - - getFileSupport().seek(fd, start); - writePaddedInt(writtenBytes); - getFileSupport().seek(fd, end); - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public void writeBoolean(boolean value) { - assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); - writeByte((byte) (value ? 1 : 0)); - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public void writeByte(byte value) { - assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); - getFileSupport().writeByte(fd, value); - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public void writeBytes(byte[] values) { - assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); - getFileSupport().write(fd, values); - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public void writeCompressedInt(int value) { - assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); - writeCompressedLong(value & 0xFFFFFFFFL); - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public void writePaddedInt(long value) { - assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); - assert (int) value == value; - getFileSupport().writeInt(fd, JfrNativeEventWriter.makePaddedInt((int) value)); - } - - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public void writeCompressedLong(long value) { - assert lock.isOwner() || VMOperationControl.isDedicatedVMOperationThread() && lock.hasOwner(); - long v = value; - if ((v & ~0x7FL) == 0L) { - getFileSupport().writeByte(fd, (byte) v); // 0-6 - return; - } - getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 0-6 - v >>>= 7; - if ((v & ~0x7FL) == 0L) { - getFileSupport().writeByte(fd, (byte) v); // 7-13 - return; - } - getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 7-13 - v >>>= 7; - if ((v & ~0x7FL) == 0L) { - getFileSupport().writeByte(fd, (byte) v); // 14-20 - return; - } - getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 14-20 - v >>>= 7; - if ((v & ~0x7FL) == 0L) { - getFileSupport().writeByte(fd, (byte) v); // 21-27 - return; - } - getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 21-27 - v >>>= 7; - if ((v & ~0x7FL) == 0L) { - getFileSupport().writeByte(fd, (byte) v); // 28-34 - return; - } - getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 28-34 - v >>>= 7; - if ((v & ~0x7FL) == 0L) { - getFileSupport().writeByte(fd, (byte) v); // 35-41 - return; - } - getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 35-41 - v >>>= 7; - if ((v & ~0x7FL) == 0L) { - getFileSupport().writeByte(fd, (byte) v); // 42-48 - return; - } - getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 42-48 - v >>>= 7; - - if ((v & ~0x7FL) == 0L) { - getFileSupport().writeByte(fd, (byte) v); // 49-55 - return; - } - getFileSupport().writeByte(fd, (byte) (v | 0x80L)); // 49-55 - getFileSupport().writeByte(fd, (byte) (v >>> 7)); // 56-63, last byte as is. - } - - @Fold - static RawFileOperationSupport getFileSupport() { - return RawFileOperationSupport.bigEndian(); - } - - public void writeString(String str) { - if (str.isEmpty()) { - getFileSupport().writeByte(fd, StringEncoding.EMPTY_STRING.getValue()); - } else { - byte[] bytes = str.getBytes(StandardCharsets.UTF_8); - getFileSupport().writeByte(fd, StringEncoding.UTF8_BYTE_ARRAY.getValue()); - writeCompressedInt(bytes.length); - getFileSupport().write(fd, bytes); - } - } - - @Uninterruptible(reason = "Prevent pollution of the current thread's thread local JFR buffer.") - private void flushStorage(boolean flushpoint) { - traverseThreadLocalBuffers(getJavaBufferList(), flushpoint); - traverseThreadLocalBuffers(getNativeBufferList(), flushpoint); - - flushGlobalMemory(flushpoint); - } - - @Uninterruptible(reason = "Locking without transition requires that the whole critical section is uninterruptible.") - private void traverseThreadLocalBuffers(JfrBufferList list, boolean flushpoint) { - JfrBufferNode node = list.getHead(); - JfrBufferNode prev = WordFactory.nullPointer(); - - while (node.isNonNull()) { - JfrBufferNode next = node.getNext(); - boolean lockAcquired = JfrBufferNodeAccess.tryLock(node); - if (lockAcquired) { - JfrBuffer buffer = JfrBufferNodeAccess.getBuffer(node); - if (buffer.isNull()) { - list.removeNode(node, prev); - JfrBufferNodeAccess.free(node); - node = next; - continue; - } - - try { - if (flushpoint) { - /* - * I/O operations may be slow, so this flushes to the global buffers instead - * of writing to disk directly. This mitigates the risk of acquiring the - * thread-local buffers for too long. - */ - SubstrateJVM.getGlobalMemory().write(buffer, true); - } else { - write(buffer); - } - /* - * The flushed position is modified in the calls above. We do *not* reinitialize - * the thread-local buffers as the individual threads will handle space - * reclamation on their own time. - */ - } finally { - JfrBufferNodeAccess.unlock(node); - } - } + void maybeOpenFile(); - assert lockAcquired || flushpoint; - prev = node; - node = next; - } - } + void openFile(String outputFile); - @Uninterruptible(reason = "Locking without transition requires that the whole critical section is uninterruptible.") - private void flushGlobalMemory(boolean flushpoint) { - JfrBufferList buffers = globalMemory.getBuffers(); - JfrBufferNode node = buffers.getHead(); - while (node.isNonNull()) { - boolean lockAcquired = JfrBufferNodeAccess.tryLock(node); - if (lockAcquired) { - try { - JfrBuffer buffer = JfrBufferNodeAccess.getBuffer(node); - write(buffer); - JfrBufferAccess.reinitialize(buffer); - } finally { - JfrBufferNodeAccess.unlock(node); - } - } - assert lockAcquired || flushpoint; - node = node.getNext(); - } - } + void write(JfrBuffer buffer); - @Override - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public boolean isLockedByCurrentThread() { - return lock.isOwner(); - } + void flush(); - public enum StringEncoding { - NULL(0), - EMPTY_STRING(1), - CONSTANT_POOL(2), - UTF8_BYTE_ARRAY(3), - CHAR_ARRAY(4), - LATIN1_BYTE_ARRAY(5); + void markChunkFinal(); - private final byte value; + void closeFile(); - StringEncoding(int value) { - this.value = NumUtil.safeToByte(value); - } + void setMetadata(byte[] bytes); - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public byte getValue() { - return value; - } - } + boolean shouldRotateDisk(); - private class JfrChangeEpochOperation extends JavaVMOperation { - protected JfrChangeEpochOperation() { - super(VMOperationInfos.get(JfrChangeEpochOperation.class, "JFR change epoch", SystemEffect.SAFEPOINT)); - } + long beginEvent(); - @Override - protected void operate() { - changeEpoch(); - } + void endEvent(long start); - /** - * We need to ensure that all JFR events that are triggered by the current thread are - * recorded for the next epoch. Otherwise, those JFR events could pollute the data that we - * currently try to persist. To ensure that, we must uninterruptedly flush all data that is - * currently in-flight. - */ - @Uninterruptible(reason = "Prevent pollution of the current thread's thread local JFR buffer.") - private void changeEpoch() { - processSamplerBuffers(); - flushStorage(false); + void writeBoolean(boolean value); - /* Notify all event writers that the epoch changed. */ - for (IsolateThread thread = VMThreads.firstThread(); thread.isNonNull(); thread = VMThreads.nextThread(thread)) { - JfrThreadLocal.notifyEventWriter(thread); - } + void writeByte(byte value); - JfrTraceIdEpoch.getInstance().changeEpoch(); + void writeBytes(byte[] values); - // Now that the epoch changed, re-register all running threads for the new epoch. - SubstrateJVM.getThreadRepo().registerRunningThreads(); - } + void writeCompressedInt(int value); - /** - * The VM is at a safepoint, so all other threads have a native state. However, execution - * sampling could still be executed. For the {@link JfrRecurringCallbackExecutionSampler}, - * it is sufficient to mark this method as uninterruptible to prevent execution of the - * recurring callbacks. If the SIGPROF-based sampler is used, the signal handler may still - * be executed at any time for any thread (including the current thread). To prevent races, - * we need to ensure that there are no threads that execute the SIGPROF handler while we are - * accessing the currently active buffers of other threads. - */ - @Uninterruptible(reason = "Prevent JFR recording.") - private static void processSamplerBuffers() { - assert VMOperation.isInProgressAtSafepoint(); - assert ThreadingSupportImpl.isRecurringCallbackPaused(); + void writePaddedInt(long value); - JfrExecutionSampler.singleton().disallowThreadsInSamplerCode(); - try { - processSamplerBuffers0(); - } finally { - JfrExecutionSampler.singleton().allowThreadsInSamplerCode(); - } - } + void writeCompressedLong(long value); - @Uninterruptible(reason = "Prevent JFR recording.") - private static void processSamplerBuffers0() { - SamplerBuffersAccess.processActiveBuffers(); - SamplerBuffersAccess.processFullBuffers(false); - } - } + void writeString(String str); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrExecutionSamplerSupported.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrExecutionSamplerSupported.java new file mode 100644 index 000000000000..60ab5fe561e3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrExecutionSamplerSupported.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.jfr; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.graal.RuntimeCompilation; + +/** + * Returns {@code true} if the Native Image is built with JFR execution sampler support. + */ +@Platforms(Platform.HOSTED_ONLY.class) +public class JfrExecutionSamplerSupported { + + public static boolean isSupported() { + if (ImageSingletons.contains(JfrExecutionSamplerSupported.class)) { + return !RuntimeCompilation.isEnabled() && ImageSingletons.lookup(JfrExecutionSamplerSupported.class).isSupported0(); + } else { + return false; + } + } + + protected boolean isSupported0() { + return HasJfrSupport.get(); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java index 7de9d64a2c49..67e977e6aca8 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrFeature.java @@ -34,12 +34,13 @@ import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.VMInspectionOptions; -import com.oracle.svm.core.deopt.DeoptimizationSupport; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.jdk.RuntimeSupport; import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch; import com.oracle.svm.core.jfr.traceid.JfrTraceIdMap; +import com.oracle.svm.core.sampler.SamplerJfrStackTraceSerializer; +import com.oracle.svm.core.sampler.SamplerStackTraceSerializer; import com.oracle.svm.core.sampler.SamplerStackWalkVisitor; import com.oracle.svm.core.thread.ThreadListenerSupport; import com.oracle.svm.core.thread.ThreadListenerSupportFeature; @@ -128,10 +129,6 @@ private static boolean osSupported() { return Platform.includedIn(Platform.LINUX.class) || Platform.includedIn(Platform.DARWIN.class); } - public static boolean isExecutionSamplerSupported() { - return HasJfrSupport.get() && !DeoptimizationSupport.enabled(); - } - /** * We cannot use the proper way of looking up the bean via * {@link java.lang.management.ManagementFactory} because that initializes too many classes at @@ -160,12 +157,14 @@ public void afterRegistration(AfterRegistrationAccess access) { JfrJdkCompatibility.createNativeJFR(); ImageSingletons.add(JfrManager.class, new JfrManager(HOSTED_ENABLED)); - ImageSingletons.add(SubstrateJVM.class, new SubstrateJVM(knownConfigurations)); + ImageSingletons.add(SubstrateJVM.class, new SubstrateJVM(knownConfigurations, true)); ImageSingletons.add(JfrSerializerSupport.class, new JfrSerializerSupport()); ImageSingletons.add(JfrTraceIdMap.class, new JfrTraceIdMap()); ImageSingletons.add(JfrTraceIdEpoch.class, new JfrTraceIdEpoch()); ImageSingletons.add(JfrGCNames.class, new JfrGCNames()); ImageSingletons.add(SamplerStackWalkVisitor.class, new SamplerStackWalkVisitor()); + ImageSingletons.add(JfrExecutionSamplerSupported.class, new JfrExecutionSamplerSupported()); + ImageSingletons.add(SamplerStackTraceSerializer.class, new SamplerJfrStackTraceSerializer()); JfrSerializerSupport.get().register(new JfrFrameTypeSerializer()); JfrSerializerSupport.get().register(new JfrThreadStateSerializer()); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java index a4149425088d..90e6a73bd539 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrNativeEventWriter.java @@ -209,12 +209,12 @@ public static void putString(JfrNativeEventWriterData data, String string) { @Uninterruptible(reason = "Accesses a native JFR buffer.", callerMustBe = true) public static void putString(JfrNativeEventWriterData data, String string, CharReplacer replacer) { if (string == null) { - putByte(data, JfrChunkWriter.StringEncoding.NULL.getValue()); + putByte(data, JfrChunkFileWriter.StringEncoding.NULL.getValue()); } else if (string.isEmpty()) { - putByte(data, JfrChunkWriter.StringEncoding.EMPTY_STRING.getValue()); + putByte(data, JfrChunkFileWriter.StringEncoding.EMPTY_STRING.getValue()); } else { int mUTF8Length = UninterruptibleUtils.String.modifiedUTF8Length(string, false, replacer); - putByte(data, JfrChunkWriter.StringEncoding.UTF8_BYTE_ARRAY.getValue()); + putByte(data, JfrChunkFileWriter.StringEncoding.UTF8_BYTE_ARRAY.getValue()); putInt(data, mUTF8Length); if (ensureSize(data, mUTF8Length)) { Pointer newPosition = UninterruptibleUtils.String.toModifiedUTF8(string, data.getCurrentPos(), data.getEndPos(), false, replacer); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java index 8536602d691c..17581671dcc6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrRecorderThread.java @@ -83,7 +83,7 @@ public void run() { } } } catch (Throwable e) { - VMError.shouldNotReachHere("No exception must by thrown in the JFR recorder thread as this could break file IO operations."); + VMError.shouldNotReachHere("No exception must by thrown in the JFR recorder thread as this could break file IO operations.", e); } } 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 d09667b187fe..fc8a9f4e3262 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 @@ -92,7 +92,7 @@ public class SubstrateJVM { private String dumpPath; @Platforms(Platform.HOSTED_ONLY.class) - public SubstrateJVM(List configurations) { + public SubstrateJVM(List configurations, boolean writeFile) { this.knownConfigurations = configurations; options = new JfrOptionSet(); @@ -112,7 +112,7 @@ public SubstrateJVM(List configurations) { threadLocal = new JfrThreadLocal(); globalMemory = new JfrGlobalMemory(); samplerBufferPool = new SamplerBufferPool(); - unlockedChunkWriter = new JfrChunkWriter(globalMemory, stackTraceRepo, methodRepo, typeRepo, symbolRepo, threadRepo); + unlockedChunkWriter = writeFile ? new JfrChunkFileWriter(globalMemory, stackTraceRepo, methodRepo, typeRepo, symbolRepo, threadRepo) : new JfrChunkNoWriter(); recorderThread = new JfrRecorderThread(globalMemory, unlockedChunkWriter); jfrLogging = new JfrLogging(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrNoExecutionSampler.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrNoExecutionSampler.java index 6f991c8b1199..614dec36b14b 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrNoExecutionSampler.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrNoExecutionSampler.java @@ -27,14 +27,15 @@ import java.util.Collections; import java.util.List; -import com.oracle.svm.core.Uninterruptible; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; import org.graalvm.nativeimage.hosted.Feature; +import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.jfr.JfrExecutionSamplerSupported; import com.oracle.svm.core.jfr.JfrFeature; public class JfrNoExecutionSampler extends JfrExecutionSampler { @@ -92,7 +93,7 @@ public List> getRequiredFeatures() { @Override public void afterRegistration(AfterRegistrationAccess access) { - if (!JfrFeature.isExecutionSamplerSupported()) { + if (!JfrExecutionSamplerSupported.isSupported()) { ImageSingletons.add(JfrExecutionSampler.class, new JfrNoExecutionSampler()); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrRecurringCallbackExecutionSampler.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrRecurringCallbackExecutionSampler.java index b2166a787b65..33a5372d1fa1 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrRecurringCallbackExecutionSampler.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrRecurringCallbackExecutionSampler.java @@ -30,6 +30,7 @@ import java.util.Collections; import java.util.List; +import com.oracle.svm.core.jfr.JfrExecutionSamplerSupported; import org.graalvm.nativeimage.CurrentIsolate; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.IsolateThread; @@ -153,7 +154,7 @@ public List> getRequiredFeatures() { @Override public void duringSetup(DuringSetupAccess access) { - if (JfrFeature.isExecutionSamplerSupported() && !ImageSingletons.contains(JfrExecutionSampler.class)) { + if (JfrExecutionSamplerSupported.isSupported() && !ImageSingletons.contains(JfrExecutionSampler.class)) { JfrRecurringCallbackExecutionSampler sampler = new JfrRecurringCallbackExecutionSampler(); ImageSingletons.add(JfrExecutionSampler.class, sampler); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/ProfilingSampler.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/ProfilingSampler.java index bd02d3b60e28..2be44e25f038 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/ProfilingSampler.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/ProfilingSampler.java @@ -30,4 +30,6 @@ public interface ProfilingSampler { LockFreePrefixTree prefixTree(); void reset(); + + boolean isAsyncSampler(); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SafepointProfilingSampler.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SafepointProfilingSampler.java index aa559088cf0a..883e51519f0c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SafepointProfilingSampler.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SafepointProfilingSampler.java @@ -102,6 +102,11 @@ public void reset() { prefixTree.reset(); } + @Override + public boolean isAsyncSampler() { + return false; + } + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Must not allocate inside the safepoint sampler.") private void sampleThreadStack(SamplingStackVisitor.StackTrace stackTrace, SamplerStats samplerStats) { samplerStats.started(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java index d84377c8a9f9..d10a6c9569ba 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java @@ -28,36 +28,15 @@ import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; -import org.graalvm.nativeimage.StackValue; -import org.graalvm.nativeimage.c.function.CodePointer; -import org.graalvm.nativeimage.c.type.CIntPointer; import org.graalvm.word.Pointer; -import org.graalvm.word.WordFactory; import com.oracle.svm.core.Uninterruptible; -import com.oracle.svm.core.code.CodeInfo; -import com.oracle.svm.core.code.CodeInfoAccess; -import com.oracle.svm.core.code.CodeInfoDecoder.FrameInfoCursor; -import com.oracle.svm.core.code.CodeInfoTable; -import com.oracle.svm.core.code.FrameInfoQueryResult; -import com.oracle.svm.core.code.UntetheredCodeInfo; -import com.oracle.svm.core.jfr.JfrBuffer; -import com.oracle.svm.core.jfr.JfrFrameType; -import com.oracle.svm.core.jfr.JfrNativeEventWriter; -import com.oracle.svm.core.jfr.JfrNativeEventWriterData; -import com.oracle.svm.core.jfr.JfrNativeEventWriterDataAccess; -import com.oracle.svm.core.jfr.JfrStackTraceRepository.JfrStackTraceTableEntry; -import com.oracle.svm.core.jfr.JfrStackTraceRepository.JfrStackTraceTableEntryStatus; import com.oracle.svm.core.jfr.JfrThreadLocal; import com.oracle.svm.core.jfr.SubstrateJVM; -import com.oracle.svm.core.jfr.events.ExecutionSampleEvent; import com.oracle.svm.core.thread.VMOperation; import com.oracle.svm.core.thread.VMThreads; -import com.oracle.svm.core.util.VMError; public final class SamplerBuffersAccess { - /** This value is used by multiple threads but only by a single thread at a time. */ - private static final FrameInfoCursor FRAME_INFO_CURSOR = new FrameInfoCursor(); @Platforms(Platform.HOSTED_ONLY.class) private SamplerBuffersAccess() { @@ -153,127 +132,16 @@ private static void serializeStackTraces(SamplerBuffer rawStackTraceBuffer) { assert current.subtract(entryStart).equal(SamplerSampleWriter.getHeaderSize()); - CIntPointer statusPtr = StackValue.get(CIntPointer.class); - JfrStackTraceTableEntry entry = SubstrateJVM.getStackTraceRepo().getOrPutStackTrace(current, WordFactory.unsigned(sampleSize), sampleHash, statusPtr); - long stackTraceId = entry.isNull() ? 0 : entry.getId(); - - int status = statusPtr.read(); - if (status == JfrStackTraceTableEntryStatus.INSERTED || status == JfrStackTraceTableEntryStatus.EXISTING_RAW) { - /* Walk the IPs and serialize the stacktrace. */ - assert current.add(sampleSize).belowThan(end); - boolean serialized = serializeStackTrace(current, sampleSize, isTruncated, stackTraceId); - if (serialized) { - SubstrateJVM.getStackTraceRepo().commitSerializedStackTrace(entry); - } - } else { - /* Processing is not needed: skip the rest of the data. */ - assert status == JfrStackTraceTableEntryStatus.EXISTING_SERIALIZED || status == JfrStackTraceTableEntryStatus.INSERT_FAILED; - } - current = current.add(sampleSize); - - /* - * Emit an event depending on the end marker of the raw stack trace. This needs to be - * done here because the sampler can't emit the event directly. - */ - long endMarker = current.readLong(0); - if (endMarker == SamplerSampleWriter.EXECUTION_SAMPLE_END) { - if (stackTraceId != 0) { - ExecutionSampleEvent.writeExecutionSample(sampleTick, threadId, stackTraceId, threadState); - } else { - JfrThreadLocal.increaseMissedSamples(); - } - } else { - assert endMarker == SamplerSampleWriter.JFR_STACK_TRACE_END; - } - current = current.add(SamplerSampleWriter.END_MARKER_SIZE); + current = serializeStackTrace(current, end, sampleSize, sampleHash, isTruncated, sampleTick, threadId, threadState); } SamplerBufferAccess.reinitialize(rawStackTraceBuffer); } - @Uninterruptible(reason = "Prevent JFR recording and epoch change.") - private static boolean serializeStackTrace(Pointer rawStackTrace, int sampleSize, boolean isTruncated, long stackTraceId) { - assert sampleSize % Long.BYTES == 0; - - JfrBuffer targetBuffer = SubstrateJVM.getStackTraceRepo().getCurrentBuffer(); - if (targetBuffer.isNull()) { - return false; - } - - /* - * One IP may correspond to multiple Java-level stack frames. We need to precompute the - * number of stack trace elements because the count can't be patched later on - * (JfrNativeEventWriter.putInt() would not necessarily reserve enough bytes). - */ - int numStackTraceElements = visitRawStackTrace(rawStackTrace, sampleSize, WordFactory.nullPointer()); - if (numStackTraceElements == 0) { - return false; - } - - JfrNativeEventWriterData data = StackValue.get(JfrNativeEventWriterData.class); - JfrNativeEventWriterDataAccess.initialize(data, targetBuffer); - JfrNativeEventWriter.putLong(data, stackTraceId); - JfrNativeEventWriter.putBoolean(data, isTruncated); - JfrNativeEventWriter.putInt(data, numStackTraceElements); - visitRawStackTrace(rawStackTrace, sampleSize, data); - boolean success = JfrNativeEventWriter.commit(data); - - /* Buffer can get replaced with a larger one. */ - SubstrateJVM.getStackTraceRepo().setCurrentBuffer(data.getJfrBuffer()); - return success; - } - - @Uninterruptible(reason = "Prevent JFR recording and epoch change.") - private static int visitRawStackTrace(Pointer rawStackTrace, int sampleSize, JfrNativeEventWriterData data) { - int numStackTraceElements = 0; - Pointer rawStackTraceEnd = rawStackTrace.add(sampleSize); - Pointer ipPtr = rawStackTrace; - while (ipPtr.belowThan(rawStackTraceEnd)) { - long ip = ipPtr.readLong(0); - numStackTraceElements += visitFrame(data, ip); - ipPtr = ipPtr.add(Long.BYTES); - } - return numStackTraceElements; - } - - @Uninterruptible(reason = "Prevent JFR recording, epoch change, and that the GC frees the CodeInfo.") - private static int visitFrame(JfrNativeEventWriterData data, long address) { - CodePointer ip = WordFactory.pointer(address); - UntetheredCodeInfo untetheredInfo = CodeInfoTable.lookupCodeInfo(ip); - if (untetheredInfo.isNull()) { - /* Unknown frame. Must not happen for AOT-compiled code. */ - VMError.shouldNotReachHere("Stack walk must walk only frames of known code."); - } - - Object tether = CodeInfoAccess.acquireTether(untetheredInfo); - try { - CodeInfo tetheredCodeInfo = CodeInfoAccess.convert(untetheredInfo, tether); - return visitFrame(data, tetheredCodeInfo, ip); - } finally { - CodeInfoAccess.releaseTether(untetheredInfo, tether); - } - } - - @Uninterruptible(reason = "Prevent JFR recording and epoch change.") - private static int visitFrame(JfrNativeEventWriterData data, CodeInfo codeInfo, CodePointer ip) { - int numStackTraceElements = 0; - FRAME_INFO_CURSOR.initialize(codeInfo, ip, false); - while (FRAME_INFO_CURSOR.advance()) { - if (data.isNonNull()) { - FrameInfoQueryResult frame = FRAME_INFO_CURSOR.get(); - serializeStackTraceElement(data, frame); - } - numStackTraceElements++; - } - return numStackTraceElements; - } - - @Uninterruptible(reason = "Prevent JFR recording and epoch change.") - private static void serializeStackTraceElement(JfrNativeEventWriterData data, FrameInfoQueryResult stackTraceElement) { - long methodId = SubstrateJVM.getMethodRepo().getMethodId(stackTraceElement.getSourceClass(), stackTraceElement.getSourceMethodName(), stackTraceElement.getMethodId()); - JfrNativeEventWriter.putLong(data, methodId); - JfrNativeEventWriter.putInt(data, stackTraceElement.getSourceLineNumber()); - JfrNativeEventWriter.putInt(data, stackTraceElement.getBci()); - JfrNativeEventWriter.putLong(data, JfrFrameType.FRAME_AOT_COMPILED.getId()); + @Uninterruptible(reason = "Wraps the call to the possibly interruptible serializer.", calleeMustBe = false) + private static Pointer serializeStackTrace(Pointer rawStackTrace, Pointer bufferEnd, int sampleSize, int sampleHash, + boolean isTruncated, long sampleTick, long threadId, long threadState) { + return SamplerStackTraceSerializer.singleton().serializeStackTrace(rawStackTrace, bufferEnd, sampleSize, + sampleHash, isTruncated, sampleTick, threadId, threadState); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerJfrStackTraceSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerJfrStackTraceSerializer.java new file mode 100644 index 000000000000..c88eb66fce3a --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerJfrStackTraceSerializer.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.sampler; + +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.function.CodePointer; +import org.graalvm.nativeimage.c.type.CIntPointer; +import org.graalvm.word.Pointer; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.code.CodeInfo; +import com.oracle.svm.core.code.CodeInfoAccess; +import com.oracle.svm.core.code.CodeInfoDecoder; +import com.oracle.svm.core.code.CodeInfoTable; +import com.oracle.svm.core.code.FrameInfoQueryResult; +import com.oracle.svm.core.code.UntetheredCodeInfo; +import com.oracle.svm.core.jfr.JfrBuffer; +import com.oracle.svm.core.jfr.JfrFrameType; +import com.oracle.svm.core.jfr.JfrNativeEventWriter; +import com.oracle.svm.core.jfr.JfrNativeEventWriterData; +import com.oracle.svm.core.jfr.JfrNativeEventWriterDataAccess; +import com.oracle.svm.core.jfr.JfrStackTraceRepository; +import com.oracle.svm.core.jfr.JfrThreadLocal; +import com.oracle.svm.core.jfr.SubstrateJVM; +import com.oracle.svm.core.jfr.events.ExecutionSampleEvent; +import com.oracle.svm.core.util.VMError; + +/** + * A concrete implementation of {@link SamplerStackTraceSerializer} designed for JFR stack trace + * serialization. + */ +public final class SamplerJfrStackTraceSerializer implements SamplerStackTraceSerializer { + /** This value is used by multiple threads but only by a single thread at a time. */ + private static final CodeInfoDecoder.FrameInfoCursor FRAME_INFO_CURSOR = new CodeInfoDecoder.FrameInfoCursor(); + + @Override + @Uninterruptible(reason = "Prevent JFR recording and epoch change.") + public Pointer serializeStackTrace(Pointer rawStackTrace, Pointer bufferEnd, int sampleSize, int sampleHash, + boolean isTruncated, long sampleTick, long threadId, long threadState) { + Pointer current = rawStackTrace; + CIntPointer statusPtr = StackValue.get(CIntPointer.class); + JfrStackTraceRepository.JfrStackTraceTableEntry entry = SubstrateJVM.getStackTraceRepo().getOrPutStackTrace(current, WordFactory.unsigned(sampleSize), sampleHash, statusPtr); + long stackTraceId = entry.isNull() ? 0 : entry.getId(); + + int status = statusPtr.read(); + if (status == JfrStackTraceRepository.JfrStackTraceTableEntryStatus.INSERTED || status == JfrStackTraceRepository.JfrStackTraceTableEntryStatus.EXISTING_RAW) { + /* Walk the IPs and serialize the stacktrace. */ + assert current.add(sampleSize).belowThan(bufferEnd); + boolean serialized = serializeStackTrace(current, sampleSize, isTruncated, stackTraceId); + if (serialized) { + SubstrateJVM.getStackTraceRepo().commitSerializedStackTrace(entry); + } + } else { + /* Processing is not needed: skip the rest of the data. */ + assert status == JfrStackTraceRepository.JfrStackTraceTableEntryStatus.EXISTING_SERIALIZED || status == JfrStackTraceRepository.JfrStackTraceTableEntryStatus.INSERT_FAILED; + } + current = current.add(sampleSize); + + /* + * Emit an event depending on the end marker of the raw stack trace. This needs to be done + * here because the sampler can't emit the event directly. + */ + long endMarker = current.readLong(0); + if (endMarker == SamplerSampleWriter.EXECUTION_SAMPLE_END) { + if (stackTraceId != 0) { + ExecutionSampleEvent.writeExecutionSample(sampleTick, threadId, stackTraceId, threadState); + } else { + JfrThreadLocal.increaseMissedSamples(); + } + } else { + assert endMarker == SamplerSampleWriter.JFR_STACK_TRACE_END; + } + + current = current.add(SamplerSampleWriter.END_MARKER_SIZE); + return current; + } + + @Uninterruptible(reason = "Prevent JFR recording and epoch change.") + private static boolean serializeStackTrace(Pointer rawStackTrace, int sampleSize, boolean isTruncated, long stackTraceId) { + assert sampleSize % Long.BYTES == 0; + + JfrBuffer targetBuffer = SubstrateJVM.getStackTraceRepo().getCurrentBuffer(); + if (targetBuffer.isNull()) { + return false; + } + + /* + * One IP may correspond to multiple Java-level stack frames. We need to precompute the + * number of stack trace elements because the count can't be patched later on + * (JfrNativeEventWriter.putInt() would not necessarily reserve enough bytes). + */ + int numStackTraceElements = visitRawStackTrace(rawStackTrace, sampleSize, WordFactory.nullPointer()); + if (numStackTraceElements == 0) { + return false; + } + + JfrNativeEventWriterData data = StackValue.get(JfrNativeEventWriterData.class); + JfrNativeEventWriterDataAccess.initialize(data, targetBuffer); + JfrNativeEventWriter.putLong(data, stackTraceId); + JfrNativeEventWriter.putBoolean(data, isTruncated); + JfrNativeEventWriter.putInt(data, numStackTraceElements); + visitRawStackTrace(rawStackTrace, sampleSize, data); + boolean success = JfrNativeEventWriter.commit(data); + + /* Buffer can get replaced with a larger one. */ + SubstrateJVM.getStackTraceRepo().setCurrentBuffer(data.getJfrBuffer()); + return success; + } + + @Uninterruptible(reason = "Prevent JFR recording and epoch change.") + private static int visitRawStackTrace(Pointer rawStackTrace, int sampleSize, JfrNativeEventWriterData data) { + int numStackTraceElements = 0; + Pointer rawStackTraceEnd = rawStackTrace.add(sampleSize); + Pointer ipPtr = rawStackTrace; + while (ipPtr.belowThan(rawStackTraceEnd)) { + long ip = ipPtr.readLong(0); + numStackTraceElements += visitFrame(data, ip); + ipPtr = ipPtr.add(Long.BYTES); + } + return numStackTraceElements; + } + + @Uninterruptible(reason = "Prevent JFR recording, epoch change, and that the GC frees the CodeInfo.") + private static int visitFrame(JfrNativeEventWriterData data, long address) { + CodePointer ip = WordFactory.pointer(address); + UntetheredCodeInfo untetheredInfo = CodeInfoTable.lookupCodeInfo(ip); + if (untetheredInfo.isNull()) { + /* Unknown frame. Must not happen for AOT-compiled code. */ + VMError.shouldNotReachHere("Stack walk must walk only frames of known code."); + } + + Object tether = CodeInfoAccess.acquireTether(untetheredInfo); + try { + CodeInfo tetheredCodeInfo = CodeInfoAccess.convert(untetheredInfo, tether); + return visitFrame(data, tetheredCodeInfo, ip); + } finally { + CodeInfoAccess.releaseTether(untetheredInfo, tether); + } + } + + @Uninterruptible(reason = "Prevent JFR recording and epoch change.") + private static int visitFrame(JfrNativeEventWriterData data, CodeInfo codeInfo, CodePointer ip) { + int numStackTraceElements = 0; + FRAME_INFO_CURSOR.initialize(codeInfo, ip, false); + while (FRAME_INFO_CURSOR.advance()) { + if (data.isNonNull()) { + FrameInfoQueryResult frame = FRAME_INFO_CURSOR.get(); + serializeStackTraceElement(data, frame); + } + numStackTraceElements++; + } + return numStackTraceElements; + } + + @Uninterruptible(reason = "Prevent JFR recording and epoch change.") + private static void serializeStackTraceElement(JfrNativeEventWriterData data, FrameInfoQueryResult stackTraceElement) { + long methodId = SubstrateJVM.getMethodRepo().getMethodId(stackTraceElement.getSourceClass(), stackTraceElement.getSourceMethodName(), stackTraceElement.getMethodId()); + JfrNativeEventWriter.putLong(data, methodId); + JfrNativeEventWriter.putInt(data, stackTraceElement.getSourceLineNumber()); + JfrNativeEventWriter.putInt(data, stackTraceElement.getBci()); + JfrNativeEventWriter.putLong(data, JfrFrameType.FRAME_AOT_COMPILED.getId()); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerSampleWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerSampleWriter.java index 1681a0c255e6..59299fd9f5d8 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerSampleWriter.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerSampleWriter.java @@ -189,7 +189,6 @@ private static boolean accommodate(SamplerSampleWriterData data, UnsignedWord un /* Put in the stack with other unprocessed buffers and send a signal to the JFR recorder. */ SamplerBuffer oldBuffer = data.getSamplerBuffer(); SubstrateJVM.getSamplerBufferPool().pushFullBuffer(oldBuffer); - SubstrateJVM.getRecorderThread().signal(); /* Reinitialize data structure. */ data.setSamplerBuffer(newBuffer); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerStackTraceSerializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerStackTraceSerializer.java new file mode 100644 index 000000000000..f69df38bb144 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerStackTraceSerializer.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.core.sampler; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.word.Pointer; + +import jdk.graal.compiler.api.replacements.Fold; + +/** + * An interface for serializing stack traces. Subtypes should implement the actual serialization + * logic based on their specific requirements. + */ +public interface SamplerStackTraceSerializer { + + @Fold + static SamplerStackTraceSerializer singleton() { + return ImageSingletons.lookup(SamplerStackTraceSerializer.class); + } + + /** + * Serializes a stack trace sample and related information. + * + * @param rawStackTrace The pointer to the beginning of the stack trace in the buffer. + * @param bufferEnd The pointer to the end of the buffer. + * @param sampleSize The size of the stack trace sample. + * @param sampleHash A unique hash code representing the sample. + * @param isTruncated Indicates whether the sample is truncated. + * @param sampleTick The timestamp of the sample. + * @param threadId A unique identifier for the sampled thread. + * @param threadState The state of the sampled thread. + * @return A pointer to the next stack trace entry or the end of the buffer. + */ + Pointer serializeStackTrace(Pointer rawStackTrace, Pointer bufferEnd, int sampleSize, int sampleHash, + boolean isTruncated, long sampleTick, long threadId, long threadState); +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java index 1e22318a764a..1585ccb2023e 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jfr/utils/JfrFileParser.java @@ -39,7 +39,7 @@ import java.util.HashMap; import com.oracle.svm.core.jfr.JfrCheckpointType; -import com.oracle.svm.core.jfr.JfrChunkWriter; +import com.oracle.svm.core.jfr.JfrChunkFileWriter; import com.oracle.svm.core.jfr.JfrReservedEvent; import com.oracle.svm.core.jfr.JfrTicks; import com.oracle.svm.core.jfr.JfrType; @@ -127,11 +127,11 @@ private static void parseMetadataHeader(RecordingInput input, long metadataPosit } private static FileHeaderInfo parseFileHeader(RecordingInput input) throws IOException { - byte[] fileMagic = new byte[JfrChunkWriter.FILE_MAGIC.length]; + byte[] fileMagic = new byte[JfrChunkFileWriter.FILE_MAGIC.length]; input.readFully(fileMagic); // File magic. - assertEquals("File magic is not correct!", new String(JfrChunkWriter.FILE_MAGIC), new String(fileMagic)); - assertEquals("JFR version major is not correct!", JfrChunkWriter.JFR_VERSION_MAJOR, input.readRawShort()); - assertEquals("JFR version minor is not correct!", JfrChunkWriter.JFR_VERSION_MINOR, input.readRawShort()); + assertEquals("File magic is not correct!", new String(JfrChunkFileWriter.FILE_MAGIC), new String(fileMagic)); + assertEquals("JFR version major is not correct!", JfrChunkFileWriter.JFR_VERSION_MAJOR, input.readRawShort()); + assertEquals("JFR version minor is not correct!", JfrChunkFileWriter.JFR_VERSION_MINOR, input.readRawShort()); assertTrue("Chunk size is invalid!", input.readRawLong() > 0); long checkpointPosition = input.readRawLong();