diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index 93233da4236c..9b91fa029ef9 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -4,8 +4,10 @@ This changelog summarizes major changes to GraalVM Native Image. ## Version 22.2.0 * (GR-20653) Re-enable the usage of all CPU features for JIT compilation on AMD64. -* (GR-38413) Add support for -XX:+ExitOnOutOfMemoryError. +* (GR-38413) Add support for `-XX:+ExitOnOutOfMemoryError`. * (GR-37606) Add support for URLs and short descriptions to `Feature`. This info is shown as part of the build output. +* (GR-38965) Heap dumps are now supported in Community Edition. +* (GR-38951) Add `-XX:+DumpHeapAndExit` option to dump the initial heap of a native executable. ## Version 22.1.0 * (GR-36568) Add "Quick build" mode, enabled through option `-Ob`, for quicker native image builds. diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/heapdump/AllocationFreeFileOutputStreamPosix.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/heapdump/AllocationFreeFileOutputStreamPosix.java new file mode 100644 index 000000000000..04baa6fcc1b0 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/heapdump/AllocationFreeFileOutputStreamPosix.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2022, 2022, 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.posix.heapdump; + +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.annotate.AutomaticFeature; +import com.oracle.svm.core.heapdump.HeapDumpWriterImpl.AllocationFreeFileOutputStream; +import com.oracle.svm.core.posix.PosixUtils; +import com.oracle.svm.core.posix.headers.Unistd; +import com.oracle.svm.core.util.VMError; + +/** + * Posix implementation of allocation-free output stream created from FileOutputStream. + * + */ +final class AllocationFreeFileOutputStreamPosix extends AllocationFreeFileOutputStream { + + /** + * Pre-allocated exceptions, for throwing from code that must not allocate. + */ + private static final IOException preallocatedIOException = new IOException("Write failed."); + private static final ArrayIndexOutOfBoundsException preallocatedArrayIndexOutOfBoundsException = new ArrayIndexOutOfBoundsException(); + + private FileOutputStream fos; + private FileDescriptor fileDescriptor; + + AllocationFreeFileOutputStreamPosix() { + + } + + private AllocationFreeFileOutputStreamPosix(FileOutputStream fileOutputStream) throws IOException { + fos = fileOutputStream; + fileDescriptor = fos.getFD(); + + if (!(Platform.includedIn(Platform.LINUX.class) || Platform.includedIn(Platform.DARWIN.class))) { + /* See GR-9725 */ + throw VMError.unsupportedFeature("Heap dump writing currently contains Posix specific code"); + } + } + + @Override + public AllocationFreeFileOutputStream newStreamFor(FileOutputStream fileOutputStream) throws IOException { + return new AllocationFreeFileOutputStreamPosix(fileOutputStream); + } + + @Override + public void write(int b) throws IOException { + final CCharPointer buffer = StackValue.get(CCharPointer.class); + buffer.write((byte) b); + final boolean writeResult = PosixUtils.writeBytes(fileDescriptor, buffer, WordFactory.unsigned(1)); + if (!writeResult) { + throw preallocatedIOException; + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + /* Sanity check the arguments. */ + if ((b == null) || ((off < 0) || (len < 0) || ((b.length - off) < len))) { + throw preallocatedArrayIndexOutOfBoundsException; + } + + /* + * Stack allocation needs an allocation size that is a compile time constant, so we split + * the byte array up in multiple chunks and write them separately. + */ + final int chunkSize = 256; + final CCharPointer bytes = StackValue.get(chunkSize); + + int chunkOffset = off; + int inputLength = len; + while (inputLength > 0) { + int chunkLength = Math.min(inputLength, chunkSize); + + for (int i = 0; i < chunkLength; i++) { + bytes.write(i, b[chunkOffset + i]); + } + + if (!PosixUtils.writeBytes(fileDescriptor, bytes, WordFactory.unsigned(chunkLength))) { + throw preallocatedIOException; + } + + chunkOffset += chunkLength; + inputLength -= chunkLength; + } + } + + @Override + public void close() throws IOException { + fos.close(); + } + + @Override + public void flush() throws IOException { + } + + /** + * Read the current position in a file descriptor. + */ + @Override + protected long position() { + int fd = PosixUtils.getFD(fileDescriptor); + return Unistd.lseek(fd, WordFactory.zero(), Unistd.SEEK_CUR()).rawValue(); + } + + /** + * Set the current position in a file descriptor. + */ + @Override + protected long position(long offset) { + int fd = PosixUtils.getFD(fileDescriptor); + return Unistd.lseek(fd, WordFactory.signed(offset), Unistd.SEEK_SET()).rawValue(); + } +} + +/* + * The limitation to Linux and Darwin is necessary because the implementation currently uses + * posix-dependent low-level code. See GR-9725. + */ +@Platforms({Platform.LINUX.class, Platform.DARWIN.class}) +@AutomaticFeature +class AllocationFreeFileOutputStreamFeature implements Feature { + + @Override + public void afterRegistration(Feature.AfterRegistrationAccess access) { + ImageSingletons.add(AllocationFreeFileOutputStream.class, new AllocationFreeFileOutputStreamPosix()); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/AllocationFreeOutputStream.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/AllocationFreeOutputStream.java new file mode 100644 index 000000000000..fe10df6adff1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/AllocationFreeOutputStream.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022, 2022, 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.heapdump; + +import java.io.IOException; + +/** + * Simple interface for writing heap dump to arbitrary data source. + * + */ +public interface AllocationFreeOutputStream { + + void write(int b) throws IOException; + + void write(byte[] b, int offset, int length) throws IOException; + + /** + * close method is called outside of critical section and can allocate objects. + * + * @throws IOException + */ + void close() throws IOException; + + void flush() throws IOException; + +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpFeature.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpFeature.java new file mode 100644 index 000000000000..def30346c186 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpFeature.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022, 2022, 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.heapdump; + +import java.io.FileOutputStream; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.impl.HeapDumpSupport; + +import com.oracle.svm.core.annotate.AutomaticFeature; + +@AutomaticFeature +public final class HeapDumpFeature implements Feature { + @Override + public void afterRegistration(AfterRegistrationAccess access) { + ImageSingletons.add(HeapDumpSupport.class, new HeapDumpSupportImpl()); + } +} + +final class HeapDumpSupportImpl implements HeapDumpSupport { + @Override + public void dumpHeap(String outputFile, boolean live) throws java.io.IOException { + try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile)) { + HeapDumpWriter.singleton().writeHeapTo(fileOutputStream, live); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpUtils.java new file mode 100644 index 000000000000..4e3955276d7c --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpUtils.java @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2022, 2022, 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.heapdump; + +import static com.oracle.svm.core.snippets.KnownIntrinsics.readCallerStackPointer; + +import com.oracle.svm.core.heap.VMOperationInfos; +import org.graalvm.compiler.word.Word; +import org.graalvm.nativeimage.CurrentIsolate; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.IsolateThread; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.c.function.CodePointer; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.word.Pointer; +import org.graalvm.word.UnsignedWord; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.annotate.AutomaticFeature; +import com.oracle.svm.core.annotate.NeverInline; +import com.oracle.svm.core.annotate.RestrictHeapAccess; +import com.oracle.svm.core.annotate.UnknownObjectField; +import com.oracle.svm.core.code.CodeInfo; +import com.oracle.svm.core.code.CodeInfoTable; +import com.oracle.svm.core.deopt.DeoptimizedFrame; +import com.oracle.svm.core.heap.Heap; +import com.oracle.svm.core.heap.ObjectReferenceVisitor; +import com.oracle.svm.core.heap.ObjectVisitor; +import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.hub.InteriorObjRefWalker; +import com.oracle.svm.core.hub.LayoutEncoding; +import com.oracle.svm.core.snippets.KnownIntrinsics; +import com.oracle.svm.core.stack.JavaStackWalker; +import com.oracle.svm.core.stack.StackFrameVisitor; +import com.oracle.svm.core.thread.JavaVMOperation; +import com.oracle.svm.core.thread.VMThreads; + +/** A collection of utilities that might assist heap dumps. */ +public class HeapDumpUtils { + + @UnknownObjectField(types = {byte[].class}) private byte[] fieldsMap; + + /** Extra methods for testing. */ + private final TestingBackDoor testingBackDoor; + + /** Constructor. */ + HeapDumpUtils() { + this.testingBackDoor = new TestingBackDoor(this); + } + + /** Accessor for the singleton. */ + public static HeapDumpUtils getHeapDumpUtils() { + return ImageSingletons.lookup(HeapDumpUtils.class); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void setFieldsMap(byte[] map) { + fieldsMap = map; + } + + /** + * Walk all the objects in the heap, both the image heap and the garbage collected heap applying + * a visitor to each. + */ + public boolean walkHeapObjects(ObjectVisitor imageHeapVisitor, ObjectVisitor collectedHeapVisitor) { + final WalkHeapObjectsOperation operation = new WalkHeapObjectsOperation(imageHeapVisitor, collectedHeapVisitor); + return walkHeapObjectsWithoutAllocating(operation); + } + + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Iterating the heap must not allocate in the heap.") + boolean walkHeapObjectsWithoutAllocating(WalkHeapObjectsOperation operation) { + operation.enqueue(); + return operation.getResult(); + } + + public int instanceSizeOf(Class cls) { + final int encoding = DynamicHub.fromClass(cls).getLayoutEncoding(); + if (LayoutEncoding.isPureInstance(encoding)) { + return (int) LayoutEncoding.getPureInstanceSize(encoding).rawValue(); + } else { + return 0; + } + } + + boolean isPrimitiveArray(Object obj) { + final int encoding = KnownIntrinsics.readHub(obj).getLayoutEncoding(); + return LayoutEncoding.isPrimitiveArray(encoding); + } + + public boolean isJavaPrimitiveArray(Object obj) { + return (isPrimitiveArray(obj) && + ((obj instanceof char[]) || + (obj instanceof byte[]) || + (obj instanceof int[]) || + (obj instanceof long[]) || + (obj instanceof boolean[]) || + (obj instanceof short[]) || + (obj instanceof double[]) || + (obj instanceof float[]))); + } + + /** + * Return a pointer to an object. The result is untracked by the collector: it is treated as an + * integer. + */ + public Pointer objectToPointer(Object obj) { + return Word.objectToUntrackedPointer(obj); + } + + public byte[] getFieldsMap() { + return fieldsMap; + } + + public boolean walkStacks(StacksSlotsVisitor stacksSlotsVisitor) { + final WalkStacksSlotsOperation walkStacksOperation = new WalkStacksSlotsOperation(stacksSlotsVisitor); + walkStacksOperation.enqueue(); + return walkStacksOperation.getResult(); + } + + private static final class WalkHeapObjectsOperation extends JavaVMOperation { + + /* Instance state. */ + private final ObjectVisitor imageHeapVisitor; + private final ObjectVisitor collectedHeapVisitor; + boolean result; + + /** Constructor. */ + WalkHeapObjectsOperation(ObjectVisitor imageHeapVisitor, ObjectVisitor collectedHeapVisitor) { + super(VMOperationInfos.get(WalkHeapObjectsOperation.class, "Walk Java heap for heap dump", SystemEffect.SAFEPOINT)); + this.imageHeapVisitor = imageHeapVisitor; + this.collectedHeapVisitor = collectedHeapVisitor; + } + + @Override + public void operate() { + operateWithoutAllocation(); + } + + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Do not allocate while walking the heap.") + private void operateWithoutAllocation() { + result = Heap.getHeap().walkImageHeapObjects(imageHeapVisitor) && Heap.getHeap().walkCollectedHeapObjects(collectedHeapVisitor); + } + + private boolean getResult() { + return result; + } + } + + private static final class WalkStacksSlotsOperation extends JavaVMOperation { + + private final StacksSlotsVisitor stacksSlotsVisitor; + private boolean result; + + WalkStacksSlotsOperation(StacksSlotsVisitor stacksSlotsVisitor) { + super(VMOperationInfos.get(WalkStacksSlotsOperation.class, "Walk stack for heap dump", SystemEffect.SAFEPOINT)); + this.stacksSlotsVisitor = stacksSlotsVisitor; + } + + @Override + public void operate() { + operateWithoutAllocation(); + } + + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Do not allocate while walking thread stacks.") + private void operateWithoutAllocation() { + result = stacksSlotsVisitor.visitStacksSlots(); + } + + private boolean getResult() { + return result; + } + } + + public abstract static class StacksSlotsVisitor extends StackFrameVisitor implements ObjectReferenceVisitor { + + private IsolateThread currentVMThread; + private Pointer currentStackSP; + private Pointer currentFrameSP; + private CodePointer currentFrameIP; + private DeoptimizedFrame currentDeoptimizedFrame; + + /** Constructor for subclasses. */ + public StacksSlotsVisitor() { + /* Nothing to do. */ + } + + /* + * Access methods for subclasses. + */ + + /** The current VMThread. */ + protected IsolateThread getVMThread() { + return currentVMThread; + } + + /** The stack pointer for the current VMThread. */ + protected Pointer getStackSP() { + return currentStackSP; + } + + /** The stack pointer for the current frame. */ + protected Pointer getFrameSP() { + return currentFrameSP; + } + + /** The instruction pointer for the current frame. */ + protected CodePointer getFrameIP() { + return currentFrameIP; + } + + /** The DeoptimizedFrame for the current frame. */ + protected DeoptimizedFrame getDeoptimizedFrame() { + return currentDeoptimizedFrame; + } + + @NeverInline("Starting a stack walk in the caller frame") + protected boolean visitStacksSlots() { + /* Visit the current thread, because it does not have a JavaFrameAnchor. */ + currentVMThread = CurrentIsolate.getCurrentThread(); + currentStackSP = readCallerStackPointer(); + JavaStackWalker.walkCurrentThread(currentStackSP, this); + if (SubstrateOptions.MultiThreaded.getValue()) { + /* + * Scan the stacks of all the threads. Other threads will be blocked at a safepoint + * (or in native code) so they will each have a JavaFrameAnchor in their VMThread. + */ + for (IsolateThread vmThread = VMThreads.firstThread(); vmThread.isNonNull(); vmThread = VMThreads.nextThread(vmThread)) { + if (vmThread == CurrentIsolate.getCurrentThread()) { + /* + * The current thread is already scanned by code above, so we do not have to + * do anything for it here. It might have a JavaFrameAnchor from earlier + * Java-to-C transitions, but certainly not at the top of the stack since it + * is running this code, so just this scan would be incomplete. + */ + continue; + } + currentVMThread = vmThread; + currentStackSP = WordFactory.nullPointer(); + currentFrameSP = WordFactory.nullPointer(); + currentFrameIP = WordFactory.nullPointer(); + currentDeoptimizedFrame = null; + JavaStackWalker.walkThread(vmThread, this); + } + } + return true; + } + + @Override + public boolean visitFrame(Pointer sp, CodePointer ip, CodeInfo codeInfo, DeoptimizedFrame deoptimizedFrame) { + /* Notice a change in thread. */ + if (currentStackSP.isNull()) { + currentStackSP = sp; + } + currentFrameSP = sp; + currentFrameIP = ip; + currentDeoptimizedFrame = deoptimizedFrame; + return CodeInfoTable.visitObjectReferences(sp, ip, codeInfo, deoptimizedFrame, this); + } + } + + public TestingBackDoor getTestingBackDoor() { + return testingBackDoor; + } + + /** Expose some {@link HeapDumpUtils} methods for testing. */ + public static class TestingBackDoor { + + /** The HeapDumpUtils instance to use. */ + private final HeapDumpUtils heapDumpUtils; + + /** Constructor. */ + TestingBackDoor(HeapDumpUtils utils) { + this.heapDumpUtils = utils; + } + + public byte[] getFieldsMap() { + return heapDumpUtils.getFieldsMap(); + } + + public int instanceSizeOf(Class cls) { + return heapDumpUtils.instanceSizeOf(cls); + } + + public boolean isJavaPrimitiveArray(Object obj) { + return heapDumpUtils.isJavaPrimitiveArray(obj); + } + + public boolean isPrimitiveArray(Object obj) { + return heapDumpUtils.isPrimitiveArray(obj); + } + + public Pointer objectToPointer(Object obj) { + return heapDumpUtils.objectToPointer(obj); + } + + public long sizeOf(Object obj) { + final long result; + if (obj == null) { + result = 0; + } else { + final UnsignedWord objectSize = LayoutEncoding.getSizeFromObject(obj); + result = objectSize.rawValue(); + } + return result; + } + + public boolean walkHeapObjects(ObjectVisitor imageHeapVisitor, ObjectVisitor collectedHeapVisitor) { + return heapDumpUtils.walkHeapObjects(imageHeapVisitor, collectedHeapVisitor); + } + + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Iterating the heap must not allocate in the heap.") + public boolean walkInteriorReferences(Object obj, ObjectReferenceVisitor visitor) { + return InteriorObjRefWalker.walkObject(obj, visitor); + } + + public boolean walkStacks(StacksSlotsVisitor stacksSlotsVisitor) { + return heapDumpUtils.walkStacks(stacksSlotsVisitor); + } + } +} + +@AutomaticFeature +class HeapDumpUtilsFeature implements Feature { + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + ImageSingletons.add(HeapDumpUtils.class, new HeapDumpUtils()); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpWriter.java new file mode 100644 index 000000000000..84449a1c073a --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpWriter.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022, 2022, 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.heapdump; + +import java.io.FileOutputStream; +import java.io.IOException; + +import org.graalvm.nativeimage.ImageSingletons; + +public abstract class HeapDumpWriter { + /** + * Writes heap in hprof format to ordinary file. + * + * @param fileOutputStream Underlying file stream to write the bytes to + * @param gcBefore Run GC before dumping the heap. + * @throws IOException + */ + public abstract void writeHeapTo(FileOutputStream fileOutputStream, boolean gcBefore) throws IOException; + + /** + * Writes heap in hprof format to output stream, which does not support seeking. + * + * @param outputStream Underlying stream to write the bytes to + * @param gcBefore Run GC before dumping the heap. + * @throws IOException + */ + public abstract void writeHeapTo(AllocationFreeOutputStream outputStream, boolean gcBefore) throws IOException; + + public static HeapDumpWriter singleton() { + return ImageSingletons.lookup(HeapDumpWriter.class); + } + +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpWriterImpl.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpWriterImpl.java new file mode 100644 index 000000000000..1a98891ed571 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/HeapDumpWriterImpl.java @@ -0,0 +1,1772 @@ +/* + * Copyright (c) 2022, 2022, 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.heapdump; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.management.ManagementFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.graalvm.nativeimage.CurrentIsolate; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.IsolateThread; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.word.Pointer; +import org.graalvm.word.WordBase; + +import com.oracle.svm.core.StaticFieldsSupport; +import com.oracle.svm.core.annotate.AutomaticFeature; +import com.oracle.svm.core.code.FrameInfoQueryResult; +import com.oracle.svm.core.config.ConfigurationValues; +import com.oracle.svm.core.heap.Heap; +import com.oracle.svm.core.heap.ObjectVisitor; +import com.oracle.svm.core.heap.ReferenceAccess; +import com.oracle.svm.core.heap.VMOperationInfos; +import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.stack.JavaStackFrameVisitor; +import com.oracle.svm.core.stack.JavaStackWalker; +import com.oracle.svm.core.thread.JavaVMOperation; +import com.oracle.svm.core.thread.PlatformThreads; +import com.oracle.svm.core.thread.VMOperation; +import com.oracle.svm.core.thread.VMThreads; +import com.oracle.svm.core.util.VMError; + +/* + * This class writes Java heap in hprof binary format. The class is heavily + * influenced by 'HeapHprofBinWriter' implementation. + */ + +/* hprof binary format originally published at: +* +* +* +* header "JAVA PROFILE 1.0.1" or "JAVA PROFILE 1.0.2" (0-terminated) +* u4 size of identifiers. Identifiers are used to represent +* UTF8 strings, objects, stack traces, etc. They usually +* have the same size as host pointers. For example, on +* Solaris and Win32, the size is 4. +* u4 high word +* u4 low word number of milliseconds since 0:00 GMT, 1/1/70 +* [record]* a sequence of records. +* +*/ + +/* +* +* Record format: +* +* u1 a TAG denoting the type of the record +* u4 number of *microseconds* since the time stamp in the +* header. (wraps around in a little more than an hour) +* u4 number of bytes *remaining* in the record. Note that +* this number excludes the tag and the length field itself. +* [u1]* BODY of the record (a sequence of bytes) +*/ + +/* +* The following TAGs are supported: +* +* TAG BODY notes +*---------------------------------------------------------- +* HPROF_UTF8 a UTF8-encoded name +* +* id name ID +* [u1]* UTF8 characters (no trailing zero) +* +* HPROF_LOAD_CLASS a newly loaded class +* +* u4 class serial number (> 0) +* id class object ID +* u4 stack trace serial number +* id class name ID +* +* HPROF_UNLOAD_CLASS an unloading class +* +* u4 class serial_number +* +* HPROF_FRAME a Java stack frame +* +* id stack frame ID +* id method name ID +* id method signature ID +* id source file name ID +* u4 class serial number +* i4 line number. >0: normal +* -1: unknown +* -2: compiled method +* -3: native method +* +* HPROF_TRACE a Java stack trace +* +* u4 stack trace serial number +* u4 thread serial number +* u4 number of frames +* [id]* stack frame IDs +* +* +* HPROF_ALLOC_SITES a set of heap allocation sites, obtained after GC +* +* u2 flags 0x0001: incremental vs. complete +* 0x0002: sorted by allocation vs. live +* 0x0004: whether to force a GC +* u4 cutoff ratio +* u4 total live bytes +* u4 total live instances +* u8 total bytes allocated +* u8 total instances allocated +* u4 number of sites that follow +* [u1 is_array: 0: normal object +* 2: object array +* 4: boolean array +* 5: char array +* 6: float array +* 7: double array +* 8: byte array +* 9: short array +* 10: int array +* 11: long array +* u4 class serial number (may be zero during startup) +* u4 stack trace serial number +* u4 number of bytes alive +* u4 number of instances alive +* u4 number of bytes allocated +* u4]* number of instance allocated +* +* HPROF_START_THREAD a newly started thread. +* +* u4 thread serial number (> 0) +* id thread object ID +* u4 stack trace serial number +* id thread name ID +* id thread group name ID +* id thread group parent name ID +* +* HPROF_END_THREAD a terminating thread. +* +* u4 thread serial number +* +* HPROF_HEAP_SUMMARY heap summary +* +* u4 total live bytes +* u4 total live instances +* u8 total bytes allocated +* u8 total instances allocated +* +* HPROF_HEAP_DUMP denote a heap dump +* +* [heap dump sub-records]* +* +* There are four kinds of heap dump sub-records: +* +* u1 sub-record type +* +* HPROF_GC_ROOT_UNKNOWN unknown root +* +* id object ID +* +* HPROF_GC_ROOT_THREAD_OBJ thread object +* +* id thread object ID (may be 0 for a +* thread newly attached through JNI) +* u4 thread sequence number +* u4 stack trace sequence number +* +* HPROF_GC_ROOT_JNI_GLOBAL JNI global ref root +* +* id object ID +* id JNI global ref ID +* +* HPROF_GC_ROOT_JNI_LOCAL JNI local ref +* +* id object ID +* u4 thread serial number +* u4 frame # in stack trace (-1 for empty) +* +* HPROF_GC_ROOT_JAVA_FRAME Java stack frame +* +* id object ID +* u4 thread serial number +* u4 frame # in stack trace (-1 for empty) +* +* HPROF_GC_ROOT_NATIVE_STACK Native stack +* +* id object ID +* u4 thread serial number +* +* HPROF_GC_ROOT_STICKY_CLASS System class +* +* id object ID +* +* HPROF_GC_ROOT_THREAD_BLOCK Reference from thread block +* +* id object ID +* u4 thread serial number +* +* HPROF_GC_ROOT_MONITOR_USED Busy monitor +* +* id object ID +* +* HPROF_GC_CLASS_DUMP dump of a class object +* +* id class object ID +* u4 stack trace serial number +* id super class object ID +* id class loader object ID +* id signers object ID +* id protection domain object ID +* id reserved +* id reserved +* +* u4 instance size (in bytes) +* +* u2 size of constant pool +* [u2, constant pool index, +* ty, type +* 2: object +* 4: boolean +* 5: char +* 6: float +* 7: double +* 8: byte +* 9: short +* 10: int +* 11: long +* vl]* and value +* +* u2 number of static fields +* [id, static field name, +* ty, type, +* vl]* and value +* +* u2 number of inst. fields (not inc. super) +* [id, instance field name, +* ty]* type +* +* HPROF_GC_INSTANCE_DUMP dump of a normal object +* +* id object ID +* u4 stack trace serial number +* id class object ID +* u4 number of bytes that follow +* [vl]* instance field values (class, followed +* by super, super's super ...) +* +* HPROF_GC_OBJ_ARRAY_DUMP dump of an object array +* +* id array object ID +* u4 stack trace serial number +* u4 number of elements +* id array class ID +* [id]* elements +* +* HPROF_GC_PRIM_ARRAY_DUMP dump of a primitive array +* +* id array object ID +* u4 stack trace serial number +* u4 number of elements +* u1 element type +* 4: boolean array +* 5: char array +* 6: float array +* 7: double array +* 8: byte array +* 9: short array +* 10: int array +* 11: long array +* [u1]* elements +* +* HPROF_CPU_SAMPLES a set of sample traces of running threads +* +* u4 total number of samples +* u4 # of traces +* [u4 # of samples +* u4]* stack trace serial number +* +* HPROF_CONTROL_SETTINGS the settings of on/off switches +* +* u4 0x00000001: alloc traces on/off +* 0x00000002: cpu sampling on/off +* u2 stack trace depth +* +* +* When the header is "JAVA PROFILE 1.0.2" a heap dump can optionally +* be generated as a sequence of heap dump segments. This sequence is +* terminated by an end record. The additional tags allowed by format +* "JAVA PROFILE 1.0.2" are: +* +* HPROF_HEAP_DUMP_SEGMENT denote a heap dump segment +* +* [heap dump sub-records]* +* The same sub-record types allowed by HPROF_HEAP_DUMP +* +* HPROF_HEAP_DUMP_END denotes the end of a heap dump +* +*/ +public class HeapDumpWriterImpl extends HeapDumpWriter { + + /** + * The heap size threshold used to determine if segmented format ("JAVA PROFILE 1.0.2") should + * be used. + */ + private static final long HPROF_SEGMENTED_HEAP_DUMP_THRESHOLD = 2L * 1024 * 1024 * 1024; + + /** + * The approximate size of a heap segment. Used to calculate when to create a new segment. + */ + private static final long HPROF_SEGMENTED_HEAP_DUMP_SEGMENT_SIZE = 1L * 1024 * 1024 * 1024; + + /** + * The approximate size of a heap segment for no seek case. Used to calculate when to create a + * new segment. + */ + private static final int HPROF_NOSEEK_HEAP_DUMP_SEGMENT_SIZE = 1 * 1024 * 1024; + + /** hprof binary file header. */ + private static final String HPROF_HEADER_1_0_1 = "JAVA PROFILE 1.0.1"; + private static final String HPROF_HEADER_1_0_2 = "JAVA PROFILE 1.0.2"; + + /** Constants in enum HprofTag. */ + private static final int HPROF_UTF8 = 0x01; + private static final int HPROF_LOAD_CLASS = 0x02; + /* private static final int HPROF_UNLOAD_CLASS = 0x03; */ + private static final int HPROF_FRAME = 0x04; + private static final int HPROF_TRACE = 0x05; + /* private static final int HPROF_ALLOC_SITES = 0x06; */ + /* private static final int HPROF_HEAP_SUMMARY = 0x07; */ + /* private static final int HPROF_START_THREAD = 0x0A; */ + /* private static final int HPROF_END_THREAD = 0x0B; */ + private static final int HPROF_HEAP_DUMP = 0x0C; + /* private static final int HPROF_CPU_SAMPLES = 0x0D; */ + /* private static final int HPROF_CONTROL_SETTINGS = 0x0E; */ + + /* 1.0.2 record types. */ + private static final int HPROF_HEAP_DUMP_SEGMENT = 0x1C; + private static final int HPROF_HEAP_DUMP_END = 0x2C; + + /* Heap dump constants */ + /* Constants in enum HprofGcTag. */ + private static final int HPROF_GC_ROOT_UNKNOWN = 0xFF; + private static final int HPROF_GC_ROOT_JNI_GLOBAL = 0x01; + /* private static final int HPROF_GC_ROOT_JNI_LOCAL = 0x02; */ + /* private static final int HPROF_GC_ROOT_JAVA_FRAME = 0x03; */ + /* private static final int HPROF_GC_ROOT_NATIVE_STACK = 0x04; */ + private static final int HPROF_GC_ROOT_STICKY_CLASS = 0x05; + /* private static final int HPROF_GC_ROOT_THREAD_BLOCK = 0x06; */ + /* private static final int HPROF_GC_ROOT_MONITOR_USED = 0x07; */ + private static final int HPROF_GC_ROOT_THREAD_OBJ = 0x08; + private static final int HPROF_GC_CLASS_DUMP = 0x20; + private static final int HPROF_GC_INSTANCE_DUMP = 0x21; + private static final int HPROF_GC_OBJ_ARRAY_DUMP = 0x22; + private static final int HPROF_GC_PRIM_ARRAY_DUMP = 0x23; + + /* Constants in enum HprofType. */ + private static final int HPROF_NORMAL_OBJECT = 2; + private static final int HPROF_BOOLEAN = 4; + private static final int HPROF_CHAR = 5; + private static final int HPROF_FLOAT = 6; + private static final int HPROF_DOUBLE = 7; + private static final int HPROF_BYTE = 8; + private static final int HPROF_SHORT = 9; + private static final int HPROF_INT = 10; + private static final int HPROF_LONG = 11; + + /* Java type codes. */ + private static final char JVM_SIGNATURE_BOOLEAN = 'Z'; + private static final char JVM_SIGNATURE_CHAR = 'C'; + private static final char JVM_SIGNATURE_BYTE = 'B'; + private static final char JVM_SIGNATURE_SHORT = 'S'; + private static final char JVM_SIGNATURE_INT = 'I'; + private static final char JVM_SIGNATURE_LONG = 'J'; + private static final char JVM_SIGNATURE_FLOAT = 'F'; + private static final char JVM_SIGNATURE_DOUBLE = 'D'; + private static final char JVM_SIGNATURE_ARRAY = '['; + private static final char JVM_SIGNATURE_CLASS = 'L'; + + /* + * We don't have allocation site info. We write a dummy stack trace with this id. + */ + private static final int DUMMY_STACK_TRACE_ID = 1; + /* private static final int EMPTY_FRAME_DEPTH = -1; */ + + private static final Field[] ZERO_FIELD_ARR = new Field[0]; + + /** Pre-allocated exceptions, for throwing from code that must not allocate. */ + private static final RuntimeException heapSegmentSizeOverflowException = new RuntimeException("Heap segment size overflow."); + + private AllocationFreeDataOutputStream out; + private HeapDumpUtils heapDumpUtils; + private Map> fieldsMap; + private ClassToClassDataMap classDataCache; + + /* Added for hprof file format 1.0.2 support. */ + private boolean useSegmentedHeapDump; + private long currentSegmentStart; + private long segmentSize; + + @Override + public void writeHeapTo(AllocationFreeOutputStream dataOutputStream, boolean gcBefore) throws IOException { + initialize(true, HPROF_NOSEEK_HEAP_DUMP_SEGMENT_SIZE); + + WriterOperation writerOperation = new WriterOperation(dataOutputStream, gcBefore, HPROF_NOSEEK_HEAP_DUMP_SEGMENT_SIZE); + writerOperation.enqueue(); + IOException operationException = writerOperation.getException(); + if (operationException != null) { + throw operationException; + } + + /* + * Close the data stream. Needs to be done outside of the VMOperation because it uses + * synchronization. + */ + dataOutputStream.close(); + } + + @Override + public void writeHeapTo(FileOutputStream fileOutputStream, boolean gcBefore) throws IOException { + initialize(ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed() > HPROF_SEGMENTED_HEAP_DUMP_THRESHOLD, HPROF_SEGMENTED_HEAP_DUMP_SEGMENT_SIZE); + + WriterOperation writerOperation = new WriterOperation(fileOutputStream, gcBefore); + writerOperation.enqueue(); + IOException operationException = writerOperation.getException(); + if (operationException != null) { + throw operationException; + } + + /* + * Close the file stream. Needs to be done outside of the VMOperation because it uses + * synchronization. + */ + fileOutputStream.close(); + } + + @SuppressWarnings("hiding") + private void initialize(boolean useSegmentedHeapDump, long segmentSize) { + this.currentSegmentStart = 0L; + this.useSegmentedHeapDump = useSegmentedHeapDump; + this.segmentSize = segmentSize; + } + + /* This method runs as part of a VMOperation. */ + private void writeTo(AllocationFreeDataOutputStream outputStream, boolean gcBefore) throws IOException { + /* If requested, clean up the heap. */ + if (gcBefore) { + System.gc(); + } + + out = outputStream; + heapDumpUtils = HeapDumpUtils.getHeapDumpUtils(); + + /* hprof bin format header. */ + writeFileHeader(); + + /* Dummy stack trace. */ + writeDummyTrace(); + + List> classList = Heap.getHeap().getLoadedClasses(); + + /* hprof UTF-8 symbols section. */ + writeClassNames(classList); + + byte[] fieldsMapData = heapDumpUtils.getFieldsMap(); + if (fieldsMapData.length == 0) { + throw new IOException("Empty fieldsMap"); + } + fieldsMap = createFieldsMap(fieldsMapData); + + /* HPROF_LOAD_CLASS records for all classes. */ + writeClasses(classList); + + /* write HPROF_FRAME and HPROF_TRACE records */ + dumpStackTraces(); + + /* Write CLASS_DUMP records. */ + writeClassDumpRecords(classList); + + /* Write HEAP_DUMP record. */ + writeInstanceDumpRecords(classList); + + /* get current position to calculate length. */ + long dumpEnd = out.position(); + + /* Calculate length of heap data. */ + long dumpLenLong = (dumpEnd - currentSegmentStart - 4L); + /* Fill in final length. */ + fillInHeapRecordLength(dumpLenLong); + + if (useSegmentedHeapDump) { + /* Write heap segment-end record. */ + out.writeByte((byte) HPROF_HEAP_DUMP_END); + out.writeInt(0); + out.writeInt(0); + } + + /* Flush buffer stream and throw it away. */ + out.flush(); + out = null; + } + + private void writeHeapRecordPrologue() throws IOException { + if (currentSegmentStart == 0) { + out.flush(); + /* Write heap data header, depending on heap size use segmented heap format. */ + out.writeByte((byte) (useSegmentedHeapDump ? HPROF_HEAP_DUMP_SEGMENT + : HPROF_HEAP_DUMP)); + out.writeInt(0); + + /* + * Remember position of dump length, we will fixup length later: hprof format requires + * length. + */ + currentSegmentStart = out.position(); + + /* Write dummy length of 0 and we'll fix it later. */ + out.writeInt(0); + } + } + + private void writeHeapRecordEpilogue() throws IOException { + writeHeapRecordEpilogue(0); + } + + private void writeHeapRecordEpilogue(long dumpLenSize) throws IOException { + if (useSegmentedHeapDump) { + /* get current position (plus dumpLenLong) to calculate length. */ + long dumpEnd = out.position() + dumpLenSize; + + /* Calculate length of heap data. */ + long dumpLenLong = dumpEnd - currentSegmentStart - 4L; + if (dumpLenLong >= segmentSize) { + fillInHeapRecordLength(dumpLenLong); + out.flush(); + currentSegmentStart = 0; + } + } + } + + private void fillInHeapRecordLength(long dumpLenLong) throws IOException { + /* Check length boundary, overflow of 4GB could happen but is _very_ unlikely. */ + if (dumpLenLong >= (4L * 1024 * 1024 * 1024)) { + throw heapSegmentSizeOverflowException; + } + + /* Save the current position. */ + long currentPosition = out.position(); + + /* Seek the position to write length. */ + out.position(currentSegmentStart); + + int dumpLen = (int) dumpLenLong; + + /* Write length as integer. */ + out.writeInt(dumpLen); + + /* Reset to previous current position. */ + out.position(currentPosition); + } + + private void writeClassDumpRecords(List> classList) throws IOException { + for (Class cls : classList) { + writeHeapRecordPrologue(); + writeClassDumpRecord(cls); + writeHeapRecordEpilogue(); + } + } + + private void writeInstanceDumpRecords(List> classList) throws IOException { + final StacksSlotsVisitorImpl stackVisitor = new StacksSlotsVisitorImpl(); + final CollectedHeapVisitorImpl collectedVisitor = new CollectedHeapVisitorImpl(); + final ImageHeapVisitorImpl imageVisitor = new ImageHeapVisitorImpl(); + IOException visitorException; + + heapDumpUtils.walkStacks(stackVisitor); + visitorException = stackVisitor.getException(); + if (visitorException != null) { + throw visitorException; + } + + heapDumpUtils.walkHeapObjects(imageVisitor, collectedVisitor); + visitorException = collectedVisitor.getException(); + if (visitorException != null) { + throw visitorException; + } + visitorException = imageVisitor.getException(); + if (visitorException != null) { + throw visitorException; + } + /* Write root sticky class. */ + writeStickyClasses(classList); + + /* write JavaThreads */ + writeJavaThreads(); + } + + private void writeStickyClasses(List> classList) throws IOException { + for (Class cls : classList) { + writeHeapRecordPrologue(); + out.writeByte((byte) HPROF_GC_ROOT_STICKY_CLASS); + writeObjectID(cls); + writeHeapRecordEpilogue(); + } + } + + private void writeImageGCRoot(Object obj) throws IOException { + if (!(obj instanceof Class)) { + writeHeapRecordPrologue(); + out.writeByte((byte) HPROF_GC_ROOT_JNI_GLOBAL); + writeObjectID(obj); + writeObjectID(null); + writeHeapRecordEpilogue(); + } + } + + private void writeUnknownGCRoot(Object obj) throws IOException { + if (obj != null) { + writeHeapRecordPrologue(); + out.writeByte((byte) HPROF_GC_ROOT_UNKNOWN); + writeObjectID(obj); + writeHeapRecordEpilogue(); + } + } + + private void writeJavaThreads() throws IOException { + int threadSerialNum = 1; // Note that the thread serial number range is 1-to-N + + for (IsolateThread vmThread = VMThreads.firstThread(); vmThread.isNonNull(); vmThread = VMThreads.nextThread(vmThread)) { + if (vmThread == CurrentIsolate.getCurrentThread()) { + /* Skip itself */ + continue; + } + + Thread jt = PlatformThreads.fromVMThread(vmThread); + writeJavaThread(jt, threadSerialNum++); + } + } + + private void writeJavaThread(Thread jt, int threadSerialNum) throws IOException { + writeHeapRecordPrologue(); + out.writeByte((byte) HPROF_GC_ROOT_THREAD_OBJ); + writeObjectID(jt); + out.writeInt(threadSerialNum); // thread serial number + out.writeInt(threadSerialNum + DUMMY_STACK_TRACE_ID); // stack trace serial number + writeHeapRecordEpilogue(); + } + + private void writeClass(Class clazz) throws IOException { + /* + * All ordinary Class objects are covered by writeClassDumpRecords and they are in + * classDataCache. + */ + if (classDataCache.get(clazz) == null) { + /* Unknown class. */ + writeInstance(clazz); + } + } + + private void writeClassDumpRecord(Class cls) throws IOException { + out.writeByte((byte) HPROF_GC_CLASS_DUMP); + writeObjectID(cls); + out.writeInt(DUMMY_STACK_TRACE_ID); + writeObjectID(cls.getSuperclass()); + + if (!isArray(cls)) { + writeObjectID(cls.getClassLoader()); + writeObjectID(null); /* Signers. */ + writeObjectID(null); /* Protection domain. */ + writeObjectID(null); /* Reserved field 1. */ + writeObjectID(null); /* Reserved field 2. */ + out.writeInt(heapDumpUtils.instanceSizeOf(cls)); + + /* Ignore constant pool output number of cp entries as zero. */ + out.writeShort((short) 0); + + List declaredFields = getImmediateFields(cls); + int staticFields = 0; + int instanceFields = 0; + for (int i = 0; i < declaredFields.size(); i++) { + Field field = declaredFields.get(i); + if (field.isStatic()) { + staticFields++; + } else { + instanceFields++; + } + } + + /* Dump static field descriptors. */ + writeFieldDescriptors(true, staticFields, declaredFields); + + /* Dump instance field descriptors. */ + writeFieldDescriptors(false, instanceFields, declaredFields); + } else { + /* Array. */ + Class baseClass = getBaseClass(cls); + writeObjectID(baseClass.getClassLoader()); + writeObjectID(null); + writeObjectID(null); + /* Two reserved id fields. */ + writeObjectID(null); + writeObjectID(null); + /* Write zero instance size: instance size is variable for arrays. */ + out.writeInt(0); + /* No constant pool for array klasses. */ + out.writeShort((short) 0); + /* No static fields for array klasses. */ + out.writeShort((short) 0); + /* No instance fields for array klasses. */ + out.writeShort((short) 0); + } + } + + private void dumpStackTraces() throws IOException { + // write a HPROF_TRACE record without any frames to be referenced as object alloc sites + writeHeader(HPROF_TRACE, 3 * 4); + out.writeInt(DUMMY_STACK_TRACE_ID); + out.writeInt(0); // thread number + out.writeInt(0); // frame count + + int frameSerialNum = 0; + int numThreads = 0; + Set names = new HashSet<>(); + for (IsolateThread vmThread = VMThreads.firstThread(); vmThread.isNonNull(); vmThread = VMThreads.nextThread(vmThread)) { + if (vmThread == CurrentIsolate.getCurrentThread()) { + /* Skip itself */ + continue; + } + + final List stack = new ArrayList<>(); + + // dump thread stack trace + JavaStackFrameVisitor visitor = new JavaStackFrameVisitor() { + @Override + public boolean visitFrame(FrameInfoQueryResult frameInfo) { + if (frameInfo.getSourceClass() != null) { + stack.add(frameInfo); + } + return true; + } + }; + JavaStackWalker.walkThread(vmThread, visitor); + numThreads++; + + // write HPROF_FRAME records for this thread's stack trace + int depth = stack.size(); + int threadFrameStart = frameSerialNum; + for (int j = 0; j < depth; j++) { + FrameInfoQueryResult frame = stack.get(j); + ClassData cd = classDataCache.get(frame.getSourceClass()); + int classSerialNum = cd.serialNum; + + // the class serial number starts from 1 + assert classSerialNum > 0 : "class not found"; + dumpStackFrame(++frameSerialNum, classSerialNum, frame, names); + } + + // write HPROF_TRACE record for one thread + writeHeader(HPROF_TRACE, 3 * 4 + depth * getObjIDSize()); + int stackSerialNum = numThreads + DUMMY_STACK_TRACE_ID; + out.writeInt(stackSerialNum); // stack trace serial number + out.writeInt(numThreads); // thread serial number + out.writeInt(depth); // frame count + for (int j = 1; j <= depth; j++) { + writeObjectAddress(threadFrameStart + j); + } + } + names = null; + } + + private void dumpStackFrame(int frameSN, int classSN, FrameInfoQueryResult frame, Set names) throws IOException { + int lineNumber; + if (frame.isNativeMethod()) { + lineNumber = -3; // native frame + } else { + lineNumber = frame.getSourceLineNumber(); + } + // First dump UTF8 if needed + String method = frame.getSourceMethodName(); + String source = frame.getSourceFileName(); + if (method == null || method.isEmpty()) { + method = ""; + } + if (source == null || source.isEmpty()) { + source = "Unknown Source"; + } + writeName(method, names); // method's name + writeName("", names); // method's signature + writeName(source, names); // source file name + // Then write FRAME descriptor + writeHeader(HPROF_FRAME, 4 * getObjIDSize() + 2 * 4); + writeObjectAddress(frameSN); // frame serial number + writeSymbolID(method); // method's name + writeSymbolID(""); // method's signature + writeSymbolID(source); // source file name + out.writeInt(classSN); // class serial number + out.writeInt(lineNumber); // line number + } + + private void writeName(String name, Set names) throws IOException { + if (names.add(name)) { + writeSymbol(name); + } + } + + private void writeHeapInstance(Object obj) throws IOException { + writeHeapRecordPrologue(); + if (obj instanceof Class) { + writeClass((Class) obj); + writeHeapRecordEpilogue(); + } else if (heapDumpUtils.isJavaPrimitiveArray(obj)) { + writePrimitiveArray(obj); + } else if (isArray(obj.getClass())) { + writeObjectArray((Object[]) obj); + } else { + writeInstance(obj); + writeHeapRecordEpilogue(); + } + } + + private List getImmediateFields(Class cls) { + String clsName = cls.getName(); + List fields = fieldsMap.get(clsName); + if (fields == null) { + return Collections.emptyList(); + } + return fields; + } + + private void writeObjectArray(Object[] array) throws IOException { + out.writeByte((byte) HPROF_GC_OBJ_ARRAY_DUMP); + writeObjectID(array); + out.writeInt(DUMMY_STACK_TRACE_ID); + out.writeInt(array.length); + writeObjectID(array.getClass()); + writeHeapRecordEpilogue(array.length * getObjIDSize()); + for (Object o : array) { + writeObjectID(o); + } + } + + private void writePrimitiveArray(Object pArray) throws IOException { + out.writeByte((byte) HPROF_GC_PRIM_ARRAY_DUMP); + writeObjectID(pArray); + out.writeInt(DUMMY_STACK_TRACE_ID); + /* These are ordered by expected frequency. */ + if (pArray instanceof char[]) { + writeCharArray((char[]) pArray); + } else if (pArray instanceof byte[]) { + writeByteArray((byte[]) pArray); + } else if (pArray instanceof int[]) { + writeIntArray((int[]) pArray); + } else if (pArray instanceof long[]) { + writeLongArray((long[]) pArray); + } else if (pArray instanceof boolean[]) { + writeBooleanArray((boolean[]) pArray); + } else if (pArray instanceof short[]) { + writeShortArray((short[]) pArray); + } else if (pArray instanceof double[]) { + writeDoubleArray((double[]) pArray); + } else if (pArray instanceof float[]) { + writeFloatArray((float[]) pArray); + } else { + throw VMError.shouldNotReachHere(pArray.getClass().getName()); + } + } + + private void writeBooleanArray(boolean[] array) throws IOException { + out.writeInt(array.length); + out.writeByte((byte) HPROF_BOOLEAN); + writeHeapRecordEpilogue(array.length * 1); + for (boolean b : array) { + out.writeBoolean(b); + } + } + + private void writeByteArray(byte[] array) throws IOException { + out.writeInt(array.length); + out.writeByte((byte) HPROF_BYTE); + writeHeapRecordEpilogue(array.length * 1); + for (byte b : array) { + out.writeByte(b); + } + } + + private void writeShortArray(short[] array) throws IOException { + out.writeInt(array.length); + out.writeByte((byte) HPROF_SHORT); + writeHeapRecordEpilogue(array.length * 2); + for (short s : array) { + out.writeShort(s); + } + } + + private void writeIntArray(int[] array) throws IOException { + out.writeInt(array.length); + out.writeByte((byte) HPROF_INT); + writeHeapRecordEpilogue(array.length * 4); + for (int i : array) { + out.writeInt(i); + } + } + + private void writeLongArray(long[] array) throws IOException { + out.writeInt(array.length); + out.writeByte((byte) HPROF_LONG); + writeHeapRecordEpilogue(array.length * 8); + for (long l : array) { + out.writeLong(l); + } + } + + private void writeCharArray(char[] array) throws IOException { + out.writeInt(array.length); + out.writeByte((byte) HPROF_CHAR); + writeHeapRecordEpilogue(array.length * 2); + for (char c : array) { + out.writeChar(c); + } + } + + private void writeFloatArray(float[] array) throws IOException { + out.writeInt(array.length); + out.writeByte((byte) HPROF_FLOAT); + writeHeapRecordEpilogue(array.length * 4); + for (float f : array) { + out.writeFloat(f); + } + } + + private void writeDoubleArray(double[] array) throws IOException { + out.writeInt(array.length); + out.writeByte((byte) HPROF_DOUBLE); + writeHeapRecordEpilogue(array.length * 8); + for (double d : array) { + out.writeDouble(d); + } + } + + private void writeInstance(Object instance) throws IOException { + out.writeByte((byte) HPROF_GC_INSTANCE_DUMP); + writeObjectID(instance); + out.writeInt(DUMMY_STACK_TRACE_ID); + Class cls = instance.getClass(); + writeObjectID(cls); + + final ClassData cd = classDataCache.get(cls); + out.writeInt(cd.instSize); + final Pointer objRef = heapDumpUtils.objectToPointer(instance); + for (int i = 0; i < cd.fields.length; i++) { + writeField(cd.fields[i], objRef); + } + } + + private void writeFieldDescriptors(boolean isStatic, int size, List fields) throws IOException { + /* cls == null for instance fields. */ + out.writeShort((short) size); + for (int i = 0; i < fields.size(); i++) { + Field field = fields.get(i); + if (isStatic == field.isStatic()) { + writeSymbolIDFromField(field); + char typeCode = field.getStorageSignature(); + int kind = signatureToHprofKind(typeCode); + out.writeByte((byte) kind); + if (field.isStatic()) { + /* Static field. */ + Object staticData; + char javaSignature = field.getStorageSignature(); + if (javaSignature == JVM_SIGNATURE_CLASS || javaSignature == JVM_SIGNATURE_ARRAY) { + staticData = StaticFieldsSupport.getStaticObjectFields(); + } else { + staticData = StaticFieldsSupport.getStaticPrimitiveFields(); + } + writeField(field, heapDumpUtils.objectToPointer(staticData)); + } + } + } + } + + private static int signatureToHprofKind(char ch) { + switch (ch) { + case JVM_SIGNATURE_CLASS: + case JVM_SIGNATURE_ARRAY: + return HPROF_NORMAL_OBJECT; + case JVM_SIGNATURE_BOOLEAN: + return HPROF_BOOLEAN; + case JVM_SIGNATURE_CHAR: + return HPROF_CHAR; + case JVM_SIGNATURE_FLOAT: + return HPROF_FLOAT; + case JVM_SIGNATURE_DOUBLE: + return HPROF_DOUBLE; + case JVM_SIGNATURE_BYTE: + return HPROF_BYTE; + case JVM_SIGNATURE_SHORT: + return HPROF_SHORT; + case JVM_SIGNATURE_INT: + return HPROF_INT; + case JVM_SIGNATURE_LONG: + return HPROF_LONG; + default: + throw new RuntimeException("should not reach here"); + } + } + + private void writeField(Field field, Pointer p) throws IOException { + char storageSignature = field.getStorageSignature(); + int location = field.getLocation(); + + switch (storageSignature) { + case JVM_SIGNATURE_BOOLEAN: + out.writeByte(p.readByte(location)); + break; + case JVM_SIGNATURE_CHAR: + out.writeChar(p.readChar(location)); + break; + case JVM_SIGNATURE_BYTE: + out.writeByte(p.readByte(location)); + break; + case JVM_SIGNATURE_SHORT: + out.writeShort(p.readShort(location)); + break; + case JVM_SIGNATURE_INT: + out.writeInt(p.readInt(location)); + break; + case JVM_SIGNATURE_LONG: + out.writeLong(p.readLong(location)); + break; + case JVM_SIGNATURE_FLOAT: + out.writeFloat(p.readFloat(location)); + break; + case JVM_SIGNATURE_DOUBLE: + out.writeDouble(p.readDouble(location)); + break; + case JVM_SIGNATURE_CLASS: + case JVM_SIGNATURE_ARRAY: + writeObjectID(ReferenceAccess.singleton().readObjectAt(p.add(location), true)); + break; + default: + throw VMError.shouldNotReachHere("HeapDumpWriter.writeField: storageSignature"); + } + } + + private void writeHeader(int tag, int len) throws IOException { + out.writeByte((byte) tag); + out.writeInt(0); /* current ticks. */ + out.writeInt(len); + } + + private void writeDummyTrace() throws IOException { + writeHeader(HPROF_TRACE, 3 * 4); + out.writeInt(DUMMY_STACK_TRACE_ID); + out.writeInt(0); + out.writeInt(0); + } + + private void writeSymbolFromField(byte[] data, Field field) throws IOException { + writeHeader(HPROF_UTF8, field.getNameLength() + getObjIDSize()); + writeSymbolIDFromField(field); + out.write(data, field.getNameStartOffset(), field.getNameLength()); + } + + private void writeSymbol(String clsName) throws IOException { + byte[] buf = clsName.getBytes("UTF-8"); + writeHeader(HPROF_UTF8, buf.length + getObjIDSize()); + writeSymbolID(clsName); + out.write(buf); + } + + private void writeClassNames(List> classList) throws IOException { + /* hprof UTF-8 symbols section. */ + for (Class cls : classList) { + writeSymbol(cls.getName()); + } + } + + private void writeClasses(List> classList) throws IOException { + int serialNum = 1; + /* + * Build a temporary map from Class to ClassData while I can allocate, but turn it into a + * map between arrays for later use. + */ + Map, ClassData> classDataMap = new HashMap<>(); + List fields = new ArrayList<>(); + for (Class cls : classList) { + writeHeader(HPROF_LOAD_CLASS, 2 * (getObjIDSize() + 4)); + out.writeInt(serialNum); + writeObjectID(cls); + out.writeInt(DUMMY_STACK_TRACE_ID); + writeSymbolID(cls.getName()); + assert fields.isEmpty(); + addInstanceFieldsTo(fields, cls); + int instSize = getSizeForFields(fields); + classDataMap.put(cls, new ClassData(serialNum, instSize, fields.toArray(ZERO_FIELD_ARR))); + fields.clear(); + serialNum++; + } + classDataCache = new ClassToClassDataMap(classDataMap); + } + + /** Writes hprof binary file header. */ + private void writeFileHeader() throws IOException { + /* Version string. */ + if (useSegmentedHeapDump) { + out.writeBytes(HPROF_HEADER_1_0_2); + } else { + out.writeBytes(HPROF_HEADER_1_0_1); + } + out.writeByte((byte) '\0'); + + /* Write identifier size. we use pointers as identifiers. */ + out.writeInt(getObjIDSize()); + + /* Time stamp: file creation time. */ + out.writeLong(System.currentTimeMillis()); + } + + /** Writes unique ID for an object. */ + private void writeObjectID(Object obj) throws IOException { + if (obj != null) { + WordBase ptr = ReferenceAccess.singleton().getCompressedRepresentation(obj); + writeObjectAddress(ptr.rawValue()); + } else { + writeObjectAddress(0L); + } + } + + private void writeSymbolID(String clsName) throws IOException { + writeObjectID(clsName); + } + + private void writeSymbolIDFromField(Field field) throws IOException { + writeObjectID(field); + } + + private void writeObjectAddress(long address) throws IOException { + if (getObjIDSize() == 4) { + out.writeInt((int) address); + } else { + out.writeLong(address); + } + } + + /** Get all declared as well as inherited (directly/indirectly) fields. */ + private void addInstanceFieldsTo(List res, final Class cls) { + Class clazz = cls; + while (clazz != null) { + List curFields = getImmediateFields(clazz); + for (int i = 0; i < curFields.size(); i++) { + Field f = curFields.get(i); + + if (!f.isStatic()) { + res.add(f); + } + } + clazz = clazz.getSuperclass(); + } + } + + /** + * Get size in bytes (in stream) required for given fields. Note that this is not the same as + * object size in heap. The size in heap will include size of padding/alignment bytes as well. + */ + private static int getSizeForFields(List fields) { + int size = 0; + for (int i = 0; i < fields.size(); i++) { + Field field = fields.get(i); + char typeCode = field.getStorageSignature(); + switch (typeCode) { + case JVM_SIGNATURE_BOOLEAN: + case JVM_SIGNATURE_BYTE: + size++; + break; + case JVM_SIGNATURE_CHAR: + case JVM_SIGNATURE_SHORT: + size += 2; + break; + case JVM_SIGNATURE_INT: + case JVM_SIGNATURE_FLOAT: + size += 4; + break; + case JVM_SIGNATURE_CLASS: + case JVM_SIGNATURE_ARRAY: + size += getObjIDSize(); + break; + case JVM_SIGNATURE_LONG: + case JVM_SIGNATURE_DOUBLE: + size += 8; + break; + default: + throw new RuntimeException("should not reach here"); + } + } + return size; + } + + private static int getObjIDSize() { + return ConfigurationValues.getObjectLayout().getReferenceSize(); + } + + private static boolean isArray(Class cls) { + return cls.getName().startsWith("["); + } + + private static Class getBaseClass(final Class array) { + Class arr = array; + while (isArray(arr)) { + arr = arr.getComponentType(); + } + return arr; + } + + private Map> createFieldsMap(byte[] data) throws IOException { + int offset = 0; + Map> fldMap = new HashMap<>(); + while (offset < data.length) { + List fields; + String className = readString(data, offset); + offset += className.length() + 1; + + if (data[offset] == 0 && data[offset + 1] == 0) { + /* No fields. */ + fields = Collections.emptyList(); + offset += 2; + } else { + fields = new ArrayList<>(); + offset = readFields(false, data, offset, fields); + offset++; + offset = readFields(true, data, offset, fields); + offset++; + } + fldMap.put(className, fields); + } + return fldMap; + } + + private int readFields(boolean isStatic, byte[] data, int dataOffset, List fields) throws IOException { + int offset = dataOffset; + while (data[offset] != 0) { + /* Read field. */ + int stringStart = offset; + int stringLength = readStringLength(data, offset); + offset += stringLength + 1; + char javaSig = (char) data[offset++]; + char storageSig = (char) data[offset++]; + int location = readInt(data, offset); + offset += 4; + Field fieldDef = new Field(stringStart, stringLength, javaSig, storageSig, isStatic, location); + writeSymbolFromField(data, fieldDef); + fields.add(fieldDef); + } + return offset; + } + + private static int readInt(final byte[] data, final int st) { + int start = st; + int ch1 = data[start++] & 0xFF; + int ch2 = data[start++] & 0xFF; + int ch3 = data[start++] & 0xFF; + int ch4 = data[start++] & 0xFF; + return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0)); + } + + private static String readString(byte[] data, int start) throws IOException { + int len = readStringLength(data, start); + + return new String(data, start, len, "UTF-8"); + } + + /** + * Returns size of the null-terminated string. + * + * @param data byte[] array that is the source of string. + * @param start the initial offset to data array. + * @return the number of characters (bytes) in a null-terminated character sequence, without + * including the null-terminating character. + */ + private static int readStringLength(byte[] data, int start) { + int offset = start; + + while (data[offset] != 0) { + offset++; + } + return offset - start; + } + + private static class ClassData { + + int serialNum; + int instSize; + Field[] fields; + + ClassData(int serialNum, int instSize, Field[] fields) { + this.serialNum = serialNum; + this.instSize = instSize; + this.fields = fields; + } + } + + private static final class Field { + + private final int nameStart; + private final int nameLength; + private final char javaSig; + private final char storageSig; + private final boolean isStatic; + private final int location; + + private Field(final int ss, final int sl, final char jsig, final char ssig, + final boolean s, int loc) { + nameStart = ss; + nameLength = sl; + javaSig = jsig == 'A' ? JVM_SIGNATURE_CLASS : jsig; + storageSig = ssig == 'A' ? JVM_SIGNATURE_CLASS : ssig; + isStatic = s; + location = loc; + } + + private boolean isStatic() { + return isStatic; + } + + @SuppressWarnings({"unused"}) + private char getJavaSignature() { + return javaSig; + } + + private char getStorageSignature() { + return storageSig; + } + + private int getNameStartOffset() { + return nameStart; + } + + private int getNameLength() { + return nameLength; + } + + private int getLocation() { + return location; + } + } + + private class CollectedHeapVisitorImpl implements ObjectVisitor { + + private IOException exception; + + @Override + public boolean visitObject(Object obj) { + Object asObject = obj; + try { + writeHeapInstance(asObject); + } catch (IOException ex) { + /* Remember exception and abort VM operation. */ + exception = ex; + return false; + } + return true; + } + + private IOException getException() { + return exception; + } + } + + private class ImageHeapVisitorImpl implements ObjectVisitor { + + private IOException exception; + + @Override + public boolean visitObject(Object obj) { + Object asObject = obj; + try { + writeHeapInstance(asObject); + writeImageGCRoot(asObject); + } catch (IOException ex) { + /* Remember exception and abort VM operation. */ + exception = ex; + return false; + } + return true; + } + + private IOException getException() { + return exception; + } + } + + private class StacksSlotsVisitorImpl extends HeapDumpUtils.StacksSlotsVisitor { + + private IOException exception; + + @Override + public boolean visitObjectReference(Pointer objRef, boolean compressed, Object holderObject) { + try { + /* Get the referent of the reference. */ + final Object obj = ReferenceAccess.singleton().readObjectAt(objRef, compressed); + writeUnknownGCRoot(obj); + return true; + } catch (IOException ex) { + /* Remember exception. */ + exception = ex; + return false; + } + } + + private IOException getException() { + return exception; + } + } + + /** + * Abstract Allocation-free output stream created from FileOutputStream. This is the base class + * used by HeapDumpWriteImpl class. + */ + public abstract static class AllocationFreeFileOutputStream extends OutputStream { + + // constructor + public abstract AllocationFreeFileOutputStream newStreamFor(FileOutputStream fileOutputStream) throws IOException; + + @Override + public abstract void write(int b) throws IOException; + + @Override + public abstract void write(byte[] b, int offset, int length) throws IOException; + + @Override + public abstract void close() throws IOException; + + @Override + public void flush() throws IOException { + } + + /** Read the current position in a file descriptor. */ + protected abstract long position() throws IOException; + + /** Set the current position in a file descriptor. */ + protected abstract long position(long offset) throws IOException; + } + + /** + * Implementation of allocation-free output stream, which delegates to + * AllocationFreeOutputStream interface. + */ + private final class AllocationFreeFileOutputStreamWrapper extends AllocationFreeFileOutputStream { + + private final AllocationFreeOutputStream out; + private long position; + + private AllocationFreeFileOutputStreamWrapper(AllocationFreeOutputStream outputStream) { + out = outputStream; + position = 0; + } + + @Override + public AllocationFreeFileOutputStream newStreamFor(FileOutputStream fileOutputStream) throws IOException { + throw VMError.shouldNotReachHere(); + } + + @Override + public void write(int b) throws IOException { + out.write(b); + position++; + } + + @Override + public void write(byte[] b, int offset, int length) throws IOException { + out.write(b, offset, length); + position += length; + } + + @Override + public void close() throws IOException { + out.close(); + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + @Override + protected long position() throws IOException { + return position; + } + + @Override + protected long position(long offset) throws IOException { + throw VMError.shouldNotReachHere(); + } + } + + private static final class AllocationFreeDataOutputStream { + + private final AllocationFreeBufferedOutputStream out; + + private AllocationFreeDataOutputStream(AllocationFreeBufferedOutputStream o) { + out = o; + } + + private void writeBoolean(boolean v) throws IOException { + out.write(v ? 1 : 0); + } + + private void writeByte(int v) throws IOException { + out.write(v); + } + + private void writeChar(int v) throws IOException { + out.write((v >>> 8) & 0xFF); + out.write((v >>> 0) & 0xFF); + } + + private void writeShort(int v) throws IOException { + out.write((v >>> 8) & 0xFF); + out.write((v >>> 0) & 0xFF); + } + + private void writeInt(int v) throws IOException { + out.write((v >>> 24) & 0xFF); + out.write((v >>> 16) & 0xFF); + out.write((v >>> 8) & 0xFF); + out.write((v >>> 0) & 0xFF); + } + + private void writeFloat(float v) throws IOException { + writeInt(Float.floatToIntBits(v)); + } + + private void writeLong(long v) throws IOException { + out.write((byte) (v >>> 56)); + out.write((byte) (v >>> 48)); + out.write((byte) (v >>> 40)); + out.write((byte) (v >>> 32)); + out.write((byte) (v >>> 24)); + out.write((byte) (v >>> 16)); + out.write((byte) (v >>> 8)); + out.write((byte) (v >>> 0)); + } + + private void writeDouble(double v) throws IOException { + writeLong(Double.doubleToLongBits(v)); + } + + private void flush() throws IOException { + out.flush(); + } + + private void write(byte[] buf) throws IOException { + out.write(buf, 0, buf.length); + } + + private void write(byte[] buf, int off, int len) throws IOException { + out.write(buf, off, len); + } + + private void writeBytes(String s) throws IOException { + int len = s.length(); + for (int i = 0; i < len; i++) { + out.write((byte) s.charAt(i)); + } + } + + private long position() throws IOException { + return out.position(); + } + + private void position(long pos) throws IOException { + out.position(pos); + } + } + + private static final class AllocationFreeBufferedOutputStream { + + private byte[] buf; + // current index in buf array + private int position; + // size of valid data in buf array + private int size; + private AllocationFreeFileOutputStream out; + + private AllocationFreeBufferedOutputStream(AllocationFreeFileOutputStream out) { + this(out, 8192); + } + + private AllocationFreeBufferedOutputStream(AllocationFreeFileOutputStream out, int size) { + this.out = out; + if (size <= 0) { + throw new IllegalArgumentException("Buffer size <= 0"); + } + buf = new byte[size]; + } + + private void flushBuffer() throws IOException { + if (size > 0) { + out.write(buf, 0, size); + position = 0; + size = 0; + } + } + + private void write(int b) throws IOException { + if (position >= buf.length) { + flushBuffer(); + } + buf[position++] = (byte) b; + setSize(); + } + + public void write(byte[] b, int off, int len) throws IOException { + if (len >= buf.length) { + flushBuffer(); + out.write(b, off, len); + return; + } + if (len > buf.length - position) { + flushBuffer(); + } + System.arraycopy(b, off, buf, position, len); + position += len; + setSize(); + } + + void flush() throws IOException { + flushBuffer(); + out.flush(); + } + + private long position() throws IOException { + return out.position() + position; + } + + private void position(long pos) throws IOException { + long currentFlushPos = out.position(); + long newCount = pos - currentFlushPos; + + if (newCount >= 0 && newCount <= size) { + position = (int) newCount; + } else { + flush(); + out.position(pos); + } + } + + private void setSize() { + if (position > size) { + size = position; + } + } + } + + private class WriterOperation extends JavaVMOperation { + + private final AllocationFreeDataOutputStream dataOutput; + private final boolean gcBefore; + private IOException exception; + + WriterOperation(FileOutputStream fileOutputStream, boolean gcBefore) throws IOException { + super(VMOperationInfos.get(WriterOperation.class, "Write heap dump", VMOperation.SystemEffect.SAFEPOINT)); + /* open file stream and create buffered data output stream. */ + AllocationFreeFileOutputStream fos = ImageSingletons.lookup(AllocationFreeFileOutputStream.class).newStreamFor(fileOutputStream); + dataOutput = new AllocationFreeDataOutputStream(new AllocationFreeBufferedOutputStream(fos)); + this.gcBefore = gcBefore; + } + + WriterOperation(AllocationFreeOutputStream outputStream, boolean gcBefore, int bufferSize) { + super(VMOperationInfos.get(WriterOperation.class, "Write heap dump", VMOperation.SystemEffect.SAFEPOINT)); + /* open file stream and create buffered data output stream. */ + AllocationFreeFileOutputStream fos = new AllocationFreeFileOutputStreamWrapper(outputStream); + dataOutput = new AllocationFreeDataOutputStream(new AllocationFreeBufferedOutputStream(fos, bufferSize + 32768)); + this.gcBefore = gcBefore; + } + + @Override + protected void operate() { + try { + writeTo(dataOutput, gcBefore); + } catch (IOException ex) { + exception = ex; + } + } + + private IOException getException() { + return exception; + } + + } + + /** A map from Class to ClassData. */ + private static class ClassToClassDataMap { + + private final ClassData[] classDataArray; + + ClassToClassDataMap(Map, ClassData> map) { + /* Find the maximum typeID among the classes. */ + int maxTypeID = 0; + for (Class key : map.keySet()) { + maxTypeID = Integer.max(maxTypeID, typeIDFromClass(key)); + } + /* Make up an array large enough to be indexed by typeID. */ + classDataArray = new ClassData[maxTypeID + 1]; + /* Fill in the array. */ + for (Class key : map.keySet()) { + classDataArray[typeIDFromClass(key)] = map.get(key); + } + } + + /** Use typeID to find the Class. */ + ClassData get(Class clazz) { + int id = typeIDFromClass(clazz); + if (id >= classDataArray.length) { + return null; // class not loaded, there can be no instances + } + return classDataArray[id]; + } + + /** Look up the typeID of a Class from the DynamicHub. */ + private static int typeIDFromClass(Class clazz) { + return DynamicHub.fromClass(clazz).getTypeID(); + } + } +} + +@AutomaticFeature +class HeapDumpWriterFeature implements Feature { + + @Override + public void afterRegistration(Feature.AfterRegistrationAccess access) { + if (Platform.includedIn(Platform.WINDOWS.class)) { + ImageSingletons.add(HeapDumpWriter.class, new UnimplementedHeapDumpWriter("Currently not supported for " + ImageSingletons.lookup(Platform.class))); + } else { + ImageSingletons.add(HeapDumpWriter.class, new HeapDumpWriterImpl()); + } + } + +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/UnimplementedHeapDumpWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/UnimplementedHeapDumpWriter.java new file mode 100644 index 000000000000..9356d05ddb8c --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heapdump/UnimplementedHeapDumpWriter.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022, 2022, 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.heapdump; + +import java.io.FileOutputStream; +import java.io.IOException; + +import com.oracle.svm.core.util.VMError; + +public final class UnimplementedHeapDumpWriter extends HeapDumpWriter { + private final String message; + + public UnimplementedHeapDumpWriter(String message) { + this.message = message; + } + + @Override + public void writeHeapTo(FileOutputStream fileOutputStream, boolean gcBefore) throws IOException { + throw VMError.unimplemented(message); + } + + @Override + public void writeHeapTo(AllocationFreeOutputStream fileOutputStream, boolean gcBefore) throws IOException { + throw VMError.unimplemented(message); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/HeapDumpHostedUtils.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/HeapDumpHostedUtils.java new file mode 100644 index 000000000000..369eab19993a --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/HeapDumpHostedUtils.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2022, 2022, 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.hosted.heap; + +import java.io.UnsupportedEncodingException; +import java.util.Collection; + +import org.graalvm.compiler.core.common.util.TypeConversion; +import org.graalvm.compiler.core.common.util.UnsafeArrayTypeWriter; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.hosted.Feature; + +import com.oracle.svm.core.annotate.AutomaticFeature; +import com.oracle.svm.core.heapdump.HeapDumpUtils; +import com.oracle.svm.core.meta.SharedField; +import com.oracle.svm.core.meta.SharedType; +import com.oracle.svm.core.util.ByteArrayReader; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.hosted.FeatureImpl.AfterCompilationAccessImpl; + +import jdk.vm.ci.meta.ResolvedJavaField; + +public class HeapDumpHostedUtils { + + @Platforms(Platform.HOSTED_ONLY.class) + public static byte[] dumpFieldsMap(Collection types) { + UnsafeArrayTypeWriter writeBuffer = UnsafeArrayTypeWriter.create(ByteArrayReader.supportsUnalignedMemoryAccess()); + + writeFieldsInfo(writeBuffer, types); + int length = TypeConversion.asS4(writeBuffer.getBytesWritten()); + return writeBuffer.toArray(new byte[length]); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static void writeFieldsInfo(UnsafeArrayTypeWriter writeBuffer, Collection types) { + for (SharedType type : types) { + /* I am only interested in instance types. */ + if (type.isInstanceClass()) { + /* Get the direct fields of the class. */ + final ResolvedJavaField[] fields = type.getInstanceFields(false); + /* Get the static fields of the class. */ + final ResolvedJavaField[] sfields = type.getStaticFields(); + /* I am only interested in classes with some fields. */ + if (fields.length == 0 && sfields.length == 0) { + continue; + } + /* Write the class name */ + writeString(writeBuffer, type.toClassName()); + + /* Write each direct field and offset. */ + for (ResolvedJavaField resolvedJavaField : inHotSpotFieldOrder(fields)) { + if (resolvedJavaField instanceof SharedField) { + final SharedField field = (SharedField) resolvedJavaField; + + writeField(field, writeBuffer); + } + } + writeBuffer.putU1(0); + /* Write each static field and offset. */ + for (ResolvedJavaField resolvedJavaField : inHotSpotFieldOrder(sfields)) { + if (resolvedJavaField instanceof SharedField) { + final SharedField field = (SharedField) resolvedJavaField; + if (!field.isWritten()) { + /* I am only interested in fields that are not constants. */ + continue; + } + if (!field.isAccessed()) { + /* I am only interested in fields that are used. */ + continue; + } + writeField(field, writeBuffer); + } + } + writeBuffer.putU1(0); + } + } + } + + @Platforms(Platform.HOSTED_ONLY.class) + private static void writeField(final SharedField field, UnsafeArrayTypeWriter writeBuffer) { + final int location = field.getLocation(); + /* I am only interested in fields that have locations. */ + if (location < 0) { + return; + } + writeString(writeBuffer, field.getName()); + writeBuffer.putU1(field.getJavaKind().getTypeChar()); + writeBuffer.putU1(field.getStorageKind().getTypeChar()); + writeBuffer.putU1((location >>> 24) & 0xFF); + writeBuffer.putU1((location >>> 16) & 0xFF); + writeBuffer.putU1((location >>> 8) & 0xFF); + writeBuffer.putU1((location >>> 0) & 0xFF); + } + + /* + * Write fields in the same order as in HotSpot heap dump. This is the reverse order of what SVM + * hands out. See also GR-6758. + */ + @Platforms(Platform.HOSTED_ONLY.class) + private static ResolvedJavaField[] inHotSpotFieldOrder(ResolvedJavaField[] fields) { + ResolvedJavaField[] reversed = new ResolvedJavaField[fields.length]; + + for (int i = 0; i < fields.length; i++) { + reversed[fields.length - 1 - i] = fields[i]; + } + return reversed; + } + + @Platforms(Platform.HOSTED_ONLY.class) + private static void writeString(UnsafeArrayTypeWriter writeBuffer, String name) { + try { + byte[] buf = name.getBytes("UTF-8"); + for (byte b : buf) { + writeBuffer.putU1(b); + } + writeBuffer.putU1(0); + } catch (UnsupportedEncodingException ex) { + VMError.shouldNotReachHere(ex); + } + } +} + +@AutomaticFeature +class HeapDumpFieldsMapFeature implements Feature { + + /** + * Write out fields info and their offsets. + */ + @Override + @Platforms(Platform.HOSTED_ONLY.class) + public void afterCompilation(Feature.AfterCompilationAccess access) { + AfterCompilationAccessImpl accessImpl = (AfterCompilationAccessImpl) access; + byte[] fieldMap = HeapDumpHostedUtils.dumpFieldsMap(accessImpl.getTypes()); + + HeapDumpUtils.getHeapDumpUtils().setFieldsMap(fieldMap); + access.registerAsImmutable(fieldMap); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/package-info.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/package-info.java new file mode 100644 index 000000000000..9c555e060e1e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022, 2022, 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. + */ + +@Platforms(Platform.HOSTED_ONLY.class) +package com.oracle.svm.hosted.heap; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms;