🆙 Database performance fixes

1. HabboManager - O(1) username lookup
Before: getHabbo(String) held a synchronized lock and iterated ALL online users every call
After: Secondary ConcurrentHashMap<String, Habbo> keyed by lowercase username → instant get() lookup, no lock contention

2. ItemsComponent - Batch DB saves
Before: dispose() spawned a separate thread per dirty item, each opening its own DB connection
After: Single connection, JDBC addBatch()/executeBatch() for both UPDATE and DELETE, flushed every 100 items. A user with 500 dirty items now does 5 batch executions instead of 500 thread spawns + 500 connections.

3. AcceptFriendRequestEvent - N+1 elimination
Before: For each offline user: query 1 (getOfflineHabboInfo by ID) → query 2 (load full Habbo by username) = 2 queries × up to 100 users = 200 queries
After: Single query by user ID directly = 1 query × up to 100 users = 100 queries (50% reduction)

4. RoomManager - Direct HashMap lookup
Before: getCategory(int id) iterated all categories checking getId() == id even though the map is already keyed by ID
After: Direct roomCategories.get(id) → O(1) instead of O(n)
This commit is contained in:
duckietm
2026-03-26 09:41:31 +01:00
parent f2b0ba0138
commit 5319e5e5c3
4 changed files with 70 additions and 48 deletions
@@ -179,12 +179,7 @@ public class RoomManager {
}
public RoomCategory getCategory(int id) {
for (RoomCategory category : this.roomCategories.values()) {
if (category.getId() == id)
return category;
}
return null;
return this.roomCategories.get(id);
}
public RoomCategory getCategory(String name) {
@@ -220,15 +215,8 @@ public class RoomManager {
}
public boolean hasCategory(int categoryId, Habbo habbo) {
for (RoomCategory category : this.roomCategories.values()) {
if (category.getId() == categoryId) {
if (category.getMinRank() <= habbo.getHabboInfo().getRank().getId()) {
return true;
}
}
}
return false;
RoomCategory category = this.roomCategories.get(categoryId);
return category != null && category.getMinRank() <= habbo.getHabboInfo().getRank().getId();
}
public THashMap<Integer, RoomCategory> getRoomCategories() {
@@ -35,11 +35,13 @@ public class HabboManager {
public static boolean NAMECHANGE_ENABLED = false;
private final ConcurrentHashMap<Integer, Habbo> onlineHabbos;
private final ConcurrentHashMap<String, Habbo> onlineHabbosByName;
public HabboManager() {
long millis = System.currentTimeMillis();
this.onlineHabbos = new ConcurrentHashMap<>();
this.onlineHabbosByName = new ConcurrentHashMap<>();
LOGGER.info("Habbo Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis);
}
@@ -80,10 +82,12 @@ public class HabboManager {
public void addHabbo(Habbo habbo) {
this.onlineHabbos.put(habbo.getHabboInfo().getId(), habbo);
this.onlineHabbosByName.put(habbo.getHabboInfo().getUsername().toLowerCase(), habbo);
}
public void removeHabbo(Habbo habbo) {
this.onlineHabbos.remove(habbo.getHabboInfo().getId());
this.onlineHabbosByName.remove(habbo.getHabboInfo().getUsername().toLowerCase());
}
public Habbo getHabbo(int id) {
@@ -91,14 +95,7 @@ public class HabboManager {
}
public Habbo getHabbo(String username) {
synchronized (this.onlineHabbos) {
for (Map.Entry<Integer, Habbo> map : this.onlineHabbos.entrySet()) {
if (map.getValue().getHabboInfo().getUsername().equalsIgnoreCase(username))
return map.getValue();
}
}
return null;
return this.onlineHabbosByName.get(username.toLowerCase());
}
public Habbo loadHabbo(String sso) {
@@ -178,11 +175,9 @@ public class HabboManager {
}
public void sendPacketToHabbosWithPermission(ServerMessage message, String perm) {
synchronized (this.onlineHabbos) {
for (Habbo habbo : this.onlineHabbos.values()) {
if (habbo.hasPermission(perm)) {
habbo.getClient().sendResponse(message);
}
for (Habbo habbo : this.onlineHabbos.values()) {
if (habbo.hasPermission(perm)) {
habbo.getClient().sendResponse(message);
}
}
}
@@ -159,14 +159,63 @@ public class ItemsComponent {
}
if (!this.items.isEmpty()) {
for (int i = this.items.size(); i-- > 0; ) {
try {
items.advance();
} catch (NoSuchElementException e) {
break;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement updateStmt = connection.prepareStatement(
"UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?")) {
try (PreparedStatement deleteStmt = connection.prepareStatement(
"DELETE FROM items WHERE id = ?")) {
int updateCount = 0;
int deleteCount = 0;
for (int i = this.items.size(); i-- > 0; ) {
try {
items.advance();
} catch (NoSuchElementException e) {
break;
}
HabboItem item = items.value();
if (item.needsDelete()) {
deleteStmt.setInt(1, item.getId());
deleteStmt.addBatch();
deleteCount++;
item.needsUpdate(false);
item.needsDelete(false);
} else if (item.needsUpdate()) {
updateStmt.setInt(1, item.getUserId());
updateStmt.setInt(2, item.getRoomId());
updateStmt.setString(3, item.getWallPosition());
updateStmt.setInt(4, item.getX());
updateStmt.setInt(5, item.getY());
updateStmt.setDouble(6, item.getZ());
updateStmt.setInt(7, item.getRotation());
updateStmt.setString(8, item.getExtradata());
updateStmt.setString(9, item.getLimitedStack() + ":" + item.getLimitedSells());
updateStmt.setInt(10, item.getId());
updateStmt.addBatch();
updateCount++;
item.needsUpdate(false);
}
if (updateCount > 0 && updateCount % 100 == 0) {
updateStmt.executeBatch();
}
if (deleteCount > 0 && deleteCount % 100 == 0) {
deleteStmt.executeBatch();
}
}
if (deleteCount % 100 != 0) {
deleteStmt.executeBatch();
}
if (updateCount % 100 != 0) {
updateStmt.executeBatch();
}
}
}
if (items.value().needsUpdate())
Emulator.getThreading().run(items.value());
} catch (SQLException e) {
LOGGER.error("Caught SQL exception during batch item save", e);
}
}
@@ -3,7 +3,6 @@ package com.eu.habbo.messages.incoming.friends;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.messenger.Messenger;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.friends.FriendRequestErrorComposer;
import org.slf4j.Logger;
@@ -14,7 +13,6 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import static com.eu.habbo.habbohotel.users.HabboManager.getOfflineHabboInfo;
public class AcceptFriendRequestEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(AcceptFriendRequestEvent.class);
@@ -44,18 +42,10 @@ public class AcceptFriendRequestEvent extends MessageHandler {
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if(target == null) {
HabboInfo habboInfo = getOfflineHabboInfo(userId);
if(habboInfo == null) {
this.client.sendResponse(new FriendRequestErrorComposer(FriendRequestErrorComposer.TARGET_NOT_FOUND));
this.client.getHabbo().getMessenger().deleteFriendRequests(userId, this.client.getHabbo().getHabboInfo().getId());
continue;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.*, users_settings.block_friendrequests FROM users INNER JOIN users_settings ON users.id = users_settings.user_id WHERE username = ? LIMIT 1")) {
statement.setString(1, habboInfo.getUsername());
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.*, users_settings.block_friendrequests FROM users INNER JOIN users_settings ON users.id = users_settings.user_id WHERE users.id = ? LIMIT 1")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
if (set.next()) {
target = new Habbo(set);
}
}