🆙 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.users.Habbo;
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.threading.runnables.AchievementUpdater;
import org.slf4j.Logger;
@@ -101,8 +100,7 @@ public class CleanerThread implements Runnable {
LAST_HABBO_CACHE_CLEARED = time;
}
SearchRoomsEvent.cachedResults.clear();
SearchUserEvent.cachedResults.clear();
SearchUserEvent.cleanExpiredCache();
}
@@ -130,8 +130,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
private String password;
private RoomState state;
private int usersMax;
private volatile int score;
private volatile int category;
private int score;
private int category;
private String floorPaint;
private String wallPaint;
private String backgroundPaint;
@@ -140,37 +140,37 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
private int floorSize;
private int guild;
private String tags;
private volatile boolean publicRoom;
private volatile boolean staffPromotedRoom;
private volatile boolean allowPets;
private volatile boolean allowPetsEat;
private volatile boolean allowWalkthrough;
private volatile boolean allowBotsWalk;
private volatile boolean allowEffects;
private volatile boolean hideWall;
private volatile int chatMode;
private volatile int chatWeight;
private volatile int chatSpeed;
private volatile int chatDistance;
private volatile int chatProtection;
private volatile int muteOption;
private volatile int kickOption;
private volatile int banOption;
private volatile int pollId;
private volatile boolean promoted;
private volatile int tradeMode;
private volatile boolean moveDiagonally;
private volatile boolean allowUnderpass;
private volatile boolean jukeboxActive;
private volatile boolean hideWired;
private boolean publicRoom;
private boolean staffPromotedRoom;
private boolean allowPets;
private boolean allowPetsEat;
private boolean allowWalkthrough;
private boolean allowBotsWalk;
private boolean allowEffects;
private boolean hideWall;
private int chatMode;
private int chatWeight;
private int chatSpeed;
private int chatDistance;
private int chatProtection;
private int muteOption;
private int kickOption;
private int banOption;
private int pollId;
private boolean promoted;
private int tradeMode;
private boolean moveDiagonally;
private boolean allowUnderpass;
private boolean jukeboxActive;
private boolean hideWired;
private RoomPromotion promotion;
private volatile boolean needsUpdate;
private volatile boolean loaded;
private volatile boolean preLoaded;
private volatile boolean loadingInProgress;
private volatile CompletableFuture<Void> loadingFuture;
private volatile int rollerSpeed;
private volatile int lastTimerReset = Emulator.getIntUnixTimestamp();
private int rollerSpeed;
private int lastTimerReset = Emulator.getIntUnixTimestamp();
private volatile boolean muted;
private RoomSpecialTypes roomSpecialTypes;
private TraxManager traxManager;
@@ -72,6 +72,7 @@ public class RoomManager {
private final THashMap<Integer, RoomCategory> roomCategories;
private final List<String> mapNames;
private final ConcurrentHashMap<Integer, Room> activeRooms;
private final ConcurrentHashMap<Integer, Set<Integer>> roomsByOwner;
private final ArrayList<Class<? extends Game>> gameTypes;
public RoomManager() {
@@ -79,6 +80,7 @@ public class RoomManager {
this.roomCategories = new THashMap<>();
this.mapNames = new ArrayList<>();
this.activeRooms = new ConcurrentHashMap<>();
this.roomsByOwner = new ConcurrentHashMap<>();
this.loadRoomCategories();
this.loadRoomModels();
@@ -95,6 +97,20 @@ public class RoomManager {
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() {
this.mapNames.clear();
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.preventUncaching = true;
this.activeRooms.put(set.getInt("id"), room);
this.trackRoomOwner(room);
}
}
} catch (SQLException e) {
@@ -162,6 +179,7 @@ public class RoomManager {
if (room == null) {
room = new Room(set);
this.activeRooms.put(set.getInt("id"), room);
this.trackRoomOwner(room);
}
if (!rooms.containsKey(set.getInt("category"))) {
@@ -321,6 +339,7 @@ public class RoomManager {
if (room != null) {
this.activeRooms.put(room.getId(), room);
this.trackRoomOwner(room);
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
@@ -368,8 +387,11 @@ public class RoomManager {
statement.setInt(1, habbo.getHabboInfo().getId());
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
if (!this.activeRooms.containsKey(set.getInt("id")))
this.activeRooms.put(set.getInt("id"), new Room(set));
if (!this.activeRooms.containsKey(set.getInt("id"))) {
Room room = new Room(set);
this.activeRooms.put(room.getId(), room);
this.trackRoomOwner(room);
}
}
}
} catch (SQLException e) {
@@ -390,24 +412,33 @@ public class RoomManager {
continue;
room.dispose();
this.untrackRoomOwner(room);
this.activeRooms.remove(room.getId());
}
}
public void clearInactiveRooms() {
THashSet<Room> roomsToDispose = new THashSet<>();
for (Room room : this.activeRooms.values()) {
if (!room.isPublicRoom() && !room.isStaffPromotedRoom() && !Emulator.getGameServer().getGameClientManager().containsHabbo(room.getOwnerId()) && room.isPreLoaded()) {
for (Map.Entry<Integer, Set<Integer>> entry : this.roomsByOwner.entrySet()) {
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);
}
}
}
}
for (Room room : roomsToDispose) {
room.dispose();
if (room.getUserCount() == 0)
if (room.getUserCount() == 0) {
this.untrackRoomOwner(room);
this.activeRooms.remove(room.getId());
}
}
}
public boolean layoutExists(String name) {
return this.mapNames.contains(name);
@@ -434,6 +465,7 @@ public class RoomManager {
}
public void uncacheRoom(Room room) {
this.untrackRoomOwner(room);
this.activeRooms.remove(room.getId());
}
@@ -1125,6 +1157,7 @@ public class RoomManager {
Room r = new Room(set);
rooms.add(r);
this.activeRooms.put(r.getId(), r);
this.trackRoomOwner(r);
}
}
} catch (SQLException e) {
@@ -1185,6 +1218,7 @@ public class RoomManager {
rooms.add(r);
this.activeRooms.put(r.getId(), r);
this.trackRoomOwner(r);
}
}
} catch (SQLException e) {
@@ -1248,6 +1282,7 @@ public class RoomManager {
room = new Room(set);
this.activeRooms.put(room.getId(), room);
this.trackRoomOwner(room);
}
rooms.add(room);
@@ -1489,6 +1524,7 @@ public class RoomManager {
room.dispose();
}
this.roomsByOwner.clear();
this.activeRooms.clear();
LOGGER.info("Room Manager -> Disposed!");
@@ -9,7 +9,20 @@ import gnu.trove.set.hash.THashSet;
import java.util.concurrent.ConcurrentHashMap;
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
public void handle() throws Exception {
@@ -31,6 +44,7 @@ public class SearchUserEvent extends MessageHandler {
if (buddies == null) {
buddies = Messenger.searchUsers(username);
cachedResults.put(username, buddies);
cacheTimestamps.put(username, System.currentTimeMillis());
}
this.client.sendResponse(new UserSearchResultComposer(buddies, this.client.getHabbo().getMessenger().getFriends(username), this.client.getHabbo()));