Skip to content

Commit

Permalink
Add JMX support for statement cache, and unit tests. (#1972)
Browse files Browse the repository at this point in the history
JMX allows a developer to use `jconsole` to connect to the app, and introspect
various metrics about it.  This PR adds metrics about the `PreparedStatement`
cache, and allows a developer to turn on/off at runtime on demand.
  • Loading branch information
tychobrailleur authored Dec 5, 2023
1 parent 863ac6b commit 1fb794f
Show file tree
Hide file tree
Showing 17 changed files with 283 additions and 129 deletions.
10 changes: 9 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id 'java'
id 'groovy'
id 'org.jetbrains.kotlin.jvm' version '1.9.10'
id 'org.jetbrains.kotlin.jvm' version '1.9.21'
id 'application'
id 'de.jansauer.poeditor' version '1.1.0'
id 'org.kordamp.gradle.markdown' version '2.2.0'
Expand Down Expand Up @@ -111,6 +113,12 @@ java {
// modularity.inferModulePath.set(true)
}

tasks.withType(KotlinCompile).all {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
}
}

sourceSets {
main {
java {
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/core/HO.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import core.gui.model.UserColumnController;
import core.gui.theme.ImageUtilities;
import core.gui.theme.ThemeManager;
import core.jmx.StatementCacheMonitor;
import core.model.HOVerwaltung;
import core.model.UserParameter;
import core.training.TrainingManager;
Expand All @@ -16,7 +17,9 @@
import core.util.OSUtils;
import java.io.File;
import javax.imageio.ImageIO;
import javax.management.*;
import javax.swing.*;
import java.lang.management.ManagementFactory;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -212,6 +215,7 @@ public static void main(String[] args) {
DBManager.instance().updateConfig();
}

initJmxSupport();

// Training
interruptionWindow.setInfoText(8, "Initialize Training");
Expand All @@ -230,6 +234,21 @@ public static void main(String[] args) {
});
}

private static void initJmxSupport() {
if (HO.isDevelopment()) {
try {
MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer();
platformMBeanServer.registerMBean(
new StatementCacheMonitor(),
new ObjectName("io.github.ho-dev:name=StatementCacheMonitor")
);
} catch (MalformedObjectNameException | NotCompliantMBeanException | InstanceAlreadyExistsException |
MBeanRegistrationException e) {
throw new RuntimeException(e);
}
}
}

private static Object[] createOptionsArray() {
var buttons = new ArrayList<JButton>();
int keyEvent = VK_1;
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/core/db/ConnectionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ public class ConnectionManager {

private StatementCache statementCache;

public StatementCache getStatementCache() {
return this.statementCache;
}

/**
* Closes the connection
*/
Expand Down
67 changes: 0 additions & 67 deletions src/main/java/core/db/StatementCache.java

This file was deleted.

101 changes: 101 additions & 0 deletions src/main/java/core/db/StatementCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package core.db

import core.util.HOLogger
import java.sql.PreparedStatement
import java.sql.SQLException
import java.time.Instant
import java.util.*

/**
* Cache for [PreparedStatement]s instances.
*
*
* This cache tracks statistics about the various prepared statements:
*
* * Creation timestamp,
* * Last access timestamp,
* * NUmber of accesses.
*
*
*
* The cache can be disabled by setting `cachedEnabled` to `false`. When the cache
* is disabled, the existing entries are closed and evicted, the stats dumped and cleared. The cache can
* be enabled or disabled via JMX in development mode. By default, the cache is on.
*/
class StatementCache(private val connectionManager: ConnectionManager) {
var cachedEnabled = true
set(enabled) {
field = enabled
HOLogger.instance().info(StatementCache::class.java, "Cache enabled = $enabled")
if (!field) {
clearCache()
}
}

private val cache = Collections.synchronizedMap(HashMap<String, PreparedStatement?>())

val statementStats: MutableMap<String, CachedStatementStats> = Collections.synchronizedMap(HashMap())

private fun getFromCache(query: String): PreparedStatement? {
if (cachedEnabled) {
val statement = cache[query]
if (statement != null) {
val stats = statementStats[query]
statementStats[query] = CachedStatementStats(stats!!.created, Instant.now(), stats.count + 1)
return statement
}
}
return null
}

private fun createStatement(query: String): PreparedStatement? {
var statement: PreparedStatement? = null
try {
statement = connectionManager.connection.prepareStatement(query)
if (cachedEnabled) {
cache[query] = statement
statementStats[query] = CachedStatementStats(Instant.now(), Instant.now(), 1)
}
} catch (e: SQLException) {
HOLogger.instance().error(
StatementCache::class.java, """Error creating statement: $query
Error: ${e.message}"""
)
}
return statement
}

fun getPreparedStatement(query: String): PreparedStatement? {
var statement = getFromCache(query)
if (statement == null) {
statement = createStatement(query)
}
return statement
}

fun clearCache() {
for ((key, value) in cache) {
try {
value!!.close()
} catch (e: SQLException) {
HOLogger.instance().error(
StatementCache::class.java,
"""Error closing prepared statement: $key
${e.message}"""
)
}
}
cache.clear()
dumpStats()
statementStats.clear()
}

fun dumpStats() {
for ((key, value) in statementStats) {
HOLogger.instance().info(StatementCache::class.java, "$key: $value")
}
}

@JvmRecord
data class CachedStatementStats(val created: Instant, val lastAccessed: Instant, val count: Int)
}
22 changes: 22 additions & 0 deletions src/main/java/core/jmx/StatementCacheMonitor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package core.jmx

import core.db.DBManager

class StatementCacheMonitor: StatementCacheMonitorMBean {
override fun getStatistics(): Map<String, String> {
val connectionManager = DBManager.instance().connectionManager
return connectionManager.statementCache.statementStats.map {
entry -> entry.key to entry.value.toString()
}.toMap()
}

override fun getCachedStatementCount(): Int {
val connectionManager = DBManager.instance().connectionManager
return connectionManager.statementCache.statementStats.size
}

override fun setCacheEnabled(enabled: Boolean) {
val connectionManager = DBManager.instance().connectionManager
connectionManager.statementCache.cachedEnabled = enabled
}
}
10 changes: 10 additions & 0 deletions src/main/java/core/jmx/StatementCacheMonitorMBean.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package core.jmx

import core.db.StatementCache

interface StatementCacheMonitorMBean {
fun getStatistics():Map<String, String>
fun getCachedStatementCount(): Int

fun setCacheEnabled(enabled: Boolean)
}
28 changes: 1 addition & 27 deletions src/main/java/module/lineup/Lineup.java
Original file line number Diff line number Diff line change
Expand Up @@ -1170,7 +1170,7 @@ public final void checkAufgestellteSpieler() {
}

/**
* Assitant to create automatically the lineup
* Assistant to create automatically the lineup
*/
public final void optimizeLineup(List<Player> players, byte sectorsStrengthPriority, boolean withForm,
boolean idealPosFirst, boolean considerInjured, boolean considereSuspended) {
Expand All @@ -1180,32 +1180,6 @@ public final void optimizeLineup(List<Player> players, byte sectorsStrengthPrior
setAutoKapitaen(null);
}

// /**
// * Clone this lineup, creates and returns a new Lineup object.
// */
// public final @NotNull Lineup duplicate() {
//
// Lineup clone = new Lineup();
// clone.setPenaltyTakers(getPenaltyTakers());
// clone.setLocation(getLocation());
// clone.setPullBackMinute(getPullBackMinute());
// clone.setWeather(getWeather());
// clone.setWeatherForecast(getWeatherForecast());
// clone.setArenaId(getArenaId());
// clone.setRegionId(getRegionId());
//
// clone.m_vFieldPositions = copyPositions(m_vFieldPositions);
// clone.m_vBenchPositions = copyPositions(m_vBenchPositions);
// clone.setKicker(this.getKicker());
// clone.setCaptain(this.getCaptain());
// clone.setTacticType(this.getTacticType());
// clone.setAttitude(this.getAttitude());
// clone.setStyleOfPlay(this.getCoachModifier());
//
// clone.substitutions = copySubstitutions();
// return clone;
// }

public final String getCurrentTeamFormationString() {
final int iNbDefs = getNbDefenders();
final int iNbMids = getNbMidfields();
Expand Down
11 changes: 3 additions & 8 deletions src/main/java/module/lineup/assistant/LineupAssistant.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public final boolean isPlayerInStartingEleven(int spielerId, Vector<MatchLineupP
}

/**
* Assitant to create automatic lineup
* Assistant to create automatic lineup
*
* @param lPositions: list of positions to be filled
* @param lPlayers: list of available players
Expand Down Expand Up @@ -361,14 +361,9 @@ protected final Player getBestPlayerForPosition(byte position, boolean considerF

var ratingPredictionModel = HOVerwaltung.instance().getModel().getRatingPredictionModel();

if ( players != null) {
for ( var player : players){

if (players != null) {
for (var player : players) {
var r = ratingPredictionModel.getPlayerMatchAverageRating(player, RatingPredictionModel.getPlayerRatingPosition(position), getBehaviour(position));

// stk inklusive Wetter effekt errechnen
// currentRating = player.calcPosValue(position, considerForm, weather, true);

if ((!isPlayerInLineup(player.getPlayerId(), positions))
&& ((bestPlayer == null) || (maxRating < r))
&& ((ignoreRedCarded) || (!player.isRedCarded()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,7 @@ private void displayGUI() {

// Add overlays to player panels

for (Map.Entry<PlayerPositionPanel, LineupAssistantSelectorOverlay> entry : positions
.entrySet()) {
for (Map.Entry<PlayerPositionPanel, LineupAssistantSelectorOverlay> entry : positions.entrySet()) {
if (entry.getValue() == null) {
boolean selected = true;
LineupAssistantSelectorOverlay laso = new LineupAssistantSelectorOverlay();
Expand Down
Loading

0 comments on commit 1fb794f

Please sign in to comment.