Skip to content

Commit 2bedc52

Browse files
author
Jasper Geurtz
committed
Reduce Async FrameBuffer memory usage with PackedWrappedBuffer and static offsets (#90)
- Add PackedWrappedBuffer with pre-shifted slice views (slice2, slice3, slice4) so all BWAPI field offsets remain static compile-time constants with zero runtime branches - Keep original WrappedBuffer pure, only adding a package-private view constructor - Auto-generate AsyncClientData and slice-routed struct views (AsyncPosition, AsyncEvent, AsyncCommand, AsyncUnitCommand) via DumpToClient - Preserve unsafe async handoffs across live and packed buffers for both in-flight reads and retained struct views - Add unit and synchronization tests covering memory reduction, static/dynamic routing, and unsafe async handoffs
1 parent 484be45 commit 2bedc52

11 files changed

Lines changed: 1191 additions & 249 deletions

‎src/main/java/bwapi/AsyncClientData.java‎

Lines changed: 491 additions & 0 deletions
Large diffs are not rendered by default.

‎src/main/java/bwapi/BotWrapperAsync.java‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ class BotWrapperAsync extends BotWrapper {
2929
void startNewGame(WrappedBuffer liveData, PerformanceMetrics performanceMetrics) {
3030
frameBuffer.initialize(liveData, performanceMetrics);
3131

32-
super.startNewGame(liveData, performanceMetrics);
32+
this.performanceMetrics = performanceMetrics;
33+
AsyncClientData asyncClientData = new AsyncClientData(liveData);
34+
botGame = new Game(asyncClientData);
35+
botGame.setConfiguration(configuration);
36+
botGame.botClientData().setBuffer(liveData);
37+
gameOver = false;
3338

3439
liveClientData.setBuffer(liveData);
3540
this.liveData = liveData;

‎src/main/java/bwapi/ClientData.java‎

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import com.sun.jna.Pointer;
44

5-
final class ClientData {
6-
private WrappedBuffer buffer;
7-
private GameData gameData;
5+
class ClientData {
6+
WrappedBuffer buffer;
7+
GameData gameData;
88
ClientData() {
9-
gameData = new ClientData.GameData(0);
9+
gameData = createGameData(0);
10+
}
11+
GameData createGameData(int offset) {
12+
return new ClientData.GameData(offset);
1013
}
1114
GameData gameData() {
1215
return gameData;
@@ -17,9 +20,12 @@ void setBuffer(WrappedBuffer buffer) {
1720
void setPointer(Pointer pointer) {
1821
setBuffer(new WrappedBuffer(pointer, GameData.SIZE));
1922
}
23+
WrappedBuffer getStaticBuffer() {
24+
return buffer;
25+
}
2026
class UnitCommand {
2127
static final int SIZE = 24;
22-
private int myOffset;
28+
protected int myOffset;
2329
UnitCommand(int myOffset) {
2430
this.myOffset = myOffset;
2531
}
@@ -68,7 +74,7 @@ void setExtra(int value) {
6874
}
6975
class GameData {
7076
static final int SIZE = 33017048;
71-
private int myOffset;
77+
protected int myOffset;
7278
GameData(int myOffset) {
7379
this.myOffset = myOffset;
7480
}
@@ -701,7 +707,7 @@ void setIsSolid(boolean value) {
701707
}
702708
class Command {
703709
static final int SIZE = 12;
704-
private int myOffset;
710+
protected int myOffset;
705711
Command(int myOffset) {
706712
this.myOffset = myOffset;
707713
}
@@ -729,7 +735,7 @@ void setValue2(int value) {
729735
}
730736
class Position {
731737
static final int SIZE = 8;
732-
private int myOffset;
738+
protected int myOffset;
733739
Position(int myOffset) {
734740
this.myOffset = myOffset;
735741
}
@@ -750,7 +756,7 @@ void setY(int value) {
750756
}
751757
class Event {
752758
static final int SIZE = 12;
753-
private int myOffset;
759+
protected int myOffset;
754760
Event(int myOffset) {
755761
this.myOffset = myOffset;
756762
}
@@ -784,94 +790,94 @@ class RegionData {
784790
}
785791
int getId() {
786792
int offset = myOffset + 0;
787-
return buffer.getInt(offset);
793+
return getStaticBuffer().getInt(offset);
788794
}
789795
void setId(int value) {
790-
buffer.putInt(myOffset + 0, value);
796+
getStaticBuffer().putInt(myOffset + 0, value);
791797
}
792798
int islandID() {
793799
int offset = myOffset + 4;
794-
return buffer.getInt(offset);
800+
return getStaticBuffer().getInt(offset);
795801
}
796802
void setIslandID(int value) {
797-
buffer.putInt(myOffset + 4, value);
803+
getStaticBuffer().putInt(myOffset + 4, value);
798804
}
799805
int getCenter_x() {
800806
int offset = myOffset + 8;
801-
return buffer.getInt(offset);
807+
return getStaticBuffer().getInt(offset);
802808
}
803809
void setCenter_x(int value) {
804-
buffer.putInt(myOffset + 8, value);
810+
getStaticBuffer().putInt(myOffset + 8, value);
805811
}
806812
int getCenter_y() {
807813
int offset = myOffset + 12;
808-
return buffer.getInt(offset);
814+
return getStaticBuffer().getInt(offset);
809815
}
810816
void setCenter_y(int value) {
811-
buffer.putInt(myOffset + 12, value);
817+
getStaticBuffer().putInt(myOffset + 12, value);
812818
}
813819
int getPriority() {
814820
int offset = myOffset + 16;
815-
return buffer.getInt(offset);
821+
return getStaticBuffer().getInt(offset);
816822
}
817823
void setPriority(int value) {
818-
buffer.putInt(myOffset + 16, value);
824+
getStaticBuffer().putInt(myOffset + 16, value);
819825
}
820826
int getLeftMost() {
821827
int offset = myOffset + 20;
822-
return buffer.getInt(offset);
828+
return getStaticBuffer().getInt(offset);
823829
}
824830
void setLeftMost(int value) {
825-
buffer.putInt(myOffset + 20, value);
831+
getStaticBuffer().putInt(myOffset + 20, value);
826832
}
827833
int getRightMost() {
828834
int offset = myOffset + 24;
829-
return buffer.getInt(offset);
835+
return getStaticBuffer().getInt(offset);
830836
}
831837
void setRightMost(int value) {
832-
buffer.putInt(myOffset + 24, value);
838+
getStaticBuffer().putInt(myOffset + 24, value);
833839
}
834840
int getTopMost() {
835841
int offset = myOffset + 28;
836-
return buffer.getInt(offset);
842+
return getStaticBuffer().getInt(offset);
837843
}
838844
void setTopMost(int value) {
839-
buffer.putInt(myOffset + 28, value);
845+
getStaticBuffer().putInt(myOffset + 28, value);
840846
}
841847
int getBottomMost() {
842848
int offset = myOffset + 32;
843-
return buffer.getInt(offset);
849+
return getStaticBuffer().getInt(offset);
844850
}
845851
void setBottomMost(int value) {
846-
buffer.putInt(myOffset + 32, value);
852+
getStaticBuffer().putInt(myOffset + 32, value);
847853
}
848854
int getNeighborCount() {
849855
int offset = myOffset + 36;
850-
return buffer.getInt(offset);
856+
return getStaticBuffer().getInt(offset);
851857
}
852858
void setNeighborCount(int value) {
853-
buffer.putInt(myOffset + 36, value);
859+
getStaticBuffer().putInt(myOffset + 36, value);
854860
}
855861
int getNeighbors(int i) {
856862
int offset = myOffset + 40 + 4 * 1 * i;
857-
return buffer.getInt(offset);
863+
return getStaticBuffer().getInt(offset);
858864
}
859865
void setNeighbors(int i, int value) {
860-
buffer.putInt(myOffset + 40 + 4 * 1 * i, value);
866+
getStaticBuffer().putInt(myOffset + 40 + 4 * 1 * i, value);
861867
}
862868
boolean isAccessible() {
863869
int offset = myOffset + 1064;
864-
return buffer.getByte(offset) != 0;
870+
return getStaticBuffer().getByte(offset) != 0;
865871
}
866872
void setIsAccessible(boolean value) {
867-
buffer.putByte(myOffset + 1064, (byte) (value ? 1 : 0));
873+
getStaticBuffer().putByte(myOffset + 1064, (byte) (value ? 1 : 0));
868874
}
869875
boolean isHigherGround() {
870876
int offset = myOffset + 1065;
871-
return buffer.getByte(offset) != 0;
877+
return getStaticBuffer().getByte(offset) != 0;
872878
}
873879
void setIsHigherGround(boolean value) {
874-
buffer.putByte(myOffset + 1065, (byte) (value ? 1 : 0));
880+
getStaticBuffer().putByte(myOffset + 1065, (byte) (value ? 1 : 0));
875881
}
876882
}
877883
class ForceData {
@@ -2002,4 +2008,3 @@ void setReplayID(int value) {
20022008
}
20032009
}
20042010
}
2005-

‎src/main/java/bwapi/FrameBuffer.java‎

Lines changed: 12 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* Circular buffer of game states.
1212
*/
1313
class FrameBuffer {
14-
private static final int BUFFER_SIZE = ClientData.GameData.SIZE;
14+
static final int BUFFER_SIZE = PackedWrappedBuffer.BUFFER_SIZE;
1515
private static final Unsafe unsafe = UnsafeTools.getUnsafe();
1616

1717
private WrappedBuffer liveData;
@@ -30,7 +30,7 @@ class FrameBuffer {
3030
this.capacity = configuration.getAsyncFrameBufferCapacity();
3131
this.configuration = configuration;
3232
while(dataBuffer.size() < capacity) {
33-
dataBuffer.add(new WrappedBuffer(BUFFER_SIZE));
33+
dataBuffer.add(new PackedWrappedBuffer());
3434
}
3535
}
3636

@@ -107,17 +107,15 @@ void enqueueFrame() {
107107
performanceMetrics.getIntentionallyBlocking().stopTiming();
108108
} finally { lockSize.unlock(); };
109109

110-
// For the first frame of the game, populate all buffers completely
111-
// This is to ensure all buffers have access to immutable data like regions/walkability/buildability
112-
// Afterwards, we want to shorten this process by only copying important and mutable data
110+
// Copy dynamic game information into the current frame buffer slot.
111+
// Static information (e.g. walkability/buildability/regions) is accessed
112+
// directly from the main shared memory buffer by AsyncClientData.
113113
if (stepGame == 0) {
114-
for (WrappedBuffer frameBuffer : dataBuffer) {
115-
copyBuffer(liveData, frameBuffer, true);
116-
}
114+
copyBuffer(liveData, dataBuffer.get(indexGame()));
117115
} else {
118116
performanceMetrics.getCopyingToBuffer().time(() -> {
119117
WrappedBuffer dataTarget = dataBuffer.get(indexGame());
120-
copyBuffer(liveData, dataTarget, false);
118+
copyBuffer(liveData, dataTarget);
121119
});
122120
}
123121

@@ -153,45 +151,10 @@ void dequeue() {
153151
} finally { lockSize.unlock(); }
154152
}
155153

156-
/**
157-
*
158-
* @param source Address to copy from
159-
* @param destination Address to copy to
160-
* @param size Number of bytes to copy
161-
*/
162-
private void copyBuffer(WrappedBuffer source, WrappedBuffer destination, long offset, int size) {
163-
long addressSource = source.getAddress() + offset;
164-
long addressDestination = destination.getAddress() + offset;
165-
unsafe.copyMemory(addressSource, addressDestination, size);
166-
}
167-
168-
void copyBuffer(WrappedBuffer source, WrappedBuffer destination, boolean copyEverything) {
169-
/*
170-
The speed at which we copy data into the frame buffer is a major cost of JBWAPI's asynchronous operation.
171-
Copy times observed in the wild for the complete buffer usually range from 2.6ms - 19ms
172-
but are prone to large amounts of variance.
173-
174-
The normal Java way to execute this copy is via ByteBuffer.put(), which has reasonably good performance characteristics.
175-
*/
176-
177-
if (copyEverything) {
178-
copyBuffer(source, destination, 0, FrameBuffer.BUFFER_SIZE);
179-
} else {
180-
// After the buffer has been filled the first time,
181-
// we can omit copying blocks of data which are unused or which don't change after game start.
182-
// These blocks account for *most* of the 33MB shared memory,
183-
// so omitting them drastically reduces the copy duration
184-
final int STATICTILES_START = 3447004; // getGroundHeight, isWalkable, isBuildable
185-
final int STATICTILES_END = 4823260;
186-
final int REGION_START = 5085404; // getMapTileRegionId, ..., getRegions
187-
final int REGION_END = 10586480;
188-
final int STRINGSSHAPES_START = 10962632; // getStringCount, ... getShapes
189-
final int STRINGSHAPES_END = 32242636;
190-
final int UNITFINDER_START = 32962644;
191-
copyBuffer(source, destination, 0, STATICTILES_START);
192-
copyBuffer(source, destination, STATICTILES_END, REGION_START - STATICTILES_END);
193-
copyBuffer(source, destination, REGION_END, STRINGSSHAPES_START - REGION_END);
194-
copyBuffer(source, destination, STRINGSHAPES_END, UNITFINDER_START - STRINGSHAPES_END);
195-
}
154+
void copyBuffer(WrappedBuffer source, WrappedBuffer destination) {
155+
unsafe.copyMemory(source.getAddress() + 0, destination.getAddress() + PackedWrappedBuffer.DEST_SLICE1_OFFSET, PackedWrappedBuffer.DYNAMIC_SLICE1_SIZE);
156+
unsafe.copyMemory(source.getAddress() + PackedWrappedBuffer.STATICTILES_END, destination.getAddress() + PackedWrappedBuffer.DEST_SLICE2_OFFSET, PackedWrappedBuffer.DYNAMIC_SLICE2_SIZE);
157+
unsafe.copyMemory(source.getAddress() + PackedWrappedBuffer.REGION_END, destination.getAddress() + PackedWrappedBuffer.DEST_SLICE3_OFFSET, PackedWrappedBuffer.DYNAMIC_SLICE3_SIZE);
158+
unsafe.copyMemory(source.getAddress() + PackedWrappedBuffer.STRINGSHAPES_END, destination.getAddress() + PackedWrappedBuffer.DEST_SLICE4_OFFSET, PackedWrappedBuffer.DYNAMIC_SLICE4_SIZE);
196159
}
197160
}

‎src/main/java/bwapi/Game.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ public final class Game {
106106
final SideEffectQueue sideEffects = new SideEffectQueue();
107107

108108
Game() {
109-
clientData = new ClientData();
109+
this(new ClientData());
110+
}
111+
112+
Game(ClientData clientData) {
113+
this.clientData = clientData;
110114
}
111115

112116
void setConfiguration(BWClientConfiguration configuration) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package bwapi;
2+
3+
/**
4+
* Packed off-heap buffer for asynchronous frame slots that stores only dynamic game data slices
5+
* and exposes pre-shifted slice views so static BWAPI offsets work without runtime branching.
6+
*/
7+
class PackedWrappedBuffer extends WrappedBuffer {
8+
static final int STATICTILES_START = 3447004;
9+
static final int STATICTILES_END = 4823260;
10+
static final int REGION_START = 5085404;
11+
static final int REGION_END = 10586480;
12+
static final int STRINGSSHAPES_START = 10962632;
13+
static final int STRINGSHAPES_END = 32242636;
14+
static final int UNITFINDER_START = 32962644;
15+
16+
static final int DYNAMIC_SLICE1_SIZE = STATICTILES_START;
17+
static final int DYNAMIC_SLICE2_SIZE = REGION_START - STATICTILES_END;
18+
static final int DYNAMIC_SLICE3_SIZE = STRINGSSHAPES_START - REGION_END;
19+
static final int DYNAMIC_SLICE4_SIZE = UNITFINDER_START - STRINGSHAPES_END;
20+
21+
static final int DEST_SLICE1_OFFSET = 0;
22+
static final int DEST_SLICE2_OFFSET = DYNAMIC_SLICE1_SIZE;
23+
static final int DEST_SLICE3_OFFSET = DEST_SLICE2_OFFSET + DYNAMIC_SLICE2_SIZE;
24+
static final int DEST_SLICE4_OFFSET = DEST_SLICE3_OFFSET + DYNAMIC_SLICE3_SIZE;
25+
26+
static final int BUFFER_SIZE = DEST_SLICE4_OFFSET + DYNAMIC_SLICE4_SIZE;
27+
28+
static final int DELTA_RANGE2 = STATICTILES_END - STATICTILES_START;
29+
static final int DELTA_RANGE3 = DELTA_RANGE2 + (REGION_END - REGION_START);
30+
static final int DELTA_RANGE4 = DELTA_RANGE3 + (STRINGSHAPES_END - STRINGSSHAPES_START);
31+
32+
final WrappedBuffer slice2;
33+
final WrappedBuffer slice3;
34+
final WrappedBuffer slice4;
35+
36+
PackedWrappedBuffer() {
37+
this(BUFFER_SIZE);
38+
}
39+
40+
PackedWrappedBuffer(final int size) {
41+
super(size);
42+
long addr = getAddress();
43+
this.slice2 = new WrappedBuffer(getBuffer(), addr - DELTA_RANGE2);
44+
this.slice3 = new WrappedBuffer(getBuffer(), addr - DELTA_RANGE3);
45+
this.slice4 = new WrappedBuffer(getBuffer(), addr - DELTA_RANGE4);
46+
}
47+
48+
PackedWrappedBuffer(final WrappedBuffer unpacked) {
49+
super(null, 0L);
50+
this.slice2 = unpacked;
51+
this.slice3 = unpacked;
52+
this.slice4 = unpacked;
53+
}
54+
}

‎src/main/java/bwapi/WrappedBuffer.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ class WrappedBuffer {
2424
this.address = Pointer.nativeValue(pointer);
2525
}
2626

27+
WrappedBuffer(final ByteBuffer buffer, final long address) {
28+
this.buffer = buffer;
29+
this.address = address;
30+
}
31+
2732
byte getByte(final int offset) {
2833
return unsafe.getByte(address + offset);
2934
}

0 commit comments

Comments
 (0)