Skip to content

Asynchronous mode #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 58 commits into from
Dec 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
3808f73
Added client configuration object to replace individual constructor a…
dgant Jul 17, 2020
0cae24e
Extracted portions of Client to GameDataUtils. Removed the bot loop e…
dgant Jul 19, 2020
486bf4f
Restored existing (sync) behavior.
dgant Jul 20, 2020
43c9ac3
Finished first pass async implementation, untested
dgant Jul 21, 2020
165db79
Assigning the asynchronous bot perspective to Game
dgant Jul 26, 2020
79f6fe0
Corrected copying of shared memory to FrameBuffer. Corrected measurem…
dgant Jul 27, 2020
d4d03b6
First end-to-end working version. Added side effect queue to route co…
dgant Jul 28, 2020
fd921e8
Fixed strings
dgant Jul 29, 2020
931beb1
Replaced sleeps with awaits
dgant Jul 30, 2020
5c9dc36
Was getting memory allocation exceptions when replacing FrameBuffer's…
dgant Jul 30, 2020
341056d
Fixed end-of-game behavior. Moved synchronization out of BWClient and…
dgant Jul 31, 2020
30e7a52
Implemented frame timeouts.
dgant Jul 31, 2020
5b36174
Reduced stdout spamming
dgant Jul 31, 2020
156c382
Fixed unit test which relied on ClientData being non-final for mocking
dgant Aug 7, 2020
4cf39fc
Removed commented-out printlns. Stubbed diagnostics configuration var…
dgant Aug 7, 2020
3159f73
Fixed erroneous conversion between nanoseconds and milliseconds. Firs…
dgant Aug 8, 2020
be52a24
Fixed some collection and display of performance metrics
dgant Aug 8, 2020
edddd2b
Fixed condition for waiting for bot to finish (instead of returning c…
dgant Aug 10, 2020
24aa1cb
use memcpy to copy the framebuffer
JasperGeurtz Aug 10, 2020
80d37cf
[Tests failing] Finished test environment for sync/async tests. Confi…
dgant Aug 13, 2020
d8e045a
[Failing tests] Unit tests now terminate even if the bot thread dies …
dgant Aug 13, 2020
4be2706
Restored stepwise logging to monitor asynchronous steps
dgant Aug 13, 2020
c30289b
Fixed bug in which BotWrapper was looking at bot data to determine th…
dgant Aug 13, 2020
1970dd5
Removed console noise caused by bot thread dying (as expected) from e…
dgant Aug 13, 2020
20f9d00
All tests passing. Stubbed out performance metrics tests and added bo…
dgant Aug 14, 2020
af4c986
Fixing performance metrics. Fixed framebuffer thinking it's full one …
dgant Aug 15, 2020
0e35168
Finished asynchronous unit tests; fixed miscellaneous performance mea…
dgant Aug 16, 2020
93a9791
Cleanup
dgant Aug 16, 2020
cb5a3fd
Fixed unit tests
dgant Aug 16, 2020
4520108
Minor tweaks
dgant Aug 16, 2020
879f6cf
Clarified thresholds for performance metrics
dgant Sep 20, 2020
8bb4f80
Implemented unsafe asynchronous mode, allowing the bot to read direct…
dgant Sep 21, 2020
512795b
Implemented partial frame copies, cutting about 3/4 off the amount of…
dgant Sep 22, 2020
d016e03
Added some more metrics. Added multiple thresholds to metrics.
dgant Sep 23, 2020
71dbc8d
Trying various tactics to resolve timeouts. Added a timer resolution …
dgant Sep 23, 2020
b735923
Removed unnecessary allocations in Game API.
dgant Sep 24, 2020
024630b
Made thread priority reduction dependent on async mode
dgant Sep 24, 2020
a333f15
Added performance metric for frame duration which includes time to wr…
dgant Sep 25, 2020
ab695ad
Assembled complete suite of frame-duration and BWAPI communication pe…
dgant Sep 26, 2020
ebae46c
Fixed typo in performance logging
dgant Sep 26, 2020
b40e612
Added performance metrics related to event count to diagnose whether …
dgant Sep 26, 2020
aa17f4f
Fixed up unit tests
dgant Oct 5, 2020
dcaaa12
Moved PerformanceMetric(s) properties behind getters
dgant Oct 5, 2020
e021d19
Put configuration properties behind getters and chain-setters
dgant Oct 5, 2020
736ff2c
Configuration now can not be modified after game starts
dgant Oct 5, 2020
26ee6a2
Removed the attempted TimerResolutionThread hack
dgant Oct 5, 2020
30d6792
Cosmetic changes/cleanup
dgant Oct 5, 2020
4bbef45
Merged jdk9 fixes into async branch
dgant Oct 5, 2020
00b1266
Fixed use case of copying frames via ByteBuffer API
dgant Oct 5, 2020
04db785
Disabled latency compensation in async mode.
dgant Oct 5, 2020
e76bed2
Made configuration accessors public
dgant Oct 5, 2020
c193d9f
remove parallel tests and replace memcpy with unsafe.copyMemory
JasperGeurtz Oct 6, 2020
a078758
Reenabled memory arguments for unit tests
dgant Oct 6, 2020
a7f2f08
Removed more Windows-specific JNA calls
dgant Oct 6, 2020
34e5d4c
Added test re-runs. Made classes final to aid inlining
dgant Dec 27, 2020
43f5bd5
Merge remote-tracking branch 'origin/develop' into async
dgant Dec 28, 2020
bcaafb9
De-finalized classes to fix mocking. Widened threshold on synchroniza…
dgant Dec 28, 2020
cdb67d5
Trying to ensure tests are run with adequate memory available
dgant Dec 28, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<parallel>all</parallel> <!-- Run tests in parallel-->
<useUnlimitedThreads>true</useUnlimitedThreads>
<!-- <parallel>all</parallel> &lt;!&ndash; Run tests in parallel&ndash;&gt;-->
<!-- <useUnlimitedThreads>true</useUnlimitedThreads>-->
<rerunFailingTestsCount>10</rerunFailingTestsCount>
<argLine>-Xms1g -Xmx1g</argLine>
</configuration>
</plugin>
<plugin>
Expand Down
118 changes: 98 additions & 20 deletions src/main/java/bwapi/BWClient.java
Original file line number Diff line number Diff line change
@@ -1,64 +1,142 @@
package bwapi;

import com.sun.jna.platform.win32.Kernel32;

import java.util.Objects;

/**
* Client class to connect to the game with.
*/
public class BWClient {
private BWClientConfiguration configuration = new BWClientConfiguration();
private final BWEventListener eventListener;
private final boolean debugConnection;
private EventHandler handler;
private BotWrapper botWrapper;
private Client client;
private PerformanceMetrics performanceMetrics;

public BWClient(final BWEventListener eventListener) {
this(eventListener, false);
}

/**
* @param debugConnection set to `true` for more explicit error messages (might spam the terminal).
* `false` by default
*/
public BWClient(final BWEventListener eventListener, final boolean debugConnection) {
Objects.requireNonNull(eventListener);
this.debugConnection = debugConnection;
this.eventListener = eventListener;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debugConnection moved to BWClientConfiguration


/**
* Get the {@link Game} instance of the currently running game.
* When running in asynchronous mode, this is the game from the bot's perspective, eg. potentially a previous frame.
*/
public Game getGame() {
return handler == null ? null : handler.getGame();
return botWrapper == null ? null : botWrapper.getGame();
}

/**
* @return JBWAPI performance metrics.
*/
public PerformanceMetrics getPerformanceMetrics() {
return performanceMetrics;
}

/**
* @return The current configuration
*/
public BWClientConfiguration getConfiguration() {
return configuration;
}

/**
* @return Whether the current frame should be subject to timing.
*/
boolean doTime() {
return ! configuration.getUnlimitedFrameZero() || (client.isConnected() && client.liveClientData().gameData().getFrameCount() > 0);
}

/**
* @return The number of frames between the one exposed to the bot and the most recent received by JBWAPI.
* This tracks the size of the frame buffer except when the game is paused (which results in multiple frames arriving with the same count).
*/
public int framesBehind() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iis this tested in the synchro test? impl looks good to me, but just making sure

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there are a few tests which verify its values.

return botWrapper == null ? 0 : Math.max(0, client.liveClientData().gameData().getFrameCount() - getGame().getFrameCount());
}

/**
* For internal test use.
*/
Client getClient() {
return client;
}

/**
* Start the game with default settings.
*/
public void startGame() {
startGame(false);
BWClientConfiguration configuration = new BWClientConfiguration();
startGame(configuration);
}

/**
* Start the game.
*
* @param autoContinue automatically continue playing the next game(s). false by default
*/
@Deprecated
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This overload is replaced by one which takes a BWClientConfiguration to indicate thhe autoContinue setting.

public void startGame(boolean autoContinue) {
Client client = new Client(debugConnection);
BWClientConfiguration configuration = new BWClientConfiguration();
configuration.withAutoContinue(autoContinue);
startGame(configuration);
}

/**
* Start the game.
*
* @param gameConfiguration Settings for playing games with this client.
*/
public void startGame(BWClientConfiguration gameConfiguration) {
gameConfiguration.validateAndLock();
this.configuration = gameConfiguration;
this.performanceMetrics = new PerformanceMetrics(configuration);
botWrapper = new BotWrapper(configuration, eventListener);

// Use reduced priority to encourage Windows to give priority to StarCraft.exe/BWAPI.
// If BWAPI doesn't get priority, it may not detect completion of a frame on our end in timely fashion.
Thread.currentThread().setName("JBWAPI Client");
if (configuration.getAsync()) {
Thread.currentThread().setPriority(4);
}

if (client == null) {
client = new Client(this);
}
client.reconnect();
handler = new EventHandler(eventListener, client);

do {
while (!getGame().isInGame()) {
ClientData.GameData liveGameData = client.liveClientData().gameData();
while (!liveGameData.isInGame()) {
if (!client.isConnected()) {
return;
}
client.update(handler);
client.sendFrameReceiveFrame();
if (liveGameData.isInGame()) {
performanceMetrics = new PerformanceMetrics(configuration);
botWrapper.startNewGame(client.mapFile(), performanceMetrics);
}
}
while (getGame().isInGame()) {
client.update(handler);
while (liveGameData.isInGame()) {
botWrapper.onFrame();
performanceMetrics.getFlushSideEffects().time(() -> getGame().sideEffects.flushTo(liveGameData));
performanceMetrics.getFrameDurationReceiveToSend().stopTiming();

client.sendFrameReceiveFrame();
if (!client.isConnected()) {
System.out.println("Reconnecting...");
client.reconnect();
}
}
} while (autoContinue); // lgtm [java/constant-loop-condition]
botWrapper.endGame();
} while (configuration.getAutoContinue());
}

/**
* Provides a Client. Intended for test consumers only.
*/
void setClient(Client client) {
this.client = client;
}
}
163 changes: 163 additions & 0 deletions src/main/java/bwapi/BWClientConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package bwapi;

/**
* Configuration for constructing a BWClient
*/
public class BWClientConfiguration {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer the variables to be private and modifiable using a withDebugConnection() , with the comment above the variable added to the withX method

example API in my mind:

BWClientConfiguration bwc = new BWClientConfiguration()
    .withAutoContinue()
    .withAsync()
    .withMaxFrameDuration(30);
/**Javadoc*/
public BWClientConfiguration withMaxFrameDuration(int value) {
    if (value < 1) {
        throw new IllegalArgumentException("maxFrameDuration needs to be a non-negative number (it's how long JBWAPI waits for a bot response before returning control to BWAPI).");
    }
    this.maxFrameDuration = value;
    return this;
}

This way we don't need a "validate" method as we can check the arguments on creating the configuration
and users don't need to change internal variable names of objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, although I think the validate method is still necessary since it considers interactions between properties and it would be annoying to require them to be set in a specific order, and it doesn't prevent modification of the properties.


/**
* Set to `true` for more explicit error messages (which might spam the terminal).
*/
public BWClientConfiguration withDebugConnection(boolean value) {
throwIfLocked();
debugConnection = value;
return this;
}
public boolean getDebugConnection() {
return debugConnection;
}
private boolean debugConnection;

/**
* When true, restarts the client loop when a game ends, allowing the client to play multiple games without restarting.
*/
public BWClientConfiguration withAutoContinue(boolean value) {
throwIfLocked();
autoContinue = value;
return this;
}
public boolean getAutoContinue() {
return autoContinue;
}
private boolean autoContinue = false;

/**
* Most bot tournaments allow bots to take an indefinite amount of time on frame #0 (the first frame of the game) to analyze the map and load data,
* as the bot has no prior access to BWAPI or game information.
*
* This flag indicates that taking arbitrarily long on frame zero is acceptable.
* Performance metrics omit the frame as an outlier.
* Asynchronous operation will block until the bot's event handlers are complete.
*/
public BWClientConfiguration withUnlimitedFrameZero(boolean value) {
throwIfLocked();
unlimitedFrameZero = value;
return this;
}
public boolean getUnlimitedFrameZero() {
return unlimitedFrameZero;
}
private boolean unlimitedFrameZero = true;

/**
* The maximum amount of time the bot is supposed to spend on a single frame.
* 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.
* In synchronous mode, JBWAPI is not empowered to prevent the bot to exceed this amount, but will record overruns in performance metrics.
* Real-time human play typically uses the "fastest" game speed, which has 42.86ms (42,860ns) between frames.
*/
public BWClientConfiguration withMaxFrameDurationMs(int value) {
throwIfLocked();
maxFrameDurationMs = value;
return this;
}
public int getMaxFrameDurationMs() {
return maxFrameDurationMs;
}
private int maxFrameDurationMs = 40;

/**
* Runs the bot in asynchronous mode. Asynchronous mode helps attempt to ensure that the bot adheres to real-time performance constraints.
*
* Humans playing StarCraft (and some tournaments) expect bots to return commands within a certain period of time; ~42ms for humans ("fastesT" game speed),
* and some tournaments enforce frame-wise time limits (at time of writing, 55ms for COG and AIIDE; 85ms for SSCAIT).
*
* Asynchronous mode invokes bot event handlers in a separate thread, and if all event handlers haven't returned by a specified period of time, sends an
* returns control to StarCraft, allowing the game to proceed while the bot continues to step in the background. This increases the likelihood of meeting
* real-time performance requirements, while not fully guaranteeing it (subject to the whims of the JVM thread scheduler), at a cost of the bot possibly
* issuing commands later than intended, and a marginally larger memory footprint.
*
* Asynchronous mode is not compatible with latency compensation. Enabling asynchronous mode automatically disables latency compensation.
*/
public BWClientConfiguration withAsync(boolean value) {
throwIfLocked();
async = value;
return this;
}
public boolean getAsync() {
return async;
}
private boolean async = false;

/**
* The maximum number of frames to buffer while waiting on a bot.
* Each frame buffered adds about 33 megabytes to JBWAPI's memory footprint.
*/
public BWClientConfiguration withAsyncFrameBufferCapacity(int size) {
throwIfLocked();
asyncFrameBufferCapacity = size;
return this;
}
public int getAsyncFrameBufferCapacity() {
return asyncFrameBufferCapacity;
}
private int asyncFrameBufferCapacity = 10;

/**
* Enables thread-unsafe async mode.
* In this mode, the bot is allowed to read directly from shared memory until shared memory has been copied into the frame buffer,
* at wihch point the bot switches to using the frame buffer.
* This should enhance performance by allowing the bot to act while the frame is copied, but poses unidentified risk due to
* the non-thread-safe switc from shared memory reads to frame buffer reads.
*/
public BWClientConfiguration withAsyncUnsafe(boolean value) {
throwIfLocked();
asyncUnsafe = value;
return this;
}
public boolean getAsyncUnsafe() {
return asyncUnsafe;
}
private boolean asyncUnsafe = false;

/**
* Toggles verbose logging, particularly of synchronization steps.
*/
public BWClientConfiguration withLogVerbosely(boolean value) {
throwIfLocked();
logVerbosely = value;
return this;
}
public boolean getLogVerbosely() {
return logVerbosely;
}
private boolean logVerbosely = false;

/**
* Checks that the configuration is in a valid state. Throws an IllegalArgumentException if it isn't.
*/
void validateAndLock() {
if (asyncUnsafe && ! async) {
throw new IllegalArgumentException("asyncUnsafe mode needs async mode.");
}
if (async && maxFrameDurationMs < 0) {
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).");
}
if (async && asyncFrameBufferCapacity < 1) {
throw new IllegalArgumentException("asyncFrameBufferCapacity needs to be a positive number (There needs to be at least one frame buffer).");
}
locked = true;
}
private boolean locked = false;

void throwIfLocked() {
if (locked) {
throw new RuntimeException("Configuration can not be modified after the game has started");
}
}

void log(String value) {
if (logVerbosely) {
System.out.println(value);
}
}
}
Loading