Foundation’s LagCatcher helps you identify performance bottlenecks and optimize your plugin.
Basic usage
Measuring execution time
Start timing
Call LagCatcher.start() with a unique section name.import org.mineacademy.fo.debug.LagCatcher;
LagCatcher.start("database-query");
Execute your code
Run the code you want to measure.List<PlayerData> data = database.loadAllPlayers();
End timing
Call LagCatcher.end() with the same section name.LagCatcher.end("database-query");
If execution time exceeds the threshold, you’ll see a console message:
[MyPlugin 1.0.0] database-query took 156.42 ms
Setting the threshold
Configure the lag threshold in your settings.yml:
# Lag threshold in milliseconds (-1 to disable)
Lag_Threshold_Milliseconds: 100
Operations slower than this threshold will be logged.
Set to -1 to completely disable lag catching for production servers.
Advanced usage
Rapid mode
Always log execution time, regardless of threshold:
LagCatcher.start("quick-operation");
performOperation();
LagCatcher.end("quick-operation", true); // true = rapid mode
Output:
[MyPlugin 1.0.0] quick-operation took 5.32 ms
Custom threshold and message
LagCatcher.start("critical-section");
performCriticalOperation();
// Only log if over 50ms, with custom message
LagCatcher.end("critical-section", 50,
"CRITICAL: {section} exceeded threshold at {time} ms!");
Use placeholders:
{section} - The section name
{time} - Time taken in milliseconds
Running benchmarks
Test code performance across multiple iterations:
LagCatcher.performanceTest(1000, "item-creation", () -> {
ItemStack item = new ItemStack(Material.DIAMOND_SWORD);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName("Test Sword");
item.setItemMeta(meta);
});
Output:
Test 'item-creation' took 245.67 ms. Average 0.24567 ms
Measuring subsections
Track individual parts within a performance test:
LagCatcher.performanceTest(100, "complex-operation", () -> {
// Measure database read
LagCatcher.performancePartStart("db-read");
PlayerData data = loadFromDatabase();
LagCatcher.performancePartSnap("db-read");
// Measure processing
LagCatcher.performancePartStart("processing");
processData(data);
LagCatcher.performancePartSnap("processing");
// Measure database write
LagCatcher.performancePartStart("db-write");
saveToDatabase(data);
LagCatcher.performancePartSnap("db-write");
});
Output:
Test 'complex-operation' took 1842.34 ms. Average 18.4234 ms
Section 'db-read' took 892.12 ms
Section 'processing' took 234.56 ms
Section 'db-write' took 715.66 ms
Section measurement ended.
Performance parts automatically accumulate across all test iterations, giving you accurate averages.
Real-world examples
Database operations
public class DatabaseManager {
public void saveAllPlayers() {
LagCatcher.start("save-all-players");
Collection<? extends Player> players = Bukkit.getOnlinePlayers();
int saved = 0;
for (Player player : players) {
LagCatcher.start("save-player-" + player.getName());
savePlayer(player);
LagCatcher.end("save-player-" + player.getName());
saved++;
}
LagCatcher.end("save-all-players");
System.out.println("Saved " + saved + " players");
}
private void savePlayer(Player player) {
// Database save logic
}
}
Region scanning
public void scanRegion(Location corner1, Location corner2) {
LagCatcher.start("region-scan");
int blocksScanned = 0;
int minX = Math.min(corner1.getBlockX(), corner2.getBlockX());
int maxX = Math.max(corner1.getBlockX(), corner2.getBlockX());
int minY = Math.min(corner1.getBlockY(), corner2.getBlockY());
int maxY = Math.max(corner1.getBlockY(), corner2.getBlockY());
int minZ = Math.min(corner1.getBlockZ(), corner2.getBlockZ());
int maxZ = Math.max(corner1.getBlockZ(), corner2.getBlockZ());
World world = corner1.getWorld();
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
for (int z = minZ; z <= maxZ; z++) {
Block block = world.getBlockAt(x, y, z);
processBlock(block);
blocksScanned++;
}
}
}
LagCatcher.end("region-scan");
System.out.println("Scanned " + blocksScanned + " blocks");
}
public class CustomMenu extends Menu {
@Override
protected void onDisplay(Player player) {
LagCatcher.start("menu-render-" + getTitle());
// Render items
for (int slot = 0; slot < getSize(); slot++) {
ItemStack item = createMenuItem(slot, player);
setItem(slot, item);
}
LagCatcher.end("menu-render-" + getTitle());
}
private ItemStack createMenuItem(int slot, Player player) {
// Complex item creation
return new ItemStack(Material.STONE);
}
}
Event processing
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
LagCatcher.start("block-break-handler");
Block block = event.getBlock();
Player player = event.getPlayer();
// Check permissions
if (!canBreak(player, block)) {
event.setCancelled(true);
LagCatcher.end("block-break-handler");
return;
}
// Custom drop logic
LagCatcher.start("calculate-drops");
List<ItemStack> drops = calculateCustomDrops(block, player);
LagCatcher.end("calculate-drops");
// Give items
for (ItemStack drop : drops) {
player.getInventory().addItem(drop);
}
LagCatcher.end("block-break-handler");
}
Optimization strategies
Identify bottlenecks
public void complexOperation() {
LagCatcher.performanceTest(100, "full-operation", () -> {
LagCatcher.performancePartStart("part-1");
operationPart1();
LagCatcher.performancePartSnap("part-1");
LagCatcher.performancePartStart("part-2");
operationPart2();
LagCatcher.performancePartSnap("part-2");
LagCatcher.performancePartStart("part-3");
operationPart3();
LagCatcher.performancePartSnap("part-3");
});
}
This reveals which part is slowest, so you know where to optimize.
Before/after comparison
public void testOptimization() {
// Test old implementation
System.out.println("Testing old implementation:");
LagCatcher.performanceTest(1000, "old-method", this::oldImplementation);
// Test new implementation
System.out.println("Testing new implementation:");
LagCatcher.performanceTest(1000, "new-method", this::newImplementation);
}
Compare results to measure improvement.
Caching effectiveness
private final Map<UUID, PlayerData> cache = new HashMap<>();
public PlayerData getData(UUID playerId) {
// Check cache
if (cache.containsKey(playerId)) {
LagCatcher.start("get-cached-data");
PlayerData data = cache.get(playerId);
LagCatcher.end("get-cached-data", true); // Rapid to always log
return data;
}
// Load from database
LagCatcher.start("load-data-from-db");
PlayerData data = loadFromDatabase(playerId);
LagCatcher.end("load-data-from-db", true);
cache.put(playerId, data);
return data;
}
Compare cache hits vs database loads to measure cache effectiveness.
Profile before optimizing
Measure actual performance to identify real bottlenecks, not assumed ones.// Add profiling first
LagCatcher.start("suspected-slow-code");
suspectedSlowMethod();
LagCatcher.end("suspected-slow-code", true);
Test with realistic data
Use production-like data volumes for accurate results.// Test with 1000 players, not 10
LagCatcher.performanceTest(1000, "operation", () -> {
processPlayers(generateTestPlayers(1000));
});
Disable in production
Set threshold to -1 in production to avoid overhead.Lag_Threshold_Milliseconds: -1
Clean up timing calls
Remove or comment out LagCatcher calls after optimization.// LagCatcher.start("optimized-operation");
optimizedOperation();
// LagCatcher.end("optimized-operation");
Synchronous database calls
// Bad - blocks main thread
public void savePlayer(Player player) {
LagCatcher.start("save-sync");
database.save(player);
LagCatcher.end("save-sync"); // Often > 100ms!
}
// Good - async operation
public void savePlayer(Player player) {
Common.runAsync(() -> {
LagCatcher.start("save-async");
database.save(player);
LagCatcher.end("save-async");
});
}
Inefficient loops
// Bad - O(n²) complexity
LagCatcher.performanceTest(100, "slow-loop", () -> {
for (Player p1 : Bukkit.getOnlinePlayers()) {
for (Player p2 : Bukkit.getOnlinePlayers()) {
checkPlayers(p1, p2);
}
}
});
// Good - O(n) complexity
LagCatcher.performanceTest(100, "fast-loop", () -> {
List<Player> players = new ArrayList<>(Bukkit.getOnlinePlayers());
for (int i = 0; i < players.size(); i++) {
for (int j = i + 1; j < players.size(); j++) {
checkPlayers(players.get(i), players.get(j));
}
}
});
Repeated calculations
// Bad - recalculates every time
public void updatePlayer(Player player) {
LagCatcher.start("update-player-slow");
for (int i = 0; i < 100; i++) {
if (hasPermission(player)) { // Checks permission 100 times!
doSomething(i);
}
}
LagCatcher.end("update-player-slow");
}
// Good - calculate once
public void updatePlayer(Player player) {
LagCatcher.start("update-player-fast");
boolean hasPerm = hasPermission(player);
for (int i = 0; i < 100; i++) {
if (hasPerm) {
doSomething(i);
}
}
LagCatcher.end("update-player-fast");
}
Always move expensive operations (permissions checks, database queries, complex calculations) outside loops when possible.