Skip to content

Commit 71dbc8d

Browse files
committed
Trying various tactics to resolve timeouts. Added a timer resolution hack which should force Windows to use the lowest resolution (16ms) for GetTickCount() (though it appeared to be doing that in all environments anyway). Reduced thread priority for JBWAPI + bot threads to increase likelihood that BWAPI receives priority sooner.
1 parent d016e03 commit 71dbc8d

File tree

7 files changed

+101
-14
lines changed

7 files changed

+101
-14
lines changed

src/main/java/bwapi/BWClient.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package bwapi;
22

3+
import com.sun.jna.platform.win32.Kernel32;
4+
35
import java.util.Objects;
46

57
/**
@@ -10,10 +12,12 @@ public class BWClient {
1012
private BotWrapper botWrapper;
1113
private Client client;
1214
private PerformanceMetrics performanceMetrics;
15+
private TimerResolutionThread timerResolutionThread;
1316

1417
public BWClient(final BWEventListener eventListener) {
1518
Objects.requireNonNull(eventListener);
1619
this.eventListener = eventListener;
20+
this.timerResolutionThread = new TimerResolutionThread();
1721
}
1822

1923
/**
@@ -75,6 +79,10 @@ public void startGame(BWClientConfiguration configuration) {
7579
configuration.validate();
7680
botWrapper = new BotWrapper(configuration, eventListener);
7781

82+
// Use reduced priority to encourage Windows to give priority to StarCraft.exe/BWAPI.
83+
// If BWAPI doesn't get priority, it may not detect completion of a frame on our end in timely fashion.
84+
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
85+
7886
if (client == null) {
7987
client = new Client(configuration);
8088
}
@@ -94,12 +102,29 @@ public void startGame(BWClientConfiguration configuration) {
94102
}
95103
while (liveGameData.isInGame()) {
96104
boolean timeFrame = liveGameData.getFrameCount() > 0 || ! configuration.unlimitedFrameZero;
105+
106+
long ticksBefore = Kernel32.INSTANCE.GetTickCount();
97107
performanceMetrics.totalFrameDuration.timeIf(
98108
timeFrame,
99109
() -> {
100110
botWrapper.onFrame();
101111
performanceMetrics.flushSideEffects.time(() -> getGame().sideEffects.flushTo(liveGameData));
102112
});
113+
long ticksAfter = Kernel32.INSTANCE.GetTickCount();
114+
if (timeFrame) {
115+
long deltaTicks = ticksAfter - ticksBefore;
116+
long deltaMillis = (long) performanceMetrics.totalFrameDuration.runningTotal.last;
117+
long delta = deltaMillis - deltaTicks;
118+
if (Math.abs(delta) > 1000) {
119+
System.out.println("Got weird tick delta: " + ticksAfter + ", " + ticksBefore + ", " + deltaTicks + ", " + deltaMillis + ", " + delta);
120+
performanceMetrics.weirdTimeDelta.record(1);
121+
} else if (delta > 0) {
122+
performanceMetrics.positiveTimeDelta.record(delta);
123+
} else if (delta < 0) {
124+
performanceMetrics.negativeTimeDelta.record(-delta);
125+
}
126+
}
127+
103128
performanceMetrics.bwapiResponse.time(client::update);
104129
if (!client.isConnected()) {
105130
System.out.println("Reconnecting...");

src/main/java/bwapi/BotWrapper.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ void onFrame() {
8181
configuration.log("Main: Starting bot thread");
8282
botThread = createBotThread();
8383
botThread.setName("JBWAPI Bot");
84+
// Reduced priority helps ensure that StarCraft.exe/BWAPI pick up on our frame completion in timely fashion
85+
botThread.setPriority(Thread.MIN_PRIORITY);
8486
botThread.start();
8587
}
8688

src/main/java/bwapi/Client.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ void update() {
247247
while (code != 2) {
248248
try {
249249
code = pipeObjectHandle.readByte();
250+
if (code != 2) {
251+
Thread.sleep(0, 1);
252+
}
250253
}
251254
catch (Exception e) {
252255
System.err.println("failed, disconnecting");

src/main/java/bwapi/MSVCRT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import com.sun.jna.Native;
55

66
/**
7-
* JNI interface for access the native MSVC implementation of memcpy.
7+
* JNI interface for accessing native MSVC code.
88
*/
99
interface MSVCRT extends Library {
1010
MSVCRT INSTANCE = Native.load("msvcrt.dll", MSVCRT.class);

src/main/java/bwapi/PerformanceMetric.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
public class PerformanceMetric {
1010
class RunningTotal {
1111
int samples = 0;
12+
double last = 0d;
1213
double mean = 0d;
1314
double min = Long.MAX_VALUE;
1415
double max = Long.MIN_VALUE;
1516
void record(double value) {
17+
last = value;
1618
min = Math.min(min, value);
1719
max = Math.max(max, value);
1820
mean = (mean * samples + value) / (samples + 1d);
@@ -35,7 +37,7 @@ public String toString() {
3537
return "";
3638
}
3739
DecimalFormat formatter = new DecimalFormat("###,###.#");
38-
return "\n>= " + formatter.format(threshold) + ": " + runningTotal.samples + " samples averaging " + formatter.format(runningTotal.mean) + ").";
40+
return "\n>= " + formatter.format(threshold) + ": " + runningTotal.samples + " samples averaging " + formatter.format(runningTotal.mean);
3941
}
4042
}
4143

src/main/java/bwapi/PerformanceMetrics.java

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,29 @@ public class PerformanceMetrics {
6969
*/
7070
PerformanceMetric excessSleep;
7171

72+
/**
73+
* Instances of System.nanoTime() measuring a longer frame duration with respect to WinAPI's GetTickCount.
74+
*/
75+
PerformanceMetric positiveTimeDelta;
76+
77+
/**
78+
* Instances of System.nanoTime() measuring a shorter frame duration with respect to WinAPI's GetTickCount.
79+
*/
80+
PerformanceMetric negativeTimeDelta;
81+
82+
/**
83+
* When Kernel32.INSTANCE.GetTickCount() returns at least one inexplicable value
84+
*/
85+
PerformanceMetric weirdTimeDelta;
86+
7287
private BWClientConfiguration configuration;
7388

7489
PerformanceMetrics(BWClientConfiguration configuration) {
7590
this.configuration = configuration;
7691
reset();
7792
}
7893

79-
void reset() {
94+
public void reset() {
8095
totalFrameDuration = new PerformanceMetric("JBWAPI frame duration", 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 85);
8196
copyingToBuffer = new PerformanceMetric("Time copying to buffer", 5, 10, 15, 20, 25, 30);
8297
intentionallyBlocking = new PerformanceMetric("Blocking with full buffer", 0);
@@ -88,21 +103,27 @@ void reset() {
88103
botIdle = new PerformanceMetric("Bot idle", Long.MAX_VALUE);
89104
clientIdle = new PerformanceMetric("Client idling", configuration.maxFrameDurationMs);
90105
excessSleep = new PerformanceMetric("Excess sleep", 1, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 85);
106+
positiveTimeDelta = new PerformanceMetric("Positive timer delta", 1, 2, 3, 4, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 85);
107+
negativeTimeDelta = new PerformanceMetric("Negative timer delta", 1, 2, 3, 4, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 85);
108+
weirdTimeDelta = new PerformanceMetric("Weird timer delta");
91109
}
92110

93111
@Override
94112
public String toString() {
95113
return "Performance metrics:"
96-
+ "\n" + totalFrameDuration.toString()
97-
+ "\n" + copyingToBuffer.toString()
98-
+ "\n" + intentionallyBlocking.toString()
99-
+ "\n" + frameBufferSize.toString()
100-
+ "\n" + framesBehind.toString()
101-
+ "\n" + flushSideEffects.toString()
102-
+ "\n" + botResponse.toString()
103-
+ "\n" + bwapiResponse.toString()
104-
+ "\n" + botIdle.toString()
105-
+ "\n" + clientIdle.toString()
106-
+ "\n" + excessSleep.toString();
114+
+ "\n" + totalFrameDuration.toString()
115+
+ "\n" + copyingToBuffer.toString()
116+
+ "\n" + intentionallyBlocking.toString()
117+
+ "\n" + frameBufferSize.toString()
118+
+ "\n" + framesBehind.toString()
119+
+ "\n" + flushSideEffects.toString()
120+
+ "\n" + botResponse.toString()
121+
+ "\n" + bwapiResponse.toString()
122+
+ "\n" + botIdle.toString()
123+
+ "\n" + clientIdle.toString()
124+
+ "\n" + excessSleep.toString()
125+
+ "\n" + positiveTimeDelta.toString()
126+
+ "\n" + negativeTimeDelta.toString()
127+
+ "\n" + weirdTimeDelta.toString();
107128
}
108129
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package bwapi;
2+
3+
/**
4+
* See https://hazelcast.com/blog/locksupport-parknanos-under-the-hood-and-the-curious-case-of-parking-part-ii-windows/
5+
*
6+
* "Is this it? Just a single flag and my JVM will use high-resolution timers?
7+
* Well, it turns out it’s not that simple.
8+
* This flag has been reported to be broken since 2006!
9+
* However, the very same bug report suggests a very interesting workaround:"
10+
*
11+
* “Do not use ForceTimeHighResolution but instead,
12+
* at the start of the application create and start
13+
* a daemon Thread that simply sleeps for a very long time
14+
* (that isn’t a multiple of 10ms)
15+
* as this will set the timer period to be 1ms for the duration of that sleep
16+
* – which in this case is the lifetime of the VM"
17+
*
18+
* "What the R$#@#@ have I just read?
19+
* Let me rephrase it:
20+
* “If you want a high-resolution timer then just start a new thread and let it sleep forever”.
21+
* That’s simply hilarious!
22+
*/
23+
class TimerResolutionThread {
24+
TimerResolutionThread() {
25+
Thread t = new Thread(() -> {
26+
try {
27+
Thread.sleep(Long.MAX_VALUE);
28+
} catch (InterruptedException e) { // a delicious interrupt, omm, omm
29+
}
30+
});
31+
t.setDaemon(true);
32+
t.start();
33+
}
34+
}

0 commit comments

Comments
 (0)