Skip to content

Commit 2234cb5

Browse files
authored
Improve local replays tab loading time (#3394)
* Implemented replay caching for ReplayService Parallelized local replay loading to improve performance Fixed a bug, where two separate subscribed actions prompted the VaultEntityController to load the replays twice on changing the number of replays per page combobox. * Adjust ReplayService and tests to java-common replay data type change * Bump faf-java-commons to 317e19f * Reduced local replay cache size and expiry Added check for processor count for parallelization * Change commons to mvn repo artifacts * Revert to original single threaded local replays page loading * Remove unused nThreads field * Remove unused changePerPageCount method, adjust corresponding unit test
1 parent 04c4740 commit 2234cb5

File tree

6 files changed

+48
-35
lines changed

6 files changed

+48
-35
lines changed

build.gradle

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,13 +256,13 @@ dependencies {
256256
implementation("io.projectreactor.addons:reactor-extra")
257257
implementation("io.projectreactor:reactor-tools")
258258

259-
def commonsVersion = "07e30bcb925ff34273a9a4eb08c8c506b13d5d78"
259+
def commonsVersion = "20250913-317e19f"
260260

261-
implementation("com.github.faforever.faf-java-commons:data:${commonsVersion}") {
261+
implementation("com.faforever.commons:data:${commonsVersion}") {
262262
exclude module: 'guava'
263263
}
264-
implementation("com.github.faforever.faf-java-commons:api:${commonsVersion}")
265-
implementation("com.github.faforever.faf-java-commons:lobby:${commonsVersion}")
264+
implementation("com.faforever.commons:api:${commonsVersion}")
265+
implementation("com.faforever.commons:lobby:${commonsVersion}")
266266
implementation("com.google.guava:guava:33.5.0-jre")
267267
implementation("org.apache.commons:commons-compress:1.28.0")
268268
implementation("net.java.dev.jna:jna:5.18.1")

src/main/java/com/faforever/client/replay/ReplayService.java

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@
3434
import com.faforever.commons.api.elide.ElideNavigatorOnId;
3535
import com.faforever.commons.replay.ReplayDataParser;
3636
import com.faforever.commons.replay.ReplayMetadata;
37+
import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream;
3738
import com.google.common.annotations.VisibleForTesting;
39+
import com.google.common.cache.Cache;
40+
import com.google.common.cache.CacheBuilder;
3841
import lombok.RequiredArgsConstructor;
3942
import lombok.extern.slf4j.Slf4j;
43+
import org.jetbrains.annotations.NotNull;
4044
import org.springframework.beans.factory.ObjectFactory;
4145
import org.springframework.cache.annotation.Cacheable;
4246
import org.springframework.context.annotation.Lazy;
@@ -45,11 +49,11 @@
4549
import reactor.core.publisher.Mono;
4650
import reactor.util.function.Tuple2;
4751

48-
import java.io.ByteArrayInputStream;
4952
import java.io.FileNotFoundException;
5053
import java.io.IOException;
5154
import java.net.MalformedURLException;
5255
import java.net.URL;
56+
import java.nio.ByteBuffer;
5357
import java.nio.file.DirectoryStream;
5458
import java.nio.file.Files;
5559
import java.nio.file.Path;
@@ -63,6 +67,7 @@
6367
import java.util.Objects;
6468
import java.util.Set;
6569
import java.util.concurrent.CompletableFuture;
70+
import java.util.concurrent.TimeUnit;
6671
import java.util.regex.Matcher;
6772
import java.util.regex.Pattern;
6873
import java.util.stream.Collectors;
@@ -101,6 +106,10 @@ public class ReplayService {
101106
private final DataPrefs dataPrefs;
102107
private final ObjectFactory<ReplayDownloadTask> replayDownloadTaskFactory;
103108
private final ReplayHistoryPrefs replayHistory;
109+
private final Cache<@NotNull Path, @NotNull Replay> replayCache = CacheBuilder.newBuilder()
110+
.maximumSize(500)
111+
.expireAfterWrite(20, TimeUnit.MINUTES)
112+
.build();
104113

105114
@VisibleForTesting
106115
static Integer parseSupComVersion(ReplayDataParser parser) {
@@ -179,16 +188,20 @@ public Mono<Tuple2<List<Replay>, Integer>> loadLocalReplayPage(int pageSize, int
179188

180189
return Mono.fromFuture(CompletableFuture.allOf(replayFutures.toArray(new CompletableFuture[0]))
181190
.thenApply(_ -> replayFutures.stream()
182-
.map(CompletableFuture::join)
183-
.filter(Objects::nonNull)
184-
.collect(Collectors.toList())))
191+
.map(CompletableFuture::join)
192+
.filter(Objects::nonNull)
193+
.collect(Collectors.toList())))
185194
.zipWith(Mono.just(numPages));
186195
}
187196
}
188197

189-
190198
private CompletableFuture<Replay> tryLoadingLocalReplay(Path replayFile) {
191199
try {
200+
final Replay cachedReplay = this.replayCache.getIfPresent(replayFile);
201+
if (cachedReplay != null) {
202+
return CompletableFuture.completedFuture(cachedReplay);
203+
}
204+
192205
ReplayDataParser replayData = replayFileReader.parseReplay(replayFile);
193206
ReplayMetadata replayMetadata = replayData.getMetadata();
194207

@@ -203,7 +216,10 @@ private CompletableFuture<Replay> tryLoadingLocalReplay(Path replayFile) {
203216
if (mapVersion == null) {
204217
log.warn("Could not find map for replay file `{}`", replayFile);
205218
}
206-
return replayMapper.map(replayData, replayFile, featuredMod, mapVersion);
219+
220+
final Replay replay = replayMapper.map(replayData, replayFile, featuredMod, mapVersion);
221+
this.replayCache.put(replayFile, replay);
222+
return replay;
207223
}).exceptionally(throwable -> {
208224
log.warn("Could not read replay file `{}`", replayFile, throwable);
209225
moveCorruptedReplayFile(replayFile);
@@ -287,7 +303,10 @@ public ReplayDetails loadReplayDetails(Path path) throws IOException {
287303
List<ChatMessage> chatMessages = replayDataParser.getChatMessages().stream().map(replayMapper::map).toList();
288304
List<GameOption> gameOptions = Stream.concat(
289305
Stream.of(new GameOption("FAF Version", String.valueOf(parseSupComVersion(replayDataParser)))),
290-
replayDataParser.getGameOptions().stream().map(replayMapper::map).sorted(Comparator.comparing(GameOption::key, String.CASE_INSENSITIVE_ORDER))).toList();
306+
replayDataParser.getGameOptions()
307+
.stream()
308+
.map(replayMapper::map)
309+
.sorted(Comparator.comparing(GameOption::key, String.CASE_INSENSITIVE_ORDER))).toList();
291310

292311
String mapFolderName = parseMapFolderName(replayDataParser);
293312
Map map = new Map(null, mapFolderName, 0, null, false, null, null);
@@ -351,12 +370,13 @@ private void runOnlineReplay(int replayId) {
351370

352371
private void runFafReplayFile(Path path) throws IOException {
353372
ReplayDataParser replayData = replayFileReader.parseReplay(path);
354-
byte[] rawReplayBytes = replayData.getData();
373+
ByteBuffer rawReplayByteBuffer = replayData.getData();
355374

356375
Path tempSupComReplayFile = dataPrefs.getCacheDirectory().resolve(TEMP_SCFA_REPLAY_FILE_NAME);
357376

358377
Files.createDirectories(tempSupComReplayFile.getParent());
359-
Files.copy(new ByteArrayInputStream(rawReplayBytes), tempSupComReplayFile, StandardCopyOption.REPLACE_EXISTING);
378+
Files.copy(new ByteBufferBackedInputStream(rawReplayByteBuffer), tempSupComReplayFile,
379+
StandardCopyOption.REPLACE_EXISTING);
360380

361381
ReplayMetadata replayMetadata = replayData.getMetadata();
362382
String gameType = replayMetadata.getFeaturedMod();
@@ -445,8 +465,7 @@ public Mono<Tuple2<List<Replay>, Integer>> findByQueryWithPageCount(SearchConfig
445465
@Cacheable(value = CacheNames.REPLAYS_SEARCH, sync = true)
446466
public Mono<Replay> findById(int id) {
447467
ElideNavigatorOnId<Game> navigator = ElideNavigator.of(Game.class).id(String.valueOf(id));
448-
return fafApiAccessor.getOne(navigator).map(replayMapper::map)
449-
.cache();
468+
return fafApiAccessor.getOne(navigator).map(replayMapper::map).cache();
450469
}
451470

452471
@Cacheable(value = CacheNames.REPLAYS_MINE, sync = true)
@@ -467,10 +486,13 @@ private Mono<Tuple2<List<Replay>, Integer>> getReplayPage(ElideNavigatorOnCollec
467486
}
468487

469488
public Flux<LeagueScoreJournal> getLeagueScoreJournalForReplay(Replay replay) {
470-
ElideNavigatorOnCollection<com.faforever.commons.api.dto.LeagueScoreJournal> navigator = ElideNavigator.of(com.faforever.commons.api.dto.LeagueScoreJournal.class).collection()
471-
.setFilter(qBuilder().intNum("gameId").eq(replay.id()));
472-
return fafApiAccessor.getMany(navigator)
473-
.map(replayMapper::map)
474-
.cache();
489+
ElideNavigatorOnCollection<com.faforever.commons.api.dto.LeagueScoreJournal> navigator = ElideNavigator.of(
490+
com.faforever.commons.api.dto.LeagueScoreJournal.class)
491+
.collection()
492+
.setFilter(
493+
qBuilder().intNum(
494+
"gameId")
495+
.eq(replay.id()));
496+
return fafApiAccessor.getMany(navigator).map(replayMapper::map).cache();
475497
}
476498
}

src/main/java/com/faforever/client/vault/VaultEntityController.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ protected void onInitialize() {
134134
((observable, oldValue, newValue) -> onPerPageCountChanged(oldValue == null ? 0 : oldValue,
135135
newValue)));
136136
perPageComboBox.setValue(20);
137-
perPageComboBox.setOnAction((event -> changePerPageCount()));
138137
pageSize = perPageComboBox.getValue();
139138

140139
initSearchController();
@@ -265,14 +264,6 @@ private VaultEntityShowRoomController loadShowRoom(ShowRoomCategory<T> showRoomC
265264
return vaultEntityShowRoomController;
266265
}
267266

268-
protected void changePerPageCount() {
269-
pageSize = perPageComboBox.getValue();
270-
if (state.get() == State.RESULT) {
271-
SearchConfig searchConfig = searchController.getLastSearchConfig();
272-
onPageChange(searchConfig, true);
273-
}
274-
}
275-
276267
protected void onPageChange(SearchConfig searchConfig, boolean firstLoad) {
277268
enterSearchingState();
278269
setSupplier(searchConfig);

src/test/java/com/faforever/client/replay/ReplayFileReaderImplTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ public void readReplayData() throws Exception {
2828
try (InputStream inputStream = new BufferedInputStream(getClass().getResourceAsStream("/replay/test.fafreplay"))) {
2929
Files.copy(inputStream, tempFile);
3030
}
31-
assertThat(instance.parseReplay(tempFile).getData().length, is(197007));
31+
assertThat(instance.parseReplay(tempFile).getData().capacity(), is(197007));
3232
}
3333
}

src/test/java/com/faforever/client/replay/ReplayServiceTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import reactor.util.function.Tuple2;
5252
import reactor.util.function.Tuples;
5353

54+
import java.nio.ByteBuffer;
5455
import java.nio.file.Files;
5556
import java.nio.file.Path;
5657
import java.time.Instant;
@@ -185,7 +186,7 @@ public void setUp() throws Exception {
185186

186187
lenient().when(replayFileReader.parseReplay(any())).thenReturn(replayDataParser);
187188
lenient().when(replayDataParser.getMetadata()).thenReturn(replayMetadata);
188-
lenient().when(replayDataParser.getData()).thenReturn(REPLAY_FIRST_BYTES);
189+
lenient().when(replayDataParser.getData()).thenReturn(ByteBuffer.wrap(REPLAY_FIRST_BYTES));
189190
lenient().when(replayDataParser.getChatMessages()).thenReturn(List.of());
190191
lenient().when(replayDataParser.getGameOptions()).thenReturn(List.of());
191192
lenient().when(replayDataParser.getMods()).thenReturn(Map.of());

src/test/java/com/faforever/client/vault/VaultEntityControllerTest.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public class VaultEntityControllerTest extends PlatformTest {
7070

7171
private List<Integer> getMockPageElements(List<Integer> elements, int pageSize, int page) {
7272
return elements.subList(Math.min((page) * pageSize, elements.size()),
73-
Math.min((page + 1) * pageSize, elements.size()));
73+
Math.min((page + 1) * pageSize, elements.size()));
7474
}
7575

7676
private Mono<Tuple2<List<Integer>, Integer>> mocksAsMono(List<Integer> elements, int pageSize, int page) {
@@ -94,7 +94,8 @@ public void setUp() throws Exception {
9494
when(vaultEntityShowRoomController.getPane()).thenReturn(showRoomPane);
9595

9696
items = IntStream.range(0, 50).boxed().toList();
97-
instance = new VaultEntityController<>(uiService, notificationService, i18n, reportingService, vaultPrefs, fxApplicationThreadExecutor) {
97+
instance = new VaultEntityController<>(uiService, notificationService, i18n, reportingService, vaultPrefs,
98+
fxApplicationThreadExecutor) {
9899
@Override
99100
protected void initSearchController() {
100101
//Do Nothing
@@ -259,8 +260,6 @@ public void testPageSizeChange() {
259260

260261
instance.perPageComboBox.setValue(newPageSize);
261262
WaitForAsyncUtils.waitForFxEvents();
262-
instance.changePerPageCount();
263-
WaitForAsyncUtils.waitForFxEvents();
264263

265264
moreButton.fire();
266265
WaitForAsyncUtils.waitForFxEvents();

0 commit comments

Comments
 (0)