v24: Resolve GitHub Issues #1-#5
Issue #1 — Winner's life total now captured: - Parse verbose Forge log for life-total-change events - Track last-known life per seat via 3 regex patterns - Winner's final life set from log data (not left at default 40) - Losers who died to mill/poison retain their actual life total Issue #2 — Win condition classification formalized: - New WinCondition enum (schema/WinCondition.java) with 8 categories - Centralized Forge loss-reason-string → enum mapping (regex-based) - UNKNOWN fallback with logging for unrecognized loss reasons - All classification now goes through WinCondition.classify() Issue #3 — JVM startup overhead reduced: - Added -XX:+UseSerialGC (lighter GC for batch processes) - Added -XX:TieredStopAtLevel=1 (faster JIT, less compilation) - ForgeWorker stub class for future persistent batch mode - Estimated reduction: ~2-3s → ~1-1.5s per game startup Issue #4 — Single-threaded throughput optimization: - New CLI flags: --ai-simplified, --ai-think-time, --benchmark - JVM flags passed through to Forge subprocess - Benchmark mode: run 1 game before batch, report wall/Forge/overhead times - API support: aiSimplified, aiThinkTimeMs in StartRequest Issue #5 — Threading, progress, and backpressure: - --threads 0 or -1 auto-detects optimal count from CPU topology - detectOptimalThreads() estimates physical cores (excludes HT) - ProgressCallback interface for real-time sims/sec reporting - Per-game progress emitted as [PROGRESS] lines, parsed by lab_api.py - --max-queue flag for graceful backpressure (limits concurrent JVMs) - Final throughput summary printed at batch completion - StatusResponse now includes simsPerSec field Also: .gitignore restored, lab_api.py updated with all new API fields
This commit is contained in:
1 parent
a5fe8cb3fa
commit
a0adfc37e8
7 files changed
+736
-81
No files matched your search
@@ -75,7 +75,7 @@ public class LabCli implements Callable<Integer> {
|
||||
private int numGames;
|
||||
|
||||
@Option(names = {"--threads", "-t"}, defaultValue = "1",
|
||||
description = "Number of parallel threads (default: 1)")
|
||||
description = "Number of parallel threads (default: 1, 0 or -1 for auto-detect based on CPU cores)")
|
||||
private int threads;
|
||||
|
||||
@Option(names = {"--seed", "-s"}, defaultValue = "",
|
||||
@@ -166,6 +166,24 @@ public class LabCli implements Callable<Integer> {
|
||||
description = "Use greedy (argmax) action selection for learned policy (default: true)")
|
||||
private boolean policyGreedy;
|
||||
|
||||
// ── v6 Options — Performance (Issues #3-#5) ─────────────────
|
||||
|
||||
@Option(names = {"--ai-simplified"}, defaultValue = "false",
|
||||
description = "Use simplified/faster AI profile for simulations (less lookahead, faster games)")
|
||||
private boolean aiSimplified;
|
||||
|
||||
@Option(names = {"--ai-think-time"}, defaultValue = "-1",
|
||||
description = "Cap AI think time in ms per decision (-1 for unlimited, 500-2000 recommended)")
|
||||
private int aiThinkTimeMs;
|
||||
|
||||
@Option(names = {"--max-queue"}, defaultValue = "-1",
|
||||
description = "Max concurrent Forge subprocesses for backpressure (-1 for unlimited)")
|
||||
private int maxQueueDepth;
|
||||
|
||||
@Option(names = {"--benchmark"}, defaultValue = "false",
|
||||
description = "Run a single-game benchmark before the batch and report throughput metrics")
|
||||
private boolean benchmark;
|
||||
|
||||
@Option(names = {"--java17"}, defaultValue = "",
|
||||
description = "Path to Java 17 executable for running Forge (Forge requires Java 17, not 25+)")
|
||||
private String java17Path;
|
||||
@@ -213,7 +231,13 @@ public class LabCli implements Callable<Integer> {
|
||||
}
|
||||
}
|
||||
System.out.printf("Games: %d%n", numGames);
|
||||
System.out.printf("Threads: %d%n", threads);
|
||||
// Issue #5: Auto-detect thread count
|
||||
int effectiveThreads = threads;
|
||||
if (threads <= 0) {
|
||||
effectiveThreads = MultiThreadBatchRunner.detectOptimalThreads();
|
||||
}
|
||||
System.out.printf("Threads: %d%s%n", effectiveThreads,
|
||||
threads <= 0 ? " (auto-detected)" : "");
|
||||
System.out.printf("Clock: %ds per game%n", clockSeconds);
|
||||
|
||||
Long masterSeed = seed.isEmpty() ? null : Long.parseLong(seed);
|
||||
@@ -276,17 +300,49 @@ public class LabCli implements Callable<Integer> {
|
||||
|
||||
// Resolve Java 17 path for Forge subprocess
|
||||
String resolvedJava = resolveJava17Path();
|
||||
System.out.printf("Java: %s%n%n", resolvedJava);
|
||||
System.out.printf("Java: %s%n", resolvedJava);
|
||||
|
||||
if (threads > 1) {
|
||||
System.out.printf("Running %d games across %d threads...%n%n", numGames, threads);
|
||||
// Issue #4: Print AI optimization settings
|
||||
if (aiSimplified || aiThinkTimeMs > 0) {
|
||||
System.out.printf("AI Opts: simplified=%s, thinkTime=%s%n",
|
||||
aiSimplified, aiThinkTimeMs > 0 ? aiThinkTimeMs + "ms" : "unlimited");
|
||||
}
|
||||
System.out.println();
|
||||
|
||||
// Issue #4: Single-game benchmark if requested
|
||||
if (benchmark) {
|
||||
System.out.println("═══ BENCHMARK (single game) ═══");
|
||||
BatchRunner benchRunner = new BatchRunner(
|
||||
forgeJarPath, forgeWorkDir, decks, policy, quietMode, clockSeconds, resolvedJava);
|
||||
benchRunner.setAiOptimization(aiSimplified, aiThinkTimeMs);
|
||||
long benchStart = System.currentTimeMillis();
|
||||
List<GameResult> benchGames = benchRunner.runBatchSingleThread(1, 42L);
|
||||
long benchElapsed = System.currentTimeMillis() - benchStart;
|
||||
if (!benchGames.isEmpty()) {
|
||||
GameResult bg = benchGames.get(0);
|
||||
System.out.printf(" Wall time: %dms%n", benchElapsed);
|
||||
System.out.printf(" Forge time: %dms%n", bg.elapsedMs);
|
||||
System.out.printf(" JVM overhead: ~%dms%n", benchElapsed - bg.elapsedMs);
|
||||
System.out.printf(" Turns: %d%n", bg.totalTurns);
|
||||
System.out.printf(" Throughput: %.4f sims/sec%n", 1000.0 / benchElapsed);
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
if (effectiveThreads > 1) {
|
||||
System.out.printf("Running %d games across %d threads...%n%n", numGames, effectiveThreads);
|
||||
MultiThreadBatchRunner mtRunner = new MultiThreadBatchRunner(
|
||||
forgeJarPath, forgeWorkDir, decks, policy, threads, quietMode, clockSeconds, resolvedJava);
|
||||
forgeJarPath, forgeWorkDir, decks, policy, effectiveThreads, quietMode, clockSeconds, resolvedJava);
|
||||
mtRunner.setAiOptimization(aiSimplified, aiThinkTimeMs);
|
||||
if (maxQueueDepth > 0) {
|
||||
mtRunner.setMaxQueueDepth(maxQueueDepth);
|
||||
}
|
||||
games = mtRunner.runBatch(numGames, masterSeed);
|
||||
} else {
|
||||
System.out.printf("Running %d games (single thread)...%n%n", numGames);
|
||||
BatchRunner runner = new BatchRunner(
|
||||
forgeJarPath, forgeWorkDir, decks, policy, quietMode, clockSeconds, resolvedJava);
|
||||
runner.setAiOptimization(aiSimplified, aiThinkTimeMs);
|
||||
|
||||
// v4: Enable ML decision logging if requested
|
||||
if (mlLog) {
|
||||
|
||||
Reference in new issue
Block a user