Skip to content

Commit 8bb4f80

Browse files
committed
Implemented unsafe asynchronous mode, allowing the bot to read directly from live game data while the frame buffer is being populated. There's some additional cruft lying around and there's no guarantee that this will actually work in practice, but the test case is passing.
1 parent 879f6cf commit 8bb4f80

File tree

7 files changed

+201
-63
lines changed

7 files changed

+201
-63
lines changed

src/main/java/bwapi/BWClient.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,25 @@ public void startGame(BWClientConfiguration configuration) {
9393
}
9494
}
9595
while (liveGameData.isInGame()) {
96+
boolean timeFrame = liveGameData.getFrameCount() > 0 || ! configuration.unlimitedFrameZero;
97+
performanceMetrics.frameDuration5.timeIf(timeFrame, () ->
98+
performanceMetrics.frameDuration10.timeIf(timeFrame, () ->
99+
performanceMetrics.frameDuration15.timeIf(timeFrame, () ->
100+
performanceMetrics.frameDuration20.timeIf(timeFrame, () ->
101+
performanceMetrics.frameDuration25.timeIf(timeFrame, () ->
102+
performanceMetrics.frameDuration30.timeIf(timeFrame, () ->
103+
performanceMetrics.frameDuration35.timeIf(timeFrame, () ->
104+
performanceMetrics.frameDuration40.timeIf(timeFrame, () ->
105+
performanceMetrics.frameDuration45.timeIf(timeFrame, () ->
106+
performanceMetrics.frameDuration50.timeIf(timeFrame, () ->
107+
performanceMetrics.frameDuration55.timeIf(timeFrame, () ->
96108
performanceMetrics.totalFrameDuration.timeIf(
97-
liveGameData.getFrameCount() > 0 || ! configuration.unlimitedFrameZero,
109+
timeFrame,
98110
() -> {
99111
botWrapper.onFrame();
100112
performanceMetrics.flushSideEffects.time(() -> getGame().sideEffects.flushTo(liveGameData));
101-
});
113+
})
114+
)))))))))));
102115
performanceMetrics.bwapiResponse.time(client::update);
103116
if (!client.isConnected()) {
104117
System.out.println("Reconnecting...");

src/main/java/bwapi/BWClientConfiguration.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,16 @@ public class BWClientConfiguration {
5050
* The maximum number of frames to buffer while waiting on a bot.
5151
* Each frame buffered adds about 33 megabytes to JBWAPI's memory footprint.
5252
*/
53-
public int asyncFrameBufferSize = 10;
53+
public int asyncFrameBufferCapacity = 10;
54+
55+
/**
56+
* Enables thread-unsafe async mode.
57+
* In this mode, the bot is allowed to read directly from shared memory until shared memory has been copied into the frame buffer,
58+
* at wihch point the bot switches to using the frame buffer.
59+
* This should enhance performance by allowing the bot to act while the frame is copied, but poses unidentified risk due to
60+
* the non-thread-safe switc from shared memory reads to frame buffer reads.
61+
*/
62+
public boolean asyncUnsafe = false;
5463

5564
/**
5665
* Toggles verbose logging, particularly of synchronization steps.
@@ -64,8 +73,8 @@ public void validate() {
6473
if (async && maxFrameDurationMs < 0) {
6574
throw new IllegalArgumentException("maxFrameDurationMs needs to be a non-negative number (it's how long JBWAPI waits for a bot response before returning control to BWAPI).");
6675
}
67-
if (async && asyncFrameBufferSize < 1) {
68-
throw new IllegalArgumentException("asyncFrameBufferSize needs to be a positive number (There needs to be at least one frame buffer).");
76+
if (async && asyncFrameBufferCapacity < 1) {
77+
throw new IllegalArgumentException("asyncFrameBufferCapacity needs to be a positive number (There needs to be at least one frame buffer).");
6978
}
7079
}
7180

src/main/java/bwapi/BotWrapper.java

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ class BotWrapper {
1111
private final BWClientConfiguration configuration;
1212
private final BWEventListener eventListener;
1313
private final FrameBuffer frameBuffer;
14-
private Game game;
14+
private ByteBuffer liveData;
15+
private Game botGame;
1516
private Thread botThread;
1617
private boolean gameOver;
1718
private PerformanceMetrics performanceMetrics;
1819
private Throwable lastBotThrow;
1920
private ReentrantLock lastBotThrowLock = new ReentrantLock();
21+
private ReentrantLock unsafeReadReadyLock = new ReentrantLock();
22+
private boolean unsafeReadReady = false;
2023

2124
BotWrapper(BWClientConfiguration configuration, BWEventListener eventListener) {
2225
this.configuration = configuration;
@@ -25,16 +28,17 @@ class BotWrapper {
2528
}
2629

2730
/**
28-
* Resets the BotWrapper for a new game.
31+
* Resets the BotWrapper for a new botGame.
2932
*/
3033
void startNewGame(ByteBuffer liveData, PerformanceMetrics performanceMetrics) {
3134
if (configuration.async) {
3235
frameBuffer.initialize(liveData, performanceMetrics);
3336
}
3437
this.performanceMetrics = performanceMetrics;
35-
game = new Game();
36-
game.clientData().setBuffer(liveData);
38+
botGame = new Game();
39+
botGame.clientData().setBuffer(liveData);
3740
liveClientData.setBuffer(liveData);
41+
this.liveData = liveData;
3842
botThread = null;
3943
gameOver = false;
4044
}
@@ -44,7 +48,25 @@ void startNewGame(ByteBuffer liveData, PerformanceMetrics performanceMetrics) {
4448
* In asynchronous mode this Game object may point at a copy of a previous frame.
4549
*/
4650
Game getGame() {
47-
return game;
51+
return botGame;
52+
}
53+
54+
private boolean isUnsafeReadReady() {
55+
unsafeReadReadyLock.lock();
56+
try { return unsafeReadReady; }
57+
finally { unsafeReadReadyLock.unlock(); }
58+
}
59+
60+
private void setUnsafeReadReady(boolean value) {
61+
unsafeReadReadyLock.lock();
62+
try { unsafeReadReady = value; }
63+
finally { unsafeReadReadyLock.unlock(); }
64+
frameBuffer.lockSize.lock();
65+
try {
66+
frameBuffer.conditionSize.signalAll();
67+
} finally {
68+
frameBuffer.lockSize.unlock();
69+
}
4870
}
4971

5072
/**
@@ -61,17 +83,44 @@ void onFrame() {
6183
botThread.setName("JBWAPI Bot");
6284
botThread.start();
6385
}
64-
/*
65-
Add a frame to buffer
66-
If buffer is full, it will wait until it has capacity
67-
Wait for empty buffer OR termination condition
68-
*/
86+
87+
// Unsafe mode:
88+
// If the frame buffer is empty (meaning the bot must be idle)
89+
// allow the bot to read directly from shared memory while we copy it over
90+
if (configuration.asyncUnsafe) {
91+
frameBuffer.lockSize.lock();
92+
try {
93+
if (frameBuffer.empty()) {
94+
configuration.log("Main: Putting bot on live data");
95+
botGame.clientData().setBuffer(liveData);
96+
setUnsafeReadReady(true);
97+
} else {
98+
setUnsafeReadReady(false);
99+
}
100+
} finally {
101+
frameBuffer.lockSize.unlock();
102+
}
103+
}
104+
105+
// Add a frame to buffer
106+
// If buffer is full, will wait until it has capacity.
107+
// Then wait for the buffer to empty or to run out of time in the frame.
69108
int frame = liveClientData.gameData().getFrameCount();
70109
configuration.log("Main: Enqueuing frame #" + frame);
71110
frameBuffer.enqueueFrame();
111+
configuration.log("Main: Enqueued frame #" + frame);
72112
frameBuffer.lockSize.lock();
73113
try {
74114
while (!frameBuffer.empty()) {
115+
// Unsafe mode: Move the bot off of live data onto the frame buffer
116+
// This is the unsafe step!
117+
// We don't synchronize on calls which access the buffer
118+
// (to avoid tens of thousands of synchronized calls per frame)
119+
// so there's no guarantee of safety here.
120+
if (configuration.asyncUnsafe && frameBuffer.size() == 1) {
121+
configuration.log("Main: Weaning bot off live data");
122+
botGame.clientData().setBuffer(frameBuffer.peek());
123+
}
75124

76125
// Make bot exceptions fall through to the main thread.
77126
Throwable lastThrow = getLastBotThrow();
@@ -130,26 +179,35 @@ private Thread createBotThread() {
130179
configuration.log("Bot: Thread started");
131180
while (!gameOver) {
132181

182+
boolean doUnsafeRead = false;
133183
configuration.log("Bot: Ready for another frame");
134-
performanceMetrics.botIdle.time(() -> {
135-
frameBuffer.lockSize.lock();
136-
try {
137-
while (frameBuffer.empty()) {
138-
configuration.log("Bot: Waiting for a frame");
139-
frameBuffer.conditionSize.awaitUninterruptibly();
140-
}
141-
} finally {
142-
frameBuffer.lockSize.unlock();
184+
performanceMetrics.botIdle.startTiming();
185+
frameBuffer.lockSize.lock();
186+
try {
187+
doUnsafeRead = isUnsafeReadReady();
188+
while ( ! doUnsafeRead && frameBuffer.empty()) {
189+
configuration.log("Bot: Waiting for a frame");
190+
frameBuffer.conditionSize.awaitUninterruptibly();
191+
doUnsafeRead = isUnsafeReadReady();
143192
}
144-
});
193+
} finally {
194+
frameBuffer.lockSize.unlock();
195+
}
196+
performanceMetrics.botIdle.stopTiming();
145197

146-
configuration.log("Bot: Peeking next frame");
147-
game.clientData().setBuffer(frameBuffer.peek());
198+
if (doUnsafeRead) {
199+
configuration.log("Bot: Reading live frame");
200+
setUnsafeReadReady(false);
201+
// TODO: Maybe we should point it at live data from here?
202+
} else {
203+
configuration.log("Bot: Peeking next frame from buffer");
204+
botGame.clientData().setBuffer(frameBuffer.peek());
205+
}
148206

149-
configuration.log("Bot: Handling frame #" + game.getFrameCount());
207+
configuration.log("Bot: Handling events on frame #" + botGame.getFrameCount());
150208
handleEvents();
151209

152-
configuration.log("Bot: Events done. Dequeuing frame #" + game.getFrameCount());
210+
configuration.log("Bot: Events handled. Dequeuing frame #" + botGame.getFrameCount());
153211
frameBuffer.dequeue();
154212
}
155213
} catch (Throwable throwable) {
@@ -169,7 +227,7 @@ private Thread createBotThread() {
169227
}
170228

171229
private void handleEvents() {
172-
ClientData.GameData gameData = game.clientData().gameData();
230+
ClientData.GameData gameData = botGame.clientData().gameData();
173231

174232
// Populate gameOver before invoking event handlers (in case the bot throws)
175233
for (int i = 0; i < gameData.getEventCount(); i++) {
@@ -184,7 +242,7 @@ private void handleEvents() {
184242
! gameOver && (gameData.getFrameCount() > 0 || ! configuration.unlimitedFrameZero),
185243
() -> {
186244
for (int i = 0; i < gameData.getEventCount(); i++) {
187-
EventHandler.operation(eventListener, game, gameData.getEvents(i));
245+
EventHandler.operation(eventListener, botGame, gameData.getEvents(i));
188246
}
189247
});
190248
}

src/main/java/bwapi/FrameBuffer.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class FrameBuffer {
1717
private ByteBuffer liveData;
1818
private PerformanceMetrics performanceMetrics;
1919
private BWClientConfiguration configuration;
20-
private int size;
20+
private int capacity;
2121
private int stepGame = 0;
2222
private int stepBot = 0;
2323
private ArrayList<ByteBuffer> dataBuffer = new ArrayList<>();
@@ -27,9 +27,9 @@ class FrameBuffer {
2727
final Condition conditionSize = lockSize.newCondition();
2828

2929
FrameBuffer(BWClientConfiguration configuration) {
30-
this.size = configuration.asyncFrameBufferSize;
30+
this.capacity = configuration.asyncFrameBufferCapacity;
3131
this.configuration = configuration;
32-
while(dataBuffer.size() < size) {
32+
while(dataBuffer.size() < capacity) {
3333
dataBuffer.add(ByteBuffer.allocateDirect(BUFFER_SIZE));
3434
}
3535
}
@@ -52,36 +52,43 @@ synchronized int framesBuffered() {
5252
}
5353

5454
/**
55-
* @return Whether the frame buffer is empty and has no frames available for the bot to consume.
55+
* @return Number of frames currently stored in the buffer
5656
*/
57-
boolean empty() {
57+
int size() {
5858
lockSize.lock();
5959
try {
60-
return framesBuffered() <= 0;
60+
return framesBuffered();
6161
} finally {
6262
lockSize.unlock();
6363
}
6464
}
6565

66+
/**
67+
* @return Whether the frame buffer is empty and has no frames available for the bot to consume.
68+
*/
69+
boolean empty() {
70+
return size() <= 0;
71+
}
72+
6673
/**
6774
* @return Whether the frame buffer is full and can not buffer any additional frames.
6875
* When the frame buffer is full, JBWAPI must wait for the bot to complete a frame before returning control to StarCraft.
6976
*/
7077
boolean full() {
7178
lockSize.lock();
7279
try {
73-
return framesBuffered() >= size;
80+
return framesBuffered() >= capacity;
7481
} finally {
7582
lockSize.unlock();
7683
}
7784
}
7885

7986
private int indexGame() {
80-
return stepGame % size;
87+
return stepGame % capacity;
8188
}
8289

8390
private int indexBot() {
84-
return stepBot % size;
91+
return stepBot % capacity;
8592
}
8693

8794
/**

src/main/java/bwapi/PerformanceMetrics.java

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ public class PerformanceMetrics {
5757
*/
5858
public PerformanceMetric botIdle;
5959

60+
public PerformanceMetric frameDuration5;
61+
public PerformanceMetric frameDuration10;
62+
public PerformanceMetric frameDuration15;
63+
public PerformanceMetric frameDuration20;
64+
public PerformanceMetric frameDuration25;
65+
public PerformanceMetric frameDuration30;
66+
public PerformanceMetric frameDuration35;
67+
public PerformanceMetric frameDuration40;
68+
public PerformanceMetric frameDuration45;
69+
public PerformanceMetric frameDuration50;
70+
public PerformanceMetric frameDuration55;
71+
6072
private BWClientConfiguration configuration;
6173

6274
public PerformanceMetrics(BWClientConfiguration configuration) {
@@ -65,18 +77,28 @@ public PerformanceMetrics(BWClientConfiguration configuration) {
6577
}
6678

6779
void reset() {
68-
final int frameDurationBufferMs = 5;
6980
final int sideEffectsBufferMs = 1;
7081
final int realTimeFrameMs = 42;
71-
totalFrameDuration = new PerformanceMetric("Total frame duration", configuration.maxFrameDurationMs + frameDurationBufferMs);
72-
copyingToBuffer = new PerformanceMetric("Time copying to buffer", 15);
73-
intentionallyBlocking = new PerformanceMetric("Intentionally blocking", 0);
82+
totalFrameDuration = new PerformanceMetric("JBWAPI frame duration", configuration.maxFrameDurationMs + 5);
83+
copyingToBuffer = new PerformanceMetric("Time copying to buffer", configuration.maxFrameDurationMs + 5);
84+
intentionallyBlocking = new PerformanceMetric("Blocking with full buffer", 0);
7485
frameBufferSize = new PerformanceMetric("Frames buffered", 0);
75-
framesBehind = new PerformanceMetric("Frames behind", 0);
76-
flushSideEffects = new PerformanceMetric("Flush side effects", sideEffectsBufferMs );
77-
botResponse = new PerformanceMetric("Bot responses", configuration.maxFrameDurationMs);
78-
bwapiResponse = new PerformanceMetric("BWAPI responses", realTimeFrameMs);
86+
framesBehind = new PerformanceMetric("Frames behind real-time", 0);
87+
flushSideEffects = new PerformanceMetric("Flushing side effects", sideEffectsBufferMs );
88+
botResponse = new PerformanceMetric("Bot event handlers", configuration.maxFrameDurationMs);
89+
bwapiResponse = new PerformanceMetric("Responses from BWAPI", realTimeFrameMs);
7990
botIdle = new PerformanceMetric("Bot idle", Long.MAX_VALUE);
91+
frameDuration5 = new PerformanceMetric("JBWAPI frame @ 5ms", 5);
92+
frameDuration10 = new PerformanceMetric("JBWAPI frame @ 10ms", 10);
93+
frameDuration15 = new PerformanceMetric("JBWAPI frame @ 15ms", 15);
94+
frameDuration20 = new PerformanceMetric("JBWAPI frame @ 20ms", 20);
95+
frameDuration25 = new PerformanceMetric("JBWAPI frame @ 25ms", 25);
96+
frameDuration30 = new PerformanceMetric("JBWAPI frame @ 30ms", 30);
97+
frameDuration35 = new PerformanceMetric("JBWAPI frame @ 35ms", 35);
98+
frameDuration40 = new PerformanceMetric("JBWAPI frame @ 40ms", 40);
99+
frameDuration45 = new PerformanceMetric("JBWAPI frame @ 45ms", 45);
100+
frameDuration50 = new PerformanceMetric("JBWAPI frame @ 50ms", 50);
101+
frameDuration55 = new PerformanceMetric("JBWAPI frame @ 55ms", 55);
80102
}
81103

82104
@Override
@@ -90,6 +112,17 @@ public String toString() {
90112
+ "\n" + flushSideEffects.toString()
91113
+ "\n" + botResponse.toString()
92114
+ "\n" + bwapiResponse.toString()
93-
+ "\n" + botIdle.toString();
115+
+ "\n" + botIdle.toString()
116+
+ "\n" + frameDuration5.toString()
117+
+ "\n" + frameDuration10.toString()
118+
+ "\n" + frameDuration15.toString()
119+
+ "\n" + frameDuration20.toString()
120+
+ "\n" + frameDuration25.toString()
121+
+ "\n" + frameDuration30.toString()
122+
+ "\n" + frameDuration35.toString()
123+
+ "\n" + frameDuration40.toString()
124+
+ "\n" + frameDuration45.toString()
125+
+ "\n" + frameDuration50.toString()
126+
+ "\n" + frameDuration55.toString();
94127
}
95128
}

src/test/java/bwapi/SynchronizationEnvironment.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ void runGame(int onEndFrame) {
8686
if (configuration.async) {
8787
final long MEGABYTE = 1024 * 1024;
8888
long memoryFree = Runtime.getRuntime().freeMemory() / MEGABYTE;
89-
long memoryRequired = configuration.asyncFrameBufferSize * ClientData.GameData.SIZE / MEGABYTE;
89+
long memoryRequired = configuration.asyncFrameBufferCapacity * ClientData.GameData.SIZE / MEGABYTE;
9090
assertTrue(
9191
"Unit test needs to be run with sufficient memory to allocate frame buffer. Has "
9292
+ memoryFree

0 commit comments

Comments
 (0)