Often during simulation model building, one comes to a point where you need to optimize your model's performance. Either through improved memory usage or CPU utilization. This can be a near-impossible task through just code review or manual trial and error changes. Luckily, a specific tool is available for Java applications to help you and make life easier.
This is part 1 of a 2 part series where we look at using a profiler to analyze model performance.
In this post, we will look at an example of how to detect memory leaks and improve memory consumption.
Rather watch the 10 min video? Click here
Example
Let's have a look at the following super simple simulation model. We have a super basic simulation model of a system where we have agents arriving into a queue, being serviced and then exiting the system. If an agent waits too long in the system, it leaves the queue prematurely, does not get serviced, and exits the system.
We want to measure the time the agents that do get serviced spend in the system, so we have added a time measure start and time measure end block right before and after the service block for the objects that exit the system prematurely.
This basic service flow simulation can be representative of a number of real-life processes, from call centers, where callers are only willing to wait a certain amount of time for service, or, e.g., a carwash where potential patrons will only wait a few minutes in the queue before deciding rather to skip wash day.
P.S. If you consider yourself and advanced AnyLogic user you should be able to spot the issue in the above model as well as the impact it will have on your model performance. Take a moment and make a prediction and write the outcome of your prediction in the comment section below.
The model is really simple, and we plan to make major improvements to it, adding a significant amount of details and features. But before we do let's give it a quick run for one full year and check the results.
So here goes...
Everything looks good, the model completed the full year in under 9 seconds, and the results look good... but wait, have a look at the memory consumption... 500MB of memory. We have not even begun to add significant detail to the model! How on earth are we going to run the model after we have doubled the number of agents and added tons of statistics and additional logic....
Finding the memory leak
Luckily, optimizing java application performance has been around for a long time. We don't need to rely on our own intuition or trial and error to try and optimize our simulation model.
There are a number of commercial tools available for Java application profiling, one I prefer is from eJ Technologies called Jprofiler and you can read up on it here. However, if you are starting out you probably don't want to spend money on a tool that you don't know how to use. Luckily, free applications like Visual VM are available that you can download for Mac and Windows here.
FYI: Monitoring an application using VisualVM Monitor view
CPU Usage: This charts plots the CPU usage over time and is used to indicate when model requirements are overloading the CPU
Heap: The Heap graph displays the total heap size and how much of the heap is currently used. Heap is similar to memory
Classes: The Classes graph displays an overview of the total number of loaded classes and shared classes.
Threads: The Threads graph displays an overview of the number of live and daemon threads in the application's JVM. You can use VisualVM to take a thread dump if you want to capture and view exact data on application threads at a specific point in time.
What is Visual VM
VisualVM is a visual tool integrating several command‐line JDK tools and lightweight profiling capabilities. Designed for both production and development time use, it further enhances the capability of monitoring and performance analysis for the Java SE platform.
Application Developer enables you to monitor, profile, take thread dumps and browse heap dumps.
The main window of VisualVM opens when you launch the application. By default, the Applications window is displayed in the left pane of the main window. The Applications window enables you to quickly see the Java applications running on local and remote JVMs.
Using Visual VM to detect memory leaks
Use the steps below to conduct a memory analysis on your application or watch the video here for step by step instructions
1) Run your model up to the point where the memory consumption is a problem and then pause your model. In our case, this is at the model end
2) Open the Visual VM application.
3) In the left pane, you will see 2 applications linked to your model. The AnyLogic database as well as the model itself.
4) Double click the application associated with your model, and select the monitor tab in the right pane
As you can see from the overview statistics we are using zero CPU, which makes sense as the model finished running. Yet we are consuming almost all of the 500 MB available for memory.
FYI: Monitoring an application using VisualVM Monitor view
CPU Usage: This chart plots the CPU usage over time and is used to indicate when model requirements are overloading the CPU
Heap: The Heap graph displays the total heap size and how much of the heap is currently used. Heap is similar to memory
Classes: The Classes graph displays an overview of the total number of loaded classes and shared classes.
Threads: The Threads graph displays an overview of the number of live and daemon threads in the application's JVM. You can use VisualVM to take a thread dump if you want to capture and view exact data on application threads at a specific point in time.
5) Navigate to the Sampler page, select memory in the sample section and pause the profiler and select the Heap Dump
FYI: Memory Profiling
In the memory profiling mode VisualVM displays the total number of objects allocated by each class loaded by the application. For each class loaded in the Java Virtual Machine (JVM), the profiling results display the size and number of objects allocated since the profiling session started. The results are automatically updated as new objects are allocated and as new classes are loaded. The bytes allocated are also displayed as a graph representing the percentage of bytes as well as the total number of bytes allocated by each class
6) On the heap dump page, navigate to the Objects view in order to analyze the heap dump
Now, this is where it becomes a bit tricky and you need to gain some experience in finding leaks quickly. The best advice is to start at the top of the dump and work your way until you find something that makes sense and you can change the model. Given that AnyLogic contains numerous internal functions and features that you have no control over, often you might come across possible issues where you cant make any changes. Continue until you find parts of the model that you created.
In order to get to the issue quicker, you need to calculate the retained size of the objects and sort them accordingly. The retained size is not calculated by default, but sorting by this column will calculate them for you.
It can take a while... so keep track of the progress in the status bar in the bottom of the screen...
When done you will see the following.
There are 98k LinkedHashMap objects which account for most of the size 500MB
There is 1 TimeMeasureStart object which also has about 500MB of memory
There are also 98k LinkedHashMap$Entryfields which also have about 500MB of memory
There are 97k cars in the system, also accounting for about 500MB of memory
There are several signs that show these things are related, the memory size the retained. It has to be related and can't be mutually exclusive; else the total memory consumption would be way above 2 GB. The number of instances is also suspicious.
What is curious is why there are so many references to the Car object. Even though all the cars get destroyed in the sink block... We only have four cars in the system when the model ends.... (First red light)
So let's start with the object that takes up the most size and in this case, LinkedHashMap. If we drill down we try to see what objects are keeping a reference to this object.
And as suspected the TimeMeasureStart is keeping a reference to this LinkedHashMap.
So let's look at the TimeMeasureStart. Since there is only one instance of this object we will look at its fields to see what is it that is causing this object to consume so much memory... and low and behold.. it appears to be the same hashMap (#1551) as what we saw consumed the most memory in the previous screenshot.
So now we know that the TimeMeasureStart object keeps way too many references to some object... Since we know that is is measure the entry time for cars, and we have 97,976 cars in the system, while expecting only 4 we can assume that it is keeping a reference to cars even though they have been destroyed at the sink...
And that is when you realize that all the cars that exited through the queue timeout is still being kept as a reference by the TimeMeasureStart... Since they never pass a TimeMeasurerEnd.
The total number of cars in the model memory is equal to the number of cars currently in the system + the number of cars that exit through the queue time out.
Fixing the leak
Now that we know exactly where the issue is fixing the leak is super easy. We can simply add a timeMeasureEnd in the queue time-out exit connector.
Running the model now, we see that the memory consumption has gone down from 500 MB to about 250MB
Which is better but not great... So what now?
One last thing to remember about Java is that the Garbage Collector (GC) only runs when needed. Garbage Collections is when the memory is being cleaned of all objects for which there are no active references. Thus all things being used are retained and those not needed are deposed. Since we are only at 50% memory usage, it might just be the case that the GC.
You can force the garbage collections before doing your analysis inside the Visual VM.
Doing this results in less than 4% memory consumption, which is most likely the bare minimum required to run the model. Forcing garbage collection on the previous version of our model would have resulted in any memory decrease as all the car objects had active references inside the TimeMeasureStart object.
Why not download the little example model yourself and try repeating the exercise above to test your profiling skills. You can download the model from the cloud here - or the link below.
Keep modeling
Jaco-Ben Vosloo
What next?
If you liked this post, you are welcome to read more posts by following the links above to similar posts. Remember to subscribe, so that you can get notifications via email or you can also join the mobile app here! Or follow us on any of the social media accounts for future updates. The links are in the Menu bar at the top or the footer at the bottom.
If you want to contact us for some advice, maybe a potential partnership or project or just to say "Hi!", feel free to get in touch here, and we will get back to you soon!
Comments