August 5th, 2021

Introducing Microsoft GCToolkit

Microsoft’s Java Engineering Group is excited to announce we have open-sourced the Microsoft GCToolkit on GitHub. GCToolkit is a set of libraries for analyzing Java garbage collection (GC) log files. The toolkit parses GC log files into discrete events and provides an API for aggregating data from those events. This allows the user to create arbitrary and complex analyses of the state of managed memory in the Java Virtual Machine (JVM) represented by the garbage collection log. In this blog post, I will introduce some of the key features to help you get the most from this project. 

Managed memory in the Java Virtual Machine (JVM) is comprised of 3 main pieces, memory buffers known as Java heap, allocators which perform the work of getting data into Java heap, and garbage collection (GC). While GC is responsible for recovering memory in Java heap that is no longer in use, the term is often used as a euphemism for memory management and tuning GC or tuning the collector are often used with the understanding that it refers to tuning the JVM’s memory management subsystem.  

More importantly, it has long been known that a suboptimal configuration collector will result in your application requiring more CPU and memory while at the same time, degrade your end-users experience. In other words, poorly tuned often equates to a more expensive runtime and unhappy users. The challenge is that to optimally tune GC, one needs to create a delicate balance between several concerns all of which are not easily seen without the assistance of tooling. GCToolKit has been helpful in making this easier. So what is GCToolkit? Let’s take a tour to find out. 

GCToolkit Modules 

GCToolkit is made up of 3 Java modules that cover the API, GC log file parsers, and a messaging backplane based on Vert.x. The API module is the entry point into GCToolkit. It hides the details of using the parser and Vert.x to analyze a GC log file into a few method calls. The parser module is a collection of regular expressions and code that has been developed over many years to be the most robust GC log parser available. The Vert.x-based messaging backplane makes use of 2 message buses. The first message bus streams from a DataSource. The current implementation is to stream log lines from the GC log file. The listeners on this bus are the parsers that convert the data from the data source into events that represent either a GC cycle or safe point. These events are then published on an event bus. Listeners on the event bus are then able to receive and process events that are of interest to them.  

Aggregators and Aggregations 

The parser emits discrete JVM events (GC cycle events or safe point events) which makes it possible to write code to capture and analyze the data from those events. What data you want to analyze and what kind of analysis you want to perform are up to you. GCToolkit provides a simple Aggregator/Aggregation framework for capturing and analyzing GC log file data.  

The code that captures an event is called an Aggregator, and the code that analyzes the data is called an Aggregation. An Aggregator can capture several different events for the purpose of feeding the analysis. For example, one may want to capture pause events for the purpose of analyzing heap occupancy. The Aggregator captures the event, extracts the relevant data, and passes the data to the Aggregation. The Aggregation collates the data into meaningful analyses, for example, total heap occupancy after GC.     

Example

Let us make this real by looking at an example that reports on total heap occupancy after a GC cycle has been completed. The following code is a minimal implementation that makes use of the key elements of the API. 

public class Main { 
    public static void main(String[] args) throws Exception { 
        var path = Path.of(args[0]); 
        var logFile = new SingleGCLogFile(path); 
        var gcToolKit = new GCToolKit(); 
        var jvm = gcToolKit.analyze(logFile); 
        var results = jvm.getAggregation(HeapOccupancyAfterCollectionSummary.class); 
        System.out.println(results.toString()); 
    } 
}

The flow is to first create a DataSource. In this case the DataSource is a GCLogFIle and more specifically, all the data is contained in a single file. The next step is to create an instance of GCToolkit. This starts the process of constructing all the trussing needed to support the processing of the DataSource. Once we have an instance of GCToolkit, we can use it by calling the analyze method with the DataSource as a parameter. What is returned to us is a JavaVirtualMachine. This is our API which we can interrogate for the state and configuration of the JVM. In this case, we are asking for the Aggregation that is associated with the HeapOccupancyAfterCollectionSummary Aggregator. Finally, we can process the results. For this simple example, the results are printed to the terminal. But the data could be rendered as a graph, a table, or some other more human-friendly format. 

There is a little bit of magic here in that neither the Aggregator nor the HeapOccupancyAfterCollectionSummary Aggregation appears in the sample. These classes are provided to the API via Java’s module system discovery services. Let us start by first looking at the implementations before moving on to understand how GCToolkit discovers and makes use of them. 

@Aggregates({EventSource.G1GC,EventSource.GENERATIONAL,EventSource.ZGC}) 
public class HeapOccupancyAfterCollection extends Aggregator<HeapOccupancyAfterCollectionAggregation> { 

    public HeapOccupancyAfterCollection(HeapOccupancyAfterCollectionAggregation aggregation) { 
        super(aggregation); 
        register(GenerationalGCPauseEvent.class, this::extractHeapOccupancy); 
        register(G1GCPauseEvent.class, this::extractHeapOccupancy); 
        register(ZGCCycle.class,this::extractHeapOccupancy); 
    } 

    private void extractHeapOccupancy(GenerationalGCPauseEvent event) { 
        aggregation()
.addDataPoint(event.getGarbageCollectionType(),
event.getDateTimeStamp(),
event.getHeap().getOccupancyAfterCollection());
    }

   private void extractHeapOccupancy(G1GCPauseEvent event) {          aggregation()
.addDataPoint(event.getGarbageCollectionType(),
event.getDateTimeStamp(),
event.getHeap().getOccupancyAfterCollection());      }      private void extractHeapOccupancy(ZGCCycle event) {          aggregation()
.addDataPoint(event.getGarbageCollectionType(),
event.getDateTimeStamp(),
event.getLive().getReclaimEnd());      }  }

The listing above starts with the @Aggregates annotation which indicates the event sources this aggregator will work with. As can be seen here, this aggregation is designed to work with G1GC, the older generational collectors, and ZGC. 

In the constructor, the specific events that this Aggregator will work with are registered with the corresponding consumer method. All GC pause events report on heap occupancy before and after the collection phase. Thus, we can register for the super class instead of each individual event. Finally, the individual methods harvest the data of interest and then pass it along to the Aggregation, which acts as a view on the incoming events. In this case, the Aggregation is a HeapOccupancyAfterCollectionAggregation, which is an interface that defines a single method, addDataPoint. Notice that the parameter to the HeapOccupancyAfterCollection  aggregator constructor is an interface. This allows the Aggregator to populate an Aggregation that is specific to your particular use case. 

The following is an implementation of HeapOccupancyAfterCollectionSummary. 

@Collates(HeapOccupancyAfterCollection.class) 
public class HeapOccupancyAfterCollectionSummary implements HeapOccupancyAfterCollectionAggregation { 

    private HashMap<GarbageCollectionTypes, XYDataSet> aggregations = new HashMap<>(); 

    public void addDataPoint(GarbageCollectionTypes gcType, DateTimeStamp timeStamp, long heapOccupancy) { 
        var dataSet = aggregations.computeIfAbsent(gcType, k -> new XYDataSet()); 
        dataSet.add(timeStamp.getTimeStamp(),heapOccupancy); 
    } 

    public HashMap<GarbageCollectionTypes, XYDataSet> get() { 
        return aggregations; 
    } 
}

The implementation starts with the @Collates annotation. This tells the API that this implementation is intended to work with HeapOccupancyAfterCollection. The rest of the implementation collects the data in a form that is suitable for its intended use. For example, XYDataSet is intended to support the rendering of an X-Y scatter plot. 

The final magic is the ‘provides Aggregation with HeapOccupancyAfterCollectionSummary’ in the sample’s module-info. This makes the sample service provider. When GCToolkit is instantiated, it looks for any module that provides the Aggregation service. Thus, the Aggregator/Aggregation are automatically loaded and used when called for by the GCToolkit analyze method. GCToolkit also provides API to programmatically register Aggregation classes if you choose not to use the service provider paradigm.  

Making it a Module 

Finally, the module-info.java provides the HeapOccupancyAfterCollectionSummary implementation for Aggregation. 

module com.microsoft.gctoolkit.sample { 
    requires gctoolkit.api; 
    requires gctoolkit.vertx; 
    requires java.logging;

    exports com.microsoft.gctoolkit.sample.aggregation to gctoolkit.vertx;

    provides Aggregation with HeapOccupancyAfterCollectionSummary; 
} 

As can be seen here, the sample module requires each of the 3 GCToolkit modules. The module exports the aggregation package to the gctoolkit.vertx module. The dependency is a work-around for a known bug that has been reported and is scheduled to be fixed. 

Finally, let us get this app to run. A sample shell script is included with the project to demonstrate how to run the app from the command line. The command-line sets the paths to the modules using the -p parameter and the main class using the -m to specify the main. For this GC log, the output will look like this:

$ ./sample.sh 
Collected 3 different collection types. 

Contribute!

If you’re interested in contributing, or you just want to follow along, do join us at  github.com/microsoft/gctoolkit/discussions. 

Microsoft Java Engineering Group’s Tooling Team 

0 comments

Discussion are closed.