Performance and Memory Analysis with GraalVM and VisualVM in VS Code
A recent survey by JetBrains shows that approximately 1 in 5 Java developers uses VisualVM, which makes it, biases and all that aside, the most widely used standalone profiling tool in the ecosystem.
With the recent GraalVM 21.2 release, we improved the tooling support for VS Code which is now tightly integrated with VisualVM. It is actually more than a profiler and is better described as an all-in-one Java monitoring and troubleshooting tool. This means that now it’s much easier and more comfortable to do the performance and memory analysis of your Java projects, right from the VS Code!
This article provides an example of using the GraalVM Extension Pack for Java to develop and analyze code, and focuses on the VisualVM integration features. Using a very simple scenario, you’ll learn how to start VisualVM along with your project and immediately profile it using automatically generated settings. You’ll also see how easy is to navigate from the profiling results in VisualVM back to the sources in VS Code editor once you’ve found the problem. And much more!
If you want to follow the steps in this article, you need the necessary tooling installed:
If you find you don’t have a VS Code installation ready at the moment, get one.
Install the GraalVM Extension Pack for Java extension using the Extensions activity. This way you’ll get everything needed for the Java 8+ development within the VS Code, including some cool stuff for GraalVM and Micronaut. See the marketplace entry for more details on the extension.
Please disable or uninstall any other extensions for Java development which you may have already installed to make sure you’ll be able to follow this article exactly step by step.
You need a recent GraalVM release installed and set up within the VS Code. Switch to the Gr activity and click the Download & Install GraalVM button, or add an existing GraalVM 21.2 installation or newer. When downloading a fresh GraalVM instance, you can pick the distribution of your choice — either Community Edition which is free for all purposes, or Enterprise Edition which is free for evaluation and development. Visit the GraalVM website at graalvm.org to learn more about the GraalVM distributions and features.
Once GraalVM has been downloaded and installed, make sure it’s marked as active, eventually making it active using the Set Active GraalVM Installation action (“home” icon) for that installation. This sets up the VS Code environment to use that particular GraalVM for (not only!) Java development.
So far so good, but where’s the VisualVM? Isn’t this all about it?! Actually, you’ve just got one. VisualVM is bundled with each GraalVM installation, configured to run on top of it, and tested to work with it. There’s no need to download a standalone version from visualvm.github.io, but still you’re welcome to visit in order to learn something new and useful.
Creating a Project
Let’s get a sample project ready for the experiments! We’ll use a project based on Micronaut — an easy to use, yet very powerful microservice and serverless framework which works great with GraalVM. You can learn more about Micronaut at micronaut.io.
Generate the Project
Open the command palette using View | Command Palette… and type “Micronaut” to display the available Micronaut-related commands. Invoke the Micronaut: Create Micronaut Project command and provide the following inputs: latest stable Micronaut version, Micronaut Application as the application type, the active GraalVM instance as the project Java,
FibonacciDemo as the project name, com.example as the base package, Java as the project language, no extra project features,
Gradle as the project build tool,
JUnit as the test framework, and select the project parent folder location. At this point a fresh, new Micronaut project is created and ready to be used.
Alternatively, you could generate the project using the Micronaut Launch service, then manually extract and open it in the VS Code.
Implement the Logic
Now let’s add some business logic to the project! We’ll implement a simple Fibonacci number generator, which is easy to understand and provides the right behavior for our experiments. Here’s a short refresher for your knowledge of the Fibonacci numbers.
Make sure the Explorer activity is displayed, and expand the
java nodes to see the
java.com.example package. Right-click the
example part and invoke the New from Template... action from the Context menu. Choose Java as the template type, Java Class as the template to use, and enter FibonacciController as the class name to be created. Eventually a
FibonacciController.java file is created right next to the
Application.java, which has been generated with the project.
FibonacciController.java file in editor, enter the following implementation, and save it:
As you can see, the code is very straightforward. The
nthFibonacci method is the entrypoint for the requests, computing the actual result using the
computeNthFibonacci method, and storing it into a global buffer, including the elapsed time and number of steps needed for the computation.
In case you want to get the sample project without even touching any wizard or editor, simply download it here.
Profiling the Project
The key feature which makes the profiling in VS Code really easy is the VISUALVM section in the Gr activity view. The section contains a handle to the process to be analyzed and actions to control and invoke the most useful VisualVM features. The process to be analyzed can be selected either in advance using the Select process action for the Process: node, or by invoking any of the VisualVM actions which require a concrete process context. A third way to set the process handle is letting the VisualVM support pick the project process automatically as soon as it’s started. The VisualVM instance used for the analysis is defined by the active GraalVM installation.
Set Up for VisualVM
To enable smooth integration of the VS Code project with VisualVM, switch to the Run and Debug activity, click the create a launch.json file link, select the Java 8+ environment, and add a special launch configuration called Launch VisualVM & Java 8+ Application using the Add Configuration… button in the
launch.json editor. Don't forget to save the modified file! The Run and Debug activity view is updated and now displays a RUN AND DEBUG selector which defines the active launch configuration. Click it and select the Launch VisualVM & Java 8+ Application configuration.
Everything is now ready to run and analyze the project! Use the Run | Start Debugging or Run | Run Without Debugging action to build and start the project. At some point you’ll notice that VisualVM starts along with the project, eventually displaying its GUI and opening the project process at the Monitor tab. That’s the default setup — you can change the initial tab in the Gr | VISUALVM pane using the More Actions… menu. Note that the project process is displayed using the VS Code project name
FibonacciDemo in the VisualVM Applications pane.
At this point the Fibonacci number generator is up and running, and ready to accept requests at localhost:8080/nthFibonacci/. The definition of the nth Fibonacci number to be computed is done by appending the number to the generator address. Let’s run these three requests:
Eventually you should see a log from the generator like this:
Fibonacci number #35 is 9227465 (computed in 61 ms, 29860703 steps)
Fibonacci number #40 is 102334155 (computed in 573 ms, 331160281 steps)
Fibonacci number #45 is 1134903170 (computed in 6367 ms, 3672623805 steps)
You’ve probably noticed that computing the last Fibonacci number took a significant amount of time. At the same time, VisualVM Monitor displayed a CPU peak like this:
This is the right time to use a profiler! Switch to the VS Code Gr activity and expand the CPU sampler node in the VISUALVM section to configure the profiling session. Click the Configure action for the Filter: node and choose Include only project classes as the CPU sampling filter. Then click the Configure action for the Sampling rate: node and select 20ms as the CPU sampling rate. Configuration done!
Now invoke the Start CPU sampling action (“start”/ “triangle” icon) for the CPU sampler node. This starts the CPU sampling session for the project process in VisualVM and displays the project process’ Sampler tab in VisualVM. The sampler results are empty so far as no project code has been executed yet.
Let’s invoke the computation of the 45th Fibonacci number again using http://localhost:8080/nthFibonacci/45 and now the sampler starts doing its work. Based on the project classes filter, it displays all stack traces containing our project classes. Sort the results by Total Time (CPU) by clicking the column header, and then right-click the worker thread
default-nioEventLoopGroup-X-Y and invoke Expand / Collapse | Expand Topmost Path in the context menu to expand the execution path taking most of the time. As you can see, almost all the time is spent in the
com.example.FibonacciController.computeNthFibonacci() method which seems to repeatedly call itself. Stop the sampling session using either the Stop button in the Sampler tab in VisualVM GUI or the Stop sampling action for the CPU sampler node in VS Code.
At this point the best way to proceed is to examine the source code of
com.example.FibonacciController.computeNthFibonacci(). Right-click the method in sampler results and invoke the Go to Source action in the Context menu. This brings the VS Code window in front of your eyes again and opens the class source in editor, with the cursor placed right at the method definition. Looking at the implementation, it's immediately clear that the performance suffers because of using a highly ineffective recursive algorithm with an exponential time complexity. Shame on this code!
There are much better approaches to compute Fibonacci numbers than recursion. In fact, recursion is probably the worst approach out of all the possibilities. But to keep things simple, let’s try to fix the performance of the recursive algorithm by adding a simple cache for the already computed results. Change the
FibonacciController.java content to the improved version and save the file:
Will this little change help to improve the performance? Let’s verify it using the CPU sampler again!
Display the VS Code Gr activity and click the Configure action for the When started: node in the VISUALVM section. This node is responsible for configuring what happens when a process is started from within the VS Code using the Launch VisualVM & Java 8+ Application launch configuration. The default value is Open process; that’s why the process has been automatically opened in VisualVM when running the project for the first time. Now select the Start CPU sampler choice to have the sampling session started as soon as possible without any additional action:
Terminate the original project process using the Run | Stop Debugging action if it’s still running, and start it again using the Run | Start Debugging or Run | Run Without Debugging action. Note that this time VisualVM opens the new project process at the Sampler tab and starts the sampling session automatically. You can verify the sampler settings using the Settings checkbox in the top right corner of the Sampler view — the filter should be configured by the VS Code integration to include only project classes
com.example.*, and sampling frequency should be 20ms.
Run the three requests again to collect comparable results:
This time you should get the results instantly and see a much more reasonable report from the generator similar to:
Fibonacci number #35 is 9227465 (computed in 0 ms, 69 steps)
Fibonacci number #40 is 102334155 (computed in 0 ms, 11 steps)
Fibonacci number #45 is 1134903170 (computed in 0 ms, 11 steps)
At the same time, the CPU sampler displays no results. Why? That’s because of the nature of sampling, which periodically checks the actual stack traces at the defined interval. If the methods to be included in the results execute faster than is the sampling rate, the sampler just can’t see them. This is actually a very useful lesson — Sampler is a great instrument to find performance bottlenecks, but won’t help you much with fine-tuning algorithm nuances. It also can’t be used to investigate invocation counts — that’s why counting the steps is implemented directly in the application code.
While still not as beneficial as the other algorithms, we’ve managed to improve the recursive algorithm performance to be acceptable, and verified it using VisualVM. But what if we introduced a memory leak by the fix? Performance and memory consumption are typically a trade-off, so this scenario is perfectly possible. Let’s investigate it in the next section.
Analyze Memory Consumption
Make sure you’ve stopped the CPU sampling session, but don’t close the VisualVM yet and keep the project process running. Switch to the VS Code window again and invoke the Take heap dump action for the Heap dump node in the VISUALVM section of the Gr activity view. This will generate a .hprof memory snapshot describing all the classes and instances allocated on the heap, and the references among them. VisualVM displays this snapshot in a special heap viewer component, which allows for browsing and analyzing its content in an understandable and performant way — a great tool for hunting memory leaks!
Once the heap dump is loaded, you’re presented with a Summary overview of the process heap. Click the Summary button in the Heap Dump toolbar and switch to the Objects view, which displays a histogram of classes and their respective instances. Find the Class Filter at the bottom of the view and submit the
com.example. filter as we're only interested in our classes. Now you can see several classes from the project: the last two are directly implemented, while the others were generated by the Micronaut framework. Expand the
com.example.FibonacciController class, right-click the single instance
com.example.FibonacciController#1 and invoke the Open in New Tab action in the context menu to open it in a new view:
Now click the Retained column header to have the retained sizes computed. Retained size is a metric describing how much memory is occupied by the concrete instance and the objects it references — or more specifically, how much memory would be freed if the instance was removed from the heap. Computing retained sizes can initially take some time for large heaps, but the results will be reused on subsequent sessions.
Having the retained sizes computed, you can finally analyze the memory footprint of the cache introduced to improve the algorithm. Expand the
<fields> node to see the memory representation of the instance, and find the static field
CACHE. The heap viewer shows that it's a
java.util.HashMap instance containing 44 elements and occupying ~3KB of the heap. Seems definitely worth the speed up! And presumably it won't grow in a way we should be ever worried about. In case you'd still want to improve things, remember that you can always easily go back to the implementation in VS Code editor using the Go to Source action, even from the heap viewer:
Let’s take a quick look at the static field
LOG which represents the global buffer of results. It doesn't really take much space on the heap, but notice how easy is to discover its content in the heap viewer! A preview of the stored text is available directly in the node name. In case you need to see more of it, just click the Preview button in the Details section of the Heap Dump toolbar. The full text is displayed in a regular text component and can be easily read, copied, and even saved to a file. This is just great for searching and identifying concrete instances based on their values/content:
At this point, we can conclude that we managed to implement a Fibonacci number generator using a recursive algorithm, improved it based on the VisualVM insights to perform reasonably fast, and verified that it behaves correctly also from the memory management perspective.
So far we’ve used just a part of the VisualVM features available from within the VS Code: CPU sampler and Heap dump. Actually we’ve also enabled starting VisualVM along with the project, configured the action on project startup, and used the Go to Source callback from VisualVM back to the VS Code editor. What are the other features not mentioned yet?
- Similar to the Heap dump, a Thread dump can be invoked from within the VS Code and displayed in VisualVM.
- Similar to the CPU sampler, a Memory sampler session can be configured and controlled from within the VS Code.
- VS Code now also allows to start and stop a JFR session for the project process, and dump & display the collected events in VisualVM.
In this article we showed how to use the latest improvements in VS Code to make your code perform better, to make your work more comfortable, and as a result to deliver better software.
There’s more about the tooling in VS Code: language server, debugging, tests support, and so on. Learn all the details in the Visual Studio Code Extensions documentation.
All this development is a part of the GraalVM effort led by Oracle Labs. In case you’re interested in more cool stuff regarding GraalVM — like compiling your Java applications to binary with native image, running scripting languages in Java apps, or polyglot programming — check out the GraalVM documentation and other articles in this very blog.
We should emphasize that the actual analysis and profiling described in this article is powered by the VisualVM, one of the best known Java tools. VisualVM is bundled with your GraalVM distributions so you can use it out-of-the-box. You can also learn more about it on the VisualVM project pages.
Have you experienced a bug? Do you miss a feature? Or is there anything else you’d like to share with regards to the VS Code & VisualVM integration? Let the developers know by filing an issue at GitHub or simply leave a comment below this article.