Implement more caching where the profiler showed bottlenecks

This commit is contained in:
Johannes Frohnmeyer 2022-01-24 19:21:23 +01:00
parent 86e0f00253
commit d59e0ef0df
Signed by: Johannes
GPG Key ID: E76429612C2929F4
29 changed files with 571 additions and 346 deletions

View File

@ -9,10 +9,10 @@ import io.gitlab.jfronny.inceptum.frontend.cli.CommandArgs;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.ConfigHolder;
import io.gitlab.jfronny.inceptum.util.MetaHolder;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.api.account.AccountManager;
import io.gitlab.jfronny.inceptum.frontend.gui.window.Window;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import org.lwjgl.glfw.Callbacks;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;

View File

@ -48,8 +48,13 @@ public abstract class BaseInstanceCommand extends Command {
Utils.LOGGER.error("Could not read instance metadata", e);
return;
}
invoke(args.subArgs(), instancePath, meta);
try {
invoke(args.subArgs(), instancePath, meta);
} catch (Exception e) {
Utils.LOGGER.error("Could not execute command", e);
return;
}
}
protected abstract void invoke(CommandArgs args, Path instancePath, InstanceMeta meta);
protected abstract void invoke(CommandArgs args, Path instancePath, InstanceMeta meta) throws Exception;
}

View File

@ -31,7 +31,7 @@ public abstract class Command {
return false;
}
protected abstract void invoke(CommandArgs args);
protected abstract void invoke(CommandArgs args) throws Exception;
public CommandResolution resolve(CommandArgs args) {
if (args.length != 0) {

View File

@ -3,7 +3,7 @@ package io.gitlab.jfronny.inceptum.frontend.cli;
import java.util.List;
public record CommandResolution(Command command, CommandArgs args, List<String> resolvePath) {
public void invoke() {
public void invoke() throws Exception {
command.invoke(args);
}
}

View File

@ -18,7 +18,7 @@ public class BatchCommand extends Command {
}
@Override
protected void invoke(CommandArgs args) {
protected void invoke(CommandArgs args) throws Exception {
if (args.length == 0) {
Utils.LOGGER.error("Could not start batch execution: No source file specified");
return;

View File

@ -46,18 +46,16 @@ public class GitCommand extends Command {
}
@Override
protected void invoke(CommandArgs args, Path instancePath, InstanceMeta meta) {
protected void invoke(CommandArgs args, Path instancePath, InstanceMeta meta) throws Exception {
try (Git git = Git.open(instancePath.toFile())) {
Config.GitConfig.GitAuth config = ConfigHolder.CONFIG.git.instanceAuths.get(instancePath.getFileName().toString());
invoke(args.subArgs(), instancePath, git, config.username != null && config.password != null
? new UsernamePasswordCredentialsProvider(config.username, config.password)
: CredentialsProvider.getDefault());
} catch (IOException e) {
e.printStackTrace();
}
}
protected abstract void invoke(CommandArgs args, Path instancePath, Git git, CredentialsProvider credentials);
protected abstract void invoke(CommandArgs args, Path instancePath, Git git, CredentialsProvider credentials) throws Exception;
}
private static class CommitCommand extends BaseGitCommand {
@ -66,20 +64,16 @@ public class GitCommand extends Command {
}
@Override
protected void invoke(CommandArgs args, Path instancePath, Git git, CredentialsProvider credentials) {
try {
git.commit()
.setAll(true)
.setMessage(args.length > 1
? String.join(" ", args.getArgs())
: "Commit at t" + System.currentTimeMillis())
.setSign(ConfigHolder.CONFIG.git.signCommits)
.setAuthor(ConfigHolder.CONFIG.git.commitUsername, ConfigHolder.CONFIG.git.commitMail)
.setCredentialsProvider(credentials)
.call();
} catch (GitAPIException e) {
e.printStackTrace();
}
protected void invoke(CommandArgs args, Path instancePath, Git git, CredentialsProvider credentials) throws GitAPIException {
git.commit()
.setAll(true)
.setMessage(args.length > 1
? String.join(" ", args.getArgs())
: "Commit at t" + System.currentTimeMillis())
.setSign(ConfigHolder.CONFIG.git.signCommits)
.setAuthor(ConfigHolder.CONFIG.git.commitUsername, ConfigHolder.CONFIG.git.commitMail)
.setCredentialsProvider(credentials)
.call();
}
}
@ -89,14 +83,10 @@ public class GitCommand extends Command {
}
@Override
protected void invoke(CommandArgs args, Path instancePath, Git git, CredentialsProvider credentials) {
try {
git.push()
.setCredentialsProvider(credentials)
.call();
} catch (GitAPIException e) {
e.printStackTrace();
}
protected void invoke(CommandArgs args, Path instancePath, Git git, CredentialsProvider credentials) throws GitAPIException {
git.push()
.setCredentialsProvider(credentials)
.call();
}
}
@ -106,15 +96,11 @@ public class GitCommand extends Command {
}
@Override
protected void invoke(CommandArgs args, Path instancePath, Git git, CredentialsProvider credentials) {
try {
git.pull()
.setRebase(true)
.setCredentialsProvider(credentials)
.call();
} catch (GitAPIException e) {
e.printStackTrace();
}
protected void invoke(CommandArgs args, Path instancePath, Git git, CredentialsProvider credentials) throws GitAPIException {
git.pull()
.setRebase(true)
.setCredentialsProvider(credentials)
.call();
}
}
@ -124,18 +110,14 @@ public class GitCommand extends Command {
}
@Override
protected void invoke(CommandArgs args, Path instancePath, Git git, CredentialsProvider credentials) {
try {
git.checkout()
.setAllPaths(true)
.call();
git.clean()
.setForce(true)
.setCleanDirectories(true)
.call();
} catch (GitAPIException e) {
e.printStackTrace();
}
protected void invoke(CommandArgs args, Path instancePath, Git git, CredentialsProvider credentials) throws GitAPIException {
git.checkout()
.setAllPaths(true)
.call();
git.clean()
.setForce(true)
.setCleanDirectories(true)
.call();
}
}
@ -145,24 +127,14 @@ public class GitCommand extends Command {
}
@Override
protected void invoke(CommandArgs args) {
protected void invoke(CommandArgs args) throws Exception {
if (args.length == 0) {
System.err.println("You haven't specified a remote");
return;
}
String name;
try {
name = args.length > 1 ? args.get(1) : getName(args.get(0));
} catch (URISyntaxException e) {
e.printStackTrace();
return;
}
String name = args.length > 1 ? args.get(1) : getName(args.get(0));
name = name.replaceAll("^\\.*", "");
try {
clone(args.get(0), name);
} catch (GitAPIException | IOException e) {
e.printStackTrace();
}
clone(args.get(0), name);
}
public static String getName(String url) throws URISyntaxException {

View File

@ -1,16 +1,15 @@
package io.gitlab.jfronny.inceptum.frontend.cli.commands;
import io.gitlab.jfronny.inceptum.Inceptum;
import io.gitlab.jfronny.inceptum.frontend.cli.BaseInstanceCommand;
import io.gitlab.jfronny.inceptum.frontend.cli.Command;
import io.gitlab.jfronny.inceptum.frontend.cli.CommandArgs;
import io.gitlab.jfronny.inceptum.util.install.Steps;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.InstanceLauncher;
import io.gitlab.jfronny.inceptum.util.InstanceLock;
import io.gitlab.jfronny.inceptum.util.MetaHolder;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.api.account.AccountManager;
import io.gitlab.jfronny.inceptum.util.InstanceLauncher;
import io.gitlab.jfronny.inceptum.util.install.Steps;
import java.io.IOException;
import java.nio.file.Files;
@ -44,7 +43,7 @@ public class LaunchCommand extends BaseInstanceCommand {
}
@Override
protected void invoke(CommandArgs args, Path instancePath, InstanceMeta meta) {
protected void invoke(CommandArgs args, Path instancePath, InstanceMeta meta) throws IOException, InstanceLauncher.LaunchException {
if (args.length == 0) {
Utils.LOGGER.error("You must provide an instance name or path");
return;
@ -59,21 +58,15 @@ public class LaunchCommand extends BaseInstanceCommand {
Utils.LOGGER.error("This instance is still being set up");
return;
}
try {
if (InstanceLock.isRunningLocked(instanceDir)) {
Utils.LOGGER.error("This instance is already running");
return;
}
} catch (IOException e) {
Inceptum.showError("Could not read inceptum lock", e);
if (InstanceLock.isRunningLocked(instanceDir)) {
Utils.LOGGER.error("This instance is already running");
return;
}
InstanceMeta instance;
try {
instance = Utils.loadObject(instanceDir.resolve("instance.json"), InstanceMeta.class);
} catch (IOException e) {
Inceptum.showError("Not a valid instance", e);
return;
throw new IOException("Not a valid instance", e);
}
if (args.length > 1) {
if (instance.arguments == null) instance.arguments = new InstanceMeta.Arguments();
@ -83,17 +76,9 @@ public class LaunchCommand extends BaseInstanceCommand {
instance.arguments.client.addAll(args.after(0));
instance.arguments.server.addAll(args.after(0));
}
try {
Steps.reDownload(instanceDir, Steps.createProcessState());
} catch (IOException e) {
e.printStackTrace();
}
Steps.reDownload(instanceDir, Steps.createProcessState());
if (server) {
try {
InstanceLauncher.launch(instanceDir, instance, InstanceLauncher.LaunchType.Server, restart, AccountManager.NULL_AUTH);
} catch (InstanceLauncher.LaunchException | IOException e) {
Inceptum.showError("Could not launch instance", e);
}
InstanceLauncher.launch(instanceDir, instance, InstanceLauncher.LaunchType.Server, restart, AccountManager.NULL_AUTH);
}
else {
AccountManager.loadAccounts();

View File

@ -18,37 +18,33 @@ public class ListCommand extends Command {
}
@Override
protected void invoke(CommandArgs args) {
try {
List<Path> paths = Utils.ls(MetaHolder.INSTANCE_DIR);
if (paths.isEmpty()) System.out.println("No instances are currently present");
for (Path path : paths) {
if (!Files.exists(path.resolve("instance.json"))) {
System.out.println("- Invalid instance: " + path + " (no instance metadata)");
continue;
}
System.out.println("- \"" + path.getFileName().toString() + "\"");
if (InstanceLock.isSetupLocked(path)) {
System.out.println(" Status: Setting up");
continue;
}
InstanceMeta instance;
try {
instance = Utils.loadObject(path.resolve("instance.json"), InstanceMeta.class);
} catch (IOException e) {
Utils.LOGGER.error(" Could not load instance.json", e);
continue;
}
System.out.println(" Status: " + (InstanceLock.isRunningLocked(path) ? "Running" : "Stopped"));
System.out.println(" Version: " + instance.getMinecraftVersion());
if (instance.isFabric()) System.out.println(" Fabric Loader: " + instance.getLoaderVersion());
if (instance.java != null) System.out.println(" Custom Java: " + instance.java);
if (instance.minMem != null || instance.maxMem != null)
System.out.println(" Memory:" + (instance.minMem != null ? " Minimum: " + instance.minMem : "")
+ (instance.maxMem != null ? " Maximum: " + instance.maxMem : ""));
protected void invoke(CommandArgs args) throws IOException {
List<Path> paths = Utils.ls(MetaHolder.INSTANCE_DIR);
if (paths.isEmpty()) System.out.println("No instances are currently present");
for (Path path : paths) {
if (!Files.exists(path.resolve("instance.json"))) {
System.out.println("- Invalid instance: " + path + " (no instance metadata)");
continue;
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("- \"" + path.getFileName().toString() + "\"");
if (InstanceLock.isSetupLocked(path)) {
System.out.println(" Status: Setting up");
continue;
}
InstanceMeta instance;
try {
instance = Utils.loadObject(path.resolve("instance.json"), InstanceMeta.class);
} catch (IOException e) {
Utils.LOGGER.error(" Could not load instance.json", e);
continue;
}
System.out.println(" Status: " + (InstanceLock.isRunningLocked(path) ? "Running" : "Stopped"));
System.out.println(" Version: " + instance.getMinecraftVersion());
if (instance.isFabric()) System.out.println(" Fabric Loader: " + instance.getLoaderVersion());
if (instance.java != null) System.out.println(" Custom Java: " + instance.java);
if (instance.minMem != null || instance.maxMem != null)
System.out.println(" Memory:" + (instance.minMem != null ? " Minimum: " + instance.minMem : "")
+ (instance.maxMem != null ? " Maximum: " + instance.maxMem : ""));
}
}
}

View File

@ -6,12 +6,12 @@ import io.gitlab.jfronny.inceptum.frontend.cli.Command;
import io.gitlab.jfronny.inceptum.frontend.cli.CommandArgs;
import io.gitlab.jfronny.inceptum.frontend.gui.window.AddModWindow;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.PathUtil;
import io.gitlab.jfronny.inceptum.util.mds.IWModDescription;
import io.gitlab.jfronny.inceptum.util.source.ModSource;
import io.gitlab.jfronny.inceptum.util.ModManager;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.PathUtil;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.mds.IWModDescription;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.source.ModSource;
import java.io.IOException;
import java.nio.file.Files;
@ -48,7 +48,7 @@ public class ModCommand extends Command {
}
@Override
protected void invoke(CommandArgs args, Path instancePath, InstanceMeta meta) {
protected void invoke(CommandArgs args, Path instancePath, InstanceMeta meta) throws IOException {
if (!meta.isFabric()) {
System.err.println("This is not a fabric instance");
return;
@ -91,7 +91,7 @@ public class ModCommand extends Command {
}
@Override
protected void invoke(CommandArgs args, Path instancePath, InstanceMeta meta) {
protected void invoke(CommandArgs args, Path instancePath, InstanceMeta meta) throws IOException {
if (!meta.isFabric()) {
Utils.LOGGER.error("This is not a fabric instance");
return;
@ -132,7 +132,7 @@ public class ModCommand extends Command {
}
@Override
protected void invoke(CommandArgs args, Path instancePath, InstanceMeta meta) {
protected void invoke(CommandArgs args, Path instancePath, InstanceMeta meta) throws IOException {
if (!meta.isFabric()) {
Utils.LOGGER.error("This is not a fabric instance");
return;

View File

@ -13,28 +13,29 @@ import io.gitlab.jfronny.inceptum.model.mojang.VersionsList;
import io.gitlab.jfronny.inceptum.model.mojang.VersionsListInfo;
import io.gitlab.jfronny.inceptum.util.ConfigHolder;
import io.gitlab.jfronny.inceptum.util.MetaHolder;
import io.gitlab.jfronny.inceptum.util.Tuple;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.api.FabricMetaApi;
import io.gitlab.jfronny.inceptum.util.api.McApi;
import io.gitlab.jfronny.inceptum.util.cache.MemoryCache;
import io.gitlab.jfronny.inceptum.util.tuple.Tuple;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
public class InstanceManageControls {
private final VersionsList manifest = McApi.getVersions();
private final Map<VersionsListInfo, List<FabricVersionLoaderInfo>> loaderInfoCache = new HashMap<>();
private static final VersionsList VERSIONS = McApi.getVersions();
private static final MemoryCache<VersionsListInfo, List<FabricVersionLoaderInfo>> LOADER_INFO_CACHE = new MemoryCache<>();
private static final MemoryCache<Tuple<String, String>, VersionInfo> VERSION_INFO_CACHE = new MemoryCache<>();
private final ImInt version = new ImInt(0);
private final ImString name = new ImString("", GuiUtil.INPUT_FIELD_LENGTH);
private final ImInt fabricVersion = new ImInt(0);
private final ImBoolean snapshots = new ImBoolean(ConfigHolder.CONFIG.snapshots);
private final ImBoolean fabric = new ImBoolean(true);
private final Map<Tuple<String, String>, VersionInfo> versionInfoCache = new HashMap<>();
private VersionsListInfo selected;
private FabricVersionLoaderInfo selectedFabric;
@ -128,13 +129,11 @@ public class InstanceManageControls {
}
public VersionInfo getVersionInfo() throws IOException {
Tuple<String, String> key = new Tuple<>(selected.id, fabric.get() ? selectedFabric.loader.version : "");
if (!versionInfoCache.containsKey(key)) {
return VERSION_INFO_CACHE.get(Tuple.of(selected.id, fabric.get() ? selectedFabric.loader.version : ""), () -> {
VersionInfo vi = McApi.getVersionInfo(selected);
if (fabric.get()) FabricMetaApi.addFabric(vi, selectedFabric.loader.version, FabricMetaApi.FabricVersionInfoType.Both);
versionInfoCache.put(key, vi);
}
return versionInfoCache.get(key);
return vi;
});
}
public LoaderInfo getLoaderInfo() {
@ -142,7 +141,7 @@ public class InstanceManageControls {
}
private List<VersionsListInfo> getVersions(boolean snapshots) {
ArrayList<VersionsListInfo> res = new ArrayList<>(manifest.versions);
ArrayList<VersionsListInfo> res = new ArrayList<>(VERSIONS.versions);
res.removeIf(info -> !snapshots && !info.type.equals("release"));
return res;
}
@ -152,8 +151,6 @@ public class InstanceManageControls {
}
private List<FabricVersionLoaderInfo> getFabricLoaderInfo() {
if (!loaderInfoCache.containsKey(selected))
loaderInfoCache.put(selected, FabricMetaApi.getLoaderVersions(selected));
return loaderInfoCache.get(selected);
return LOADER_INFO_CACHE.get(selected, () -> FabricMetaApi.getLoaderVersions(selected));
}
}

View File

@ -2,20 +2,26 @@ package io.gitlab.jfronny.inceptum.frontend.gui.control;
import imgui.ImGui;
import imgui.flag.ImGuiTableFlags;
import io.gitlab.jfronny.inceptum.Inceptum;
import io.gitlab.jfronny.inceptum.InceptumGui;
import io.gitlab.jfronny.inceptum.util.install.Steps;
import io.gitlab.jfronny.inceptum.frontend.gui.window.edit.InstanceEditWindow;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.InstanceLauncher;
import io.gitlab.jfronny.inceptum.util.InstanceLock;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.frontend.gui.window.edit.InstanceEditWindow;
import io.gitlab.jfronny.inceptum.util.InstanceLauncher;
import io.gitlab.jfronny.inceptum.util.cache.CachingGsonFile;
import io.gitlab.jfronny.inceptum.util.install.Steps;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class InstanceView {
private static final Map<Path, CachingGsonFile<InstanceMeta>> metas = new HashMap<>();
public static void draw(List<Path> paths) {
if (ImGui.beginTable("Instances", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Borders)) {
for (Path path : paths) {
@ -32,7 +38,8 @@ public class InstanceView {
}
InstanceMeta instance;
try {
instance = Utils.loadObject(path.resolve("instance.json"), InstanceMeta.class); //TODO investigate the perf impact of this
if (!metas.containsKey(path)) metas.put(path, new CachingGsonFile<>(path.resolve("instance.json"), InstanceMeta.class));
instance = metas.get(path).get();
} catch (IOException e) {
Utils.LOGGER.error("Could not load instance.json", e);
continue;
@ -56,7 +63,13 @@ public class InstanceView {
}
if (runDisabled) ImGui.endDisabled();
ImGui.tableNextColumn();
if (ImGui.button("Edit##" + path)) InceptumGui.open(new InstanceEditWindow(path, instance));
if (ImGui.button("Edit##" + path)) {
try {
InceptumGui.open(new InstanceEditWindow(path, instance));
} catch (IOException e) {
Utils.LOGGER.error("Could not open instance edit window", e);
}
}
}
ImGui.endTable();
}

View File

@ -9,13 +9,13 @@ import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.model.inceptum.ModDescription;
import io.gitlab.jfronny.inceptum.util.PathUtil;
import io.gitlab.jfronny.inceptum.util.mds.IWModDescription;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.source.CurseforgeModSource;
import io.gitlab.jfronny.inceptum.util.source.ModDownload;
import io.gitlab.jfronny.inceptum.util.source.ModSource;
import io.gitlab.jfronny.inceptum.util.source.ModrinthModSource;
import io.gitlab.jfronny.inceptum.model.modrinth.ModrinthSearchResult;
import io.gitlab.jfronny.inceptum.model.modrinth.ModrinthVersion;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.api.CurseforgeApi;
import io.gitlab.jfronny.inceptum.util.api.ModrinthApi;

View File

@ -12,9 +12,7 @@ import org.eclipse.jgit.api.Git;
import java.io.IOException;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public class InstanceEditWindow extends Window {
protected final Path path;
@ -26,20 +24,14 @@ public class InstanceEditWindow extends Window {
protected boolean reDownload = false;
protected boolean lastTabWasMods = false;
public InstanceEditWindow(Path path, InstanceMeta instance) {
public InstanceEditWindow(Path path, InstanceMeta instance) throws IOException {
super(path.getFileName().toString() + " - Edit");
name = path.getFileName().toString();
this.name = path.getFileName().toString();
this.path = path;
this.instance = instance;
mds = ModsDirScanner.get(path.resolve("mods"), instance);
mds.start();
Git git = null;
try {
git = Git.open(path.toFile());
} catch (IOException e) {
Utils.LOGGER.error("Could not open instance as git repo", e);
}
this.git = git;
this.mds = ModsDirScanner.get(path.resolve("mods"), instance);
this.mds.start();
this.git = Git.open(path.toFile());
this.tabs = List.of(
new GeneralTab(this),
new ArgumentsTab(this),

View File

@ -86,7 +86,7 @@ public class ModDescription {
public void addSource(ModSource source, String gameVersion) {
try {
sources.put(source, source.getUpdate(gameVersion)); //TODO only fetch updates on first check
sources.put(source, source.getUpdate(gameVersion));
} catch (IOException e) {
Utils.LOGGER.error("Could not check " + source.getName() + " for updates", e);
}

View File

@ -1,4 +0,0 @@
package io.gitlab.jfronny.inceptum.util;
public record Tuple<T1, T2>(T1 left, T2 right) {
}

View File

@ -72,7 +72,7 @@ public record FileScanTask(Path file, BiConsumer<Path, IWModDescription> discove
}
ModSource selectedSource = null;
for (ModSource source : md.sources.keySet()) {
source.getUpdate(gameVersion); //TODO only check once for ID
source.getUpdate(gameVersion);
if (!Files.exists(source.getJarPath())) source.download();
selectedSource = source;
}

View File

@ -1,143 +1,30 @@
package io.gitlab.jfronny.inceptum.util.mds;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.PathUtil;
import io.gitlab.jfronny.inceptum.util.Utils;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.*;
import java.util.Set;
import java.util.function.BiConsumer;
public class ModsDirScanner implements Closeable {
private static final Map<Path, ModsDirScanner> SCANNERS = new HashMap<>();
private static final ExecutorService POOL = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new NamedThreadFactory("mds"));
private final Map<Path, IWModDescription> descriptions = new HashMap<>();
private final Set<Path> scannedPaths = new HashSet<>();
private final Thread th;
private final Path modsDir;
private final InstanceMeta instance;
private boolean disposed = false;
private ModsDirScanner(Path modsDir, InstanceMeta instance) {
this.modsDir = modsDir;
this.instance = instance;
this.th = new Thread(this::scanTaskInternal, "mds-" + modsDir.getParent().getFileName());
public interface ModsDirScanner extends Closeable {
static ModsDirScanner get(Path modsDir, InstanceMeta instance) throws IOException {
if (Files.exists(modsDir)) return ModsDirScannerImpl.get(modsDir, instance);
return new NoopMds(instance.getMinecraftVersion());
}
public static ModsDirScanner get(Path modsDir, InstanceMeta instance) {
if (SCANNERS.containsKey(modsDir)) {
ModsDirScanner mds = SCANNERS.get(modsDir);
if (mds.instance.equals(instance))
return mds;
mds.close();
}
ModsDirScanner mds = new ModsDirScanner(modsDir, instance);
SCANNERS.put(modsDir, mds);
return mds;
static void closeAll() {
ModsDirScannerImpl.closeAll();
}
public boolean isComplete() {
if (!Files.isDirectory(modsDir)) return true;
try {
for (Path path : Utils.ls(modsDir)) {
if (!descriptions.containsKey(path)) return false;
}
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
public void start() {
if (!th.isAlive())
th.start();
}
public String getGameVersion() {
return instance.getMinecraftVersion();
}
public Set<IWModDescription> getMods() throws IOException {
Set<IWModDescription> mods = new TreeSet<>();
if (Files.isDirectory(modsDir)) {
for (Path path : Utils.ls(modsDir)) {
if (PathUtil.isImod(path) && Files.exists(PathUtil.trimImod(path)))
continue;
mods.add(get(path));
}
}
return mods;
}
public IWModDescription get(Path path) {
if (!descriptions.containsKey(path)) {
// not yet scanned
descriptions.put(path, new IWModDescription(path));
}
return descriptions.get(path);
}
public void invalidate(Path path) {
descriptions.remove(path);
scannedPaths.remove(path);
}
public boolean hasScanned(Path path) {
return scannedPaths.contains(path);
}
private void scanTaskInternal() {
while (!disposed) {
runOnce((path, iwModDescription) -> {});
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void runOnce(BiConsumer<Path, IWModDescription> discovered) {
try {
if (!Files.isDirectory(modsDir)) {
return;
}
Set<Future<?>> tasks = new HashSet<>();
String mc = instance.getMinecraftVersion();
discovered = discovered.andThen((path, iwModDescription) -> {
scannedPaths.add(path);
descriptions.put(path, iwModDescription);
});
for (Path mods : Utils.ls(modsDir)) {
if (disposed) return;
tasks.add(POOL.submit(new FileScanTask(mods, discovered, mc)));
}
for (Future<?> task : tasks) {
try {
task.get();
} catch (ExecutionException e) {
Utils.LOGGER.error("Failed to execute ScanTask", e);
}
}
} catch (IOException | InterruptedException e) {
Utils.LOGGER.error("Could not list mods", e);
}
}
public static void closeAll() {
for (ModsDirScanner value : SCANNERS.values().toArray(ModsDirScanner[]::new)) {
value.close();
}
POOL.shutdown();
}
@Override
public void close() {
disposed = true;
SCANNERS.remove(modsDir);
}
boolean isComplete();
void start();
String getGameVersion();
Set<IWModDescription> getMods() throws IOException;
IWModDescription get(Path path);
void invalidate(Path path);
boolean hasScanned(Path path);
void runOnce(BiConsumer<Path, IWModDescription> discovered);
}

View File

@ -0,0 +1,168 @@
package io.gitlab.jfronny.inceptum.util.mds;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.PathUtil;
import io.gitlab.jfronny.inceptum.util.Utils;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.BiConsumer;
import static java.nio.file.StandardWatchEventKinds.*;
class ModsDirScannerImpl implements ModsDirScanner {
private static final Map<Path, ModsDirScannerImpl> SCANNERS = new HashMap<>();
private static final ExecutorService POOL = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new NamedThreadFactory("mds"));
private final Map<Path, IWModDescription> descriptions = new HashMap<>();
private final Set<Path> scannedPaths = new HashSet<>();
private final Thread th;
private final Path modsDir;
private final InstanceMeta instance;
private final WatchService service;
private boolean disposed = false;
private ModsDirScannerImpl(Path modsDir, InstanceMeta instance) throws IOException {
this.modsDir = modsDir;
this.instance = instance;
this.th = new Thread(this::scanTaskInternal, "mds-" + modsDir.getParent().getFileName());
this.service = FileSystems.getDefault().newWatchService();
modsDir.register(service, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);
}
protected static ModsDirScannerImpl get(Path modsDir, InstanceMeta instance) throws IOException {
if (SCANNERS.containsKey(modsDir)) {
ModsDirScannerImpl mds = SCANNERS.get(modsDir);
if (mds.instance.equals(instance))
return mds;
mds.close();
}
ModsDirScannerImpl mds = new ModsDirScannerImpl(modsDir, instance);
SCANNERS.put(modsDir, mds);
return mds;
}
@Override
public boolean isComplete() {
if (!Files.isDirectory(modsDir)) return true;
try {
for (Path path : Utils.ls(modsDir)) {
if (!descriptions.containsKey(path)) return false;
}
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
@Override
public void start() {
if (!th.isAlive())
th.start();
}
@Override
public String getGameVersion() {
return instance.getMinecraftVersion();
}
@Override
public Set<IWModDescription> getMods() throws IOException {
Set<IWModDescription> mods = new TreeSet<>();
if (Files.isDirectory(modsDir)) {
for (Path path : Utils.ls(modsDir)) {
if (PathUtil.isImod(path) && Files.exists(PathUtil.trimImod(path)))
continue;
mods.add(get(path));
}
}
return mods;
}
@Override
public IWModDescription get(Path path) {
if (!descriptions.containsKey(path)) {
// not yet scanned
descriptions.put(path, new IWModDescription(path));
}
return descriptions.get(path);
}
@Override
public void invalidate(Path path) {
descriptions.remove(path);
scannedPaths.remove(path);
}
public boolean hasScanned(Path path) {
return scannedPaths.contains(path);
}
private void scanTaskInternal() {
while (!disposed) {
runOnce((path, iwModDescription) -> {});
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void runOnce(BiConsumer<Path, IWModDescription> discovered) {
try {
if (!Files.isDirectory(modsDir)) {
return;
}
Set<Future<?>> tasks = new HashSet<>();
String mc = instance.getMinecraftVersion();
discovered = discovered.andThen((path, iwModDescription) -> {
scannedPaths.add(path);
descriptions.put(path, iwModDescription);
});
List<Path> toScan;
if (descriptions.isEmpty()) {
toScan = Utils.ls(modsDir);
}
else {
toScan = new ArrayList<>();
WatchKey key = service.poll();
if (key != null) {
for (WatchEvent<?> event : key.pollEvents()) {
if (event.context() instanceof Path p)
toScan.add(p);
}
key.reset();
}
}
for (Path p : toScan) {
tasks.add(POOL.submit(new FileScanTask(p, discovered, mc)));
}
for (Future<?> task : tasks) {
try {
task.get();
} catch (ExecutionException e) {
Utils.LOGGER.error("Failed to execute ScanTask", e);
}
}
} catch (IOException | InterruptedException e) {
Utils.LOGGER.error("Could not list mods", e);
}
}
public static void closeAll() {
for (ModsDirScannerImpl value : SCANNERS.values().toArray(ModsDirScannerImpl[]::new)) {
value.close();
}
POOL.shutdown();
}
@Override
public void close() {
disposed = true;
SCANNERS.remove(modsDir);
}
}

View File

@ -0,0 +1,49 @@
package io.gitlab.jfronny.inceptum.util.mds;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Set;
import java.util.function.BiConsumer;
public record NoopMds(String gameVersion) implements ModsDirScanner {
@Override
public void close() throws IOException {
}
@Override
public boolean isComplete() {
return true;
}
@Override
public void start() {
}
@Override
public String getGameVersion() {
return gameVersion;
}
@Override
public Set<IWModDescription> getMods() throws IOException {
return Set.of();
}
@Override
public IWModDescription get(Path path) {
return new IWModDescription(path);
}
@Override
public void invalidate(Path path) {
}
@Override
public boolean hasScanned(Path path) {
return true;
}
@Override
public void runOnce(BiConsumer<Path, IWModDescription> discovered) {
}
}

View File

@ -6,6 +6,9 @@ import io.gitlab.jfronny.inceptum.model.curseforge.CurseforgeMod;
import io.gitlab.jfronny.inceptum.util.HashUtils;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.api.CurseforgeApi;
import io.gitlab.jfronny.inceptum.util.cache.MemoryCache;
import io.gitlab.jfronny.inceptum.util.tuple.Triple;
import io.gitlab.jfronny.inceptum.util.tuple.Tuple;
import java.io.IOException;
import java.net.URISyntaxException;
@ -16,6 +19,9 @@ import java.util.Optional;
import java.util.Set;
public final class CurseforgeModSource implements ModSource {
private static final MemoryCache<Triple<Integer, Integer, String>, Optional<ModSource>> UPDATE_CACHE = new MemoryCache<>();
private static final MemoryCache<Tuple<Integer, Integer>, Set<ModSource>> DEPENDENCIES_CACHE = new MemoryCache<>();
private final int projectId;
private final int fileId;
private final CurseforgeFile current;
@ -42,24 +48,28 @@ public final class CurseforgeModSource implements ModSource {
@Override
public Set<ModSource> getDependencies() throws IOException {
Set<ModSource> deps = new HashSet<>();
for (CurseforgeDependency dependency : current.dependencies) {
if (dependency.type == 3) //TODO support other types (3=required, 2=optional)
deps.add(new CurseforgeModSource(dependency.addonId, dependency.fileId));
}
return deps;
return DEPENDENCIES_CACHE.get(Tuple.of(projectId, fileId), () -> {
Set<ModSource> deps = new HashSet<>();
for (CurseforgeDependency dependency : current.dependencies) {
if (dependency.type == 3) //TODO support other types (3=required, 2=optional)
deps.add(new CurseforgeModSource(dependency.addonId, dependency.fileId));
}
return deps;
});
}
@Override
public Optional<ModSource> getUpdate(String gameVersion) throws IOException {
for (CurseforgeMod.GameVersionLatestFile file : mod.gameVersionLatestFiles) {
if (file.gameVersion.equals(gameVersion)) {
return file.projectFileId == fileId
? Optional.empty()
: Optional.of(new CurseforgeModSource(projectId, file.projectFileId));
return UPDATE_CACHE.get(Tuple.of(projectId, fileId, gameVersion), () -> {
for (CurseforgeMod.GameVersionLatestFile file : mod.gameVersionLatestFiles) {
if (file.gameVersion.equals(gameVersion)) {
return file.projectFileId == fileId
? Optional.empty()
: Optional.of(new CurseforgeModSource(projectId, file.projectFileId));
}
}
}
return Optional.empty();
return Optional.empty();
});
}
@Override

View File

@ -5,6 +5,8 @@ import io.gitlab.jfronny.inceptum.model.modrinth.ModrinthVersion;
import io.gitlab.jfronny.inceptum.util.HashUtils;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.api.ModrinthApi;
import io.gitlab.jfronny.inceptum.util.cache.MemoryCache;
import io.gitlab.jfronny.inceptum.util.tuple.Tuple;
import java.io.IOException;
import java.net.URISyntaxException;
@ -15,6 +17,9 @@ import java.util.Optional;
import java.util.Set;
public final class ModrinthModSource implements ModSource {
private static final MemoryCache<Tuple<String, String>, Optional<ModSource>> UPDATE_CACHE = new MemoryCache<>();
private static final MemoryCache<String, Set<ModSource>> DEPENDENCIES_CACHE = new MemoryCache<>();
private final String versionId;
private final ModrinthVersion current;
private final ModrinthMod mod;
@ -39,37 +44,41 @@ public final class ModrinthModSource implements ModSource {
@Override
public Set<ModSource> getDependencies() throws IOException {
Set<ModSource> deps = new HashSet<>();
for (ModrinthVersion.Dependency dependency : current.dependencies) {
//TODO show optional dependencies
if (dependency.dependency_type == ModrinthVersion.Dependency.DependencyType.required)
deps.add(new ModrinthModSource(dependency.version_id));
}
return deps;
return DEPENDENCIES_CACHE.get(versionId, () -> {
Set<ModSource> deps = new HashSet<>();
for (ModrinthVersion.Dependency dependency : current.dependencies) {
//TODO show optional dependencies
if (dependency.dependency_type == ModrinthVersion.Dependency.DependencyType.required)
deps.add(new ModrinthModSource(dependency.version_id));
}
return Set.copyOf(deps);
});
}
@Override
public Optional<ModSource> getUpdate(String gameVersion) throws IOException {
ModrinthVersion stable = null;
ModrinthVersion beta = null;
ModrinthVersion latest = null;
for (ModrinthVersion version : ModrinthApi.getVersions(getModId())) {
if (version.game_versions.contains(gameVersion) && version.loaders.contains("fabric")) {
latest = version;
if (version.version_type == ModrinthVersion.VersionType.beta || version.version_type == ModrinthVersion.VersionType.release)
beta = version;
if (version.version_type == ModrinthVersion.VersionType.release)
stable = version;
return UPDATE_CACHE.get(Tuple.of(versionId, gameVersion), () -> {
ModrinthVersion stable = null;
ModrinthVersion beta = null;
ModrinthVersion latest = null;
for (ModrinthVersion version : ModrinthApi.getVersions(getModId())) {
if (version.game_versions.contains(gameVersion) && version.loaders.contains("fabric")) {
latest = version;
if (version.version_type == ModrinthVersion.VersionType.beta || version.version_type == ModrinthVersion.VersionType.release)
beta = version;
if (version.version_type == ModrinthVersion.VersionType.release)
stable = version;
}
}
}
ModrinthVersion next = switch (current.version_type) {
case alpha -> latest;
case beta -> beta;
case release -> stable;
};
if (next == null) return Optional.empty();
if (next.version_number.equals(current.version_number)) return Optional.empty();
return Optional.of(new ModrinthModSource(next.id));
ModrinthVersion next = switch (current.version_type) {
case alpha -> latest;
case beta -> beta;
case release -> stable;
};
if (next == null) return Optional.empty();
if (next.version_number.equals(current.version_number)) return Optional.empty();
return Optional.of(new ModrinthModSource(next.id));
});
}
@Override

View File

@ -3,6 +3,7 @@ package io.gitlab.jfronny.inceptum.util;
import io.gitlab.jfronny.inceptum.WrapperStrap;
import io.gitlab.jfronny.inceptum.gson.GsonHolder;
import io.gitlab.jfronny.inceptum.model.OSType;
import io.gitlab.jfronny.inceptum.util.cache.GsonFileCache;
import io.gitlab.jfronny.inceptum.util.lambda.ThrowingConsumer;
import io.gitlab.jfronny.inceptum.util.lambda.ThrowingSupplier;
import org.slf4j.Logger;
@ -22,7 +23,6 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@ -30,6 +30,7 @@ import java.util.stream.Stream;
public class Utils {
public static final Pattern VALID_FILENAME = Pattern.compile("[a-zA-Z0-9_\\-.][a-zA-Z0-9 _\\-.]*[a-zA-Z0-9_\\-.]");
public static final Logger LOGGER = LoggerFactory.getLogger("Inceptum");
private static final GsonFileCache OBJECT_CACHE = new GsonFileCache(MetaHolder.CACHE_DIR);
public static byte[] downloadData(String url) throws IOException, URISyntaxException {
try (InputStream is = HttpUtils.get(url).sendInputStream()) {
@ -45,28 +46,22 @@ public class Utils {
}
public static <T> T downloadObject(String url, Class<T> type) throws IOException {
return downloadObject(url, () -> HttpUtils.get(url).sendString(), s -> GsonHolder.getGson().fromJson(s, type));
return downloadObject(url, () -> HttpUtils.get(url).sendString(), type);
}
public static <T> T downloadObject(String url, Type type) throws IOException {
return downloadObject(url, () -> HttpUtils.get(url).sendString(), s -> GsonHolder.getGson().fromJson(s, type));
return downloadObject(url, () -> HttpUtils.get(url).sendString(), type);
}
public static <T> T downloadObject(String url, String sha1, Class<T> type) throws IOException {
return downloadObject(url, () -> downloadString(url, sha1), s -> GsonHolder.getGson().fromJson(s, type));
return downloadObject(url, () -> downloadString(url, sha1), type);
}
//TODO memory-based cache to avoid gson
private static <T> T downloadObject(String url, ThrowingSupplier<String, Exception> sourceString, Function<String, T> builder) throws IOException {
Path cache = MetaHolder.CACHE_DIR.resolve(HashUtils.sha1(url.getBytes(StandardCharsets.UTF_8)));
if (Files.exists(cache))
return builder.apply(Files.readString(cache));
private static <T> T downloadObject(String url, ThrowingSupplier<String, Exception> sourceString, Type type) throws IOException {
try {
String download = sourceString.get();
Files.writeString(cache, download);
return builder.apply(download);
return OBJECT_CACHE.get(HashUtils.sha1(url.getBytes(StandardCharsets.UTF_8)), () -> GsonHolder.getGson().fromJson(sourceString.get(), type), type);
} catch (Exception e) {
throw new IOException("Could not download object and no cache exists (URL=" + url + ", Cache=" + cache + ")", e);
throw new IOException("Could not download object and no cache exists", e);
}
}

View File

@ -0,0 +1,41 @@
package io.gitlab.jfronny.inceptum.util.cache;
import io.gitlab.jfronny.inceptum.util.Utils;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
public class CachingGsonFile<T> implements Closeable {
private final Path filePath;
private final Class<T> type;
private final WatchService service;
private T cache = null;
public CachingGsonFile(Path filePath, Class<T> type) throws IOException {
this.filePath = filePath;
this.type = type;
this.service = FileSystems.getDefault().newWatchService();
filePath.getParent().register(service, ENTRY_MODIFY);
}
public T get() throws IOException {
WatchKey key = service.poll();
boolean update = cache == null;
if (key != null) {
for (WatchEvent<?> event : key.pollEvents()) {
update |= event.context().equals(filePath);
}
key.reset();
}
if (update) cache = Utils.loadObject(filePath, type);
return cache;
}
@Override
public void close() throws IOException {
service.close();
}
}

View File

@ -0,0 +1,54 @@
package io.gitlab.jfronny.inceptum.util.cache;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.lambda.ThrowingSupplier;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
public class GsonFileCache {
private final ConcurrentHashMap<String, Object> container = new ConcurrentHashMap<>();
private final Path cacheDir;
public GsonFileCache(Path cacheDir) {
this.cacheDir = cacheDir;
}
public void remove(String key) {
container.remove(key);
try {
Files.delete(cacheDir.resolve(key));
} catch (IOException e) {
Utils.LOGGER.error("Could not remove cache entry", e);
}
}
public void clear() {
container.clear();
try {
Utils.ls(cacheDir, Files::delete);
} catch (IOException e) {
Utils.LOGGER.error("Could not clear cache backend", e);
}
}
public <T, TEx extends Throwable> T get(String key, ThrowingSupplier<T, TEx> builder, Type klazz) throws TEx {
if (!container.containsKey(key)) {
Path cd = cacheDir.resolve(key);
if (Files.exists(cd)) {
try {
T value = Utils.loadObject(cd, klazz);
Utils.writeObject(cd, value);
container.put(key, value);
} catch (IOException e) {
Utils.LOGGER.error("Could not read cache", e);
}
}
else container.put(key, builder.get());
}
return (T) container.get(key);
}
}

View File

@ -0,0 +1,25 @@
package io.gitlab.jfronny.inceptum.util.cache;
import io.gitlab.jfronny.inceptum.util.lambda.ThrowingSupplier;
import java.util.concurrent.ConcurrentHashMap;
public class MemoryCache<TKey, TValue> {
private final ConcurrentHashMap<TKey, TValue> container = new ConcurrentHashMap<>();
public void remove(TKey key) {
container.remove(key);
}
public void clear() {
container.clear();
}
public void write() {
}
public <TEx extends Throwable> TValue get(TKey key, ThrowingSupplier<TValue, TEx> builder) throws TEx {
if (!container.containsKey(key)) container.put(key, builder.get());
return container.get(key);
}
}

View File

@ -0,0 +1,4 @@
package io.gitlab.jfronny.inceptum.util.tuple;
public record Quadruple<T1, T2, T3, T4>(T1 val1, T2 val2, T3 val3, T4 val4) {
}

View File

@ -0,0 +1,4 @@
package io.gitlab.jfronny.inceptum.util.tuple;
public record Single<T1>(T1 val1) {
}

View File

@ -0,0 +1,4 @@
package io.gitlab.jfronny.inceptum.util.tuple;
public record Triple<T1, T2, T3>(T1 val1, T2 val2, T3 val3) {
}

View File

@ -0,0 +1,19 @@
package io.gitlab.jfronny.inceptum.util.tuple;
public record Tuple<T1, T2>(T1 val1, T2 val2) {
public static <T1> Single<T1> of(T1 val1) {
return new Single<>(val1);
}
public static <T1, T2> Tuple<T1, T2> of(T1 left, T2 right) {
return new Tuple<>(left, right);
}
public static <T1, T2, T3> Triple<T1, T2, T3> of(T1 t1, T2 t2, T3 t3) {
return new Triple<>(t1, t2, t3);
}
public static <T1, T2, T3, T4> Quadruple<T1, T2, T3, T4> of(T1 t1, T2 t2, T3 t3, T4 t4) {
return new Quadruple<>(t1, t2, t3, t4);
}
}