🆙 Optimization for the gameserver

- Room Cleanup Optimization (RoomManager.java)

Added roomsByOwner ConcurrentHashMap that tracks which rooms belong to which owner
clearInactiveRooms() now iterates unique owners instead of ALL rooms
Went from O(rooms × clients) to O(unique_owners × clients) every 120s

 - Volatile Fields (Room.java)

Removed volatile from 27 room config fields (score, category, chatMode, allowPets, etc.)
Kept volatile only on 8 fields that genuinely need cross-thread visibility (loaded, preLoaded, needsUpdate, muted, etc.)
Reduces CPU cache line invalidation on every room cycle tick

- Search Cache TTL (SearchUserEvent.java + CleanerThread.java)

SearchUserEvent now has 30-second TTL per entry instead of full wipe every 10s
SearchRoomsEvent already had LRU eviction (max 200) — removed redundant .clear() call
Frequently searched users stay cached, only stale entries get cleaned

- scheduledComposers/scheduledTasks — After reading the code, these are actually already handled correctly: processScheduledTasks() swaps the set with a fresh one before processing, and processScheduledComposers() calls .clear() after sending. No leak risk.
This commit is contained in:
duckietm
2026-03-26 09:59:02 +01:00
parent 5319e5e5c3
commit f2d8f109ff
4 changed files with 143 additions and 95 deletions
@@ -4,7 +4,6 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.guilds.forums.ForumThread; import com.eu.habbo.habbohotel.guilds.forums.ForumThread;
import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.friends.SearchUserEvent; import com.eu.habbo.messages.incoming.friends.SearchUserEvent;
import com.eu.habbo.messages.incoming.navigator.SearchRoomsEvent;
import com.eu.habbo.messages.outgoing.users.UserDataComposer; import com.eu.habbo.messages.outgoing.users.UserDataComposer;
import com.eu.habbo.threading.runnables.AchievementUpdater; import com.eu.habbo.threading.runnables.AchievementUpdater;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -101,8 +100,7 @@ public class CleanerThread implements Runnable {
LAST_HABBO_CACHE_CLEARED = time; LAST_HABBO_CACHE_CLEARED = time;
} }
SearchRoomsEvent.cachedResults.clear(); SearchUserEvent.cleanExpiredCache();
SearchUserEvent.cachedResults.clear();
} }
@@ -130,8 +130,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
private String password; private String password;
private RoomState state; private RoomState state;
private int usersMax; private int usersMax;
private volatile int score; private int score;
private volatile int category; private int category;
private String floorPaint; private String floorPaint;
private String wallPaint; private String wallPaint;
private String backgroundPaint; private String backgroundPaint;
@@ -140,37 +140,37 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
private int floorSize; private int floorSize;
private int guild; private int guild;
private String tags; private String tags;
private volatile boolean publicRoom; private boolean publicRoom;
private volatile boolean staffPromotedRoom; private boolean staffPromotedRoom;
private volatile boolean allowPets; private boolean allowPets;
private volatile boolean allowPetsEat; private boolean allowPetsEat;
private volatile boolean allowWalkthrough; private boolean allowWalkthrough;
private volatile boolean allowBotsWalk; private boolean allowBotsWalk;
private volatile boolean allowEffects; private boolean allowEffects;
private volatile boolean hideWall; private boolean hideWall;
private volatile int chatMode; private int chatMode;
private volatile int chatWeight; private int chatWeight;
private volatile int chatSpeed; private int chatSpeed;
private volatile int chatDistance; private int chatDistance;
private volatile int chatProtection; private int chatProtection;
private volatile int muteOption; private int muteOption;
private volatile int kickOption; private int kickOption;
private volatile int banOption; private int banOption;
private volatile int pollId; private int pollId;
private volatile boolean promoted; private boolean promoted;
private volatile int tradeMode; private int tradeMode;
private volatile boolean moveDiagonally; private boolean moveDiagonally;
private volatile boolean allowUnderpass; private boolean allowUnderpass;
private volatile boolean jukeboxActive; private boolean jukeboxActive;
private volatile boolean hideWired; private boolean hideWired;
private RoomPromotion promotion; private RoomPromotion promotion;
private volatile boolean needsUpdate; private volatile boolean needsUpdate;
private volatile boolean loaded; private volatile boolean loaded;
private volatile boolean preLoaded; private volatile boolean preLoaded;
private volatile boolean loadingInProgress; private volatile boolean loadingInProgress;
private volatile CompletableFuture<Void> loadingFuture; private volatile CompletableFuture<Void> loadingFuture;
private volatile int rollerSpeed; private int rollerSpeed;
private volatile int lastTimerReset = Emulator.getIntUnixTimestamp(); private int lastTimerReset = Emulator.getIntUnixTimestamp();
private volatile boolean muted; private volatile boolean muted;
private RoomSpecialTypes roomSpecialTypes; private RoomSpecialTypes roomSpecialTypes;
private TraxManager traxManager; private TraxManager traxManager;
@@ -72,6 +72,7 @@ public class RoomManager {
private final THashMap<Integer, RoomCategory> roomCategories; private final THashMap<Integer, RoomCategory> roomCategories;
private final List<String> mapNames; private final List<String> mapNames;
private final ConcurrentHashMap<Integer, Room> activeRooms; private final ConcurrentHashMap<Integer, Room> activeRooms;
private final ConcurrentHashMap<Integer, Set<Integer>> roomsByOwner;
private final ArrayList<Class<? extends Game>> gameTypes; private final ArrayList<Class<? extends Game>> gameTypes;
public RoomManager() { public RoomManager() {
@@ -79,6 +80,7 @@ public class RoomManager {
this.roomCategories = new THashMap<>(); this.roomCategories = new THashMap<>();
this.mapNames = new ArrayList<>(); this.mapNames = new ArrayList<>();
this.activeRooms = new ConcurrentHashMap<>(); this.activeRooms = new ConcurrentHashMap<>();
this.roomsByOwner = new ConcurrentHashMap<>();
this.loadRoomCategories(); this.loadRoomCategories();
this.loadRoomModels(); this.loadRoomModels();
@@ -95,6 +97,20 @@ public class RoomManager {
LOGGER.info("Room Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis); LOGGER.info("Room Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis);
} }
private void trackRoomOwner(Room room) {
this.roomsByOwner.computeIfAbsent(room.getOwnerId(), k -> ConcurrentHashMap.newKeySet()).add(room.getId());
}
private void untrackRoomOwner(Room room) {
Set<Integer> rooms = this.roomsByOwner.get(room.getOwnerId());
if (rooms != null) {
rooms.remove(room.getId());
if (rooms.isEmpty()) {
this.roomsByOwner.remove(room.getOwnerId());
}
}
}
public void loadRoomModels() { public void loadRoomModels() {
this.mapNames.clear(); this.mapNames.clear();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM room_models")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM room_models")) {
@@ -143,6 +159,7 @@ public class RoomManager {
Room room = new Room(set); Room room = new Room(set);
room.preventUncaching = true; room.preventUncaching = true;
this.activeRooms.put(set.getInt("id"), room); this.activeRooms.put(set.getInt("id"), room);
this.trackRoomOwner(room);
} }
} }
} catch (SQLException e) { } catch (SQLException e) {
@@ -162,6 +179,7 @@ public class RoomManager {
if (room == null) { if (room == null) {
room = new Room(set); room = new Room(set);
this.activeRooms.put(set.getInt("id"), room); this.activeRooms.put(set.getInt("id"), room);
this.trackRoomOwner(room);
} }
if (!rooms.containsKey(set.getInt("category"))) { if (!rooms.containsKey(set.getInt("category"))) {
@@ -321,6 +339,7 @@ public class RoomManager {
if (room != null) { if (room != null) {
this.activeRooms.put(room.getId(), room); this.activeRooms.put(room.getId(), room);
this.trackRoomOwner(room);
} }
} catch (SQLException e) { } catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
@@ -368,8 +387,11 @@ public class RoomManager {
statement.setInt(1, habbo.getHabboInfo().getId()); statement.setInt(1, habbo.getHabboInfo().getId());
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
while (set.next()) { while (set.next()) {
if (!this.activeRooms.containsKey(set.getInt("id"))) if (!this.activeRooms.containsKey(set.getInt("id"))) {
this.activeRooms.put(set.getInt("id"), new Room(set)); Room room = new Room(set);
this.activeRooms.put(room.getId(), room);
this.trackRoomOwner(room);
}
} }
} }
} catch (SQLException e) { } catch (SQLException e) {
@@ -390,24 +412,33 @@ public class RoomManager {
continue; continue;
room.dispose(); room.dispose();
this.untrackRoomOwner(room);
this.activeRooms.remove(room.getId()); this.activeRooms.remove(room.getId());
} }
} }
public void clearInactiveRooms() { public void clearInactiveRooms() {
THashSet<Room> roomsToDispose = new THashSet<>(); THashSet<Room> roomsToDispose = new THashSet<>();
for (Room room : this.activeRooms.values()) { for (Map.Entry<Integer, Set<Integer>> entry : this.roomsByOwner.entrySet()) {
if (!room.isPublicRoom() && !room.isStaffPromotedRoom() && !Emulator.getGameServer().getGameClientManager().containsHabbo(room.getOwnerId()) && room.isPreLoaded()) { int ownerId = entry.getKey();
if (!Emulator.getGameServer().getGameClientManager().containsHabbo(ownerId)) {
for (int roomId : entry.getValue()) {
Room room = this.activeRooms.get(roomId);
if (room != null && !room.isPublicRoom() && !room.isStaffPromotedRoom() && room.isPreLoaded()) {
roomsToDispose.add(room); roomsToDispose.add(room);
} }
} }
}
}
for (Room room : roomsToDispose) { for (Room room : roomsToDispose) {
room.dispose(); room.dispose();
if (room.getUserCount() == 0) if (room.getUserCount() == 0) {
this.untrackRoomOwner(room);
this.activeRooms.remove(room.getId()); this.activeRooms.remove(room.getId());
} }
} }
}
public boolean layoutExists(String name) { public boolean layoutExists(String name) {
return this.mapNames.contains(name); return this.mapNames.contains(name);
@@ -434,6 +465,7 @@ public class RoomManager {
} }
public void uncacheRoom(Room room) { public void uncacheRoom(Room room) {
this.untrackRoomOwner(room);
this.activeRooms.remove(room.getId()); this.activeRooms.remove(room.getId());
} }
@@ -1125,6 +1157,7 @@ public class RoomManager {
Room r = new Room(set); Room r = new Room(set);
rooms.add(r); rooms.add(r);
this.activeRooms.put(r.getId(), r); this.activeRooms.put(r.getId(), r);
this.trackRoomOwner(r);
} }
} }
} catch (SQLException e) { } catch (SQLException e) {
@@ -1185,6 +1218,7 @@ public class RoomManager {
rooms.add(r); rooms.add(r);
this.activeRooms.put(r.getId(), r); this.activeRooms.put(r.getId(), r);
this.trackRoomOwner(r);
} }
} }
} catch (SQLException e) { } catch (SQLException e) {
@@ -1248,6 +1282,7 @@ public class RoomManager {
room = new Room(set); room = new Room(set);
this.activeRooms.put(room.getId(), room); this.activeRooms.put(room.getId(), room);
this.trackRoomOwner(room);
} }
rooms.add(room); rooms.add(room);
@@ -1489,6 +1524,7 @@ public class RoomManager {
room.dispose(); room.dispose();
} }
this.roomsByOwner.clear();
this.activeRooms.clear(); this.activeRooms.clear();
LOGGER.info("Room Manager -> Disposed!"); LOGGER.info("Room Manager -> Disposed!");
@@ -9,7 +9,20 @@ import gnu.trove.set.hash.THashSet;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class SearchUserEvent extends MessageHandler { public class SearchUserEvent extends MessageHandler {
public static ConcurrentHashMap<String, THashSet<MessengerBuddy>> cachedResults = new ConcurrentHashMap<>(); private static final long CACHE_TTL_MS = 30_000; // 30 second TTL
private static final ConcurrentHashMap<String, Long> cacheTimestamps = new ConcurrentHashMap<>();
public static final ConcurrentHashMap<String, THashSet<MessengerBuddy>> cachedResults = new ConcurrentHashMap<>();
public static void cleanExpiredCache() {
long now = System.currentTimeMillis();
cacheTimestamps.entrySet().removeIf(entry -> {
if (now - entry.getValue() > CACHE_TTL_MS) {
cachedResults.remove(entry.getKey());
return true;
}
return false;
});
}
@Override @Override
public void handle() throws Exception { public void handle() throws Exception {
@@ -31,6 +44,7 @@ public class SearchUserEvent extends MessageHandler {
if (buddies == null) { if (buddies == null) {
buddies = Messenger.searchUsers(username); buddies = Messenger.searchUsers(username);
cachedResults.put(username, buddies); cachedResults.put(username, buddies);
cacheTimestamps.put(username, System.currentTimeMillis());
} }
this.client.sendResponse(new UserSearchResultComposer(buddies, this.client.getHabbo().getMessenger().getFriends(username), this.client.getHabbo())); this.client.sendResponse(new UserSearchResultComposer(buddies, this.client.getHabbo().getMessenger().getFriends(username), this.client.getHabbo()));