package bench;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import megamek.client.bot.princess.Princess;
import megamek.common.Player;
import megamek.common.compute.Compute;
import megamek.common.enums.GamePhase;
import megamek.common.game.IGame;
import megamek.common.loaders.MekSummaryCache;
import megamek.common.scenario.Scenario;
import megamek.common.scenario.ScenarioLoader;
import megamek.server.Server;
import megamek.server.totalWarfare.TWGameManager;
/**
* Density benchmark: how many concurrent MegaMek games fit in one JVM, and what
* does game N+1 actually cost?
*
*
The design point that matters: games are started staggered, and each is
* allowed to reach a steady state (past deployment, several rounds of real combat)
* before the next one starts. A freshly constructed Server sitting in the lobby is
* not representative - it has no turn history, no accumulated Report vectors, no
* Princess path caches. Measuring those would flatter the result badly.
*
*
Usage:
*
* MMBench --games 8 --scenario data/scenarios/... --settle-round 3 --max-rounds 30
*
*/
public final class MMBench {
private static final String LOCALHOST = "127.0.0.1";
/** One running game: its server, its manager, and the bots playing it. */
private static final class GameInstance {
final int index;
final int port;
final Server server;
final TWGameManager gameManager;
final IGame game;
final List bots = new ArrayList<>();
boolean finished;
GameInstance(int index, int port, Server server, TWGameManager gameManager, IGame game) {
this.index = index;
this.port = port;
this.server = server;
this.gameManager = gameManager;
this.game = game;
}
int round() {
try {
return game.getCurrentRound();
} catch (Exception e) {
return -1;
}
}
GamePhase phase() {
try {
return game.getPhase();
} catch (Exception e) {
return null;
}
}
boolean isOver() {
GamePhase p = phase();
return finished || p == GamePhase.VICTORY;
}
}
public static void main(String[] args) throws Exception {
Map opts = parseArgs(args);
int gameCount = Integer.parseInt(opts.getOrDefault("games", "4"));
String scenarioPath = opts.getOrDefault("scenario",
"data/scenarios/TrainingScenarios/1-FirstRun.mms");
int basePort = Integer.parseInt(opts.getOrDefault("base-port", "8800"));
int settleRound = Integer.parseInt(opts.getOrDefault("settle-round", "3"));
int maxRounds = Integer.parseInt(opts.getOrDefault("max-rounds", "40"));
int maxSeconds = Integer.parseInt(opts.getOrDefault("max-seconds", "900"));
Path csv = Path.of(opts.getOrDefault("csv", "results/bench.csv"));
System.out.printf("MMBench: games=%d scenario=%s settleRound=%d maxRounds=%d%n",
gameCount, scenarioPath, settleRound, maxRounds);
Sampler sampler = new Sampler(csv);
sampler.checkpoint("jvm-start", 0, 0, 0);
// Kick the RNG exactly as DedicatedServer does.
Compute.d6();
// Load the unit cache once. This is the big shared static allocation and the
// whole reason multi-tenanting might pay off, so measure it in isolation.
MekSummaryCache cache = MekSummaryCache.getInstance();
while (!cache.isInitialized()) {
Thread.sleep(250L);
}
System.out.printf("Unit cache initialized: %d units%n", cache.getAllMeks().length);
sampler.checkpoint("unit-cache-loaded", 0, 0, 0);
List games = new ArrayList<>();
long deadline = System.currentTimeMillis() + (maxSeconds * 1000L);
for (int i = 0; i < gameCount; i++) {
GameInstance instance = startGame(i, basePort + (i * 4), scenarioPath);
games.add(instance);
System.out.printf("-- started game %d on port %d with %d bots%n",
i, instance.port, instance.bots.size());
// Let this game reach steady state before adding the next, so the
// measured delta reflects a game that is actually playing.
waitForSettle(instance, settleRound, sampler, games, deadline);
sampler.checkpoint("settled-game-" + (i + 1), i + 1, liveCount(games), totalRounds(games));
}
System.out.println("All games started; running to completion...");
int lastReported = -1;
while (System.currentTimeMillis() < deadline && !allOver(games, maxRounds)) {
Thread.sleep(2000L);
int rounds = totalRounds(games);
sampler.sample("running", games.size(), liveCount(games), rounds);
int maxRound = games.stream().mapToInt(GameInstance::round).max().orElse(0);
if (maxRound / 5 != lastReported) {
lastReported = maxRound / 5;
sampler.checkpoint("round-" + maxRound, games.size(), liveCount(games), rounds);
}
}
sampler.checkpoint("final", games.size(), liveCount(games), totalRounds(games));
System.out.println("\n=== per-game state ===");
for (GameInstance g : games) {
System.out.printf("game %d: round=%d phase=%s over=%s%n",
g.index, g.round(), g.phase(), g.isOver());
}
for (GameInstance g : games) {
try {
g.server.die();
} catch (Exception ignored) {
// best effort teardown
}
}
System.out.println("Wrote " + csv.toAbsolutePath());
System.exit(0);
}
/** Mirrors MegaMekGUI.scenario(), minus every GUI interaction. */
private static GameInstance startGame(int index, int port, String scenarioPath) throws Exception {
ScenarioLoader loader = new ScenarioLoader(new File(scenarioPath));
Scenario scenario = loader.load();
IGame game = scenario.createGame();
TWGameManager gameManager = new TWGameManager();
Server server = new Server("", port, gameManager);
server.setGame(game);
scenario.applyDamage(gameManager);
gameManager.calculatePlayerInitialCounts();
GameInstance instance = new GameInstance(index, port, server, gameManager, game);
// Every player becomes a Princess: no human slot, so the game runs itself.
for (Player player : game.getPlayersList()) {
Princess bot = new Princess(player.getName(), LOCALHOST, port);
bot.startPrecognition();
bot.connect();
instance.bots.add(bot);
}
// Bots chat in the lobby but never ready up on their own, so the game would
// sit in LOUNGE forever. Push them out of it - but only once each bot has
// actually received its entities.
//
// A fixed sleep here is a race: under CPU contention from already-running
// games, a bot can still be waiting on its initial state when we ready it up,
// and then it deploys with a null entity ("invalid deployment packet") and
// the game wedges in DEPLOYMENT forever. Wait on observed state instead.
long connectDeadline = System.currentTimeMillis() + 60_000L;
for (Princess bot : instance.bots) {
while (!bot.isConnected() && System.currentTimeMillis() < connectDeadline) {
Thread.sleep(100L);
}
}
for (Princess bot : instance.bots) {
while (System.currentTimeMillis() < connectDeadline) {
if (bot.getLocalPlayer() != null
&& bot.getGame().getEntitiesOwnedBy(bot.getLocalPlayer()) > 0) {
break;
}
Thread.sleep(100L);
}
if (bot.getLocalPlayer() == null
|| bot.getGame().getEntitiesOwnedBy(bot.getLocalPlayer()) == 0) {
System.out.printf("!! bot %s never received entities before ready-up%n", bot.getName());
}
}
for (Princess bot : instance.bots) {
bot.sendDone(true);
}
return instance;
}
private static void waitForSettle(GameInstance instance, int settleRound, Sampler sampler,
List games, long deadline) throws InterruptedException {
long settleDeadline = Math.min(deadline, System.currentTimeMillis() + 300_000L);
while (System.currentTimeMillis() < settleDeadline) {
if (instance.round() >= settleRound || instance.isOver()) {
return;
}
Thread.sleep(1000L);
sampler.sample("settling-" + instance.index, games.size(), liveCount(games), totalRounds(games));
}
System.out.printf("!! game %d did not reach round %d before timeout (round=%d phase=%s)%n",
instance.index, settleRound, instance.round(), instance.phase());
}
private static int liveCount(List games) {
return (int) games.stream().filter(g -> !g.isOver()).count();
}
private static int totalRounds(List games) {
return games.stream().mapToInt(g -> Math.max(0, g.round())).sum();
}
private static boolean allOver(List games, int maxRounds) {
for (GameInstance g : games) {
if (!g.isOver() && g.round() < maxRounds) {
return false;
}
}
return true;
}
private static Map parseArgs(String[] args) {
Map map = new LinkedHashMap<>();
for (int i = 0; i < args.length - 1; i++) {
if (args[i].startsWith("--")) {
map.put(args[i].substring(2), args[i + 1]);
}
}
return map;
}
}