22
33import com .conaxgames .libraries .LibraryPlugin ;
44import com .conaxgames .libraries .util .CC ;
5+ import com .conaxgames .libraries .util .TaskUtil ;
56import net .md_5 .bungee .api .ChatColor ;
67import org .bukkit .entity .Player ;
78import org .bukkit .scoreboard .DisplaySlot ;
1011import org .bukkit .scoreboard .Scoreboard ;
1112
1213import java .util .*;
14+ import java .util .concurrent .ConcurrentHashMap ;
15+ import java .util .concurrent .ConcurrentLinkedQueue ;
16+ import java .util .concurrent .CompletableFuture ;
17+ import java .util .concurrent .ExecutorService ;
18+ import java .util .concurrent .Executors ;
19+ import java .util .concurrent .TimeUnit ;
1320
1421/**
1522 * Manages scoreboard boards for all players.
@@ -24,10 +31,26 @@ public class BoardManager implements Runnable {
2431 private static final String C_ELEMENT_METADATA_KEY = "cElement" ;
2532 private static final String DEFAULT_OBJECTIVE_NAME = "Default" ;
2633 private static final String DEFAULT_OBJECTIVE_CRITERIA = "dummy" ;
34+ private static final int ASYNC_THREAD_POOL_SIZE = 2 ; // Small pool for board data preparation
35+ private static final long CACHE_EXPIRY_MS = 100L ; // Cache board data for 100ms to reduce redundant calls
2736
28- // Instance fields
29- private final Map <UUID , Board > playerBoards = new HashMap <>();
37+ // Thread-safe data structures for async/sync communication
38+ private final Map <UUID , Board > playerBoards = new ConcurrentHashMap <>();
3039 private final BoardAdapter adapter ;
40+ private final ExecutorService asyncExecutor ;
41+
42+ // Cache structures for performance optimization
43+ private final Map <UUID , BoardUpdateData > pendingUpdates = new ConcurrentHashMap <>();
44+ private final Map <UUID , Long > lastUpdateTimes = new ConcurrentHashMap <>();
45+
46+ // Queue for batched sync operations
47+ private final ConcurrentLinkedQueue <Runnable > syncOperations = new ConcurrentLinkedQueue <>();
48+
49+ // Performance monitoring (optional)
50+ private long totalAsyncTime = 0 ;
51+ private long totalSyncTime = 0 ;
52+ private int updateCount = 0 ;
53+ private volatile boolean shutdown = false ;
3154
3255 /**
3356 * Creates a new board manager with the specified adapter.
@@ -36,16 +59,65 @@ public class BoardManager implements Runnable {
3659 */
3760 public BoardManager (BoardAdapter adapter ) {
3861 this .adapter = adapter ;
62+ this .asyncExecutor = Executors .newFixedThreadPool (ASYNC_THREAD_POOL_SIZE , r -> {
63+ Thread thread = new Thread (r , "BoardManager-Async-" + System .currentTimeMillis ());
64+ thread .setDaemon (true );
65+ return thread ;
66+ });
67+ }
68+
69+ /**
70+ * Shuts down the async executor and cleans up resources.
71+ * Should be called when the plugin is disabled.
72+ */
73+ public void shutdown () {
74+ shutdown = true ;
75+ asyncExecutor .shutdown ();
76+ try {
77+ if (!asyncExecutor .awaitTermination (5 , TimeUnit .SECONDS )) {
78+ asyncExecutor .shutdownNow ();
79+ }
80+ } catch (InterruptedException e ) {
81+ asyncExecutor .shutdownNow ();
82+ Thread .currentThread ().interrupt ();
83+ }
84+ }
85+
86+ /**
87+ * Internal class to hold board update data prepared asynchronously.
88+ */
89+ private static class BoardUpdateData {
90+ final List <String > scores ;
91+ final String title ;
92+ final long timestamp ;
93+ final UUID playerUUID ;
94+
95+ BoardUpdateData (UUID playerUUID , List <String > scores , String title ) {
96+ this .playerUUID = playerUUID ;
97+ this .scores = scores ;
98+ this .title = title ;
99+ this .timestamp = System .currentTimeMillis ();
100+ }
101+
102+ boolean isExpired () {
103+ return System .currentTimeMillis () - timestamp > CACHE_EXPIRY_MS ;
104+ }
39105 }
40106
41107 @ Override
42108 public void run () {
109+ if (shutdown ) return ;
110+
111+ long startTime = System .nanoTime ();
112+
43113 this .adapter .preLoop ();
44114
45115 // Clean up any boards for players with cElement metadata (zero CPU cost)
46116 cleanupAllCElementBoards ();
47117
48- // Only process players who actually have boards - major performance boost
118+ // Step 1: Prepare board data asynchronously for all players
119+ List <CompletableFuture <Void >> asyncTasks = new ArrayList <>();
120+
49121 for (Map .Entry <UUID , Board > entry : this .playerBoards .entrySet ()) {
50122 UUID playerUUID = entry .getKey ();
51123 Board board = entry .getValue ();
@@ -60,27 +132,121 @@ public void run() {
60132 continue ;
61133 }
62134
135+ // Check if we have recent cached data to avoid redundant async calls
136+ BoardUpdateData cached = pendingUpdates .get (playerUUID );
137+ Long lastUpdate = lastUpdateTimes .get (playerUUID );
138+ long now = System .currentTimeMillis ();
139+
140+ if (cached != null && !cached .isExpired ()) {
141+ // Use cached data, skip async preparation
142+ continue ;
143+ }
144+
145+ // Prepare data asynchronously
146+ CompletableFuture <Void > asyncTask = CompletableFuture .runAsync (() -> {
147+ try {
148+ long asyncStart = System .nanoTime ();
149+
150+ // These expensive calls now run off the main thread
151+ List <String > scores = this .adapter .getScoreboard (player , board );
152+ String title = this .adapter .getTitle (player );
153+
154+ // Store prepared data for sync processing
155+ pendingUpdates .put (playerUUID , new BoardUpdateData (playerUUID , scores , title ));
156+
157+ totalAsyncTime += (System .nanoTime () - asyncStart );
158+ } catch (Exception e ) {
159+ LibraryPlugin .getInstance ().getPlugin ().getLogger ()
160+ .warning ("Error preparing board data for " + player .getName () + ": " + e .getMessage ());
161+ }
162+ }, asyncExecutor );
163+
164+ asyncTasks .add (asyncTask );
165+ }
166+
167+ // Step 2: Wait for all async tasks to complete (with timeout)
168+ CompletableFuture <Void > allTasks = CompletableFuture .allOf (
169+ asyncTasks .toArray (new CompletableFuture [0 ])
170+ );
171+
172+ try {
173+ // Wait for async tasks with timeout to prevent blocking
174+ allTasks .get (50 , TimeUnit .MILLISECONDS ); // Max 50ms wait
175+ } catch (Exception e ) {
176+ // Log timeout but continue - we'll use cached data or skip updates
177+ if (updateCount % 100 == 0 ) { // Only log occasionally to avoid spam
178+ LibraryPlugin .getInstance ().getPlugin ().getLogger ()
179+ .fine ("Board async preparation timeout - continuing with available data" );
180+ }
181+ }
182+
183+ // Step 3: Apply all scoreboard updates synchronously in batch
184+ long syncStart = System .nanoTime ();
185+ processPendingUpdates ();
186+ totalSyncTime += (System .nanoTime () - syncStart );
187+
188+ updateCount ++;
189+
190+ // Optional: Log performance metrics periodically
191+ if (updateCount % 200 == 0 ) {
192+ logPerformanceMetrics ();
193+ }
194+ }
195+
196+ /**
197+ * Processes all pending board updates synchronously.
198+ * This method applies the data prepared asynchronously to the actual scoreboards.
199+ */
200+ private void processPendingUpdates () {
201+ for (Map .Entry <UUID , BoardUpdateData > entry : pendingUpdates .entrySet ()) {
202+ UUID playerUUID = entry .getKey ();
203+ BoardUpdateData updateData = entry .getValue ();
204+
205+ // Skip expired data
206+ if (updateData .isExpired ()) {
207+ continue ;
208+ }
209+
210+ Board board = playerBoards .get (playerUUID );
211+ if (board == null ) continue ;
212+
213+ Player player = LibraryPlugin .getInstance ().getPlugin ().getServer ().getPlayer (playerUUID );
214+ if (player == null || !player .isOnline ()) {
215+ continue ;
216+ }
217+
218+ // Skip players with cElement metadata - they shouldn't have boards
219+ if (player .hasMetadata (C_ELEMENT_METADATA_KEY )) {
220+ continue ;
221+ }
222+
63223 try {
64- updatePlayerBoard (player , board );
224+ updatePlayerBoardSync (player , board , updateData );
225+ lastUpdateTimes .put (playerUUID , System .currentTimeMillis ());
65226 } catch (Exception e ) {
66- e .printStackTrace ();
67227 LibraryPlugin .getInstance ().getPlugin ().getLogger ()
68- .severe ("Something went wrong while updating " + player .getName () + "'s scoreboard " + board + " - " + board .getAdapter () + ")" );
228+ .severe ("Error updating scoreboard for " + player .getName () + ": " + e .getMessage ());
229+ e .printStackTrace ();
69230 }
70231 }
232+
233+ // Clean up expired data
234+ pendingUpdates .entrySet ().removeIf (entry -> entry .getValue ().isExpired ());
71235 }
72-
236+
73237 /**
74- * Updates the scoreboard for a specific player.
238+ * Updates the scoreboard for a specific player using pre-prepared data.
239+ * This method only performs the synchronous scoreboard API operations.
75240 *
76241 * @param player The player to update the board for
77242 * @param board The board instance for this player
243+ * @param updateData Pre-prepared update data from async thread
78244 */
79- private void updatePlayerBoard (Player player , Board board ) {
245+ private void updatePlayerBoardSync (Player player , Board board , BoardUpdateData updateData ) {
80246 Scoreboard scoreboard = board .getScoreboard ();
81247 Objective objective = board .getObjective ();
82248
83- List <String > scores = this . adapter . getScoreboard ( player , board ) ;
249+ List <String > scores = updateData . scores ;
84250
85251 if (scores == null || scores .isEmpty ()) {
86252 // Clear all entries if no scores
@@ -94,8 +260,8 @@ private void updatePlayerBoard(Player player, Board board) {
94260 Collections .reverse (scores );
95261
96262 // Update title if changed
97- String newTitle = this . adapter . getTitle ( player ) ;
98- if (!objective .getDisplayName ().equals (newTitle )) {
263+ String newTitle = updateData . title ;
264+ if (newTitle != null && !objective .getDisplayName ().equals (newTitle )) {
99265 objective .setDisplayName (newTitle );
100266 }
101267
@@ -106,6 +272,28 @@ private void updatePlayerBoard(Player player, Board board) {
106272 player .setScoreboard (scoreboard );
107273 }
108274
275+ /**
276+ * Logs performance metrics for monitoring board update performance.
277+ */
278+ private void logPerformanceMetrics () {
279+ if (updateCount == 0 ) return ;
280+
281+ long avgAsyncTime = totalAsyncTime / updateCount ;
282+ long avgSyncTime = totalSyncTime / updateCount ;
283+
284+ LibraryPlugin .getInstance ().getPlugin ().getLogger ().info (
285+ String .format ("Board Performance: Updates=%d, AvgAsync=%dns, AvgSync=%dns, ActiveBoards=%d" ,
286+ updateCount , avgAsyncTime , avgSyncTime , playerBoards .size ())
287+ );
288+
289+ // Reset counters periodically to prevent overflow
290+ if (updateCount >= 1000 ) {
291+ totalAsyncTime = 0 ;
292+ totalSyncTime = 0 ;
293+ updateCount = 0 ;
294+ }
295+ }
296+
109297 /**
110298 * Updates the board entries efficiently by reusing existing entries when possible.
111299 *
0 commit comments