tquadrat's Foundation Library org.tquadrat.foundation.perflog 0.25.12 API
Performance Logging and Monitoring
This library provides a tool to permanently track the performance of an application or of subsystems inside that application. It is strictly not meant as a framework for benchmarking an application or algorithm.
It allows to observe how much time is used to perform a single operation within an application and whether this changes over time.
The application defines a Performance Section that wraps the operation to monitor. Each Performance Section has a unique name that is used for the statistics later. That name should be somehow meaningful.
The section registers the time when it was entered, and when it was left again, and the elapsed time will be made available to the monitoring system.
That monitoring system connects through JMX. This means it could either be an internal thread inside the same application, or another process on the same machine, or even a program running on another machine.
How to use it
The Performance Logging and Monitoring Manager
For the integration of the Performance Logging and Monitoring into an
application, the first step is to create an instance of
PerfLogManager;
this is the frontend to the
Performance Logging and Monitoring MBean
that does the library's work behind the scenes.
Instances of PerfLogManager are thread-safe; it can be created as
static or as an attribute:
01… 02 03/** 04 * The interface to the PerfLog MBean. 05 */ 06private static final PerfLogManager PERFLOGMANAGER; 07 08… 09 10static 11{ 12 //---* Initialise the Performance Logging *-------------------------------- 13 PERFLOGMANAGER = PerfLogUtils.createPerfLogManager(); 14} 15…
Or it will be created for each method that should be monitored:
01… 02 03//---* Initialise the Performance Logging *------------------------------------ 04final var perfLogManager = PerfLogUtils.createPerfLogManager(); 05 06… 07 08// Do something … 09 10… 11 12perfLogManager.close() 13 14…
Because PerfLogManager implements
AutoCloseable, it can also be used with
try-with-resources:
01… 02 03//---* Initialise the Performance Logging *------------------------------------ 04try( final var perfLogManager = PerfLogUtils.createPerfLogManager() ) 05{ 06 // Do something … 07 08 … 09} 10…
The Performance Sections
The next step is to define and to register the performance sections:
01… 02 03//---* Create the Performance Sections and add them *-------------------------- 04final var PS1 = PerfLogUtils.createPerformanceSectionName( "PS_Load" ); 05final var PS2 = PerfLogUtils.createPerformanceSectionName( "PS_Dump" ); 06 07perfLogManager.loadPerformanceSectionDefinition( 08 new PerformanceSection( PS1, "Loading Data", null, null, SEND_REPORT_FOR_ABORT ), 09 new PerformanceSection( PS2, "Dumping Data", new TimeValue( MILLISECOND, 500), null, 10 SEND_REPORT_ONLY_FOR_EXCEEDED_THRESHOLD, SEND_REPORT_FOR_ABORT ) 11 } 12 13…
The instances of
PerformanceSection
are stored globally by the
PerfLogMBean.
This means that each instance of PerfLogManager shares the same set
of PerformanceSection definitions.
This allows to define and register them centrally, even if local manager instances are used:
01… 02 03/** 04 * The name for the performance section for loading data. 05 */ 06public static final PerformanceSectionName PS1; 07 08/** 09 * The name for the performance section for dumping data. 10 */ 11public static final PerformanceSectionName PS2; 12 13static 14{ 15 //---* Initialise the Performance Logging *-------------------------------- 16 final var perfLogManager = PerfLogUtils.createPerfLogManager(); 17 18 //---* Create the Performance Sections and add them *---------------------- 19 PS1 = PerfLogUtils.createPerformanceSectionName( "PS_Load" ); 20 PS2 = PerfLogUtils.createPerformanceSectionName( "PS_Dump" ); 21 22 perfLogManager.loadPerformanceSectionDefinition( 23 new PerformanceSection( PS1, "Loading Data", null, null, SEND_REPORT_FOR_ABORT ), 24 new PerformanceSection( PS2, "Dumping Data", new TimeValue( MILLISECOND, 500), null, 25 SEND_REPORT_ONLY_FOR_EXCEEDED_THRESHOLD, SEND_REPORT_FOR_ABORT ) 26 } 27} 28 29…
Threshold
The threshold time for a performance section is the execution duration for that section that is usually not exceeded. If that threshold limit is exceeded, a corresponding flag is set in the performance report.
When creating the performance section with the flag
SEND_REPORT_ONLY_FOR_EXCEEDED_THRESHOLD,
a performance report will be created only when the threshold is exceeded.
Timeout
If the execution of a performance section took longer than time configured for the timeout, the performance tracker will be aborted and a performance report is created. In this case, the exact execution time for the section is not tracked.
If both a threshold and a timeout value is set for the performance section, the timeout must but greater than the threshold.
Track the Performance
Before entering the code section that should be tracked, an instance of
PerformanceTracker
is requested from the PerfLogManager through a call to
createPerformanceTracker()
with the name of the performance section. If there is currently no
performance section defined with that name, one will be created with default
values (a dummy description, no threshold, no timeout, no flags – refer to
PerformanceSection()
for the details).
When the performance section is currently not active
(PerformanceSection.isIgnored()
returns true), the call to createPerformanceTracker() returns
empty.
When entering the code section, the tracker needs to be started, and at the end, its should be stopped or, in case of an error, aborted.
That may look like this:
01final var perfLogManager = PerfLogUtils.createPerfLogManager(); 02… 03 04//---* Load Data *------------------------------------------------------------- 05final var tracker = perfLogManager.createPerformanceTracker( PS1 ); 06try 07{ 08 tracker.ifPresent( t -> 09 { 10 t.setContext( "Source", … ); 11 t.start(); 12 }); 13 14 // Do something here!! 15 … 16 17 tracker.ifPresent( PerformanceTracker::stop() ); 18} 19catch( final AnException e ) 20{ 21 tracker.ifPresent( t -> t.abort( "An exception was caught", e ) ); 22}
The utility class
PerfLogUtils
has the methods hold() and holdAndStart() that allows to use
try-with-resources:
01final var perfLogManager = PerfLogUtils.createPerfLogManager(); 02… 03 04//---* Load Data *------------------------------------------------------------- 05try( final var tracker = PerfLogUtils.hold( perfLogManager.createPerformanceTracker( PS1 ) ) ) 06{ 07 tracker.setContext( "Source", … ); 08 09 try 10 { 11 tracker.start(); 12 // Do something here!! 13 … 14 } 15 catch( final AnException e ) 16 { 17 tracker.abort( "An exception was caught", e ); 18 } 19}
Or without the option to abort the performance tracker and with an internal, volatile Performance Logging Manager:
1//---* Load Data *------------------------------------------------------------- 2try( final var tracker = PerfLogUtils.holdAndStart( PS1 ) ) 3{ 4 tracker.setContext( "Source", … ); 5 6 // Do something here!! 7 … 8}
The Context
If the execution time for a performance section is (probably) dependent on some data, this can be documented through the context that is stored with the tracker instance.
PerformanceTracker.addContext()
takes a category key and the related value; this allows to provide some
structured data:
01//---* Load Data *------------------------------------------------------------- 02try( final var tracker = PerfLogUtils.hold( perfLogManager.createPerformanceTracker( PS1 ) ) ) 03{ 04 final var source = Path.of( fileName ); 05 final var lastModified = ZonedDateTime.ofInstant( Instant.ofEpochMilli( Files.getLastModifiedTime( source ).toMillis() ), UTC ); 06 final var size = new DataSizeValue( BYTE, Files.size( source ) ); 07 final var lineCounter = new AtomicInteger( 0 ); 08 09 tracker.setContext( "Source", source.toString ); 10 tracker.setContext( "Source Date", lastModified.format( DateTimeFormatter.ISO_ZONED_DATE_TIME ); 11 tracker.setContext( "Source Size", "%0.1s".formatted( size.convert( KIBIBYTE ) ); 12 13 try 14 { 15 tracker.start(); 16 Files.lines( source ) 17 .peek( _ -> lineCounter.incrementAndGet() ) 18 .forEach( line -> /* Do something here!! */ ); 19 tracker.setContext( "Source Lines", lineCounter.toString() ); 20 21 … 22 } 23 catch( final IOException e ) 24 { 25 tracker.abort( "An exception was caught", e ); 26 throw e; 27 } 28} 29catch( final IOException e ) 30{ 31 // Handle the exception … 32}
When reusing the tracker instance, it is possible to keep the current
context and only overwrite some of the entries; refer to
PerformanceTracker#reset().
Receiving the Performance Reports
Once a performance tracker terminates, a performance report is created by
the Performance Logging and Monitoring MBean, then it will be issued by it as
a
Notification.
The report data is stored in JSON format in the message part of the notification.
Such a report may look like this:
{
"PerformanceSection":
{
"Name":"PS_Load",
"Description":"Loading Data",
"ThresholdOnlyReport":false
},
"StartTime":"2026-05-17T09:43:46.723338398Z",
"ElapsedTime":
{
"Unit":"ms",
"Value":29001.241731
},
"ExceededThreshold":false,
"IsTimedOut":false,
"IsAborted":false,
"Context":
{
"Source":"…",
"Source Date":"…",
"Source Size":"…",
"Source Lines":"…"
}
}
Internal Client
For a client that runs within the same JVM as the application itself, the
class
PerfLogClientSupport
can be used like this:
01/** 02 * Receive the Performance Logging and Monitoring Notifications and 03 * process them. 04 */ 05public final void run() 06{ 07 final var mbeanServer = obtainMBeanServer(); 08 try( final var clientSupport = new PerfLogClientSupport() ) 09 { 10 clientSupport.connect( mbeanServer, true ); 11 var proceed = true; 12 while( proceed ) 13 { 14 try 15 { 16 final var message = clientSupport.awaitMessage(); 17 // Process the message!! 18 } 19 catch( final InterruptedException _ ) 20 { 21 proceed = false; 22 } 23 } 24 } 25 catch( final InstanceNotFoundException e ) 26 { 27 throw new UnexpectedExceptionError( "Should not happen as we set force = 'true' when connecting", e ); 28 } 29} // run()
Remote Client
If a client running inside another JVM should be used, either on the same or another machine, if is necessary to expose the MBeanServer first:
01import static org.tquadrat.foundation.mgmt.JMXUtils.enableRemoteAccess; 02import java.rmi.RemoteException; 03import java.io.IOException; 04… 05 06try 07{ 08 final var mbeanServer = obtainMBeanServer(); 09 final var url = enableRemoteAccess( mbeanServer, 9999, Map.of() ); 10} 11catch( final IOException | RemoteException e ) 12{ 13 err.println( "Cannot expose MBeanServer" ); 14 e.printStackTrace( err ); 15}
The library org.tquadrat.foundation.perflog.remote provides a helper class for the implementation of a remote client.
A simple implementation could look like this:
… 047public final class RemoteClient 048{ 049 /*---------*\ 050 ====** Methods **========================================================== 051 \*---------*/ 052 /** 053 * The implementation for 054 * {@link NotificationListener}. 055 * 056 * @param notification The notification. 057 * @param handback The handback. 058 */ 059 private static final void listener( final Notification notification, final Object handback ) 060 { 061 out.printf( """ 062 Type: %1$s 063 Message: %3$s 064 SeqNo: %2$d 065 ------------------------------------------------------------------- 066 """, notification.getType(), notification.getSequenceNumber(), notification.getMessage() ); 067 } // listener() 068 069 /** 070 * The program entry point. 071 * 072 * @param args The command line arguments. 073 */ 074 public static final void main( final String... args ) 075 { 076 //---* Initialise the error handling *--------------------------------- 077 currentThread().setUncaughtExceptionHandler( RemoteClient::uncaughtExceptionHandler ); 078 079 while( true ) 080 { 081 try( final var remote = PerfLogRemote.connect( 9999, RemoteClient::listener ) ) 082 { 083 out.println( remote.getMBeanInfo() ); 084 for( final var name : remote.getPerformanceSections() ) 085 { 086 out.println( remote.getPerformanceSection( name ) ); 087 } 088 out.println( "=".repeat( 67 ) ); 089 while( true ); 090 } 091 catch( final IOException | InstanceNotFoundException | ReflectionException | IntrospectionException | AttributeNotFoundException | MBeanException e ) 092 { 093 e.printStackTrace( err ); 094 } 095 } 096 } // main() 097 098 /** 099 * An implementation of 100 * {@link Thread.UncaughtExceptionHandler}. 101 * 102 * @param t The aborted thread. 103 * @param e The 104 * {@link Throwable} 105 * that caused the abort. 106 */ 107 private static final void uncaughtExceptionHandler( final Thread t, final Throwable e ) 108 { 109 err.printf( "Thread %s aborted due to an exception%n", t.getName() ); 110 e.printStackTrace( err ); 111 } // uncaughtExceptionHandler() 112} 113// class Client …
