Skip to content

Commit

Permalink
LegionHistory memory optimizations
Browse files Browse the repository at this point in the history
Old legions had tens of thousands of history entries, all permanently loaded into memory.
Limited legion warehouse and reward logs to one year to remove most of the entries. As these two views only show the month and day but not the year, they shouldn't hold several years of data anyway.
Also reduced the memory footprint of history entries by optimizing fields/types and interning common strings.
  • Loading branch information
neon-dev committed Nov 30, 2024
1 parent 9556a4b commit 9dce22d
Show file tree
Hide file tree
Showing 18 changed files with 258 additions and 266 deletions.
1 change: 0 additions & 1 deletion game-server/sql/aion_gs.sql
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,6 @@ CREATE TABLE `legion_history` (
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`history_type` enum('CREATE','JOIN','KICK','APPOINTED','EMBLEM_REGISTER','EMBLEM_MODIFIED','ITEM_DEPOSIT','ITEM_WITHDRAW','KINAH_DEPOSIT','KINAH_WITHDRAW','LEVEL_UP','DEFENSE','OCCUPATION','LEGION_RENAME','CHARACTER_RENAME') NOT NULL,
`name` varchar(50) NOT NULL,
`tab_id` smallint(3) NOT NULL DEFAULT '0',
`description` varchar(30) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `legion_id` (`legion_id`),
Expand Down
5 changes: 2 additions & 3 deletions game-server/sql/update.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/*
* DB changes since 249c4e14 (10.08.2024)
* DB changes since 9556a4b (30.11.2024)
*/

-- remove old event items
DELETE FROM inventory WHERE item_id IN (182006999, 185000128, 188052166, 188600199);
ALTER TABLE `legion_history` DROP COLUMN `tab_id`;
70 changes: 48 additions & 22 deletions game-server/src/com/aionemu/gameserver/dao/LegionDAO.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.aionemu.gameserver.dao;

import java.sql.*;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -13,6 +17,7 @@
import com.aionemu.gameserver.model.gameobjects.Persistable.PersistentState;
import com.aionemu.gameserver.model.items.storage.StorageType;
import com.aionemu.gameserver.model.team.legion.*;
import com.aionemu.gameserver.model.team.legion.LegionHistoryAction.Type;

/**
* Class that is responsible for storing/loading legion data
Expand Down Expand Up @@ -40,8 +45,9 @@ public class LegionDAO {
/** Storage Queries **/
private static final String SELECT_STORAGE_QUERY = "SELECT * FROM `inventory` WHERE `item_owner`=? AND `item_location`=? AND `is_equipped`=?";
/** History Queries **/
private static final String INSERT_HISTORY_QUERY = "INSERT INTO legion_history(`legion_id`, `date`, `history_type`, `name`, `tab_id`, `description`) VALUES (?, ?, ?, ?, ?, ?)";
private static final String SELECT_HISTORY_QUERY = "SELECT * FROM `legion_history` WHERE legion_id=? ORDER BY date ASC;";
private static final String INSERT_HISTORY_QUERY = "INSERT INTO legion_history(`legion_id`, `date`, `history_type`, `name`, `description`) VALUES (?, ?, ?, ?, ?)";
private static final String SELECT_HISTORY_QUERY = "SELECT * FROM `legion_history` WHERE legion_id=? ORDER BY date DESC, id DESC";
private static final String DELETE_HISTORY_QUERY = "DELETE FROM `legion_history` WHERE id IN (%s)";
private static final String CLEAR_LEGION_SIEGE = "UPDATE siege_locations SET legion_id=0 WHERE legion_id=?";

public static boolean isNameUsed(String name) {
Expand Down Expand Up @@ -378,33 +384,53 @@ public void handleRead(ResultSet rset) throws SQLException {
return inventory;
}

public static void loadLegionHistory(Legion legion) {
public static void loadHistory(Legion legion) {
Map<Type, List<LegionHistoryEntry>> history = new EnumMap<>(Type.class);
for (Type type : Type.values())
history.put(type, new ArrayList<>());
try (Connection con = DatabaseFactory.getConnection(); PreparedStatement stmt = con.prepareStatement(SELECT_HISTORY_QUERY)) {
stmt.setInt(1, legion.getLegionId());
ResultSet resultSet = stmt.executeQuery();
while (resultSet.next())
legion.addHistory(new LegionHistory(LegionHistoryType.valueOf(resultSet.getString("history_type")), resultSet.getString("name"),
resultSet.getTimestamp("date"), resultSet.getInt("tab_id"), resultSet.getString("description")));
while (resultSet.next()) {
int id = resultSet.getInt("id");
int epochSeconds = (int) (resultSet.getTimestamp("date").getTime() / 1000);
LegionHistoryAction action = LegionHistoryAction.valueOf(resultSet.getString("history_type"));
String name = resultSet.getString("name");
String description = resultSet.getString("description");
history.get(action.getType()).add(new LegionHistoryEntry(id, epochSeconds, action, name, description));
}
} catch (Exception e) {
log.error("Error loading legion history of " + legion, e);
log.error("Could not load history of legion " + legion, e);
}
legion.setHistory(history);
}

public static boolean saveNewLegionHistory(int legionId, LegionHistory legionHistory) {
boolean success = DB.insertUpdate(INSERT_HISTORY_QUERY, new IUStH() {

@Override
public void handleInsertUpdate(PreparedStatement preparedStatement) throws SQLException {
preparedStatement.setInt(1, legionId);
preparedStatement.setTimestamp(2, legionHistory.getTime());
preparedStatement.setString(3, legionHistory.getLegionHistoryType().toString());
preparedStatement.setString(4, legionHistory.getName());
preparedStatement.setInt(5, legionHistory.getTabId());
preparedStatement.setString(6, legionHistory.getDescription());
preparedStatement.execute();
}
});
return success;
public static LegionHistoryEntry insertHistory(int legionId, LegionHistoryAction action, String name, String description) {
try (Connection con = DatabaseFactory.getConnection(); PreparedStatement stmt = con.prepareStatement(INSERT_HISTORY_QUERY, Statement.RETURN_GENERATED_KEYS)) {
long nowMillis = System.currentTimeMillis();
stmt.setInt(1, legionId);
stmt.setTimestamp(2, new Timestamp(nowMillis));
stmt.setString(3, action.toString());
stmt.setString(4, name);
stmt.setString(5, description);
stmt.execute();
ResultSet result = stmt.getGeneratedKeys();
result.next();
return new LegionHistoryEntry(result.getInt(1), (int) (nowMillis / 1000), action, name, description);
} catch (Exception e) {
log.error("Could not add history entry for legion " + legionId, e);
return null;
}
}

public static void deleteHistory(int legionId, List<LegionHistoryEntry> entries) {
String placeholders = ",?".repeat(entries.size()).substring(1);
try (Connection con = DatabaseFactory.getConnection(); PreparedStatement stmt = con.prepareStatement(DELETE_HISTORY_QUERY.formatted(placeholders))) {
for (int i = 0; i < entries.size(); i++)
stmt.setInt(i + 1, entries.get(i).id());
stmt.executeUpdate();
} catch (Exception e) {
log.error("Could not delete " + entries.size() + " history entries for legion " + legionId, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package com.aionemu.gameserver.model.team.legion;

import java.sql.Timestamp;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import com.aionemu.gameserver.configs.main.LegionConfig;
import com.aionemu.gameserver.model.gameobjects.AionObject;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.team.legion.LegionHistoryAction.Type;
import com.aionemu.gameserver.network.aion.serverpackets.SM_ICON_INFO;
import com.aionemu.gameserver.services.LegionService;
import com.aionemu.gameserver.utils.PacketSendUtility;
Expand All @@ -34,7 +35,7 @@ public class Legion extends AionObject {
private Announcement announcement;
private LegionEmblem legionEmblem = new LegionEmblem();
private LegionWarehouse legionWarehouse;
private final List<LegionHistory> legionHistory = new ArrayList<>();
private Map<Type, List<LegionHistoryEntry>> legionHistoryByType;
private AtomicBoolean hasBonus = new AtomicBoolean(false);
private int occupiedLegionDominion = 0;
private int currentLegionDominion = 0;
Expand Down Expand Up @@ -419,24 +420,33 @@ public int getWarehouseLevel() {
return getLegionLevel() - 1;
}

public List<LegionHistory> getLegionHistoryByTabId(int tabType) {
synchronized (legionHistory) {
if (legionHistory.isEmpty())
return Collections.emptyList();
return legionHistory.stream().filter(h -> h.getTabId() == tabType).collect(Collectors.toList());
public List<LegionHistoryEntry> getHistory(Type type) {
List<LegionHistoryEntry> history = legionHistoryByType.get(type);
synchronized (history) {
return new ArrayList<>(history);
}
}

/**
* Adds history entries sorted descending by their timestamps.
* Adds the history entry at the top of the list and removes entries older than a year (except the ones on the first page)
*/
public void addHistory(LegionHistory history) {
synchronized (legionHistory) {
int index = 0;
while (index < legionHistory.size() && history.getTime().before(legionHistory.get(index).getTime()))
index++;
legionHistory.add(index, history);
public List<LegionHistoryEntry> addHistory(LegionHistoryEntry entry) {
List<LegionHistoryEntry> removedEntries = new ArrayList<>();
Type type = entry.action().getType();
List<LegionHistoryEntry> history = legionHistoryByType.get(type);
synchronized (history) {
history.addFirst(entry);
if (type == Type.REWARD || type == Type.WAREHOUSE) {
long maxMillis = System.currentTimeMillis() / 1000 - Duration.ofDays(365).toSeconds();
while (history.getLast().epochSeconds() < maxMillis)
removedEntries.add(history.removeLast());
}
}
return removedEntries;
}

public void setHistory(Map<Type, List<LegionHistoryEntry>> history) {
legionHistoryByType = history;
}

public void addBonus() {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.aionemu.gameserver.model.team.legion;

/**
* @author Simple
*/
public enum LegionHistoryAction {

CREATE(0, Type.LEGION), // No parameters
JOIN(1, Type.LEGION), // Parameter: name
KICK(2, Type.LEGION), // Parameter: name
LEVEL_UP(3, Type.LEGION), // Parameter: legion level
APPOINTED(4, Type.LEGION), // Parameter: legion level
EMBLEM_REGISTER(5, Type.LEGION), // No parameters
EMBLEM_MODIFIED(6, Type.LEGION), // No parameters
// 7 to 10 are not used anymore or never implemented
DEFENSE(11, Type.REWARD), // Parameter: name = kinah amount, description = fortress id
OCCUPATION(12, Type.REWARD), // Parameter: name = kinah amount, description = fortress id
LEGION_RENAME(13, Type.LEGION), // Parameter: old name, new name
CHARACTER_RENAME(14, Type.LEGION), // Parameter: old name, new name
ITEM_DEPOSIT(15, Type.WAREHOUSE), // Parameter: name
ITEM_WITHDRAW(16, Type.WAREHOUSE), // Parameter: name
KINAH_DEPOSIT(17, Type.WAREHOUSE), // Parameter: name
KINAH_WITHDRAW(18, Type.WAREHOUSE); // Parameter: name

private final byte id;
private final Type type;

LegionHistoryAction(int id, Type type) {
this.id = (byte) id;
this.type = type;
}

public byte getId() {
return id;
}

public Type getType() {
return type;
}

public enum Type { LEGION, REWARD, WAREHOUSE }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.aionemu.gameserver.model.team.legion;

/**
* @author Simple, xTz
*/
public record LegionHistoryEntry(int id, int epochSeconds, LegionHistoryAction action, String name, String description) {

public LegionHistoryEntry(int id, int epochSeconds, LegionHistoryAction action, String name, String description) {
this.id = id;
this.epochSeconds = epochSeconds;
this.action = action;
this.name = name.intern();
this.description = description.intern();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public class AionClientPacketFactory {
packets[52] = new PacketInfo<>(CM_SHOW_DIALOG.class, State.IN_GAME); // [C_START_DIALOG (StartDialogPacket)]
packets[53] = new PacketInfo<>(CM_CLOSE_DIALOG.class, State.IN_GAME); // [C_END_DIALOG (EndDialogPacket)]
packets[54] = new PacketInfo<>(CM_DIALOG_SELECT.class, State.IN_GAME); // [C_HACTION (HActionPacket)]
packets[55] = new PacketInfo<>(CM_LEGION_TABS.class, State.IN_GAME); // [C_REQUEST_GUILD_HISTORY (RequestGuildHistoryPacket)]
packets[55] = new PacketInfo<>(CM_LEGION_HISTORY.class, State.IN_GAME); // [C_REQUEST_GUILD_HISTORY (RequestGuildHistoryPacket)]
// packets[56] = [C_BOOKMARK (BookmarkPacket)]
// packets[57] = [C_DELETE_BOOKMARK (DeleteBookmarkPacket)]
packets[58] = new PacketInfo<>(CM_SET_NOTE.class, State.IN_GAME); // [C_TODAY_WORDS (TodayWordsPacket)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class ServerPacketsOpcodes {
// addPacketOpcode(9, ); // [S_LOGIN_CHECK]
addPacketOpcode(10, SM_NPC_ASSEMBLER.class); // [S_CUTSCENE_NPC_INFO]
addPacketOpcode(11, SM_LEGION_UPDATE_NICKNAME.class); // [S_CHANGE_GUILD_MEMBER_NICKNAME]
addPacketOpcode(12, SM_LEGION_TABS.class); // [S_GUILD_HISTORY]
addPacketOpcode(12, SM_LEGION_HISTORY.class); // [S_GUILD_HISTORY]
addPacketOpcode(13, SM_ENTER_WORLD_CHECK.class); // [S_ENTER_WORLD_CHECK]
addPacketOpcode(14, SM_NPC_INFO.class); // [S_PUT_NPC]
addPacketOpcode(15, SM_PLAYER_SPAWN.class); // [S_WORLD]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import com.aionemu.gameserver.model.gameobjects.Item;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.team.legion.Legion;
import com.aionemu.gameserver.model.team.legion.LegionHistoryType;
import com.aionemu.gameserver.model.team.legion.LegionHistoryAction;
import com.aionemu.gameserver.model.templates.item.actions.AbstractItemAction;
import com.aionemu.gameserver.model.templates.item.actions.CosmeticItemAction;
import com.aionemu.gameserver.network.aion.AionClientPacket;
Expand Down Expand Up @@ -91,7 +91,7 @@ public static void onPlayerNameChanged(Player player, String oldName) {
World.getInstance().updateCachedPlayerName(oldName, player);
if (player.isLegionMember()) {
LegionService.getInstance().updateCachedPlayerName(oldName, player);
LegionService.getInstance().addHistory(player.getLegion(), oldName, LegionHistoryType.CHARACTER_RENAME, 0, player.getName());
LegionService.getInstance().addHistory(player.getLegion(), oldName, LegionHistoryAction.CHARACTER_RENAME, player.getName());
}
PacketSendUtility.broadcastToWorld(new SM_RENAME(player, oldName)); // broadcast to world to update all friendlists, housing npcs, etc.
}
Expand Down
Loading

0 comments on commit 9dce22d

Please sign in to comment.