spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
74.41k stars 40.51k forks source link

Allow heap dump in native image with Spring Boot Actuator #36165

Open cmdjulian opened 1 year ago

cmdjulian commented 1 year ago

Spring Boot Actuator is a tremendous help, especially when debugging prod issues.
Especially heap dump and thread profiling are very helpful.
This works very conveniently when running in JVM mode. When running in graalvm native image, this two features don't work.

I stumbled up on https://www.graalvm.org/latest/reference-manual/native-image/guides/create-heap-dump/ this graalvm feature and was wondering if we can't use this to allow heap dumps to work in native image as well by using the in-native-image detector and than run the example code conditionally.

wilkinsona commented 1 year ago

Thanks for the suggestion, @cmdjulian. I think this is something that we could support out of the box. In the meantime, you can enable it yourself by adding something like the following to your app:

package com.example.demo;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.boot.actuate.management.HeapDumpWebEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.NativeDetector;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

import com.example.demo.GraalHeapDumpWebEndpointConfiguration.GraalHeapDumpWebEndpoint.GraalHeapDumper;
import com.example.demo.GraalHeapDumpWebEndpointConfiguration.GraalHeapDumperRuntimeHints;

@Configuration(proxyBeanMethods = false)
@ImportRuntimeHints(GraalHeapDumperRuntimeHints.class)
class GraalHeapDumpWebEndpointConfiguration {

    @Bean
    HeapDumpWebEndpoint heapDumpWebEndpoint() {
        return new GraalHeapDumpWebEndpoint();
    }

    static class GraalHeapDumpWebEndpoint extends HeapDumpWebEndpoint {

        @Override
        protected HeapDumper createHeapDumper() throws HeapDumperUnavailableException {
            if (NativeDetector.inNativeImage()) {
                return new GraalHeapDumper();
            }
            return super.createHeapDumper();
        }

        static class GraalHeapDumper implements HeapDumper {

            private static final String VM_RUNTIME_CLASS_NAME = "org.graalvm.nativeimage.VMRuntime";

            private final Method dumpHeap;

            GraalHeapDumper() {
                try {
                    Class<?> vmRuntimeClass = ClassUtils.resolveClassName(GraalHeapDumper.VM_RUNTIME_CLASS_NAME, null);
                    this.dumpHeap = vmRuntimeClass.getMethod("dumpHeap", String.class, boolean.class);
                }
                catch (Throwable ex) {
                    throw new HeapDumperUnavailableException("Cound not find dumpHeap method on VMRuntime", ex);
                }
            }

            @Override
            public File dumpHeap(Boolean live) throws IOException {
                String date = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm").format(LocalDateTime.now());
                File file = File.createTempFile("heap-" + date, ".hprof");
                ReflectionUtils.invokeMethod(this.dumpHeap, null, file.getAbsolutePath(), (live != null) ? live : true);
                return file;
            }

        }

    }

    static class GraalHeapDumperRuntimeHints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            hints.reflection()
                .registerType(TypeReference.of(GraalHeapDumper.VM_RUNTIME_CLASS_NAME),
                        MemberCategory.INVOKE_PUBLIC_METHODS);
        }

    }

}
cmdjulian commented 1 year ago

Cool, thanks for your help! I will check it out later :)
I'm not certain how the heap dump and the thread profiling are correlated, any chance thread profiling can be made working with native-image as well?

wilkinsona commented 1 year ago

any chance thread profiling can be made working with native-image as well?

Please see https://github.com/spring-projects/spring-boot/issues/31680 and the Graal issue to which it links.

cmdjulian commented 1 year ago

For the sake of completeness, I added the following VM options to my build gradle file: --enable-monitoring=heapdump to enable the heap dump in graalvm.
Additionally, as I use kotlin my code looks like this:

@Configuration(proxyBeanMethods = false)
@ImportRuntimeHints(GraalHeapDumperRuntimeHints::class)
internal class GraalHeapDumpWebEndpointConfiguration {
    @Bean
    @ConditionalOnAvailableEndpoint(endpoint = HeapDumpWebEndpoint::class)
    fun heapDumpWebEndpoint(): HeapDumpWebEndpoint = GraalHeapDumpWebEndpoint()
}

object GraalHeapDumperRuntimeHints : RuntimeHintsRegistrar {
    override fun registerHints(hints: RuntimeHints, classLoader: ClassLoader?) {
        hints.reflection()
            .registerType(TypeReference.of(GraalHeapDumper.VM_RUNTIME_CLASS_NAME), MemberCategory.INVOKE_PUBLIC_METHODS)
    }
}

private class GraalHeapDumpWebEndpoint : HeapDumpWebEndpoint() {
    object GraalHeapDumper : HeapDumper {
        const val VM_RUNTIME_CLASS_NAME = "org.graalvm.nativeimage.VMRuntime"

        private val dumpHeap = try {
            val vmRuntimeClass = ClassUtils.resolveClassName(VM_RUNTIME_CLASS_NAME, null)
            vmRuntimeClass.getMethod("dumpHeap", String::class.java, Boolean::class.javaPrimitiveType)
        } catch (ex: Throwable) {
            throw HeapDumperUnavailableException("Could not find dumpHeap method on VMRuntime", ex)
        }

        override fun dumpHeap(live: Boolean?): File {
            val date = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm").format(LocalDateTime.now())
            val heapDumpPath = createTempFile("heap-$date", ".hprof")

            ReflectionUtils.invokeMethod(dumpHeap, null, heapDumpPath.pathString, live ?: true)

            return heapDumpPath.toFile()
        }
    }

    override fun createHeapDumper(): HeapDumper =
        if (NativeDetector.inNativeImage()) GraalHeapDumper else super.createHeapDumper()
}

It got me a little by surprise that HeapDumper.dumpHeap's live could be null, as there are no nullable annotations in place though.
Except that, everything works as expected, thx

tse-eche commented 8 months ago

I am attempting to create a heapdump with Spring Boot Actuator, but the creation of a temp file is giving me problems. I happen to have a large heap up to 8Gb, but only 4Gb disk space, that's not something I can change (Cloud Foundry PaaS). I wish the heapdump could be sent over as a stream with no need to have it previously written to temp disk space. Just saying.

wilkinsona commented 8 months ago

Unfortunately, that's not possible (and also unrelated to Graal native images). The JVM does not provide an API that would allow direct streaming of the heap dump, only one to write it to disk from where we can then stream it to the client.

tse-eche commented 7 months ago

My apologies for not sending the comment to the right discussion thread, and many thanks for pointing out that piece of information on the JVM. I was afraid there could be a parameter or some setting somewhere I might be missing. "If there is a solution to a problem, there is no need to worry. And if there is no solution, there is no need to worry." I can stop worrying now.

El jue, 11 ene 2024 a las 19:00, Andy Wilkinson @.***>) escribió:

Unfortunately, that's not possible (and also unrelated to Graal native images). The JVM does not provide an API that would allow direct streaming of the heap dump, only one to write it to disk from where we can then stream it to the client.

— Reply to this email directly, view it on GitHub https://github.com/spring-projects/spring-boot/issues/36165#issuecomment-1887688014, or unsubscribe https://github.com/notifications/unsubscribe-auth/A5MHK6HY36DF5O3KA72I6SLYOASDFAVCNFSM6AAAAAAZ3VS6ISVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQOBXGY4DQMBRGQ . You are receiving this because you commented.Message ID: @.***>