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:

0102
03/**
04 *  The interface to the PerfLog MBean.
05 */
06private static final PerfLogManager PERFLOGMANAGER;
07
0809
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:

0102
03//---* Initialise the Performance Logging *------------------------------------
04final var perfLogManager = PerfLogUtils.createPerfLogManager();
05
0607
08// Do something …
09
1011
12perfLogManager.close()
13
14

Because PerfLogManager implements AutoCloseable, it can also be used with try-with-resources:

0102
03//---* Initialise the Performance Logging *------------------------------------
04try( final var perfLogManager = PerfLogUtils.createPerfLogManager() )
05{
06    // Do something …
07
0809}
10

The Performance Sections

The next step is to define and to register the performance sections:

0102
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:

0102
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();
0203
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!!
1516
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();
0203
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!!
1314    }
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!!
78}

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
2122    }
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;
0405
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
…
Modules
Module
Description
The module for the performance logging and monitoring classes of the Foundation Library.