Skip to content

Commit a86017a

Browse files
committed
Even more IPC optimization, this thing is a beast
1 parent 66e79f6 commit a86017a

1 file changed

Lines changed: 71 additions & 29 deletions

File tree

src/main/java/net/evmodder/evmod/apis/PlayerPosIPC.java

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
import java.nio.ByteOrder;
99
import java.nio.MappedByteBuffer;
1010
import java.nio.channels.FileChannel;
11+
import java.util.Arrays;
1112
import java.util.function.Consumer;
1213
import net.evmodder.evmod.Main;
1314

1415
public final class PlayerPosIPC{
1516
private static final class Holder{private static final PlayerPosIPC INSTANCE = new PlayerPosIPC();}
1617
public static final PlayerPosIPC getInstance(){return Holder.INSTANCE;}
1718

18-
// Hopefully nobody is running more than this many Minecraft accounts on 1 device...
19+
// All these are static-final to take advantage of Constant Folding
1920
private static final int MAX_SLOTS = 64;
2021
// PID + TS + version + data
2122
public static final int METADATA_SIZE = 8 + 8 + 8; //=24
@@ -37,23 +38,42 @@ private static final class Holder{private static final PlayerPosIPC INSTANCE = n
3738
// private static final String MEM_TABLE_FILENAME = "minecraft_player_pos.bin";
3839
private static final VarHandle LONG_HANDLE = MethodHandles.byteBufferViewVarHandle(long[].class, ByteOrder.nativeOrder());
3940

40-
private static final long myPID = ProcessHandle.current().pid();
41-
private final long[] lastReadVersions = new long[MAX_SLOTS]; // Can't be static (static variables are shared across the entire JVM)
42-
private final byte[] data = new byte[DATA_SIZE];
41+
private static final long myPID = ProcessHandle.current().pid(); // Static for Constant Propagation (not Constant Folding)
42+
private final long[] lastReadVersions; // No benefit to making these static (memory addresses to dynamic values)
43+
private final byte[] data;
4344
private final MappedByteBuffer buffer;
44-
private int mySlot;
45+
46+
private int mySlot, readCount;
47+
48+
private static final File getFastestTempDir() {
49+
final String os = System.getProperty("os.name").toLowerCase();
50+
if(os.contains("linux")){
51+
// /dev/shm is a tmpfs (RAM). It is significantly faster than /tmp.
52+
final File shm = new File("/dev/shm");
53+
if(shm.exists() && shm.canWrite()) return shm;
54+
}
55+
// For Windows/Mac, java.io.tmpdir is the standard path.
56+
// On Windows, there is no native "RAM-disk" guaranteed to exist.
57+
return new File(System.getProperty("java.io.tmpdir"));
58+
}
4559

4660
private final void freeSlot(final long owner, final int base){
4761
if(base == LAST_SLOT_BASE) LONG_HANDLE.compareAndSet(buffer, base, owner, 0l);
48-
else if((long)LONG_HANDLE.getVolatile(buffer, base + SLOT_SIZE) != 0l) LONG_HANDLE.compareAndSet(buffer, base, owner, -1l);
62+
else if((long)LONG_HANDLE.getAcquire(buffer, base + SLOT_SIZE) != 0l) LONG_HANDLE.compareAndSet(buffer, base, owner, -1l);
4963
else if(LONG_HANDLE.compareAndSet(buffer, base, owner, -2l)){
50-
final long clearedVal = (long)LONG_HANDLE.getVolatile(buffer, base + SLOT_SIZE) == 0l ? 0l : -1l;
64+
final long clearedVal = (long)LONG_HANDLE.getAcquire(buffer, base + SLOT_SIZE) == 0l ? 0l : -1l;
65+
if(clearedVal == 0l){
66+
final long version = (long)LONG_HANDLE.get(buffer, base + VERSION_OFFSET);
67+
// final long nextEvenVer = (version + 1l) & -2l;
68+
LONG_HANDLE.setRelease(buffer, base + VERSION_OFFSET, (version + 1l) & -2l);
69+
}
5170
LONG_HANDLE.compareAndSet(buffer, base, -2l, clearedVal);
5271
}
5372
}
5473

74+
5575
private PlayerPosIPC(){
56-
final File file = new File(System.getProperty("java.io.tmpdir"), "minecraft_player_pos.bin");
76+
final File file = new File(getFastestTempDir(), "minecraft_player_pos.bin");
5777
try(final RandomAccessFile raf = new RandomAccessFile(file, "rw")){
5878
final long TOTAL_SIZE = (long) MAX_SLOTS*SLOT_SIZE;
5979
if(raf.length() < TOTAL_SIZE) raf.setLength(TOTAL_SIZE);
@@ -68,16 +88,21 @@ private PlayerPosIPC(){
6888
final int base = mySlot*SLOT_SIZE;
6989
if(buffer != null && myPID == (long)LONG_HANDLE.getVolatile(buffer, base)) freeSlot(myPID, base);
7090
}));
91+
// Non-shared variables, used exclusively by this PID
92+
lastReadVersions = new long[MAX_SLOTS];
93+
data = new byte[DATA_SIZE];
94+
Arrays.fill(lastReadVersions, -1l);
95+
claimSlot(); // Not strictly-necessary to call this prior to postData()
7196
}
7297

7398
private final int claimSlot(){
74-
final long now = System.nanoTime();
7599
for(int j=0; j<CLAIM_LOOP_MAX_ATTEMPTS; ++j){
100+
final long now = System.nanoTime();
76101
for(int i=0; i<MAX_SLOTS; ++i){
77102
final int base = i*SLOT_SIZE;
78103
final long owner = (long)LONG_HANDLE.getVolatile(buffer, base);
79-
if(owner > 0l && now - (long)LONG_HANDLE.getVolatile(buffer, base + TIME_OFFSET) < TIMEOUT_NS) continue;
80-
LONG_HANDLE.setVolatile(buffer, base + TIME_OFFSET, now); // Update ts (reduces contention fighting for this slot)
104+
if(owner > 0l && now - (long)LONG_HANDLE.getAcquire(buffer, base + TIME_OFFSET) < TIMEOUT_NS) continue;
105+
LONG_HANDLE.setOpaque(buffer, base + TIME_OFFSET, now); // Update heartbeat (reduces contention fighting for this slot)
81106
if(LONG_HANDLE.compareAndSet(buffer, base, owner, myPID)){
82107
Main.LOGGER.info("[EvMod] Claimed IPC slot "+i);
83108
return i; // Nice, we snagged this slot!
@@ -89,39 +114,56 @@ private final int claimSlot(){
89114
return -1; // Failed to acquire a slot!!
90115
}
91116

117+
private final void reapZombies() {
118+
final long now = System.nanoTime();
119+
for(int i=0; i<MAX_SLOTS; ++i){
120+
final int base = i*SLOT_SIZE;
121+
// final int base = i << 7;// Assumes `SLOT_SIZE == 128`!
122+
final long owner = (long) LONG_HANDLE.getOpaque(buffer, base);
123+
if(owner == myPID) continue; // Given its own line / branch prediction here, unlike in readData()
124+
if(owner <= 0l){
125+
if(owner == 0l) return;
126+
continue;
127+
}
128+
if(now - (long)LONG_HANDLE.getAcquire(buffer, base + TIME_OFFSET) > TIMEOUT_NS) freeSlot(owner, base);
129+
}
130+
}
131+
92132
public final void postData(final byte[] data){
93133
assert data.length == DATA_SIZE;
94-
// Ensure my slot is still valid (and update it if not).
134+
// Proactive heartbeat (even if mySlot is expired and got claimed by someone else)
135+
LONG_HANDLE.setOpaque(buffer, mySlot*SLOT_SIZE + TIME_OFFSET, System.nanoTime());
136+
// Ensure slot ownership, and claim a new slot if necessary
95137
if(myPID != (long)LONG_HANDLE.getVolatile(buffer, mySlot*SLOT_SIZE) && (mySlot=claimSlot()) == -1) return;
96138
final int base = mySlot*SLOT_SIZE;
97139
final long currentVer = (long)LONG_HANDLE.get(buffer, base + VERSION_OFFSET); // Get current version
98140
final long nextEvenVer = (currentVer+1l)&-2l;
99141
LONG_HANDLE.setRelease(buffer, base + VERSION_OFFSET, nextEvenVer); // Move current version to EVEN (signals write in-progress)
100-
LONG_HANDLE.setVolatile(buffer, base + TIME_OFFSET, System.nanoTime()); // Heartbeat
101142
buffer.put(base + DATA_OFFSET, data); // Publish data
102-
LONG_HANDLE.setRelease(buffer, base + VERSION_OFFSET, nextEvenVer + 1l); // Move current version to ODD (signals write completed)
143+
final long nextOddVer = nextEvenVer + 1l;
144+
lastReadVersions[mySlot] = nextOddVer; // Ensure I don't read my own data
145+
LONG_HANDLE.setRelease(buffer, base + VERSION_OFFSET, nextOddVer); // Move current version to ODD (signals write completed)
103146
}
104147

105148
public final void readData(final Consumer<byte[]> consumer){
106-
final long now = System.nanoTime(), prevOwner;
149+
if((++readCount & 1023) == 0) reapZombies(); // Only call every 1024 cycles
107150
for(int i=0; i<MAX_SLOTS; ++i){
108151
final int base = i*SLOT_SIZE;
152+
// final int base = i << 7; // Assumes `SLOT_SIZE == 128`!
153+
final long version = (long)LONG_HANDLE.getAcquire(buffer, base + VERSION_OFFSET);
154+
if(version <= lastReadVersions[i]) continue;
109155
final long owner = (long)LONG_HANDLE.getOpaque(buffer, base);
110-
if(owner < 0l || owner == myPID) continue;
111-
if(owner == 0l){
112-
// Attempt cleanup of trailing -1/-2 for the last slot before 0
113-
if(i > 0 && (prevOwner=(long)LONG_HANDLE.getVolatile(buffer, base-SLOT_SIZE)) < 0l) freeSlot(prevOwner, base-SLOT_SIZE);
114-
return;
156+
if(owner <= 0l){
157+
if(owner == 0l) return;
158+
continue;
115159
}
116-
if(now - (long)LONG_HANDLE.getVolatile(buffer, base + TIME_OFFSET) > TIMEOUT_NS){freeSlot(owner, base); continue;}
117-
// while(((long)LONG_HANDLE.getAcquire(buffer, base + VERSION_OFFSET)&1) == 0) Thread.onSpinWait();
118-
final long version = (long)LONG_HANDLE.getAcquire(buffer, base + VERSION_OFFSET);
119-
if((version&1l) == 0l || version == lastReadVersions[i]) continue; // Skip busy or stale
120-
VarHandle.fullFence();
121-
buffer.get(base + DATA_OFFSET, data); // Read data
122-
if(version == (long)LONG_HANDLE.getVolatile(buffer, base + VERSION_OFFSET)){ // Verify data wasn't altered mid-read
123-
consumer.accept(data);
124-
lastReadVersions[i] = version;
160+
if((version & 1l) != 0l){ // Only read if non-busy
161+
buffer.get(base + DATA_OFFSET, data); // Read data
162+
VarHandle.loadLoadFence(); // Explicitly prevent the next load from occurring before the 'get' finishes
163+
if(version == (long)LONG_HANDLE.getOpaque(buffer, base + VERSION_OFFSET)){ // Verify data wasn't altered mid-read
164+
consumer.accept(data);
165+
lastReadVersions[i] = version;
166+
}
125167
}
126168
}
127169
}

0 commit comments

Comments
 (0)