From 227bc405aa1c40bba9983a97203f97d452b5d510 Mon Sep 17 00:00:00 2001 From: M-W-K <31022105+M-W-K@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:46:33 -0700 Subject: [PATCH 1/3] Implement recipe map lookup time benchmarking command --- .../command/benchmark/CommandBenchmark.java | 26 ++ .../benchmark/CommandBenchmarkLookup.java | 278 ++++++++++++++++++ src/main/java/gregtech/core/CoreModule.java | 2 + .../resources/assets/gregtech/lang/en_us.lang | 6 + 4 files changed, 312 insertions(+) create mode 100644 src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java create mode 100644 src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java diff --git a/src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java b/src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java new file mode 100644 index 00000000000..2ba4816e789 --- /dev/null +++ b/src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java @@ -0,0 +1,26 @@ +package gregtech.common.command.benchmark; + +import net.minecraft.command.CommandBase; +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.server.command.CommandTreeBase; + +import org.jetbrains.annotations.NotNull; + +public class CommandBenchmark extends CommandTreeBase { + + public CommandBenchmark() { + addSubcommand(new CommandBenchmarkLookup()); + } + + @Override + public @NotNull String getName() { + return "benchmark"; + } + + @Override + public @NotNull String getUsage(ICommandSender sender) { + return "gregtech.command.benchmark.usage"; + } +} diff --git a/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java b/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java new file mode 100644 index 00000000000..9c83c5e466d --- /dev/null +++ b/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java @@ -0,0 +1,278 @@ +package gregtech.common.command.benchmark; + +import com.github.bsideup.jabel.Desugar; + +import com.google.common.collect.ImmutableList; + +import gregtech.api.recipes.Recipe; +import gregtech.api.recipes.RecipeMap; + +import gregtech.api.recipes.ingredients.GTRecipeInput; +import gregtech.api.util.GTLog; + +import it.unimi.dsi.fastutil.Function; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +import net.minecraft.command.CommandBase; +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.command.WrongUsageException; +import net.minecraft.item.ItemStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.text.Style; +import net.minecraft.util.text.TextComponentTranslation; +import net.minecraft.util.text.TextFormatting; +import net.minecraftforge.fluids.FluidStack; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.ToLongFunction; + +public class CommandBenchmarkLookup extends CommandBase { + + @Override + public String getName() { + return "lookup"; + } + + @Override + public String getUsage(ICommandSender sender) { + return "gregtech.command.benchmark.lookup.usage"; + } + + @Override + public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException { + int trials = 100; + if (args.length != 0) { + try { + trials = Integer.parseInt(args[0]); + if (trials <= 0) throw new NumberFormatException(); + } catch (NumberFormatException ignored) { + throw new WrongUsageException("gregtech.command.benchmark.lookup.usage"); + } + } + Object2ObjectOpenHashMap, ObjectArrayList> nsTrialTimes = new Object2ObjectOpenHashMap<>(); + Object2ObjectOpenHashMap, List> recipeLists = new Object2ObjectOpenHashMap<>(); + AtomicInteger failureCount = new AtomicInteger(); + GTLog.logger.info("[Benchmarking] Starting recipe lookup benchmarking..."); + for (int i = 0; i < trials; i++) { + for (RecipeMap map : RecipeMap.getRecipeMaps()) { + if (recipeLists.computeIfAbsent(map, m -> new ObjectArrayList<>(m.getRecipeList())).isEmpty()) continue; + nsTrialTimes.computeIfAbsent(map, m -> new ObjectArrayList<>()) + .add(trial(recipeLists.get(map), (v, it, f) -> { + Recipe r = map.findRecipe(v, it, f); + return r == null ? Collections.emptyList() : ImmutableList.of(r); + }, failureCount)); + } + if ((i > 1) && ((i & (i - 1)) == 0)) { + GTLog.logger.info("[Benchmarking] {}th trial complete...", i); + } + } + GTLog.logger.info("[Benchmarking] Benchmarking complete. Outputting results:"); + Object2ObjectOpenHashMap, BenchmarkResults> resultsCache = new Object2ObjectOpenHashMap<>(); + for (var entry : nsTrialTimes.entrySet()) { + BenchmarkResults results = composeResults(entry.getValue()); + resultsCache.put(entry.getKey(), results); + GTLog.logger.info("[Benchmarking] Recipe Map {}, measurements in nanoseconds:", entry.getKey().getLocalizedName()); + GTLog.logger.info("[Benchmarking] - Characteristic numbers for No Match: {}", results.noMatchTest()); + GTLog.logger.info("[Benchmarking] - Characteristic numbers for One Match Exact: {}", results.oneMatchExactTest()); + GTLog.logger.info("[Benchmarking] - Characteristic numbers for Three Match Excess: {}", results.threeMatchExcessTest()); + } + sender.sendMessage(new TextComponentTranslation("gregtech.command.benchmark.lookup.success") + .setStyle(new Style().setColor(TextFormatting.GREEN))); + sender.sendMessage(new TextComponentTranslation("gregtech.command.benchmark.lookup.failures", failureCount.get()) + .setStyle(new Style().setColor(TextFormatting.RED))); + try (FileWriter writer = new FileWriter("benchmark-lookup-results.csv")) { + writer.append("Recipe Map,Trial Type,Minimum,Q1,Median,Q3,Maximum\n"); + for (var entry : resultsCache.entrySet()) { + writer.append(entry.getKey().getLocalizedName()).append(',').append("No Match"); + for (double d : entry.getValue().noMatchTest()) { + writer.append(',').append(String.valueOf(d)); + } + writer.append('\n'); + writer.append(entry.getKey().getLocalizedName()).append(',').append("One Match Exact"); + for (double d : entry.getValue().oneMatchExactTest()) { + writer.append(',').append(String.valueOf(d)); + } + writer.append('\n'); + writer.append(entry.getKey().getLocalizedName()).append(',').append("Three Match Excess"); + for (double d : entry.getValue().threeMatchExcessTest()) { + writer.append(',').append(String.valueOf(d)); + } + writer.append('\n'); + } + GTLog.logger.info("[Benchmarking] Output saved to csv file 'benchmark-lookup-results.csv'"); + sender.sendMessage(new TextComponentTranslation("gregtech.command.benchmark.lookup.written") + .setStyle(new Style().setColor(TextFormatting.GREEN))); + } catch (IOException e) { + GTLog.logger.info("[Benchmarking] Failed to output to csv file 'benchmark-lookup-results.csv'"); + } + } + + private TrialResults trial(List recipeSpace, RecipeLookupFunction function, AtomicInteger failureCount) throws CommandException { + Recipe[] sample = new Recipe[10]; + for (int i = 0; i < 10; i++) { + double r = Math.random(); + sample[i] = recipeSpace.get((int) (r * recipeSpace.size())); + } + // no match trial + Set seen = new ObjectOpenHashSet<>(); + List items = new ObjectArrayList<>(); + List fluids = new ObjectArrayList<>(); + for (Recipe r : sample) { + // if adding an item input would lead to recipe matching, do not add it. + if (r.getInputs().size() > 1) { + r.getInputs().stream().filter(i -> !seen.contains(i)).findAny() + .map(GTRecipeInput::getInputStacks) + .map(arr -> arr[0]) + .ifPresent(items::add); + for (Recipe value : sample) { + if (value.matches(false, items, fluids)) { + items.remove(items.size() - 1); + } + } + } + // if adding a fluid input would lead to recipe matching, do not add it + if (r.getFluidInputs().size() > 1) { + r.getFluidInputs().stream().filter(i -> !seen.contains(i)).findAny() + .map(GTRecipeInput::getInputFluidStack) + .ifPresent(fluids::add); + for (Recipe recipe : sample) { + if (recipe.matches(false, items, fluids)) { + fluids.remove(fluids.size() - 1); + } + } + } + // prevent randomly adding inputs in the future that would lead to matching a recipe + seen.addAll(r.getInputs()); + seen.addAll(r.getFluidInputs()); + } + long start = System.nanoTime(); + Collection out = function.find(Long.MAX_VALUE, items, fluids); + // in a real situation, any outputs would be run through a count match until the first success if found. + // run this matching while timing to penalize returning too many possible matches + for (Recipe r : out) { + if (r.matches(false, items, fluids)) { + break; + } + } + long timeNoMatch = System.nanoTime() - start; + for (Recipe r : sample) { + if (out.contains(r)) { + failureCount.incrementAndGet(); + GTLog.logger.info("[Benchmarking] Recipe sample {} has failed the no match test.", (Object) sample); + } + } + + // three match excess trial + for (int i = 0; i < 3; i++) { + for (GTRecipeInput input : sample[i].getInputs()) { + ItemStack stack = input.getInputStacks()[0].copy(); + stack.setCount(input.getAmount()); + items.add(stack); + } + for (GTRecipeInput input : sample[i].getFluidInputs()) { + FluidStack stack = input.getInputFluidStack().copy(); + stack.amount = input.getAmount(); + fluids.add(stack); + } + } + start = System.nanoTime(); + out = function.find(Long.MAX_VALUE, items, fluids); + // in a real situation, any outputs would be run through a count match. + // run this matching while timing to penalize returning too many possible matches + for (Recipe r : out) { + if (r.matches(false, items, fluids)) { + break; + } + } + long timeThreeExcess = System.nanoTime() - start; + // re-enable this code block once lookup returns proper lists of matching recipes +// for (int i = 0; i < 3; i++) { +// Recipe r = sample[i]; +// if (!out.contains(r)) { +// throw new CommandException("Something in the benchmark's three match test is wrong! Report this to mod authors with context."); +// } +// } + + // one match exact trial + items.clear(); + fluids.clear(); + for (GTRecipeInput input : sample[0].getInputs()) { + ItemStack stack = input.getInputStacks()[0].copy(); + stack.setCount(input.getAmount()); + items.add(stack); + } + for (GTRecipeInput input : sample[0].getFluidInputs()) { + FluidStack stack = input.getInputFluidStack().copy(); + stack.amount = input.getAmount(); + fluids.add(stack); + } + start = System.nanoTime(); + out = function.find(Long.MAX_VALUE, items, fluids); + // in a real situation, any outputs would be run through a count match. + // run this matching while timing to penalize returning too many possible matches + for (Recipe r : out) { + if (r.matches(false, items, fluids)) { + break; + } + } + long timeOneExact = System.nanoTime() - start; + if (!out.contains(sample[0])) { + failureCount.incrementAndGet(); + GTLog.logger.info("[Benchmarking] Recipe sample {} has failed the exact match test.", (Object) sample); + } + return new TrialResults(timeNoMatch, timeOneExact, timeThreeExcess); + } + + private BenchmarkResults composeResults(List results) { + return new BenchmarkResults(representativeNumbers(results, TrialResults::noMatchTest), representativeNumbers(results, TrialResults::oneMatchExactTest), representativeNumbers(results, TrialResults::threeMatchExcessTest)); + } + + private double[] representativeNumbers(List results, ToLongFunction func) { + results.sort(Comparator.comparingLong(func)); + double[] numbers = new double[5]; + numbers[0] = func.applyAsLong(results.get(0)); + numbers[4] = func.applyAsLong(results.get(results.size() - 1)); + int offset = 0; + if (results.size() % 2 == 1) { + numbers[2] = func.applyAsLong(results.get(results.size() / 2)); + offset = 1; + } else { + numbers[2] = (func.applyAsLong(results.get(results.size() / 2 - 1)) + + func.applyAsLong(results.get(results.size() / 2))) / 2d; + } + int s = results.size() / 2; + if (s % 2 == 1) { + numbers[1] = func.applyAsLong(results.get(s / 2)); + numbers[3] = func.applyAsLong(results.get(s + offset + s / 2)); + } else { + numbers[1] = (func.applyAsLong(results.get(s / 2 - 1)) + + func.applyAsLong(results.get(s / 2))) / 2d; + numbers[3] = (func.applyAsLong(results.get(s / 2 - 1)) + + func.applyAsLong(results.get(s / 2))) / 2d; + } + return numbers; + } + + interface RecipeLookupFunction { + Collection find(long voltage, List items, List fluids); + } + + @Desugar + record TrialResults(long noMatchTest, long oneMatchExactTest, long threeMatchExcessTest) {} + + @Desugar + record BenchmarkResults(double[] noMatchTest, double[] oneMatchExactTest, double[] threeMatchExcessTest) {} +} diff --git a/src/main/java/gregtech/core/CoreModule.java b/src/main/java/gregtech/core/CoreModule.java index 6ea9f2a3d94..385eb404dad 100644 --- a/src/main/java/gregtech/core/CoreModule.java +++ b/src/main/java/gregtech/core/CoreModule.java @@ -44,6 +44,7 @@ import gregtech.common.command.CommandHand; import gregtech.common.command.CommandRecipeCheck; import gregtech.common.command.CommandShaders; +import gregtech.common.command.benchmark.CommandBenchmark; import gregtech.common.command.worldgen.CommandWorldgen; import gregtech.common.covers.CoverBehaviors; import gregtech.common.covers.filter.oreglob.impl.OreGlobParser; @@ -321,6 +322,7 @@ public void serverStarting(FMLServerStartingEvent event) { GregTechAPI.commandManager.addCommand(new CommandRecipeCheck()); GregTechAPI.commandManager.addCommand(new CommandShaders()); GregTechAPI.commandManager.addCommand(new CommandDataFix()); + GregTechAPI.commandManager.addCommand(new CommandBenchmark()); CapesRegistry.load(); if (Mods.BetterQuestingUnofficial.isModLoaded()) { diff --git a/src/main/resources/assets/gregtech/lang/en_us.lang b/src/main/resources/assets/gregtech/lang/en_us.lang index ea6df5b60b1..c00704538cc 100644 --- a/src/main/resources/assets/gregtech/lang/en_us.lang +++ b/src/main/resources/assets/gregtech/lang/en_us.lang @@ -6007,6 +6007,12 @@ gregtech.command.recipecheck.begin=Starting recipe issue check... gregtech.command.recipecheck.end=Recipe conflict check found %d possible conflicts. Check the server log for more info gregtech.command.recipecheck.end_no_conflicts=No recipe conflicts found! gregtech.command.recipecheck.end_empty_inputs=Recipe check found %d recipes with empty inputs and %d empty oredicts. Check the server log for more info +gregtech.command.benchmark.usage=Usage: /gregtech benchmark +gregtech.command.benchmark.lookup.usage=Usage: /gregtech benchmark lookup +gregtech.command.benchmark.lookup.progress=Analysis at %s / %s trials complete. +gregtech.command.benchmark.lookup.failures=%s recipe samples failed test verification. Check the log for details. +gregtech.command.benchmark.lookup.success=Analysis complete and outputted to log file. +gregtech.command.benchmark.lookup.written=Results successfully saved to 'benchmark-lookup-results.csv'. gregtech.command.copy.copied_and_click=copied to clipboard. Click to copy again gregtech.command.copy.click_to_copy=Click to copy gregtech.command.copy.copied_start=Copied [ From 57571b54bedfd86b2af36305e746d2c52c39229c Mon Sep 17 00:00:00 2001 From: M-W-K <31022105+M-W-K@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:59:51 -0700 Subject: [PATCH 2/3] spotless, my old enemy --- .../command/benchmark/CommandBenchmark.java | 3 - .../benchmark/CommandBenchmarkLookup.java | 64 +++++++++---------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java b/src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java index 2ba4816e789..d7aed05b24a 100644 --- a/src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java +++ b/src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java @@ -1,9 +1,6 @@ package gregtech.common.command.benchmark; -import net.minecraft.command.CommandBase; -import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; -import net.minecraft.server.MinecraftServer; import net.minecraftforge.server.command.CommandTreeBase; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java b/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java index 9c83c5e466d..881ea35a303 100644 --- a/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java +++ b/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java @@ -1,21 +1,10 @@ package gregtech.common.command.benchmark; -import com.github.bsideup.jabel.Desugar; - -import com.google.common.collect.ImmutableList; - import gregtech.api.recipes.Recipe; import gregtech.api.recipes.RecipeMap; - import gregtech.api.recipes.ingredients.GTRecipeInput; import gregtech.api.util.GTLog; -import it.unimi.dsi.fastutil.Function; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectArrayList; - -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; - import net.minecraft.command.CommandBase; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; @@ -27,10 +16,14 @@ import net.minecraft.util.text.TextFormatting; import net.minecraftforge.fluids.FluidStack; -import java.io.File; +import com.github.bsideup.jabel.Desugar; +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + import java.io.FileWriter; import java.io.IOException; -import java.util.AbstractCollection; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -84,15 +77,19 @@ public void execute(MinecraftServer server, ICommandSender sender, String[] args for (var entry : nsTrialTimes.entrySet()) { BenchmarkResults results = composeResults(entry.getValue()); resultsCache.put(entry.getKey(), results); - GTLog.logger.info("[Benchmarking] Recipe Map {}, measurements in nanoseconds:", entry.getKey().getLocalizedName()); + GTLog.logger.info("[Benchmarking] Recipe Map {}, measurements in nanoseconds:", + entry.getKey().getLocalizedName()); GTLog.logger.info("[Benchmarking] - Characteristic numbers for No Match: {}", results.noMatchTest()); - GTLog.logger.info("[Benchmarking] - Characteristic numbers for One Match Exact: {}", results.oneMatchExactTest()); - GTLog.logger.info("[Benchmarking] - Characteristic numbers for Three Match Excess: {}", results.threeMatchExcessTest()); + GTLog.logger.info("[Benchmarking] - Characteristic numbers for One Match Exact: {}", + results.oneMatchExactTest()); + GTLog.logger.info("[Benchmarking] - Characteristic numbers for Three Match Excess: {}", + results.threeMatchExcessTest()); } sender.sendMessage(new TextComponentTranslation("gregtech.command.benchmark.lookup.success") .setStyle(new Style().setColor(TextFormatting.GREEN))); - sender.sendMessage(new TextComponentTranslation("gregtech.command.benchmark.lookup.failures", failureCount.get()) - .setStyle(new Style().setColor(TextFormatting.RED))); + sender.sendMessage( + new TextComponentTranslation("gregtech.command.benchmark.lookup.failures", failureCount.get()) + .setStyle(new Style().setColor(TextFormatting.RED))); try (FileWriter writer = new FileWriter("benchmark-lookup-results.csv")) { writer.append("Recipe Map,Trial Type,Minimum,Q1,Median,Q3,Maximum\n"); for (var entry : resultsCache.entrySet()) { @@ -120,7 +117,8 @@ public void execute(MinecraftServer server, ICommandSender sender, String[] args } } - private TrialResults trial(List recipeSpace, RecipeLookupFunction function, AtomicInteger failureCount) throws CommandException { + private TrialResults trial(List recipeSpace, RecipeLookupFunction function, + AtomicInteger failureCount) throws CommandException { Recipe[] sample = new Recipe[10]; for (int i = 0; i < 10; i++) { double r = Math.random(); @@ -199,12 +197,13 @@ private TrialResults trial(List recipeSpace, RecipeLookupFunction functi } long timeThreeExcess = System.nanoTime() - start; // re-enable this code block once lookup returns proper lists of matching recipes -// for (int i = 0; i < 3; i++) { -// Recipe r = sample[i]; -// if (!out.contains(r)) { -// throw new CommandException("Something in the benchmark's three match test is wrong! Report this to mod authors with context."); -// } -// } + // for (int i = 0; i < 3; i++) { + // Recipe r = sample[i]; + // if (!out.contains(r)) { + // throw new CommandException("Something in the benchmark's three match test is wrong! Report this to mod + // authors with context."); + // } + // } // one match exact trial items.clear(); @@ -237,7 +236,9 @@ private TrialResults trial(List recipeSpace, RecipeLookupFunction functi } private BenchmarkResults composeResults(List results) { - return new BenchmarkResults(representativeNumbers(results, TrialResults::noMatchTest), representativeNumbers(results, TrialResults::oneMatchExactTest), representativeNumbers(results, TrialResults::threeMatchExcessTest)); + return new BenchmarkResults(representativeNumbers(results, TrialResults::noMatchTest), + representativeNumbers(results, TrialResults::oneMatchExactTest), + representativeNumbers(results, TrialResults::threeMatchExcessTest)); } private double[] representativeNumbers(List results, ToLongFunction func) { @@ -250,23 +251,22 @@ private double[] representativeNumbers(List results, ToLongFunctio numbers[2] = func.applyAsLong(results.get(results.size() / 2)); offset = 1; } else { - numbers[2] = (func.applyAsLong(results.get(results.size() / 2 - 1)) - + func.applyAsLong(results.get(results.size() / 2))) / 2d; + numbers[2] = (func.applyAsLong(results.get(results.size() / 2 - 1)) + + func.applyAsLong(results.get(results.size() / 2))) / 2d; } int s = results.size() / 2; if (s % 2 == 1) { numbers[1] = func.applyAsLong(results.get(s / 2)); numbers[3] = func.applyAsLong(results.get(s + offset + s / 2)); } else { - numbers[1] = (func.applyAsLong(results.get(s / 2 - 1)) - + func.applyAsLong(results.get(s / 2))) / 2d; - numbers[3] = (func.applyAsLong(results.get(s / 2 - 1)) - + func.applyAsLong(results.get(s / 2))) / 2d; + numbers[1] = (func.applyAsLong(results.get(s / 2 - 1)) + func.applyAsLong(results.get(s / 2))) / 2d; + numbers[3] = (func.applyAsLong(results.get(s / 2 - 1)) + func.applyAsLong(results.get(s / 2))) / 2d; } return numbers; } interface RecipeLookupFunction { + Collection find(long voltage, List items, List fluids); } From cc389c4b3754fcea0de103f8d1187f6965d5cdaf Mon Sep 17 00:00:00 2001 From: M-W-K <31022105+M-W-K@users.noreply.github.com> Date: Fri, 7 Nov 2025 22:17:58 -0700 Subject: [PATCH 3/3] Address review and spread out the benchmark across multiple ticks --- .../command/benchmark/BenchmarkTask.java | 8 + .../command/benchmark/CommandBenchmark.java | 11 +- .../benchmark/CommandBenchmarkAbort.java | 33 ++ .../benchmark/CommandBenchmarkLookup.java | 442 ++++++++++-------- .../resources/assets/gregtech/lang/en_us.lang | 5 +- 5 files changed, 307 insertions(+), 192 deletions(-) create mode 100644 src/main/java/gregtech/common/command/benchmark/BenchmarkTask.java create mode 100644 src/main/java/gregtech/common/command/benchmark/CommandBenchmarkAbort.java diff --git a/src/main/java/gregtech/common/command/benchmark/BenchmarkTask.java b/src/main/java/gregtech/common/command/benchmark/BenchmarkTask.java new file mode 100644 index 00000000000..2126ca5112f --- /dev/null +++ b/src/main/java/gregtech/common/command/benchmark/BenchmarkTask.java @@ -0,0 +1,8 @@ +package gregtech.common.command.benchmark; + +import gregtech.api.util.function.Task; + +public interface BenchmarkTask extends Task { + + void abort(); +} diff --git a/src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java b/src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java index d7aed05b24a..d8abb72c983 100644 --- a/src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java +++ b/src/main/java/gregtech/common/command/benchmark/CommandBenchmark.java @@ -4,11 +4,20 @@ import net.minecraftforge.server.command.CommandTreeBase; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class CommandBenchmark extends CommandTreeBase { + static @Nullable BenchmarkTask ACTIVE_BENCHMARK = null; + public CommandBenchmark() { addSubcommand(new CommandBenchmarkLookup()); + addSubcommand(new CommandBenchmarkAbort()); + } + + @Override + public int getRequiredPermissionLevel() { + return 3; } @Override @@ -17,7 +26,7 @@ public CommandBenchmark() { } @Override - public @NotNull String getUsage(ICommandSender sender) { + public @NotNull String getUsage(@NotNull ICommandSender sender) { return "gregtech.command.benchmark.usage"; } } diff --git a/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkAbort.java b/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkAbort.java new file mode 100644 index 00000000000..3586dbd1c6a --- /dev/null +++ b/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkAbort.java @@ -0,0 +1,33 @@ +package gregtech.common.command.benchmark; + +import net.minecraft.command.CommandBase; +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.server.MinecraftServer; + +import org.jetbrains.annotations.NotNull; + +public class CommandBenchmarkAbort extends CommandBase { + + @Override + public @NotNull String getName() { + return "abort"; + } + + @Override + public @NotNull String getUsage(@NotNull ICommandSender sender) { + return "gregtech.command.benchmark.abort.usage"; + } + + @Override + public void execute(@NotNull MinecraftServer server, @NotNull ICommandSender sender, + String @NotNull [] args) throws CommandException { + if (CommandBenchmark.ACTIVE_BENCHMARK != null) { + CommandBenchmark.ACTIVE_BENCHMARK.abort(); + CommandBenchmark.ACTIVE_BENCHMARK = null; + throw new CommandException("Currently running benchmark successfully aborted."); + } else { + throw new CommandException("No benchmark is currently running!"); + } + } +} diff --git a/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java b/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java index 881ea35a303..b9c8777449c 100644 --- a/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java +++ b/src/main/java/gregtech/common/command/benchmark/CommandBenchmarkLookup.java @@ -1,9 +1,11 @@ package gregtech.common.command.benchmark; +import gregtech.api.GTValues; import gregtech.api.recipes.Recipe; import gregtech.api.recipes.RecipeMap; import gregtech.api.recipes.ingredients.GTRecipeInput; import gregtech.api.util.GTLog; +import gregtech.api.util.TaskScheduler; import net.minecraft.command.CommandBase; import net.minecraft.command.CommandException; @@ -15,264 +17,326 @@ import net.minecraft.util.text.TextComponentTranslation; import net.minecraft.util.text.TextFormatting; import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fml.common.Loader; import com.github.bsideup.jabel.Desugar; import com.google.common.collect.ImmutableList; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import org.jetbrains.annotations.NotNull; import java.io.FileWriter; import java.io.IOException; +import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.ToLongFunction; public class CommandBenchmarkLookup extends CommandBase { @Override - public String getName() { + public @NotNull String getName() { return "lookup"; } @Override - public String getUsage(ICommandSender sender) { + public @NotNull String getUsage(@NotNull ICommandSender sender) { return "gregtech.command.benchmark.lookup.usage"; } @Override - public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException { + public void execute(@NotNull MinecraftServer server, @NotNull ICommandSender sender, + String[] args) throws CommandException { int trials = 100; + int rate = 10; if (args.length != 0) { try { trials = Integer.parseInt(args[0]); if (trials <= 0) throw new NumberFormatException(); + if (args.length != 1) { + rate = Integer.parseInt(args[0]); + if (rate <= 0) throw new NumberFormatException(); + } } catch (NumberFormatException ignored) { throw new WrongUsageException("gregtech.command.benchmark.lookup.usage"); } } - Object2ObjectOpenHashMap, ObjectArrayList> nsTrialTimes = new Object2ObjectOpenHashMap<>(); - Object2ObjectOpenHashMap, List> recipeLists = new Object2ObjectOpenHashMap<>(); - AtomicInteger failureCount = new AtomicInteger(); + if (CommandBenchmark.ACTIVE_BENCHMARK != null) { + throw new CommandException( + "A benchmark is currently running! Run '/gregtech benchmark abort' to abort the active benchmark."); + } GTLog.logger.info("[Benchmarking] Starting recipe lookup benchmarking..."); - for (int i = 0; i < trials; i++) { - for (RecipeMap map : RecipeMap.getRecipeMaps()) { - if (recipeLists.computeIfAbsent(map, m -> new ObjectArrayList<>(m.getRecipeList())).isEmpty()) continue; - nsTrialTimes.computeIfAbsent(map, m -> new ObjectArrayList<>()) - .add(trial(recipeLists.get(map), (v, it, f) -> { - Recipe r = map.findRecipe(v, it, f); - return r == null ? Collections.emptyList() : ImmutableList.of(r); - }, failureCount)); - } - if ((i > 1) && ((i & (i - 1)) == 0)) { - GTLog.logger.info("[Benchmarking] {}th trial complete...", i); - } + CommandBenchmark.ACTIVE_BENCHMARK = new BenchmarkingTask(sender, trials, rate); + TaskScheduler.scheduleTask(server.getWorld(0), CommandBenchmark.ACTIVE_BENCHMARK); + } + + interface RecipeLookupFunction { + + Collection find(long voltage, List items, List fluids); + } + + @Desugar + record TrialResults(long noMatchTest, long oneMatchExactTest, long threeMatchExcessTest) {} + + @Desugar + record BenchmarkResults(double[] noMatchTest, double[] oneMatchExactTest, double[] threeMatchExcessTest) {} + + private static class BenchmarkingTask implements BenchmarkTask { + + private final ICommandSender sender; + private final int trials; + private final int rate; + private int trialsCompleted; + private final Object2ObjectOpenHashMap, ObjectArrayList> nsTrialTimes = new Object2ObjectOpenHashMap<>(); + private final Object2ObjectOpenHashMap, List> recipeLists = new Object2ObjectOpenHashMap<>(); + private int failureCount = 0; + + private BenchmarkingTask(ICommandSender sender, int trials, int rate) { + this.sender = sender; + this.trials = trials; + this.rate = rate; } - GTLog.logger.info("[Benchmarking] Benchmarking complete. Outputting results:"); - Object2ObjectOpenHashMap, BenchmarkResults> resultsCache = new Object2ObjectOpenHashMap<>(); - for (var entry : nsTrialTimes.entrySet()) { - BenchmarkResults results = composeResults(entry.getValue()); - resultsCache.put(entry.getKey(), results); - GTLog.logger.info("[Benchmarking] Recipe Map {}, measurements in nanoseconds:", - entry.getKey().getLocalizedName()); - GTLog.logger.info("[Benchmarking] - Characteristic numbers for No Match: {}", results.noMatchTest()); - GTLog.logger.info("[Benchmarking] - Characteristic numbers for One Match Exact: {}", - results.oneMatchExactTest()); - GTLog.logger.info("[Benchmarking] - Characteristic numbers for Three Match Excess: {}", - results.threeMatchExcessTest()); + + @Override + public void abort() { + this.trialsCompleted = Integer.MIN_VALUE; } - sender.sendMessage(new TextComponentTranslation("gregtech.command.benchmark.lookup.success") - .setStyle(new Style().setColor(TextFormatting.GREEN))); - sender.sendMessage( - new TextComponentTranslation("gregtech.command.benchmark.lookup.failures", failureCount.get()) - .setStyle(new Style().setColor(TextFormatting.RED))); - try (FileWriter writer = new FileWriter("benchmark-lookup-results.csv")) { - writer.append("Recipe Map,Trial Type,Minimum,Q1,Median,Q3,Maximum\n"); - for (var entry : resultsCache.entrySet()) { - writer.append(entry.getKey().getLocalizedName()).append(',').append("No Match"); - for (double d : entry.getValue().noMatchTest()) { - writer.append(',').append(String.valueOf(d)); - } - writer.append('\n'); - writer.append(entry.getKey().getLocalizedName()).append(',').append("One Match Exact"); - for (double d : entry.getValue().oneMatchExactTest()) { - writer.append(',').append(String.valueOf(d)); + + @Override + public boolean run() { + if (trialsCompleted == Integer.MIN_VALUE) return false; + for (int i = 0; i < rate; i++) { + for (RecipeMap map : RecipeMap.getRecipeMaps()) { + if (recipeLists.computeIfAbsent(map, m -> new ObjectArrayList<>(m.getRecipeList())).isEmpty()) + continue; + nsTrialTimes.computeIfAbsent(map, m -> new ObjectArrayList<>()) + .add(trial(recipeLists.get(map), (v, it, f) -> { + Recipe r = map.findRecipe(v, it, f); + return r == null ? Collections.emptyList() : ImmutableList.of(r); + })); } - writer.append('\n'); - writer.append(entry.getKey().getLocalizedName()).append(',').append("Three Match Excess"); - for (double d : entry.getValue().threeMatchExcessTest()) { - writer.append(',').append(String.valueOf(d)); + trialsCompleted += 1; + if ((trialsCompleted > 1) && ((trialsCompleted & (trialsCompleted - 1)) == 0)) { + sender.sendMessage(new TextComponentTranslation("gregtech.command.benchmark.lookup.progress", + trialsCompleted, trials) + .setStyle(new Style().setColor(TextFormatting.GREEN))); + GTLog.logger.info("[Benchmarking] {}th trial complete...", trialsCompleted); } - writer.append('\n'); } - GTLog.logger.info("[Benchmarking] Output saved to csv file 'benchmark-lookup-results.csv'"); - sender.sendMessage(new TextComponentTranslation("gregtech.command.benchmark.lookup.written") - .setStyle(new Style().setColor(TextFormatting.GREEN))); - } catch (IOException e) { - GTLog.logger.info("[Benchmarking] Failed to output to csv file 'benchmark-lookup-results.csv'"); + return finish(); } - } - private TrialResults trial(List recipeSpace, RecipeLookupFunction function, - AtomicInteger failureCount) throws CommandException { - Recipe[] sample = new Recipe[10]; - for (int i = 0; i < 10; i++) { - double r = Math.random(); - sample[i] = recipeSpace.get((int) (r * recipeSpace.size())); - } - // no match trial - Set seen = new ObjectOpenHashSet<>(); - List items = new ObjectArrayList<>(); - List fluids = new ObjectArrayList<>(); - for (Recipe r : sample) { - // if adding an item input would lead to recipe matching, do not add it. - if (r.getInputs().size() > 1) { - r.getInputs().stream().filter(i -> !seen.contains(i)).findAny() - .map(GTRecipeInput::getInputStacks) - .map(arr -> arr[0]) - .ifPresent(items::add); - for (Recipe value : sample) { - if (value.matches(false, items, fluids)) { - items.remove(items.size() - 1); + private TrialResults trial(List recipeSpace, RecipeLookupFunction function) { + Recipe[] sample = new Recipe[10]; + for (int i = 0; i < 10; i++) { + double r = Math.random(); + sample[i] = recipeSpace.get((int) (r * recipeSpace.size())); + } + // no match trial + Set seen = new ObjectOpenHashSet<>(); + List items = new ObjectArrayList<>(); + List fluids = new ObjectArrayList<>(); + for (Recipe r : sample) { + // if adding an item input would lead to recipe matching, do not add it. + if (r.getInputs().size() > 1) { + r.getInputs().stream().filter(i -> !seen.contains(i)).findAny() + .ifPresent(input -> { + ItemStack stack = input.getInputStacks()[0].copy(); + stack.setCount(input.getAmount()); + items.add(stack); + }); + for (Recipe value : sample) { + if (value.matches(false, items, fluids)) { + items.remove(items.size() - 1); + } + } + } + // if adding a fluid input would lead to recipe matching, do not add it + if (r.getFluidInputs().size() > 1) { + r.getFluidInputs().stream().filter(i -> !seen.contains(i)).findAny() + .ifPresent(input -> { + FluidStack stack = input.getInputFluidStack().copy(); + stack.amount = input.getAmount(); + fluids.add(stack); + }); + for (Recipe recipe : sample) { + if (recipe.matches(false, items, fluids)) { + fluids.remove(fluids.size() - 1); + } } } + // prevent randomly adding inputs in the future that would lead to matching a recipe + seen.addAll(r.getInputs()); + seen.addAll(r.getFluidInputs()); + } + long start = System.nanoTime(); + Collection out = function.find(Long.MAX_VALUE, items, fluids); + // in a real situation, any outputs would be run through a count match until the first success if found. + // run this matching while timing to penalize returning too many possible matches + for (Recipe r : out) { + if (r.matches(false, items, fluids)) { + break; + } } - // if adding a fluid input would lead to recipe matching, do not add it - if (r.getFluidInputs().size() > 1) { - r.getFluidInputs().stream().filter(i -> !seen.contains(i)).findAny() - .map(GTRecipeInput::getInputFluidStack) - .ifPresent(fluids::add); - for (Recipe recipe : sample) { - if (recipe.matches(false, items, fluids)) { - fluids.remove(fluids.size() - 1); + long timeNoMatch = System.nanoTime() - start; + for (Recipe r : sample) { + if (out.contains(r)) { + failureCount += 1; + GTLog.logger.info("[Benchmarking] Recipe {} has failed the no match test for sample:", r); + for (Recipe o : sample) { + GTLog.logger.info("[Benchmarking] - {}", o); } } } - // prevent randomly adding inputs in the future that would lead to matching a recipe - seen.addAll(r.getInputs()); - seen.addAll(r.getFluidInputs()); - } - long start = System.nanoTime(); - Collection out = function.find(Long.MAX_VALUE, items, fluids); - // in a real situation, any outputs would be run through a count match until the first success if found. - // run this matching while timing to penalize returning too many possible matches - for (Recipe r : out) { - if (r.matches(false, items, fluids)) { - break; + + // three match excess trial + for (int i = 0; i < 3; i++) { + for (GTRecipeInput input : sample[i].getInputs()) { + ItemStack stack = input.getInputStacks()[0].copy(); + stack.setCount(input.getAmount()); + items.add(stack); + } + for (GTRecipeInput input : sample[i].getFluidInputs()) { + FluidStack stack = input.getInputFluidStack().copy(); + stack.amount = input.getAmount(); + fluids.add(stack); + } } - } - long timeNoMatch = System.nanoTime() - start; - for (Recipe r : sample) { - if (out.contains(r)) { - failureCount.incrementAndGet(); - GTLog.logger.info("[Benchmarking] Recipe sample {} has failed the no match test.", (Object) sample); + start = System.nanoTime(); + out = function.find(Long.MAX_VALUE, items, fluids); + // in a real situation, any outputs would be run through a count match. + // run this matching while timing to penalize returning too many possible matches + for (Recipe r : out) { + if (r.matches(false, items, fluids)) { + break; + } } - } + long timeThreeExcess = System.nanoTime() - start; + // re-enable this code block once lookup returns proper lists of matching recipes + // for (int i = 0; i < 3; i++) { + // Recipe r = sample[i]; + // if (!out.contains(r)) { + // throw new CommandException("Something in the benchmark's three match test is wrong! Report this to mod + // authors with context."); + // } + // } - // three match excess trial - for (int i = 0; i < 3; i++) { - for (GTRecipeInput input : sample[i].getInputs()) { + // one match exact trial + items.clear(); + fluids.clear(); + for (GTRecipeInput input : sample[0].getInputs()) { ItemStack stack = input.getInputStacks()[0].copy(); stack.setCount(input.getAmount()); items.add(stack); } - for (GTRecipeInput input : sample[i].getFluidInputs()) { + for (GTRecipeInput input : sample[0].getFluidInputs()) { FluidStack stack = input.getInputFluidStack().copy(); stack.amount = input.getAmount(); fluids.add(stack); } - } - start = System.nanoTime(); - out = function.find(Long.MAX_VALUE, items, fluids); - // in a real situation, any outputs would be run through a count match. - // run this matching while timing to penalize returning too many possible matches - for (Recipe r : out) { - if (r.matches(false, items, fluids)) { - break; + start = System.nanoTime(); + out = function.find(Long.MAX_VALUE, items, fluids); + // in a real situation, any outputs would be run through a count match. + // run this matching while timing to penalize returning too many possible matches + for (Recipe r : out) { + if (r.matches(false, items, fluids)) { + break; + } + } + long timeOneExact = System.nanoTime() - start; + if (!out.contains(sample[0])) { + failureCount += 1; + GTLog.logger.info("[Benchmarking] Recipe {} has failed the exact match test.", sample[0]); } + return new TrialResults(timeNoMatch, timeOneExact, timeThreeExcess); } - long timeThreeExcess = System.nanoTime() - start; - // re-enable this code block once lookup returns proper lists of matching recipes - // for (int i = 0; i < 3; i++) { - // Recipe r = sample[i]; - // if (!out.contains(r)) { - // throw new CommandException("Something in the benchmark's three match test is wrong! Report this to mod - // authors with context."); - // } - // } - // one match exact trial - items.clear(); - fluids.clear(); - for (GTRecipeInput input : sample[0].getInputs()) { - ItemStack stack = input.getInputStacks()[0].copy(); - stack.setCount(input.getAmount()); - items.add(stack); - } - for (GTRecipeInput input : sample[0].getFluidInputs()) { - FluidStack stack = input.getInputFluidStack().copy(); - stack.amount = input.getAmount(); - fluids.add(stack); - } - start = System.nanoTime(); - out = function.find(Long.MAX_VALUE, items, fluids); - // in a real situation, any outputs would be run through a count match. - // run this matching while timing to penalize returning too many possible matches - for (Recipe r : out) { - if (r.matches(false, items, fluids)) { - break; + private double[] representativeNumbers(List results, ToLongFunction func) { + results.sort(Comparator.comparingLong(func)); + double[] numbers = new double[5]; + numbers[0] = func.applyAsLong(results.get(0)); + numbers[4] = func.applyAsLong(results.get(results.size() - 1)); + int offset = 0; + if (results.size() % 2 == 1) { + numbers[2] = func.applyAsLong(results.get(results.size() / 2)); + offset = 1; + } else { + numbers[2] = (func.applyAsLong(results.get(results.size() / 2 - 1)) + + func.applyAsLong(results.get(results.size() / 2))) / 2d; } + int s = results.size() / 2; + if (s % 2 == 1) { + numbers[1] = func.applyAsLong(results.get(s / 2)); + numbers[3] = func.applyAsLong(results.get(s + offset + s / 2)); + } else { + numbers[1] = (func.applyAsLong(results.get(s / 2 - 1)) + func.applyAsLong(results.get(s / 2))) / 2d; + numbers[3] = (func.applyAsLong(results.get(s / 2 - 1)) + func.applyAsLong(results.get(s / 2))) / 2d; + } + return numbers; } - long timeOneExact = System.nanoTime() - start; - if (!out.contains(sample[0])) { - failureCount.incrementAndGet(); - GTLog.logger.info("[Benchmarking] Recipe sample {} has failed the exact match test.", (Object) sample); - } - return new TrialResults(timeNoMatch, timeOneExact, timeThreeExcess); - } - - private BenchmarkResults composeResults(List results) { - return new BenchmarkResults(representativeNumbers(results, TrialResults::noMatchTest), - representativeNumbers(results, TrialResults::oneMatchExactTest), - representativeNumbers(results, TrialResults::threeMatchExcessTest)); - } - private double[] representativeNumbers(List results, ToLongFunction func) { - results.sort(Comparator.comparingLong(func)); - double[] numbers = new double[5]; - numbers[0] = func.applyAsLong(results.get(0)); - numbers[4] = func.applyAsLong(results.get(results.size() - 1)); - int offset = 0; - if (results.size() % 2 == 1) { - numbers[2] = func.applyAsLong(results.get(results.size() / 2)); - offset = 1; - } else { - numbers[2] = (func.applyAsLong(results.get(results.size() / 2 - 1)) + - func.applyAsLong(results.get(results.size() / 2))) / 2d; - } - int s = results.size() / 2; - if (s % 2 == 1) { - numbers[1] = func.applyAsLong(results.get(s / 2)); - numbers[3] = func.applyAsLong(results.get(s + offset + s / 2)); - } else { - numbers[1] = (func.applyAsLong(results.get(s / 2 - 1)) + func.applyAsLong(results.get(s / 2))) / 2d; - numbers[3] = (func.applyAsLong(results.get(s / 2 - 1)) + func.applyAsLong(results.get(s / 2))) / 2d; + private BenchmarkResults composeResults(List results) { + return new BenchmarkResults(representativeNumbers(results, TrialResults::noMatchTest), + representativeNumbers(results, TrialResults::oneMatchExactTest), + representativeNumbers(results, TrialResults::threeMatchExcessTest)); } - return numbers; - } - interface RecipeLookupFunction { + private boolean finish() { + if (trialsCompleted < trials) return true; - Collection find(long voltage, List items, List fluids); - } - - @Desugar - record TrialResults(long noMatchTest, long oneMatchExactTest, long threeMatchExcessTest) {} + GTLog.logger.info("[Benchmarking] Benchmarking complete. Outputting results:"); + Object2ObjectOpenHashMap, BenchmarkResults> resultsCache = new Object2ObjectOpenHashMap<>(); + for (var entry : nsTrialTimes.entrySet()) { + BenchmarkResults results = composeResults(entry.getValue()); + resultsCache.put(entry.getKey(), results); + GTLog.logger.info("[Benchmarking] Recipe Map {}, measurements in nanoseconds:", + entry.getKey().getLocalizedName()); + GTLog.logger.info("[Benchmarking] - Characteristic numbers for No Match: {}", results.noMatchTest()); + GTLog.logger.info("[Benchmarking] - Characteristic numbers for One Match Exact: {}", + results.oneMatchExactTest()); + GTLog.logger.info("[Benchmarking] - Characteristic numbers for Three Match Excess: {}", + results.threeMatchExcessTest()); + } + sender.sendMessage(new TextComponentTranslation("gregtech.command.benchmark.lookup.success") + .setStyle(new Style().setColor(TextFormatting.GREEN))); + sender.sendMessage( + new TextComponentTranslation("gregtech.command.benchmark.lookup.failures", failureCount) + .setStyle(new Style().setColor(TextFormatting.RED))); + Path path = Loader.instance().getConfigDir().toPath().resolve(GTValues.MODID) + .resolve("benchmark-lookup-results.csv"); - @Desugar - record BenchmarkResults(double[] noMatchTest, double[] oneMatchExactTest, double[] threeMatchExcessTest) {} + try (FileWriter writer = new FileWriter(path.toFile())) { + writer.append("Recipe Map,Trial Type,Minimum,Q1,Median,Q3,Maximum\n"); + for (var entry : resultsCache.entrySet()) { + writer.append(entry.getKey().getLocalizedName()).append(',').append("No Match"); + for (double d : entry.getValue().noMatchTest()) { + writer.append(',').append(String.valueOf(d)); + } + writer.append('\n'); + writer.append(entry.getKey().getLocalizedName()).append(',').append("One Match Exact"); + for (double d : entry.getValue().oneMatchExactTest()) { + writer.append(',').append(String.valueOf(d)); + } + writer.append('\n'); + writer.append(entry.getKey().getLocalizedName()).append(',').append("Three Match Excess"); + for (double d : entry.getValue().threeMatchExcessTest()) { + writer.append(',').append(String.valueOf(d)); + } + writer.append('\n'); + } + GTLog.logger.info("[Benchmarking] Output saved to csv file 'benchmark-lookup-results.csv'"); + sender.sendMessage(new TextComponentTranslation("gregtech.command.benchmark.lookup.written") + .setStyle(new Style().setColor(TextFormatting.GREEN))); + } catch (IOException e) { + GTLog.logger.info( + "[Benchmarking] Failed to output to csv file 'benchmark-lookup-results.csv' in the config folder"); + } + CommandBenchmark.ACTIVE_BENCHMARK = null; + return false; + } + } } diff --git a/src/main/resources/assets/gregtech/lang/en_us.lang b/src/main/resources/assets/gregtech/lang/en_us.lang index c00704538cc..16ff35d9e7f 100644 --- a/src/main/resources/assets/gregtech/lang/en_us.lang +++ b/src/main/resources/assets/gregtech/lang/en_us.lang @@ -6008,11 +6008,12 @@ gregtech.command.recipecheck.end=Recipe conflict check found %d possible conflic gregtech.command.recipecheck.end_no_conflicts=No recipe conflicts found! gregtech.command.recipecheck.end_empty_inputs=Recipe check found %d recipes with empty inputs and %d empty oredicts. Check the server log for more info gregtech.command.benchmark.usage=Usage: /gregtech benchmark -gregtech.command.benchmark.lookup.usage=Usage: /gregtech benchmark lookup +gregtech.command.benchmark.abort.usage=Usage: /gregtech benchmark abort +gregtech.command.benchmark.lookup.usage=Usage: /gregtech benchmark lookup gregtech.command.benchmark.lookup.progress=Analysis at %s / %s trials complete. gregtech.command.benchmark.lookup.failures=%s recipe samples failed test verification. Check the log for details. gregtech.command.benchmark.lookup.success=Analysis complete and outputted to log file. -gregtech.command.benchmark.lookup.written=Results successfully saved to 'benchmark-lookup-results.csv'. +gregtech.command.benchmark.lookup.written=Results successfully saved to 'benchmark-lookup-results.csv' in the config folder. gregtech.command.copy.copied_and_click=copied to clipboard. Click to copy again gregtech.command.copy.click_to_copy=Click to copy gregtech.command.copy.copied_start=Copied [