From 5cf5964bfc73ba603df6baa03108de3bbf5d6460 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 16:25:39 +0000 Subject: [PATCH] Optimize ItemShop config saving to be asynchronous Co-authored-by: acsoto <59144459+acsoto@users.noreply.github.com> --- modules/Gem/pom.xml | 6 +- modules/GemShop/pom.xml | 26 +++- .../gemshop/shops/itemshop/ItemShop.java | 48 +++++++- .../shops/itemshop/ItemShopBenchmarkTest.java | 113 ++++++++++++++++++ 4 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 modules/GemShop/src/test/java/com/mcatk/gemshop/shops/itemshop/ItemShopBenchmarkTest.java diff --git a/modules/Gem/pom.xml b/modules/Gem/pom.xml index 6476837..f74c4bc 100644 --- a/modules/Gem/pom.xml +++ b/modules/Gem/pom.xml @@ -66,6 +66,10 @@ placeholderapi https://repo.extendedclip.com/content/repositories/placeholderapi/ + + helpch + https://repo.helpch.at/releases/ + @@ -78,7 +82,7 @@ me.clip placeholderapi - 2.11.1 + 2.11.5 provided diff --git a/modules/GemShop/pom.xml b/modules/GemShop/pom.xml index d416f07..5e5592e 100644 --- a/modules/GemShop/pom.xml +++ b/modules/GemShop/pom.xml @@ -72,10 +72,34 @@ com.mcatk - gem + Gem jar 1.0.0 + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-inline + 5.2.0 + test + + + net.bytebuddy + byte-buddy + 1.14.17 + test + + + net.bytebuddy + byte-buddy-agent + 1.14.17 + test + diff --git a/modules/GemShop/src/main/java/com/mcatk/gemshop/shops/itemshop/ItemShop.java b/modules/GemShop/src/main/java/com/mcatk/gemshop/shops/itemshop/ItemShop.java index 72ff5f0..803a346 100644 --- a/modules/GemShop/src/main/java/com/mcatk/gemshop/shops/itemshop/ItemShop.java +++ b/modules/GemShop/src/main/java/com/mcatk/gemshop/shops/itemshop/ItemShop.java @@ -5,12 +5,28 @@ import com.mcatk.gemshop.Message; import org.apache.commons.lang.text.StrBuilder; import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.HashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; public class ItemShop { + private final ExecutorService ioExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "GemShop-IO-Thread"); + t.setDaemon(true); + return t; + } + }); + private HashMap itemsMap; public ItemShop() { @@ -58,14 +74,40 @@ public void addItem(Player player, String shopId, String itemId, String price) { } itemsMap.get(shopId).getMap().put(itemId, item); GemShop.getPlugin().getConfig().set("Items." + shopId + "." + itemId, item); - GemShop.getPlugin().saveConfig(); + saveConfigAsync(); player.sendMessage(Message.INFO + "添加成功:" + itemId + " " + price + "宝石"); } public void delItem(String shopId, String itemId) { itemsMap.get(shopId).getMap().remove(itemId); GemShop.getPlugin().getConfig().set("Items." + shopId + "." + itemId, null); - GemShop.getPlugin().saveConfig(); + saveConfigAsync(); + } + + private void saveConfigAsync() { + if (!(GemShop.getPlugin().getConfig() instanceof YamlConfiguration)) { + GemShop.getPlugin().saveConfig(); + return; + } + + YamlConfiguration yaml = (YamlConfiguration) GemShop.getPlugin().getConfig(); + final String data = yaml.saveToString(); + final File file = new File(GemShop.getPlugin().getDataFolder(), "config.yml"); + + ioExecutor.submit(new Runnable() { + @Override + public void run() { + try { + if (file.getParentFile() != null && !file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + Files.write(file.toPath(), data.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + GemShop.getPlugin().getLogger().severe("Could not save config.yml asynchronously!"); + e.printStackTrace(); + } + } + }); } public Items getItems(String shopId) { diff --git a/modules/GemShop/src/test/java/com/mcatk/gemshop/shops/itemshop/ItemShopBenchmarkTest.java b/modules/GemShop/src/test/java/com/mcatk/gemshop/shops/itemshop/ItemShopBenchmarkTest.java new file mode 100644 index 0000000..2f04144 --- /dev/null +++ b/modules/GemShop/src/test/java/com/mcatk/gemshop/shops/itemshop/ItemShopBenchmarkTest.java @@ -0,0 +1,113 @@ +package com.mcatk.gemshop.shops.itemshop; + +import com.mcatk.gemshop.GemShop; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.logging.Logger; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +public class ItemShopBenchmarkTest { + + private GemShop mockPlugin; + private YamlConfiguration mockConfig; + private Player mockPlayer; + private PlayerInventory mockInventory; + + @Before + public void setUp() throws Exception { + mockPlugin = Mockito.mock(GemShop.class); + mockConfig = Mockito.mock(YamlConfiguration.class); + mockPlayer = Mockito.mock(Player.class); + mockInventory = Mockito.mock(PlayerInventory.class); + Logger mockLogger = Mockito.mock(Logger.class); + + // Inject mock plugin + Field pluginField = GemShop.class.getDeclaredField("plugin"); + pluginField.setAccessible(true); + pluginField.set(null, mockPlugin); + + when(mockPlugin.getConfig()).thenReturn(mockConfig); + when(mockPlugin.getLogger()).thenReturn(mockLogger); + // Fix NPE for new File(dataFolder, ...) + File tempFolder = new File(System.getProperty("java.io.tmpdir")); + when(mockPlugin.getDataFolder()).thenReturn(tempFolder); + + when(mockPlayer.getInventory()).thenReturn(mockInventory); + when(mockInventory.getItemInMainHand()).thenReturn(Mockito.mock(ItemStack.class)); + + when(mockConfig.getConfigurationSection(anyString())).thenReturn(null); + when(mockConfig.saveToString()).thenReturn("key: value"); + } + + @After + public void tearDown() throws Exception { + // Reset the static field + Field pluginField = GemShop.class.getDeclaredField("plugin"); + pluginField.setAccessible(true); + pluginField.set(null, null); + } + + @Test + public void testAddItemPerformance() { + // Setup slow saveConfig (which should NOT be called now) + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Thread.sleep(50); // Simulate 50ms Disk I/O + return null; + } + }).when(mockPlugin).saveConfig(); + + ItemShop itemShop = new ItemShop(); + + long startTime = System.currentTimeMillis(); + // Price "100" as string + itemShop.addItem(mockPlayer, "shop1", "item1", "100"); + long endTime = System.currentTimeMillis(); + + long duration = endTime - startTime; + System.out.println("Benchmark Execution time: " + duration + "ms"); + + // Assert that saveConfig was NOT called (because we use async save) + verify(mockPlugin, never()).saveConfig(); + + // Assert that saveToString WAS called + verify(mockConfig).saveToString(); + + // In optimized code, duration should be near 0ms + if (duration >= 50) { + throw new RuntimeException("Execution time too slow: " + duration + "ms. Optimization failed?"); + } + } + + @Test + public void testDelItemOptimization() { + // Setup initial state + ItemShop itemShop = new ItemShop(); + itemShop.addItem(mockPlayer, "shop1", "item1", "100"); + + // Reset mocks to clear interactions from addItem + Mockito.clearInvocations(mockConfig, mockPlugin); + + itemShop.delItem("shop1", "item1"); + + // Assert that saveConfig was NOT called + verify(mockPlugin, never()).saveConfig(); + + // Assert that saveToString WAS called + verify(mockConfig).saveToString(); + } +}