From 5059872bb66166e7a65abe522c3cc80ba9a1dc48 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 26 Aug 2026 20:56:49 -0700 Subject: [PATCH 1/3] Load island data off the main thread to fix periodic lag spikes The refresh task ran hook.getAllIslandData() on the main thread every refresh cycle. In both game modes that is a full synchronous database read (Database#loadObjects()), which deserializes every island record ever created and was profiled at up to ~1s per cycle on large servers. refresh(hook) now loads the island data on an async task and hops back to the main thread only for the cheap part: island registry lookups, player/permission checks, and swapping in the new top ten. Placeholder snapshots update per hook as each async load lands. Bump version to 2.1.1. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Um9roAZ8pVAqp6TZEMBjMr --- CLAUDE.md | 8 +++---- pom.xml | 2 +- .../bentobox/topblock/TopBlockManager.java | 22 +++++++++++++------ .../bentobox/topblock/CommonTestSetup.java | 9 ++++++++ 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2009ab4..67d9b8d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ Maven project, Java 21, Paper 1.21.11 API, BentoBox 3.14.0, AOneBlock 1.18.0, Ch - Run a single test method: `mvn test -Dtest=TopBlockManagerTest#testFormatLevelShorthandKilo` - The Surefire config sets a long list of `--add-opens` JVM flags — required for Mockito + MockBukkit reflection on Java 21; do not remove them when tweaking the build. -Version handling is driven by Maven properties: `build.version` is the human version (currently 2.1.0), `revision` resolves to `${build.version}-SNAPSHOT` locally and to `${build.version}` under the `master` profile (activated by `GIT_BRANCH=origin/master` on Jenkins). `build.number` is `-LOCAL` locally, `-b` on CI, empty on master. Don't hand-edit `` — bump `build.version`. +Version handling is driven by Maven properties: `build.version` is the human version (currently 2.1.1), `revision` resolves to `${build.version}-SNAPSHOT` locally and to `${build.version}` under the `master` profile (activated by `GIT_BRANCH=origin/master` on Jenkins). `build.number` is `-LOCAL` locally, `-b` on CI, empty on master. Don't hand-edit `` — bump `build.version`. ## Runtime entry points (Pladdon pattern) @@ -40,9 +40,9 @@ AOneBlock and ChunkBlock have twin APIs (`getBlockListener().getAllIslands()`, ` `TopBlockManager` is a `Listener` that reacts to `BentoBoxReadyEvent` (handler is `public void onBentoBoxReady` — Bukkit silently skips private @EventHandler methods, which is what broke the addon historically) to start a repeating Bukkit task. The task period is `settings.getRefreshTime() * 20L * 60` ticks (minutes → ticks). Each tick of the task: -1. Calls `refreshAll()` — for every hook, reads every island of that game mode via `hook.getAllIslandData()`, so the refresh interval is intentionally coarse (default 5 min, min 1 min). -2. Builds a fresh `List` (record of island + blockNumber + lifetime + phaseName) per hook, kept in a `Map>` — sorted at read time via `Comparator` on `lifetime` then `blockNumber`. -3. Updates `PlaceholderManager`'s cached per-hook snapshots. +1. Calls `refreshAll()` — for every hook, reads every island of that game mode via `hook.getAllIslandData()`, so the refresh interval is intentionally coarse (default 5 min, min 1 min). `getAllIslandData()` is a full synchronous database read (`loadObjects()` in the game mode), so `refresh(hook)` runs it on an async task and only hops back to the main thread (`processIslandData`) for the island registry / player / permission lookups — keep the async/sync split when changing this code. +2. `processIslandData` builds a fresh `List` (record of island + blockNumber + lifetime + phaseName) per hook, kept in a `Map>` — sorted at read time via `Comparator` on `lifetime` then `blockNumber`. +3. It then updates `PlaceholderManager`'s cached per-hook snapshots (per hook, as each async load completes). Placeholders are registered once per hook via a `runTaskLater` 10-tick delay after the first ready event (so PAPI / BentoBox's `PlaceholdersManager` is up). Names follow `island__top_<1..10>` and are scoped to each hook's `GameModeAddon`, so the PAPI prefix keeps game modes apart (`%aoneblock_...%` vs `%chunkblock_...%`). The `TopBlock.TEN` constant is the source of truth for the list size. diff --git a/pom.xml b/pom.xml index 38018e3..3467c68 100644 --- a/pom.xml +++ b/pom.xml @@ -71,7 +71,7 @@ -LOCAL - 2.1.0 + 2.1.1 BentoBoxWorld_TopBlock bentobox-world https://sonarcloud.io diff --git a/src/main/java/world/bentobox/topblock/TopBlockManager.java b/src/main/java/world/bentobox/topblock/TopBlockManager.java index ce465a7..d46b9b8 100644 --- a/src/main/java/world/bentobox/topblock/TopBlockManager.java +++ b/src/main/java/world/bentobox/topblock/TopBlockManager.java @@ -23,6 +23,7 @@ import world.bentobox.bentobox.api.events.BentoBoxReadyEvent; import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.topblock.hooks.IslandBlockData; import world.bentobox.topblock.hooks.TopBlockHook; @@ -73,12 +74,8 @@ public TopBlockManager(TopBlock addon) { @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onBentoBoxReady(BentoBoxReadyEvent e) { // Load the top ten from each hooked game mode every so often - Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> { - // Update TopTens - refreshAll(); - // Update placeholders - phm.updateTopTen(); - }, 0, addon.getSettings().getRefreshTime() * 20L * 60); + Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), this::refreshAll, + 0, addon.getSettings().getRefreshTime() * 20L * 60); // Register placeholders after everything is loaded Bukkit.getScheduler().runTaskLater(addon.getPlugin(), () -> addon.getHooks().forEach(phm::registerPlaceholders), 10L); @@ -89,14 +86,25 @@ void refreshAll() { } void refresh(TopBlockHook hook) { + // getAllIslandData() reads the game mode's whole island database, so keep it off the main thread + Bukkit.getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> { + List islandData = hook.getAllIslandData(); + // Island registry, players and permissions are main-thread only + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> processIslandData(hook, islandData)); + }); + } + + void processIslandData(TopBlockHook hook, List islandData) { List data = new ArrayList<>(); - hook.getAllIslandData().stream().filter(i -> i.lifetime() > 0).forEach(i -> + islandData.stream().filter(i -> i.lifetime() > 0).forEach(i -> addon.getIslands().getIslandById(i.uniqueId()) .filter(island -> hook.getGameMode().inWorld(island.getWorld())) .filter(this::ownerInTopTen) .ifPresent(island -> data.add(new TopTenData(island, i.blockNumber(), i.lifetime(), i.phaseName())))); topTens.put(hook, data); + // Update placeholders + phm.updateTopTen(); } /** diff --git a/src/test/java/world/bentobox/topblock/CommonTestSetup.java b/src/test/java/world/bentobox/topblock/CommonTestSetup.java index f68c679..cd2a348 100644 --- a/src/test/java/world/bentobox/topblock/CommonTestSetup.java +++ b/src/test/java/world/bentobox/topblock/CommonTestSetup.java @@ -123,6 +123,15 @@ public void setUp() throws Exception { mockedBukkit.when(Bukkit::getItemFactory).thenReturn(itemFactory); mockedBukkit.when(Bukkit::getServer).thenReturn(server); mockedBukkit.when(Bukkit::getScheduler).thenReturn(sch); + // TopBlockManager.refresh() hops async -> sync via the scheduler; run both legs inline in tests + when(sch.runTaskAsynchronously(any(), any(Runnable.class))).thenAnswer(i -> { + i.getArgument(1, Runnable.class).run(); + return null; + }); + when(sch.runTask(any(), any(Runnable.class))).thenAnswer(i -> { + i.getArgument(1, Runnable.class).run(); + return null; + }); // By default treat island owners as offline so the intopten filter // does not exclude them. Tests that need an online owner can override. mockedBukkit.when(() -> Bukkit.getPlayer(any(UUID.class))).thenReturn(null); From 2408b6519079d82b4fabd15b962e210f7a4196e2 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 26 Aug 2026 20:59:45 -0700 Subject: [PATCH 2/3] Update pom.xml version to 2.1.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 38018e3..3467c68 100644 --- a/pom.xml +++ b/pom.xml @@ -71,7 +71,7 @@ -LOCAL - 2.1.0 + 2.1.1 BentoBoxWorld_TopBlock bentobox-world https://sonarcloud.io From ad28f03b55a444405da2a24aab9ecf7ad85d60a6 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 26 Aug 2026 21:00:04 -0700 Subject: [PATCH 3/3] Ignore IntelliJ IDEA project files Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Um9roAZ8pVAqp6TZEMBjMr --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 8504943..d3d7e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ hs_err_pid* /dependency-reduced-pom.xml /.classpath /.DS_Store + +# IntelliJ IDEA +/.idea/ +*.iml