Skip to content

Commit d199104

Browse files
RogerstalkerBrutus5000
authored andcommitted
Switched parser logic to ByteBuffer from LittleEndianDataInputStream for more direct and performant access to the originally read replay bytes and easier reusability during parsing
Eliminated double reading of stream and multiple smaller tokenContent stream creation for tokenization and parsing by not preparing separate tokenContent byte chucks and reusing the originally created ByteBuffer Replaced tokenContent and tokenSize with token limit, which stores the read position limit in the ByteBuffer for the corresponding token Added buffer limiting to parsing to preserve robustness coming from tokenization Improved performance of readString methods by replacing the ByteArrayOutputStream and double accessing the ByteBuffer for string ending search and extracting string bytes Implemented getUnsignedByte util for ByteBuffer
1 parent d0908ce commit d199104

8 files changed

Lines changed: 444 additions & 431 deletions

File tree

data/src/main/java/com/faforever/commons/replay/ReplayDataParser.java

Lines changed: 69 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,31 @@
44
import com.faforever.commons.replay.body.ReplayBodyParser;
55
import com.faforever.commons.replay.body.ReplayBodyToken;
66
import com.faforever.commons.replay.body.ReplayBodyTokenizer;
7+
import com.faforever.commons.replay.shared.LoadUtils;
78
import com.faforever.commons.replay.shared.LuaData;
89
import com.fasterxml.jackson.databind.ObjectMapper;
910
import com.google.common.annotations.VisibleForTesting;
1011
import com.google.common.io.BaseEncoding;
11-
import com.google.common.io.LittleEndianDataInputStream;
1212
import lombok.Getter;
1313
import lombok.extern.slf4j.Slf4j;
1414
import org.apache.commons.compress.compressors.CompressorException;
1515
import org.apache.commons.compress.compressors.CompressorInputStream;
1616
import org.apache.commons.compress.compressors.CompressorStreamFactory;
17-
import org.apache.commons.compress.utils.IOUtils;
17+
import org.apache.commons.io.IOUtils;
1818
import org.jetbrains.annotations.NotNull;
1919

2020
import java.io.ByteArrayInputStream;
2121
import java.io.ByteArrayOutputStream;
2222
import java.io.IOException;
23+
import java.nio.ByteBuffer;
24+
import java.nio.ByteOrder;
2325
import java.nio.charset.StandardCharsets;
2426
import java.nio.file.Files;
2527
import java.nio.file.Path;
2628
import java.time.Duration;
2729
import java.util.*;
28-
import java.util.concurrent.atomic.AtomicInteger;
2930
import java.util.stream.Collectors;
3031

31-
3232
@SuppressWarnings("unused")
3333
@Slf4j
3434
public class ReplayDataParser {
@@ -59,7 +59,7 @@ public class ReplayDataParser {
5959
@Getter
6060
private final List<ModeratorEvent> moderatorEvents = new ArrayList<>();
6161
@Getter
62-
private final Map<Integer, Map<Integer, AtomicInteger>> commandsPerMinuteByPlayer = new HashMap<>();
62+
private Map<String, Integer> playerIdsByName;
6363

6464
private int ticks;
6565

@@ -80,48 +80,49 @@ public ReplayDataParser(Path path, ObjectMapper objectMapper) throws IOException
8080
}
8181

8282
@VisibleForTesting
83-
static String readString(LittleEndianDataInputStream dataStream) throws IOException {
84-
ByteArrayOutputStream out = new ByteArrayOutputStream();
85-
byte tempByte;
86-
while ((tempByte = dataStream.readByte()) != 0) {
87-
out.write(tempByte);
83+
static String readString(ByteBuffer buffer) {
84+
final int offset = buffer.position();
85+
while (buffer.get() != 0) {
8886
}
89-
return out.toString(StandardCharsets.UTF_8);
87+
final int length = buffer.position() - 1 - offset;
88+
byte[] stringBytes = new byte[length];
89+
buffer.get(offset, stringBytes);
90+
return new String(stringBytes, StandardCharsets.UTF_8);
9091
}
9192

92-
private Object parseLua(LittleEndianDataInputStream dataStream) throws IOException {
93-
int type = dataStream.readUnsignedByte();
93+
private Object parseLua(ByteBuffer buffer) {
94+
int type = LoadUtils.getUnsignedByte(buffer);
9495
switch (type) {
9596
case LUA_NUMBER:
96-
return dataStream.readFloat();
97+
return buffer.getFloat();
9798
case LUA_STRING:
98-
return readString(dataStream);
99+
return readString(buffer);
99100
case LUA_NIL:
100-
dataStream.skipBytes(1);
101+
buffer.get();
101102
return null;
102103
case LUA_BOOL: // bool
103-
return dataStream.readUnsignedByte() == 0;
104+
return LoadUtils.getUnsignedByte(buffer) == 0;
104105
case LUA_TABLE_START: // lua
105106
Map<String, Object> result = new HashMap<>();
106-
while (peek(dataStream) != LUA_TABLE_END) {
107-
Object key = parseLua(dataStream);
107+
while (peek(buffer) != LUA_TABLE_END) {
108+
Object key = parseLua(buffer);
108109
if (key instanceof Number) {
109110
key = ((Number) key).intValue();
110111
}
111-
result.put(String.valueOf(key), parseLua(dataStream));
112-
dataStream.mark(1);
112+
result.put(String.valueOf(key), parseLua(buffer));
113+
buffer.mark();
113114
}
114-
dataStream.skipBytes(1);
115+
buffer.get();
115116
return result;
116117
default:
117118
throw new IllegalStateException("Unexpected data type: " + type);
118119
}
119120
}
120121

121-
private int peek(LittleEndianDataInputStream dataStream) throws IOException {
122-
dataStream.mark(1);
123-
int next = dataStream.readUnsignedByte();
124-
dataStream.reset();
122+
private int peek(ByteBuffer buffer) {
123+
buffer.mark();
124+
int next = LoadUtils.getUnsignedByte(buffer);
125+
buffer.reset();
125126
return next;
126127
}
127128

@@ -164,51 +165,51 @@ private byte[] decompress(byte[] data, @NotNull ReplayMetadata metadata) throws
164165
}
165166

166167
@SuppressWarnings("unchecked")
167-
private void parseHeader(LittleEndianDataInputStream dataStream) throws IOException {
168-
replayPatchFieldId = readString(dataStream);
169-
String arg13 = readString(dataStream); // always \r\n
168+
private void parseHeader(ByteBuffer buffer) {
169+
replayPatchFieldId = readString(buffer);
170+
String arg13 = readString(buffer); // always \r\n
170171

171-
String[] split = readString(dataStream).split("\\r\\n");
172+
String[] split = readString(buffer).split("\\r\\n");
172173
String replayVersionId = split[0];
173174
map = split[1];
174-
String arg23 = readString((dataStream)); // always \r\n and some unknown character
175+
String arg23 = readString((buffer)); // always \r\n and some unknown character
175176

176-
int sizeModsInBytes = dataStream.readInt();
177-
mods = (Map<String, Map<String, ?>>) parseLua(dataStream);
177+
int sizeModsInBytes = buffer.getInt();
178+
mods = (Map<String, Map<String, ?>>) parseLua(buffer);
178179

179-
int sizeGameOptionsInBytes = dataStream.readInt();
180-
this.gameOptions = ((Map<String, Object>) parseLua(dataStream)).entrySet().stream()
180+
int sizeGameOptionsInBytes = buffer.getInt();
181+
this.gameOptions = ((Map<String, Object>) parseLua(buffer)).entrySet().stream()
181182
.filter(entry -> "Options".equals(entry.getKey()))
182183
.flatMap(entry -> ((Map<String, Object>) entry.getValue()).entrySet().stream())
183184
.map(entry -> new GameOption(entry.getKey(), entry.getValue()))
184185
.collect(Collectors.toList());
185186

186-
int numberOfSources = dataStream.readUnsignedByte();
187+
int numberOfSources = LoadUtils.getUnsignedByte(buffer);
187188

188-
Map<String, Object> playerIdsByName = new HashMap<>();
189+
playerIdsByName = new HashMap<>();
189190
for (int i = 0; i < numberOfSources; i++) {
190-
String playerName = readString(dataStream);
191-
int playerId = dataStream.readInt();
191+
String playerName = readString(buffer);
192+
int playerId = buffer.getInt();
192193
playerIdsByName.put(playerName, playerId);
193194
}
194195

195-
boolean cheatsEnabled = dataStream.readUnsignedByte() > 0;
196+
boolean cheatsEnabled = LoadUtils.getUnsignedByte(buffer) > 0;
196197

197-
int numberOfArmies = dataStream.readUnsignedByte();
198+
int numberOfArmies = LoadUtils.getUnsignedByte(buffer);
198199
for (int i = 0; i < numberOfArmies; i++) {
199-
int sizePlayerDataInBytes = dataStream.readInt();
200-
Map<String, Object> playerData = (Map<String, Object>) parseLua(dataStream);
201-
int playerSource = dataStream.readUnsignedByte();
200+
int sizePlayerDataInBytes = buffer.getInt();
201+
Map<String, Object> playerData = (Map<String, Object>) parseLua(buffer);
202+
int playerSource = LoadUtils.getUnsignedByte(buffer);
202203

203204
armies.put(playerSource, playerData);
204205
playerData.put("commands", new ArrayList<>());
205206

206207
if (playerSource != 255) {
207-
dataStream.skipBytes(1);
208+
buffer.get();
208209
}
209210
}
210211

211-
randomSeed = dataStream.readInt();
212+
randomSeed = buffer.getInt();
212213
}
213214

214215
private void interpretEvents(List<Event> events) {
@@ -288,19 +289,13 @@ private void interpretEvents(List<Event> events) {
288289
case Event.IssueCommand(
289290
Event.CommandUnits commandUnits, Event.CommandData commandData
290291
) -> {
291-
commandsPerMinuteByPlayer
292-
.computeIfAbsent(player, p -> new HashMap<>())
293-
.computeIfAbsent(ticks, t -> new AtomicInteger())
294-
.incrementAndGet();
292+
295293
}
296294

297295
case Event.IssueFactoryCommand(
298296
Event.CommandUnits commandUnits, Event.CommandData commandData
299297
) -> {
300-
commandsPerMinuteByPlayer
301-
.computeIfAbsent(player, p -> new HashMap<>())
302-
.computeIfAbsent(ticks, t -> new AtomicInteger())
303-
.incrementAndGet();
298+
304299
}
305300

306301
case Event.IncreaseCommandCount(int commandId, int delta) -> {
@@ -364,10 +359,13 @@ private void interpretEvents(List<Event> events) {
364359
}
365360

366361
private void parseGiveResourcesToPlayer(LuaData.Table lua) {
367-
if (lua.value().containsKey("Msg") && lua.value().containsKey("From") && lua.value().containsKey("Sender")) {
362+
LuaData msg;
363+
LuaData from;
364+
LuaData sender;
365+
if ((msg = lua.value().get("Msg")) != null && (from = lua.value().get("From")) != null && (sender = lua.value().get("Sender")) != null) {
368366

369367
// TODO: use the command source (player value) instead of the values from the callback. The values from the callback can be manipulated
370-
if (!(lua.value().get("From") instanceof LuaData.Number(float luaFromArmy))) {
368+
if (!(from instanceof LuaData.Number(float luaFromArmy))) {
371369
return;
372370
}
373371

@@ -376,11 +374,11 @@ private void parseGiveResourcesToPlayer(LuaData.Table lua) {
376374
return;
377375
}
378376

379-
if (!(lua.value().get("Msg") instanceof LuaData.Table(Map<String, LuaData> luaMsg))) {
377+
if (!(msg instanceof LuaData.Table(Map<String, LuaData> luaMsg))) {
380378
return;
381379
}
382380

383-
if (!(lua.value().get("Sender") instanceof LuaData.String(String luaSender))) {
381+
if (!(sender instanceof LuaData.String(String luaSender))) {
384382
return;
385383
}
386384

@@ -442,12 +440,18 @@ private Duration tickToTime(int tick) {
442440
}
443441

444442
private void parse() throws IOException, CompressorException {
445-
readReplayData(path);
446-
try (LittleEndianDataInputStream dataStream = new LittleEndianDataInputStream(new ByteArrayInputStream(data))) {
447-
parseHeader(dataStream);
448-
tokens = ReplayBodyTokenizer.tokenize(dataStream);
449-
}
450-
events = ReplayBodyParser.parseTokens(tokens);
451-
interpretEvents(events);
443+
readReplayData(path);
444+
445+
final ByteBuffer buffer = ByteBuffer.wrap(data);
446+
buffer.order(ByteOrder.LITTLE_ENDIAN);
447+
448+
parseHeader(buffer);
449+
450+
var rewindPosition = buffer.position();
451+
tokens = ReplayBodyTokenizer.tokenize(buffer);
452+
buffer.position(rewindPosition);
453+
454+
events = ReplayBodyParser.parseTokens(tokens, buffer);
455+
interpretEvents(events);
452456
}
453457
}

data/src/main/java/com/faforever/commons/replay/ReplayLoader.java

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import com.fasterxml.jackson.databind.DeserializationFeature;
1111
import com.fasterxml.jackson.databind.ObjectMapper;
1212
import com.google.common.io.BaseEncoding;
13-
import com.google.common.io.LittleEndianDataInputStream;
1413
import org.apache.commons.compress.compressors.CompressorException;
1514
import org.apache.commons.compress.compressors.CompressorInputStream;
1615
import org.apache.commons.compress.compressors.CompressorStreamFactory;
@@ -22,6 +21,8 @@
2221
import java.io.ByteArrayOutputStream;
2322
import java.io.EOFException;
2423
import java.io.IOException;
24+
import java.nio.ByteBuffer;
25+
import java.nio.ByteOrder;
2526
import java.nio.charset.StandardCharsets;
2627
import java.nio.file.Files;
2728
import java.nio.file.Path;
@@ -32,29 +33,33 @@
3233
public class ReplayLoader {
3334

3435
@Contract(pure = true)
35-
private static ReplayHeader loadSCFAReplayHeader(LittleEndianDataInputStream stream) throws IOException {
36-
return ReplayHeaderParser.parse(stream);
36+
private static ReplayHeader loadSCFAReplayHeader(ByteBuffer buffer) {
37+
return ReplayHeaderParser.parse(buffer);
3738
}
3839

3940
@Contract(pure = true)
40-
private static @NotNull List<RegisteredEvent> loadSCFAReplayBody(List<Source> sources, LittleEndianDataInputStream stream) throws IOException {
41-
List<ReplayBodyToken> bodyTokens = ReplayBodyTokenizer.tokenize(stream);
42-
List<Event> bodyEvents = ReplayBodyParser.parseTokens(bodyTokens);
41+
private static @NotNull List<RegisteredEvent> loadSCFAReplayBody(List<Source> sources, ByteBuffer buffer) {
42+
var rewindPosition = buffer.position();
43+
List<ReplayBodyToken> bodyTokens = ReplayBodyTokenizer.tokenize(buffer);
44+
buffer.position(rewindPosition);
45+
46+
List<Event> bodyEvents = ReplayBodyParser.parseTokens(bodyTokens, buffer);
4347
return ReplaySemantics.registerEvents(sources, bodyEvents);
4448
}
4549

4650
@Contract(pure = true)
4751
private static ReplayContainer loadSCFAReplayFromMemory(ReplayMetadata metadata, byte[] scfaReplayBytes) throws IOException {
48-
try (LittleEndianDataInputStream stream = new LittleEndianDataInputStream((new ByteArrayInputStream(scfaReplayBytes)))) {
49-
ReplayHeader replayHeader = loadSCFAReplayHeader(stream);
50-
List<RegisteredEvent> replayBody = loadSCFAReplayBody(replayHeader.sources(), stream);
52+
final ByteBuffer buffer = ByteBuffer.wrap(scfaReplayBytes);
53+
buffer.order(ByteOrder.LITTLE_ENDIAN);
5154

52-
if (stream.available() > 0) {
53-
throw new EOFException();
54-
}
55+
ReplayHeader replayHeader = loadSCFAReplayHeader(buffer);
56+
List<RegisteredEvent> replayBody = loadSCFAReplayBody(replayHeader.sources(), buffer);
5557

56-
return new ReplayContainer(metadata, replayHeader, replayBody);
58+
if (buffer.position() != buffer.limit()) {
59+
throw new EOFException();
5760
}
61+
62+
return new ReplayContainer(metadata, replayHeader, replayBody);
5863
}
5964

6065
public static ReplayContainer loadSCFAReplayFromDisk(Path scfaReplayFile) throws IOException, IllegalArgumentException {

0 commit comments

Comments
 (0)