FwdServerJMX.java
/*
** Module : FwdServerJMX.java
** Abstract : FWD JMX instrumentation.
**
** Copyright (c) 2024-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 HC 20240222 Created initial version.
** 002 SP 20240731 Added JMX_DEBUG flag.
** 003 HC 20250205 Removed the disabled SPREADSHEET caching logic.
** 004 ICP 20241114 Added QueryCounter JMX.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.jmx;
import java.io.*;
import java.lang.management.*;
import java.util.*;
import java.util.function.Function;
import java.util.logging.*;
import javax.management.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;
/** FWD JMX instrumentation */
public class FwdServerJMX
extends FwdJMX
{
/** When {@code true}, enable usage of some MBeans */
public static final boolean JMX_DEBUG = System.getProperty("fwd_jmx_debug") != null;
/** Logger. */
private static final CentralLogger LOG = CentralLogger.get(FwdServerJMX.class.getName());
/**
* Default constructor
*/
private FwdServerJMX()
{
super("FWD Server");
}
/**
* Register MBeans
*/
public void registerMBeans()
{
try
{
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
for (TimeStat ts: TimeStat.values())
{
server.registerMBean(ts.getCounter(),
new ObjectName("com.goldencode.p2j:name=" + ts.name()));
}
for (Counter c: Counter.values())
{
server.registerMBean(c.getCounter(),
new ObjectName("com.goldencode.p2j:name=" + c.name()));
}
for (MapCounter c: MapCounter.values())
{
server.registerMBean(c.getCounter(), new ObjectName("com.goldencode.p2j:name=" + c.name()));
}
for (ThreadsCpuTimers ts: ThreadsCpuTimers.values())
{
server.registerMBean(ts.getCounter(), new ObjectName("com.goldencode.p2j:name=" + ts.name()));
}
for (QueryProfiler ts: QueryProfiler.values())
{
server.registerMBean(ts.getProfiler(), new ObjectName("com.goldencode.p2j:name=" + ts.name()));
}
for (QueryCounterEnum qc: QueryCounterEnum.values())
{
server.registerMBean(qc.getQueryCounter(), new ObjectName("com.goldencode.p2j:name=" + qc.name()));
}
server.registerMBean(this, new ObjectName("com.goldencode.p2j:name=All"));
}
catch (Exception e)
{
LOG.log(Level.SEVERE, "MBean registration failed", e);
}
}
/**
* Initialize singleton
*/
public static void init()
{
Holder.INSTANCE.registerMBeans();
}
/**
* Register MBean
*
* @param mbean
* MBean to be registered
* @param name
* MBean name
*/
public static void register(Object mbean, String name)
{
Holder.INSTANCE.registerMBean(mbean, name);
}
/**
* Instance holder (Lazy initialization)
*/
private static class Holder
{
static final FwdServerJMX INSTANCE = new FwdServerJMX();
}
/**
* Time counters
*/
public static enum TimeStat
implements OptionalCounter
{
/** ServerStat.widgetAttrs (de)serialization */
ServerStateS11n,
/** Message.payload (de)serialization */
MessagePayloadS11n,
/** Protocol.objToByteArray */
objToByteArray,
/** SSL send time */
sslSend,
/** SSL wrap time */
sslWrap,
/** SSL wrap time */
sslUnwrap,
/** Instrumentation for <code>ContextLocal.get</code>. */
ContextLocalGet,
/** Finer instrumentation for preparing the SymbolResolver in {@code DynamicQueryHelper.parse()}. */
OrmDynQueryProcess_SymRes,
/** Finer instrumentation for preparing the SchemaDictionary in {@code DynamicQueryHelper.parse()}. */
OrmDynQueryProcess_SchemaDict,
/** Finer instrumentation for preparing the lexer and parser in {@code DynamicQueryHelper.parse()}. */
OrmDynQueryProcess_Parse,
/** Finer instrumentation for running ONLY the parser in {@code DynamicQueryHelper.parse()}. */
OrmDynQueryProcess_Parse2,
/** Instrumentation for setting up for TRPL processing in {@code DynamicQueryHelper.parse()}. */
OrmDynQueryProcess_Setup,
/** Instrumentation for running ONLY the annotations TRPL in {@code DynamicQueryHelper.parse()}. */
OrmDynQueryProcess_Annotation,
/** Instrumentation for running ONLY the base_structure TRPL in {@code DynamicQueryHelper.parse()}. */
OrmDynQueryProcess_Base,
/** Instrumentation for running ONLY the core_conversion TRPL in {@code DynamicQueryHelper.parse()}. */
OrmDynQueryProcess_Core,
/** Instrumentation for running ONLY the postprocessing TRPL in {@code DynamicQueryHelper.parse()}. */
OrmDynQueryProcess_Post,
/** Instrumentation for optimizing the result JAST by interning the {@code String}-s in
* {@code DynamicQueryHelper.parse()}. They proved to have a low impact on performance. */
OrmDynQueryProcess_Intern,
/** Instrumentation for <code>DynamicQueryHelper.parseFindQuery</code>. */
OrmDynParseFind,
/** Instrumentation for <code>DynamicQueryHelper.parseQuery</code>. */
OrmDynParseQuery,
/** Instrumentation for <code>SQLQuery.hydrateRecord</code>. */
OrmHydrateRecord,
/** Instrumentation for <code>Query.createSqlQuery</code>*/
OrmFqlParse,
/** Instrumentation for <code>UnclosablePreparedStatement</code> execute methods. */
OrmTempTableQuery,
/** Instrumentation for <code>RuntimeJastInterpreter.interpret</code>. */
OrmDynQueryInterpret,
/** Instrumentation for <code>HQLHelper.obtain</code>. */
OrmHqlHelperParse,
/** Instrumentation for <code>PreselectQuery.assembleHQL</code>. */
OrmPreselectQueryAssemble,
/** Instrumentation for <code>TempTableBuilder.tempTablePrepareImpl</code>. */
OrmCreateDynamicTable,
/** Instrumentation for <code>TemporaryBuffer.createDynamicBufferForTempTable</code>. */
OrmCreateDynamicBuffer,
/** Instrumentation for <code>TemporaryBuffer.define</code>. */
OrmBufferDefineTemp,
/** Instrumentation for <code>RecordBuffer.define</code>. */
OrmBufferDefinePerm,
/** Instrumentation for <code>BufferImpl.resourceDelete</code>. */
OrmBufferDelete,
/** Instrumentation for <code>TableMapper.mapTemporaryTable</code>. */
OrmTableMapperMap,
/** Instrumentation for <code>TemporaryBuffer.associate</code>. */
OrmTempTableParam,
/** Instrumentation for <code>TemporaryBuffer.createDynamicTable</code>. */
OrmDynamicTableParam,
/** Instrumentation for <code>DataSet.associate</code>. */
OrmDataSetParam,
/** Instrumentation for <code>DataSet.createDynamicDataSet</code>. */
OrmDynamicDataSetParam,
/** Instrumentation for <code>OutputTableCopier.finished</code> */
OutputTableCopier,
/** Instrumentation for <code>OutputTableHandleCopier.finished</code> */
OutputTableHandleCopier,
/** Instrumentation for <code>OutputDataSetCopier.finished</code> */
OutputDataSetCopier,
/** Instrumentation for <code>OutputDataSetHandleCopier.finished</code> */
OutputDataSetHandleCopier,
/** Instrumentation for <code>TransactionManager.processCommit</code>. */
TMCommit,
/** Instrumentation for <code>TransactionManager.processRollback</code>. */
TMRollback,
/** Instrumentation for <code>TransactionManager.processValidate</code>. */
TMValidate,
/** Instrumentation for <code>LegacyJavaAppserver.addTableMetaData</code>. */
AppserverAddTableMetaData,
/** Instrumentation for <code>LegacyJavaAppserver.getJavaParameter</code>. */
ApperverGetJavaParameter,
/** Instrumentation for <code>LegacyJavaAppserver.getOutputParameter</code>. */
AppserverGetOutputParameter;
/** Counter instance */
private final NanoCounter counter = new NanoCounter();
/**
* Register enable/disable setter
* @param agent
* enable/disable setter
*/
@Override
public void register(BooleanConsumer agent)
{
counter.register(agent);
}
/**
* Update counter
* @param value
* new value
*/
@Override
public void update(long value)
{
counter.update(value);
}
/**
* Return the string representation of the counter
*
* @param ts
* The measurement time stamp
* @param comment
* The comment
*
* @return The string representation of the counter
*
* @throws IOException
* If IO exception occurs
*/
@Override
public String toString(String ts, String comment) throws IOException
{
return counter.toString();
}
/**
* Return counter instance
* @return counter instance
*/
public NanoCounter getCounter()
{
return counter;
}
}
/**
* Defines the threads cpu counters.
*/
public static enum ThreadsCpuTimers
implements ThreadsCpuMBean
{
/** This counter is still unused */
ThreadsCpuUtilization
;
/** Counter instance */
private final ThreadsCpuCounter counter = new ThreadsCpuCounter();
/**
* Register enable/disable setter.
*
* @param agent
* enable/disable setter
*/
@Override
public void register(BooleanConsumer agent)
{
counter.register(agent);
}
/**
* Update counter.
*
* @param value
* new value
*/
@Override
public void update(long value)
{
counter.update(value);
}
/**
* Return the string representation of the counter
*
* @param ts
* The measurement time stamp
* @param comment
* The comment
*
* @return The string representation of the counter
*
* @throws IOException
* If IO exception occurs
*/
@Override
public String toString(String ts, String comment) throws IOException
{
return counter.toString(ts, comment);
}
/**
* Return counter instance.
*
* @return The counter instance
*/
public ThreadsCpuMBean getCounter()
{
return counter;
}
/**
* Gets the elapsed CPU time for the execution thread.
*
* @return The total CPU time in nanoseconds spend by the execution thread.
*/
@Override
public long getThreadElapsedCpuTime()
{
return counter.getThreadElapsedCpuTime();
}
/**
* Gets the elapsed CPU time for the given thread.
*
* @param tid
* The given thread id
*
* @return The total CPU time in nanoseconds spend by the given thread.
*/
@Override
public long getThreadElapsedCpuTime(long tid)
{
return counter.getThreadElapsedCpuTime(tid);
}
/**
* Updates the elapsed CPU time spent the given thread by adding the given value.
*
* @param tid
* The given thread id
* @param value
* The given value
*/
@Override
public void updateThreadElapsedCpuTime(long tid, long value)
{
counter.updateThreadElapsedCpuTime(tid, value);
}
/**
* Gets the elapsed CPU time for the execution thread in a user mode.
*
* @return The CPU time in nanoseconds spent by the execution thread in a user mode.
*/
@Override
public long getThreadElapsedUserTime()
{
return counter.getThreadElapsedUserTime();
}
/**
* Gets the elapsed CPU time for the given thread in a user mode.
*
* @param tid
* The given thread id
*
* @return The CPU time in nanoseconds spent by the given thread in a user mode.
*/
@Override
public long getThreadElapsedUserTime(long tid)
{
return counter.getThreadElapsedUserTime(tid);
}
/**
* Updates the elapsed CPU time spent the given thread in a user mode by adding the given value.
*
* @param tid
* The given thread id
* @param value
* The given value
*/
@Override
public void updateThreadElapsedUserTime(long tid, long value)
{
counter.updateThreadElapsedUserTime(tid, value);
}
/**
* Sets the execution thread.
*
* @param tid
* The thread id
*/
@Override
public void setCurrentThread(long tid)
{
counter.setCurrentThread(tid);
}
}
/**
* Long values' counter
*/
public static enum Counter
implements OptionalCounter
{
/** Incomning network traffic */
NetworkReads,
/** Outgoing network traffic */
NetworkWrites,
/** Widget attributes flush count */
WidgetAttrFlushes;
/** Counter instance */
private final SimpleLongCounter counter = new SimpleLongCounter();
/**
* Register enable/disable setter
* @param agent
* enable/disable setter
*/
@Override
public void register(BooleanConsumer agent)
{
counter.register(agent);
}
/**
* Update counter
* @param value
* new value
*/
@Override
public void update(long value)
{
counter.update(value);
}
/**
* Return the string representation of the counter
*
* @param ts
* The measurement time stamp
* @param comment
* comment
* @return string representation of the counter
* @throws IOException
* If IO exception occurs
*/
public String toString(String ts, String comment) throws IOException
{
return counter.toString();
}
/**
* Return counter instance
* @return counter instance
*/
public SimpleLongCounter getCounter()
{
return counter;
}
}
/**
* String-long map counter
*/
public static enum MapCounter
implements OptionalMapCounter
{
/** Widget attribute enqueue count */
WidgetAttrEnqueues,
/** Task counter for each legacy service worker thread. */
LegacyServiceWorkerTaskCounter,
/** Spreadsheet command invocation nano times */
SpreadsheetCmdNanos;
/** Counter instance */
private final SimpleMapCounter counter = new SimpleMapCounter();
/**
* Register enable/disable setter
* @param agent
* enable/disable setter
*/
@Override
public void register(BooleanConsumer agent)
{
counter.register(agent);
}
/**
* Update counter
*
* @param valueName
* value name to update
* @param value
* new value
*/
@Override
public void update(String valueName, long value)
{
counter.update(valueName, value);
}
/**
* Return the string representation of the counter
*
* @param ts
* The measurement time stamp
* @param comment
* The comment
*
* @return The string representation of the counter
*
* @throws IOException
* If IO exception occurs
*/
public String toString(String ts, String comment) throws IOException
{
return counter.toString();
}
/**
* Return counter instance
* @return counter instance
*/
public SimpleMapCounter getCounter()
{
return counter;
}
}
/**
* Profiler which is able to count number of cache misses and hits of each executed query
*/
public static enum QueryProfiler
implements OptionalQueryProfiler
{
/** Query profiler */
QueryProfiler;
/** Profiler instance */
private final SimpleQueryProfiler profiler = new SimpleQueryProfiler();
/**
* Register enable/disable setter
* @param agent
* enable/disable setter
*/
@Override
public void register(BooleanConsumer agent)
{
profiler.register(agent);
}
/**
* Retrieve the profiler back-end of this profiler.
*
* @return The simple query profiler
*/
public SimpleQueryProfiler getProfiler()
{
return profiler;
}
/**
* Update the number of rows retrieved from a result set
*
* @param query
* the query for which the update is done
* @param value
* new value
*/
@Override
public void updateRowsCount(String query, long value)
{
profiler.updateRowsCount(query, value);
}
/**
* Update the number of cache hits of a specified query
*
* @param query
* the query for which the update is done
* @param value
* the number of cache hits
*/
@Override
public void updateCacheHits(String query, long value)
{
profiler.updateCacheHits(query, value);
}
/**
* Update the number of cache misses of a specified query
*
* @param query
* the query for which the update is done
* @param value
* the number of cache misses
*/
@Override
public void updateCacheMisses(String query, long value)
{
profiler.updateCacheMisses(query, value);
}
/**
* Update the number of nanoseconds spent in hydrate of a specified query
*
* @param query
* the query for which the update is done
* @param value
* the number of nanoseconds spent in hydrate
*/
@Override
public void updateHydrateTotalTime(String query, long value)
{
profiler.updateHydrateTotalTime(query, value);
}
/**
* Return the string representation of a profiled query
*
* @param ts
* The measurement time stamp
* @param comment
* The comment
*
* @return The string representation of a profiled query
*/
public String toString(String ts, String comment)
{
return profiler.toString();
}
}
/**
* Counter for executed queries
*/
public static enum QueryCounterEnum
implements QueryCounter
{
/** Query counter */
QueryCounter;
/** Counter instance */
private final SimpleQueryCounter queryCounter = new SimpleQueryCounter();
/**
* Retrieve the counter back-end of this counter.
*
* @return The simple query counter
*/
public SimpleQueryCounter getQueryCounter()
{
return queryCounter;
}
/**
* Update the query counting configuration for a specific database.
*
* @param databaseName
* The database to update the configuration for.
* @param config
* The new configuration object.
*/
@Override
public void updateConfiguration(String databaseName, QueryCountConfiguration config)
{
queryCounter.updateConfiguration(databaseName, config);
}
/**
* Updates the count for a specific query executed on the specified database.
*
* @param databaseName
* The name of the database on which the query was executed.
* @param sqlInfo
* The object containing the sql query informations.
*/
@Override
public void updateCount(String databaseName, QueryInfo sqlInfo)
{
queryCounter.updateCount(databaseName, sqlInfo);
}
/**
* Bootstrap method to register all databases with the query counter.
*
* @param databases
* The list of database names to register.
*/
@Override
public void bootstrap(List<String> databases)
{
queryCounter.bootstrap(databases);
}
/**
* Updates the supplier for initial configuration.
*
* @param supplier
* The function to call to get the initial configuration.
*/
public void updateSupplier(Function<String, QueryCountConfiguration> supplier)
{
queryCounter.updateSupplier(supplier);
}
}
/**
* Print all counters' values to the file.
*
* @param file
* The output file name (will be appended if exists).
*
* @throws IOException
* If IO exception occurs
*/
@Override
public synchronized void dumpAll(String file) throws IOException
{
String annotation = getAnnotation();
for(TimeStat ts: TimeStat.values())
{
ts.getCounter().print(annotation + ":" + ts.name(), file);
}
for(Counter c: Counter.values())
{
c.getCounter().print(annotation + ":" + c.name(), file);
}
for(MapCounter c: MapCounter.values())
{
c.getCounter().print(annotation + ":" + c.name(), file);
}
for(QueryProfiler p: QueryProfiler.values())
{
p.getProfiler().print(file);
}
}
/** Reset all counters */
@Override
public synchronized void resetAll()
{
Arrays.stream(TimeStat.values()).map(TimeStat::getCounter).forEach(NanoCounter::reset);
Arrays.stream(Counter.values()).map(Counter::getCounter).forEach(SimpleLongCounter::reset);
Arrays.stream(MapCounter.values()).map(MapCounter::getCounter).forEach(SimpleLongCounter::reset);
Arrays.stream(QueryProfiler.values()).map(QueryProfiler::getProfiler).forEach(SimpleQueryProfiler::reset);
}
/**
* Enable/disable all counters
* @param val
* new value of the "enable" flag
*/
@Override
public synchronized void enableAll(boolean val)
{
Arrays.stream(TimeStat.values()).map(TimeStat::getCounter).forEach(c -> c.enable(val));
Arrays.stream(Counter.values()).map(Counter::getCounter).forEach(c -> c.enable(val));
Arrays.stream(MapCounter.values()).map(MapCounter::getCounter).forEach(c -> c.enable(val));
Arrays.stream(QueryProfiler.values()).map(QueryProfiler::getProfiler).forEach(c -> c.enable(val));
}
}