Skip to content

Commit 80d37cf

Browse files
committed
[Tests failing] Finished test environment for sync/async tests. Configured tests to run with sufficient memory to allocate framebuffers.
1 parent 24aa1cb commit 80d37cf

File tree

9 files changed

+310
-64
lines changed

9 files changed

+310
-64
lines changed

pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
<configuration>
6464
<parallel>all</parallel> <!-- Run tests in parallel-->
6565
<useUnlimitedThreads>true</useUnlimitedThreads>
66+
<argLine>-Xms1g</argLine>
67+
<argLine>-Xmx1g</argLine>
6668
</configuration>
6769
</plugin>
6870
<plugin>

src/main/java/bwapi/BWClient.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,25 @@ public Game getGame() {
2525
}
2626

2727
/**
28-
* Returns JBWAPI performance metrics.
29-
* Metrics will be mostly empty if metrics collection isn't timersEnabled in the bot configuration
28+
* @return JBWAPI performance metrics.
3029
*/
30+
@SuppressWarnings("unused")
3131
public PerformanceMetrics getPerformanceMetrics() {
3232
return performanceMetrics;
3333
}
3434

3535
/**
36-
* Number of frames
36+
* @return The number of frames between the one exposed to the bot and the most recent received by JBWAPI.
37+
* This tracks the size of the frame buffer except when the game is paused (which results in multiple frames arriving with the same count).
3738
*/
39+
@SuppressWarnings("unused")
3840
public int framesBehind() {
39-
return botWrapper == null ? 0 : client.clientData().gameData().getFrameCount() - getGame().getFrameCount();
41+
return botWrapper == null ? 0 : Math.max(0, client.clientData().gameData().getFrameCount() - getGame().getFrameCount());
4042
}
4143

44+
/**
45+
* Start the game with default settings.
46+
*/
4247
public void startGame() {
4348
BWClientConfiguration configuration = new BWClientConfiguration();
4449
startGame(configuration);
@@ -65,7 +70,9 @@ public void startGame(BWClientConfiguration configuration) {
6570
configuration.validate();
6671
botWrapper = new BotWrapper(configuration, eventListener);
6772

68-
client = new Client(configuration);
73+
if (client == null) {
74+
client = new Client(configuration);
75+
}
6976
client.reconnect();
7077

7178
do {
@@ -95,4 +102,11 @@ public void startGame(BWClientConfiguration configuration) {
95102
botWrapper.endGame();
96103
} while (configuration.autoContinue);
97104
}
105+
106+
/**
107+
* Provides a Client. Intended for test consumers only.
108+
*/
109+
void setClient(Client client) {
110+
this.client = client;
111+
}
98112
}

src/main/java/bwapi/BotWrapper.java

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ of this software and associated documentation files (the "Software"), to deal
2626
package bwapi;
2727

2828
import java.nio.ByteBuffer;
29+
import java.util.concurrent.locks.ReentrantLock;
2930

3031
/**
3132
* Manages invocation of bot event handlers
@@ -39,6 +40,8 @@ class BotWrapper {
3940
private Thread botThread;
4041
private boolean gameOver;
4142
private PerformanceMetrics performanceMetrics;
43+
private Exception lastBotException;
44+
private ReentrantLock lastBotExceptionLock = new ReentrantLock();
4245

4346
BotWrapper(BWClientConfiguration configuration, BWEventListener eventListener) {
4447
this.configuration = configuration;
@@ -50,7 +53,9 @@ class BotWrapper {
5053
* Resets the BotWrapper for a new game.
5154
*/
5255
void startNewGame(ByteBuffer dataSource, PerformanceMetrics performanceMetrics) {
53-
frameBuffer.initialize(dataSource, performanceMetrics);
56+
if (configuration.async) {
57+
frameBuffer.initialize(dataSource, performanceMetrics);
58+
}
5459
this.performanceMetrics = performanceMetrics;
5560
game = new Game(liveClientData);
5661
liveClientData.setBuffer(dataSource);
@@ -80,7 +85,7 @@ void onFrame() {
8085
}
8186
/*
8287
Add a frame to buffer
83-
If buffer is full, it will wait until it has capacity
88+
If buffer is full, it will wait until it has capacity
8489
Wait for empty buffer OR termination condition
8590
*/
8691
boolean isFrameZero = liveClientData.gameData().getFrameCount() == 0;
@@ -89,6 +94,13 @@ void onFrame() {
8994
frameBuffer.lockSize.lock();
9095
try {
9196
while (!frameBuffer.empty()) {
97+
98+
// Make bot exceptions fall through to the main thread.
99+
Exception lastBotException = getLastBotException();
100+
if (lastBotException != null) {
101+
throw new RuntimeException(lastBotException);
102+
}
103+
92104
if (configuration.unlimitedFrameZero && isFrameZero) {
93105
frameBuffer.conditionSize.await();
94106
} else {
@@ -104,6 +116,9 @@ void onFrame() {
104116
}
105117
} else {
106118
handleEvents();
119+
if (lastBotException != null) {
120+
throw new RuntimeException(lastBotException);
121+
}
107122
}
108123
}
109124

@@ -118,6 +133,13 @@ void endGame() {
118133
}
119134
}
120135

136+
Exception getLastBotException() {
137+
lastBotExceptionLock.lock();
138+
Exception output = lastBotException;
139+
lastBotExceptionLock.unlock();
140+
return output;
141+
}
142+
121143
private Thread createBotThread() {
122144
return new Thread(() -> {
123145
while ( ! gameOver) {
@@ -144,12 +166,18 @@ private void handleEvents() {
144166
if (gameData.getFrameCount() > 0 || ! configuration.unlimitedFrameZero) {
145167
performanceMetrics.botResponse.startTiming();
146168
}
147-
for (int i = 0; i < gameData.getEventCount(); i++) {
148-
ClientData.Event event = gameData.getEvents(i);
149-
EventHandler.operation(eventListener, game, event);
150-
if (event.getType() == EventType.MatchEnd) {
151-
gameOver = true;
169+
try {
170+
for (int i = 0; i < gameData.getEventCount(); i++) {
171+
ClientData.Event event = gameData.getEvents(i);
172+
EventHandler.operation(eventListener, game, event);
173+
if (event.getType() == EventType.MatchEnd) {
174+
gameOver = true;
175+
}
152176
}
177+
} catch (Exception exception) {
178+
lastBotExceptionLock.lock();
179+
lastBotException = exception;
180+
lastBotExceptionLock.unlock();
153181
}
154182
performanceMetrics.botResponse.stopTiming();
155183
}

src/main/java/bwapi/FrameBuffer.java

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ of this software and associated documentation files (the "Software"), to deal
2525

2626
package bwapi;
2727

28+
import com.sun.jna.Platform;
2829
import sun.nio.ch.DirectBuffer;
2930
import java.nio.ByteBuffer;
3031
import java.util.ArrayList;
@@ -44,6 +45,7 @@ class FrameBuffer {
4445
private int stepGame = 0;
4546
private int stepBot = 0;
4647
private ArrayList<ByteBuffer> dataBuffer = new ArrayList<>();
48+
private final String architecture;
4749

4850
// Synchronization locks
4951
private final Lock lockWrite = new ReentrantLock();
@@ -53,8 +55,10 @@ class FrameBuffer {
5355
FrameBuffer(int size) {
5456
this.size = size;
5557
while(dataBuffer.size() < size) {
58+
System.out.println("Allocating " + BUFFER_SIZE / 1024 / 1024 + "mb");
5659
dataBuffer.add(ByteBuffer.allocateDirect(BUFFER_SIZE));
5760
}
61+
architecture = System.getProperty("sun.arch.data.model");
5862
}
5963

6064
/**
@@ -124,7 +128,7 @@ void enqueueFrame() {
124128

125129
performanceMetrics.copyingToBuffer.time(() -> {
126130
ByteBuffer dataTarget = dataBuffer.get(indexGame());
127-
copyBuffer(dataSource, dataTarget, BUFFER_SIZE);
131+
copyBuffer(dataSource, dataTarget);
128132
});
129133

130134
lockSize.lock();
@@ -135,17 +139,6 @@ void enqueueFrame() {
135139
} finally { lockWrite.unlock(); }
136140
}
137141

138-
private void copyBufferOld(ByteBuffer source, ByteBuffer dest, int size) {
139-
source.rewind();
140-
dest.rewind();
141-
dest.put(dataSource);
142-
}
143-
private void copyBuffer(ByteBuffer source, ByteBuffer dest, int size) {
144-
long destAddr = ((DirectBuffer)dest).address();
145-
long sourceAddr = ((DirectBuffer)source).address();
146-
MSVCRT.INSTANCE.memcpy(destAddr, sourceAddr, size);
147-
}
148-
149142
/**
150143
* Peeks the front-most value in the buffer.
151144
*/
@@ -168,4 +161,38 @@ void dequeue() {
168161
conditionSize.signalAll();
169162
} finally { lockSize.unlock(); }
170163
}
164+
165+
void copyBuffer(ByteBuffer source, ByteBuffer destination) {
166+
/*
167+
The speed at which we copy data into the frame buffer is a major cost of JBWAPI's asynchronous operation.
168+
Copy times observed in the wild range from 2.6ms - 12ms.
169+
170+
The normal Java way to execute this copy is via ByteBuffer.put(), which has reasonably good performance characteristics.
171+
Some experiments in 64-bit JRE have shown that using a native memcpy achieves a 35% speedup.
172+
Some experiments in 32-bit JRE show no difference in performance.
173+
174+
So, speculatively, we attempt to do a native memcpy.
175+
*/
176+
long addressSource = ((DirectBuffer) source).address();
177+
long addressDestination = ((DirectBuffer) destination).address();
178+
try {
179+
if (Platform.isWindows()) {
180+
if (Platform.is64Bit()) {
181+
MSVCRT.INSTANCE.memcpy(addressDestination, addressSource, FrameBuffer.BUFFER_SIZE);
182+
return;
183+
} else {
184+
MSVCRT.INSTANCE.memcpy((int) addressDestination, (int) addressSource, FrameBuffer.BUFFER_SIZE);
185+
return;
186+
}
187+
}
188+
}
189+
catch(Exception ignored) {}
190+
191+
// There's no specific case where we expect to fail above,
192+
// but this is a safe fallback regardless,
193+
// and serves to document the known-good (and cross-platform, for BWAPI 5) way to executing the copy.
194+
source.rewind();
195+
destination.rewind();
196+
destination.put(dataSource);
197+
}
171198
}

src/main/java/bwapi/Game.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ private static boolean hasPower(final int x, final int y, final UnitType unitTyp
142142
Call this method in EventHander::OnMatchStart
143143
*/
144144
void init() {
145+
System.out.println("Game.init()"); // TODO: REMOVE!
145146
visibleUnits.clear();
146147

147148
final int forceCount = gameData().getForceCount();

src/main/java/bwapi/MSVCRT.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,25 @@
33
import com.sun.jna.Library;
44
import com.sun.jna.Native;
55

6+
/**
7+
* JNI interface for access the native MSVC implementation of memcpy.
8+
*/
69
interface MSVCRT extends Library {
710
MSVCRT INSTANCE = Native.load("msvcrt.dll", MSVCRT.class);
811

12+
/**
13+
* 32-bit implementation of memcpy.
14+
* @param dest A 32-bit address of a memory block (likely from a ByteBuffer) to copy to
15+
* @param src A 32-bit address of a memory block (likely from a ByteBuffer) to copy from
16+
* @param count The number of bytes to copy
17+
*/
18+
long memcpy(int dest, int src, int count);
19+
20+
/**
21+
* 64-bit implementation of memcpy.
22+
* @param dest A 64-bit address of a memory block (likely from a ByteBuffer) to copy to
23+
* @param src A 64-bit address of a memory block (likely from a ByteBuffer) to copy from
24+
* @param count The number of bytes to copy
25+
*/
926
long memcpy(long dest, long src, int count);
1027
}

src/test/java/bwapi/GameBuilder.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,32 @@
1010

1111
public class GameBuilder {
1212

13+
public final static String DEFAULT_MAP_FILE = "(2)Benzene.scx";
14+
public final static String DEFAULT_BUFFER_PATH = "src/test/resources/" + DEFAULT_MAP_FILE + "_frame0_buffer.bin";
15+
1316
public static Game createGame() throws IOException {
14-
return createGame("(2)Benzene.scx");
17+
return createGame(DEFAULT_MAP_FILE);
1518
}
1619

1720
public static Game createGame(String mapName) throws IOException {
18-
final ByteBuffer buffer = binToBuffer("src/test/resources/" + mapName + "_frame0_buffer.bin");
21+
final ByteBuffer buffer = binToBuffer(DEFAULT_BUFFER_PATH);
1922
return createGame(new Client(buffer));
2023
}
2124

25+
public static Game createGame(Client client) {
26+
final Game game = new Game(client.clientData());
27+
game.init();
28+
return game;
29+
}
30+
31+
public static Game createGameUnchecked() {
32+
try {
33+
return GameBuilder.createGame();
34+
} catch (IOException exception) {
35+
throw new RuntimeException(exception);
36+
}
37+
}
38+
2239
public static ByteBuffer binToBuffer(String binLocation) throws IOException {
2340
final byte[] compressedBytes = Files.readAllBytes(Paths.get(binLocation));
2441
final ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -32,9 +49,12 @@ public static ByteBuffer binToBuffer(String binLocation) throws IOException {
3249
return buffer;
3350
}
3451

35-
public static Game createGame(Client client) throws IOException {
36-
final Game game = new Game(client.clientData());
37-
game.init();
38-
return game;
52+
public static ByteBuffer binToBufferUnchecked(String binLocation) {
53+
try {
54+
return binToBuffer(binLocation);
55+
} catch(IOException exception) {
56+
throw new RuntimeException(exception);
57+
}
3958
}
59+
4060
}

0 commit comments

Comments
 (0)