Skip to content

Commit af4c986

Browse files
committed
Fixing performance metrics. Fixed framebuffer thinking it's full one frame too soon.
1 parent 20f9d00 commit af4c986

File tree

7 files changed

+92
-41
lines changed

7 files changed

+92
-41
lines changed

src/main/java/bwapi/BWClientConfiguration.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ public class BWClientConfiguration {
2525
*/
2626
public boolean unlimitedFrameZero = true;
2727

28+
/**
29+
* The maximum amount of time the bot is supposed to spend on a single frame.
30+
* In asynchronous mode, JBWAPI will attempt to let the bot use up to this much time to process all frames before returning control to BWAPI.
31+
* In synchronous mode, JBWAPI is not empowered to prevent the bot to exceed this amount, but will record overruns in performance metrics.
32+
* Real-time human play typically uses the "fastest" game speed, which has 42.86ms (42,860ns) between frames.
33+
*/
34+
public int maxFrameDurationMs = 40;
35+
2836
/**
2937
* Runs the bot in asynchronous mode. Asynchronous mode helps attempt to ensure that the bot adheres to real-time performance constraints.
3038
*
@@ -38,12 +46,6 @@ public class BWClientConfiguration {
3846
*/
3947
public boolean async = false;
4048

41-
/**
42-
* If JBWAPI detects that this much time (in nanoseconds) has passed since a bot's event handlers began, returns control back to BWAPI.
43-
* Real-time human play typically uses the "fastest" game speed, which has 42.86ms (42,860ns) between frames.
44-
*/
45-
public int asyncFrameDurationMs = 40;
46-
4749
/**
4850
* The maximum number of frames to buffer while waiting on a bot.
4951
* Each frame buffered adds about 33 megabytes to JBWAPI's memory footprint.
@@ -59,8 +61,8 @@ public class BWClientConfiguration {
5961
* Checks that the configuration is in a valid state. Throws an IllegalArgumentException if it isn't.
6062
*/
6163
public void validate() {
62-
if (async && asyncFrameDurationMs < 0) {
63-
throw new IllegalArgumentException("asyncFrameDurationMs needs to be a non-negative number (it's how long JBWAPI waits for a bot response before returning control to BWAPI).");
64+
if (async && maxFrameDurationMs < 0) {
65+
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).");
6466
}
6567
if (async && asyncFrameBufferSize < 1) {
6668
throw new IllegalArgumentException("asyncFrameBufferSize needs to be a positive number (There needs to be at least one frame buffer).");

src/main/java/bwapi/BotWrapper.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class BotWrapper {
4646
BotWrapper(BWClientConfiguration configuration, BWEventListener eventListener) {
4747
this.configuration = configuration;
4848
this.eventListener = eventListener;
49-
frameBuffer = configuration.async ? new FrameBuffer(configuration.asyncFrameBufferSize) : null;
49+
frameBuffer = configuration.async ? new FrameBuffer(configuration) : null;
5050
}
5151

5252
/**
@@ -79,7 +79,7 @@ void onFrame() {
7979
if (configuration.async) {
8080
configuration.log("Main: onFrame asynchronous start");
8181
long startNanos = System.nanoTime();
82-
long endNanos = startNanos + configuration.asyncFrameDurationMs * 1000000;
82+
long endNanos = startNanos + configuration.maxFrameDurationMs * 1000000;
8383
if (botThread == null) {
8484
configuration.log("Main: Starting bot thread");
8585
botThread = createBotThread();
@@ -98,7 +98,6 @@ void onFrame() {
9898
frameBuffer.lockSize.lock();
9999
try {
100100
while (!frameBuffer.empty()) {
101-
configuration.log("Main: Waiting for empty frame buffer");
102101

103102
// Make bot exceptions fall through to the main thread.
104103
Throwable lastThrow = getLastBotThrow();
@@ -108,12 +107,15 @@ void onFrame() {
108107
}
109108

110109
if (configuration.unlimitedFrameZero && frame == 0) {
111-
configuration.log("Main: Waiting indefinitely on frame " + frame);
110+
configuration.log("Main: Waiting indefinitely on frame #" + frame);
112111
frameBuffer.conditionSize.await();
113112
} else {
114113
long remainingNanos = endNanos - System.nanoTime();
115-
if (remainingNanos <= 0) break;
116-
configuration.log("Main: Waiting " + remainingNanos / 1000000 + "ms for bot");
114+
if (remainingNanos <= 0) {
115+
configuration.log("Main: Out of time in frame #" + frame);
116+
break;
117+
}
118+
configuration.log("Main: Waiting " + remainingNanos / 1000000 + "ms for bot on frame #" + frame);
117119
frameBuffer.conditionSize.awaitNanos(remainingNanos);
118120
}
119121
}
@@ -155,11 +157,11 @@ private Thread createBotThread() {
155157
configuration.log("Bot: Thread started");
156158
while (!gameOver) {
157159

158-
configuration.log("Bot: Attempting to handle next frame");
160+
configuration.log("Bot: Ready for another frame");
159161
frameBuffer.lockSize.lock();
160162
try {
161163
while (frameBuffer.empty()) {
162-
configuration.log("Bot: Waiting for next frame");
164+
configuration.log("Bot: Waiting for a frame");
163165
performanceMetrics.botIdle.startTiming();
164166
frameBuffer.conditionSize.awaitUninterruptibly();
165167
}
@@ -170,10 +172,11 @@ private Thread createBotThread() {
170172

171173
configuration.log("Bot: Peeking next frame");
172174
game.clientData().setBuffer(frameBuffer.peek());
173-
performanceMetrics.frameBufferSize.record(frameBuffer.framesBuffered() - 1);
174175

175176
configuration.log("Bot: Handling frame #" + game.getFrameCount());
176177
handleEvents();
178+
179+
configuration.log("Bot: Events done. Dequeuing frame #" + game.getFrameCount());
177180
frameBuffer.dequeue();
178181
}
179182
} catch (Throwable throwable) {
@@ -200,6 +203,10 @@ private void handleEvents() {
200203
gameOver = gameOver || gameData.getEvents(i).getType() == EventType.MatchEnd;
201204
}
202205

206+
if (configuration.async) {
207+
performanceMetrics.framesBehind.record(frameBuffer.framesBuffered() - 1);
208+
}
209+
203210
performanceMetrics.botResponse.timeIf(
204211
! gameOver && (gameData.getFrameCount() > 0 || ! configuration.unlimitedFrameZero),
205212
() -> {

src/main/java/bwapi/FrameBuffer.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class FrameBuffer {
4141

4242
private ByteBuffer liveData;
4343
private PerformanceMetrics performanceMetrics;
44+
private BWClientConfiguration configuration;
4445
private int size;
4546
private int stepGame = 0;
4647
private int stepBot = 0;
@@ -51,8 +52,9 @@ class FrameBuffer {
5152
final Lock lockSize = new ReentrantLock();
5253
final Condition conditionSize = lockSize.newCondition();
5354

54-
FrameBuffer(int size) {
55-
this.size = size;
55+
FrameBuffer(BWClientConfiguration configuration) {
56+
this.size = configuration.asyncFrameBufferSize;
57+
this.configuration = configuration;
5658
while(dataBuffer.size() < size) {
5759
dataBuffer.add(ByteBuffer.allocateDirect(BUFFER_SIZE));
5860
}
@@ -94,7 +96,7 @@ boolean empty() {
9496
boolean full() {
9597
lockSize.lock();
9698
try {
97-
return framesBuffered() >= size - 1;
99+
return framesBuffered() >= size;
98100
} finally {
99101
lockSize.unlock();
100102
}
@@ -117,6 +119,7 @@ void enqueueFrame() {
117119
lockSize.lock();
118120
try {
119121
while (full()) {
122+
configuration.log("Main: Waiting for frame buffer capacity");
120123
performanceMetrics.intentionallyBlocking.startTiming();
121124
conditionSize.awaitUninterruptibly();
122125
}
@@ -130,6 +133,7 @@ void enqueueFrame() {
130133

131134
lockSize.lock();
132135
try {
136+
performanceMetrics.frameBufferSize.record(framesBuffered());
133137
++stepGame;
134138
conditionSize.signalAll();
135139
} finally { lockSize.unlock(); }

src/main/java/bwapi/PerformanceMetric.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ void record(double value) {
8686
avgValueExceeding = (avgValueExceeding * samplesExceeding + value) / (samplesExceeding + 1d);
8787
++samplesExceeding;
8888
}
89-
System.out.println(name + " #" + samples + " = " + value); // TODO: REMOVE
89+
System.out.println("Metric: " + name + " (n=" + samples + "): " + value); // TODO: REMOVE
9090
}
9191

9292
/**

src/main/java/bwapi/PerformanceMetrics.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,17 @@ public class PerformanceMetrics {
2525
public PerformanceMetric intentionallyBlocking;
2626

2727
/**
28-
* Number of frames backed up in the frame buffer, after releasing each frame.
28+
* Number of frames backed up in the frame buffer, after enqueuing each frame (and not including the newest frame).
2929
* Applicable only in asynchronous mode.
3030
*/
3131
public PerformanceMetric frameBufferSize;
3232

33+
/**
34+
* Number of frames behind real-time the bot is at the time it handles events.
35+
* Applicable only in asynchronous mode.
36+
*/
37+
public PerformanceMetric framesBehind;
38+
3339
/**
3440
* Time spent applying bot commands to the live frame.
3541
*/
@@ -55,13 +61,14 @@ public PerformanceMetrics(BWClientConfiguration configuration) {
5561
final int frameDurationBufferMs = 5;
5662
final int sideEffectsBufferMs = 1;
5763
final int realTimeFrameMs = 42;
58-
totalFrameDuration = new PerformanceMetric("Total frame duration", configuration.asyncFrameDurationMs + frameDurationBufferMs);
64+
totalFrameDuration = new PerformanceMetric("Total frame duration", configuration.maxFrameDurationMs + frameDurationBufferMs);
5965
copyingToBuffer = new PerformanceMetric("Time copying to buffer", 5);
6066
intentionallyBlocking = new PerformanceMetric("Intentionally blocking", 0);
6167
frameBufferSize = new PerformanceMetric("Frames buffered", 0);
68+
framesBehind = new PerformanceMetric("Frames behind", 0);
6269
flushSideEffects = new PerformanceMetric("Flush side effects", sideEffectsBufferMs );
63-
botResponse = new PerformanceMetric("Bot Responses", configuration.asyncFrameDurationMs);
64-
bwapiResponse = new PerformanceMetric("BWAPI Responses", realTimeFrameMs);
70+
botResponse = new PerformanceMetric("Bot responses", configuration.maxFrameDurationMs);
71+
bwapiResponse = new PerformanceMetric("BWAPI responses", realTimeFrameMs);
6572
botIdle = new PerformanceMetric("Bot idle", Long.MAX_VALUE);
6673
}
6774

@@ -72,6 +79,7 @@ public String toString() {
7279
+ "\n" + copyingToBuffer.toString()
7380
+ "\n" + intentionallyBlocking.toString()
7481
+ "\n" + frameBufferSize.toString()
82+
+ "\n" + framesBehind.toString()
7583
+ "\n" + flushSideEffects.toString()
7684
+ "\n" + botResponse.toString()
7785
+ "\n" + bwapiResponse.toString();

src/test/java/bwapi/SynchronizationEnvironment.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ public class SynchronizationEnvironment {
5252
}).when(listener).onEnd(anyBoolean());
5353
doAnswer(answer -> {
5454
configuration.log("Test: onFrame()");
55-
if (onFrames.containsKey(liveFrame())) {
56-
onFrames.get(liveFrame()).run();
55+
int botFrame = bwClient.getGame().getFrameCount();
56+
if (onFrames.containsKey(botFrame)) {
57+
onFrames.get(botFrame).run();
5758
}
5859
return null;
5960
}).when(listener).onFrame();

src/test/java/bwapi/SynchronizationTest.java

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ private void assertWithin(String message, double expected, double actual, double
3939
}
4040

4141
@Test
42-
public void sync_IfException_ThrowException() throws InterruptedException {
42+
public void sync_IfException_ThrowException() {
4343
SynchronizationEnvironment environment = new SynchronizationEnvironment();
4444
environment.configuration.async = false;
4545
environment.onFrame(0, () -> { throw new RuntimeException("Simulated bot exception"); });
4646
assertThrows(RuntimeException.class, environment::runGame);
4747
}
4848

4949
@Test
50-
public void async_IfException_ThrowException() throws InterruptedException {
50+
public void async_IfException_ThrowException() {
5151
// An exception in the bot thread must be re-thrown by the main thread.
5252
SynchronizationEnvironment environment = new SynchronizationEnvironment();
5353
environment.configuration.async = true;
@@ -57,10 +57,10 @@ public void async_IfException_ThrowException() throws InterruptedException {
5757
}
5858

5959
@Test
60-
public void sync_IfDelay_ThenNoBuffer() throws InterruptedException {
60+
public void sync_IfDelay_ThenNoBuffer() {
6161
SynchronizationEnvironment environment = new SynchronizationEnvironment();
6262
environment.configuration.async = false;
63-
environment.configuration.asyncFrameDurationMs = 1;
63+
environment.configuration.maxFrameDurationMs = 1;
6464
environment.configuration.asyncFrameBufferSize = 3;
6565

6666
IntStream.range(0, 5).forEach(frame -> {
@@ -76,10 +76,10 @@ public void sync_IfDelay_ThenNoBuffer() throws InterruptedException {
7676
}
7777

7878
@Test
79-
public void async_IfBotDelay_ThenClientBuffers() throws InterruptedException {
79+
public void async_IfBotDelay_ThenClientBuffers() {
8080
SynchronizationEnvironment environment = new SynchronizationEnvironment();
8181
environment.configuration.async = true;
82-
environment.configuration.asyncFrameDurationMs = 10;
82+
environment.configuration.maxFrameDurationMs = 10;
8383
environment.configuration.asyncFrameBufferSize = 4;
8484

8585
environment.onFrame(1, () -> {
@@ -99,10 +99,10 @@ public void async_IfBotDelay_ThenClientBuffers() throws InterruptedException {
9999
}
100100

101101
@Test
102-
public void async_IfBotDelay_ThenClientStalls() throws InterruptedException {
102+
public void async_IfBotDelay_ThenClientStalls() {
103103
SynchronizationEnvironment environment = new SynchronizationEnvironment();
104104
environment.configuration.async = true;
105-
environment.configuration.asyncFrameDurationMs = 50;
105+
environment.configuration.maxFrameDurationMs = 50;
106106
environment.configuration.asyncFrameBufferSize = 5;
107107

108108
environment.onFrame(1, () -> {
@@ -124,11 +124,11 @@ public void async_IfBotDelay_ThenClientStalls() throws InterruptedException {
124124
}
125125

126126
@Test
127-
public void async_IfFrameZeroWaitsEnabled_ThenAllowInfiniteTime() throws InterruptedException {
127+
public void async_IfFrameZeroWaitsEnabled_ThenAllowInfiniteTime() {
128128
SynchronizationEnvironment environment = new SynchronizationEnvironment();
129129
environment.configuration.async = true;
130130
environment.configuration.unlimitedFrameZero = true;
131-
environment.configuration.asyncFrameDurationMs = 5;
131+
environment.configuration.maxFrameDurationMs = 5;
132132
environment.configuration.asyncFrameBufferSize = 2;
133133

134134
environment.onFrame(0, () -> {
@@ -142,11 +142,11 @@ public void async_IfFrameZeroWaitsEnabled_ThenAllowInfiniteTime() throws Interru
142142
}
143143

144144
@Test
145-
public void async_IfFrameZeroWaitsDisabled_ThenClientBuffers() throws InterruptedException {
145+
public void async_IfFrameZeroWaitsDisabled_ThenClientBuffers() {
146146
SynchronizationEnvironment environment = new SynchronizationEnvironment();
147147
environment.configuration.async = true;
148148
environment.configuration.unlimitedFrameZero = false;
149-
environment.configuration.asyncFrameDurationMs = 5;
149+
environment.configuration.maxFrameDurationMs = 5;
150150
environment.configuration.asyncFrameBufferSize = 2;
151151

152152
environment.onFrame(0, () -> {
@@ -175,8 +175,37 @@ public void async_MeasurePerformance_IntentionallyBlocking() {
175175
}
176176

177177
@Test
178-
public void async_MeasurePerformance_FrameBufferSize() {
178+
public void async_MeasurePerformance_FrameBufferSizeAndFramesBehind() {
179+
SynchronizationEnvironment environment = new SynchronizationEnvironment();
180+
environment.configuration.async = true;
181+
environment.configuration.unlimitedFrameZero = true;
182+
environment.configuration.asyncFrameBufferSize = 3;
183+
environment.configuration.maxFrameDurationMs = 20;
184+
environment.configuration.logVerbosely = true;
185+
186+
environment.onFrame(5, () -> {
187+
assertWithin("5: Frame buffer average", 0, environment.metrics().frameBufferSize.avgValue, 0.1);
188+
assertWithin("5: Frame buffer minimum", 0, environment.metrics().frameBufferSize.minValue, 0.1);
189+
assertWithin("5: Frame buffer maximum", 0, environment.metrics().frameBufferSize.maxValue, 0.1);
190+
assertWithin("5: Frame buffer previous", 0, environment.metrics().frameBufferSize.lastValue, 0.1);
191+
assertWithin("5: Frames behind average", 0, environment.metrics().framesBehind.avgValue, 0.1);
192+
assertWithin("5: Frames behind minimum", 0, environment.metrics().framesBehind.minValue, 0.1);
193+
assertWithin("5: Frames behind maximum", 0, environment.metrics().framesBehind.maxValue, 0.1);
194+
assertWithin("5: Frames behind previous", 0, environment.metrics().framesBehind.lastValue, 0.1);
195+
sleepUnchecked(200);
196+
});
197+
environment.onFrame(6, () -> {
198+
assertWithin("6: Frame buffer average", 1 / 6.0 + 2 / 7.0, environment.metrics().frameBufferSize.avgValue, 0.1);
199+
assertWithin("6: Frame buffer minimum", 0, environment.metrics().frameBufferSize.minValue, 0.1);
200+
assertWithin("6: Frame buffer maximum", 2, environment.metrics().frameBufferSize.maxValue, 0.1);
201+
assertWithin("6: Frame buffer previous", 2, environment.metrics().frameBufferSize.lastValue, 0.1);
202+
assertWithin("6: Frames behind average", 1 / 6.0, environment.metrics().framesBehind.avgValue, 0.1);
203+
assertWithin("6: Frames behind minimum", 0, environment.metrics().framesBehind.minValue, 0.1);
204+
assertWithin("6: Frames behind maximum", 1, environment.metrics().framesBehind.maxValue, 0.1);
205+
assertWithin("6: Frames behind previous", 1, environment.metrics().framesBehind.lastValue, 0.1);
206+
});
179207

208+
environment.runGame(8);
180209
}
181210

182211
@Test
@@ -191,7 +220,7 @@ public void async_MeasurePerformance_FlushSideEffects() {
191220
private final static long MS_MARGIN = 10;
192221

193222
@Test
194-
public void async_MeasurePerformance_BotResponse() {
223+
public void MeasurePerformance_BotResponse() {
195224
SynchronizationEnvironment environment = new SynchronizationEnvironment();
196225

197226
// Frame zero appears to take an extra 60ms, so let's disable timing for it

0 commit comments

Comments
 (0)