Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
cmdjulian opened this issue Jul 2, 2023 · 7 comments
Open

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

cmdjulian opened this issue Jul 2, 2023 · 7 comments
Labels
theme: aot An issue related to Ahead-of-time processing type: enhancement A general enhancement
Milestone

Comments

@cmdjulian
Copy link

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.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jul 2, 2023
@wilkinsona wilkinsona added theme: aot An issue related to Ahead-of-time processing type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Jul 3, 2023
@wilkinsona
Copy link
Member

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);
		}

	}

}

@wilkinsona wilkinsona added this to the 3.x milestone Jul 3, 2023
@cmdjulian
Copy link
Author

cmdjulian commented Jul 3, 2023

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
Copy link
Member

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

Please see #31680 and the Graal issue to which it links.

@cmdjulian
Copy link
Author

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

This comment was marked as off-topic.

@wilkinsona

This comment was marked as off-topic.

@tse-eche

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
theme: aot An issue related to Ahead-of-time processing type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

4 participants