From ce2fbe974699848597eab2c3bfabd6f1355c13f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Thu, 24 Sep 2020 14:36:10 +0200 Subject: [PATCH] Optimise literal parsing, add argument parsing metrics and add some benchmarks --- cloud-core/pom.xml | 12 ++ .../commands/CommandTree.java | 17 ++- .../commands/arguments/StaticArgument.java | 36 +++-- .../commands/context/CommandContext.java | 125 +++++++++++++++++- .../commands/CommandPerformanceTest.java | 91 +++++++++++++ .../commands/ExecutionBenchmark.java | 75 +++++++++++ 6 files changed, 336 insertions(+), 20 deletions(-) create mode 100644 cloud-core/src/test/java/com/intellectualsites/commands/CommandPerformanceTest.java create mode 100644 cloud-core/src/test/java/com/intellectualsites/commands/ExecutionBenchmark.java diff --git a/cloud-core/pom.xml b/cloud-core/pom.xml index be52ef63..22f01f6c 100644 --- a/cloud-core/pom.xml +++ b/cloud-core/pom.xml @@ -39,6 +39,18 @@ cloud-core + + org.openjdk.jmh + jmh-core + 1.25.2 + test + + + org.openjdk.jmh + jmh-generator-annprocess + 1.25.2 + test + com.intellectualsites cloud-pipeline diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java b/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java index 373f407c..0d30ed72 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java @@ -177,7 +177,13 @@ public final class CommandTree { while (childIterator.hasNext()) { final Node> child = childIterator.next(); if (child.getValue() != null) { - final ArgumentParseResult result = child.getValue().getParser().parse(commandContext, commandQueue); + final CommandArgument argument = child.getValue(); + final CommandContext.ArgumentTiming argumentTiming = commandContext.createTiming(argument); + + argumentTiming.setStart(System.nanoTime()); + final ArgumentParseResult result = argument.getParser().parse(commandContext, commandQueue); + argumentTiming.setEnd(System.nanoTime(), result.getFailure().isPresent()); + if (result.getParsedValue().isPresent()) { parsedArguments.add(child.getValue()); return this.parseCommand(parsedArguments, commandContext, commandQueue, child); @@ -251,7 +257,14 @@ public final class CommandTree { .collect(Collectors.toList())); } } - final ArgumentParseResult result = child.getValue().getParser().parse(commandContext, commandQueue); + + final CommandArgument argument = child.getValue(); + final CommandContext.ArgumentTiming argumentTiming = commandContext.createTiming(argument); + + argumentTiming.setStart(System.nanoTime()); + final ArgumentParseResult result = argument.getParser().parse(commandContext, commandQueue); + argumentTiming.setEnd(System.nanoTime(), result.getFailure().isPresent()); + if (result.getParsedValue().isPresent()) { commandContext.store(child.getValue().getName(), result.getParsedValue().get()); if (child.isLeaf()) { diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StaticArgument.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StaticArgument.java index b0aa99c8..aeafb215 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StaticArgument.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StaticArgument.java @@ -35,6 +35,7 @@ import java.util.HashSet; import java.util.List; import java.util.Queue; import java.util.Set; +import java.util.TreeSet; /** * {@link CommandArgument} type that recognizes fixed strings. This type does not parse variables. @@ -81,7 +82,7 @@ public final class StaticArgument extends CommandArgument { * @param alias New alias */ public void registerAlias(@Nonnull final String alias) { - ((StaticArgumentParser) this.getParser()).acceptedStrings.add(alias); + ((StaticArgumentParser) this.getParser()).insertAlias(alias); } /** @@ -101,20 +102,21 @@ public final class StaticArgument extends CommandArgument { */ @Nonnull public List getAlternativeAliases() { - return Collections.unmodifiableList(new ArrayList<>(((StaticArgumentParser) this.getParser()).acceptedStrings)); + return Collections.unmodifiableList(new ArrayList<>(((StaticArgumentParser) this.getParser()).alternativeAliases)); } private static final class StaticArgumentParser implements ArgumentParser { - private final String name; - private final Set acceptedStrings = new HashSet<>(); + private final Set allAcceptedAliases = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); private final Set alternativeAliases = new HashSet<>(); + private final String name; + private StaticArgumentParser(@Nonnull final String name, @Nonnull final String... aliases) { this.name = name; - this.acceptedStrings.add(this.name); - this.acceptedStrings.addAll(Arrays.asList(aliases)); + this.allAcceptedAliases.add(this.name); + this.allAcceptedAliases.addAll(Arrays.asList(aliases)); this.alternativeAliases.addAll(Arrays.asList(aliases)); } @@ -126,12 +128,9 @@ public final class StaticArgument extends CommandArgument { if (string == null) { return ArgumentParseResult.failure(new NullPointerException("No input provided")); } - for (final String acceptedString : this.acceptedStrings) { - if (string.equalsIgnoreCase(acceptedString)) { - // Remove the head of the queue - inputQueue.remove(); - return ArgumentParseResult.success(this.name); - } + if (this.allAcceptedAliases.contains(string)) { + inputQueue.remove(); + return ArgumentParseResult.success(this.name); } return ArgumentParseResult.failure(new IllegalArgumentException(string)); } @@ -149,8 +148,19 @@ public final class StaticArgument extends CommandArgument { */ @Nonnull public Set getAcceptedStrings() { - return this.acceptedStrings; + return this.allAcceptedAliases; } + + /** + * Insert a new alias + * + * @param alias New alias + */ + public void insertAlias(@Nonnull final String alias) { + this.allAcceptedAliases.add(alias); + this.alternativeAliases.add(alias); + } + } } diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContext.java b/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContext.java index 563df9aa..dd9eb3b8 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContext.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContext.java @@ -23,8 +23,11 @@ // package com.intellectualsites.commands.context; +import com.google.common.collect.Maps; +import com.intellectualsites.commands.arguments.CommandArgument; + import javax.annotation.Nonnull; -import java.util.HashMap; +import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -35,7 +38,8 @@ import java.util.Optional; */ public final class CommandContext { - private final Map internalStorage = new HashMap<>(); + private final Map, ArgumentTiming> argumentTimings = Maps.newHashMap(); + private final Map internalStorage = Maps.newHashMap(); private final C commandSender; private final boolean suggestions; @@ -128,9 +132,9 @@ public final class CommandContext { /** * Get a value if it exists, else return the provided default value * - * @param key Argument key - * @param defaultValue Default value - * @param Argument type + * @param key Argument key + * @param defaultValue Default value + * @param Argument type * @return Argument, or supplied default value */ @Nonnull @@ -138,4 +142,115 @@ public final class CommandContext { return this.get(key).orElse(defaultValue); } + /** + * Create an argument timing for a specific argument + * + * @param argument Argument + * @return Created timing instance + */ + @Nonnull + public ArgumentTiming createTiming(@Nonnull final CommandArgument argument) { + final ArgumentTiming argumentTiming = new ArgumentTiming(); + this.argumentTimings.put(argument, argumentTiming); + return argumentTiming; + } + + /** + * Get an immutable view of the argument timings map + * + * @return Argument timings + */ + @Nonnull + public Map, ArgumentTiming> getArgumentTimings() { + return Collections.unmodifiableMap(this.argumentTimings); + } + + + /** + * Used to track performance metrics related to command parsing. This is attached + * to the command context, as this depends on the command context that is being + * parsed. + *

+ * The times are measured in nanoseconds. + */ + public static final class ArgumentTiming { + + private long start; + private long end; + private boolean success; + + /** + * Created a new argument timing instance + * + * @param start Start time (in nanoseconds) + * @param end End time (in nanoseconds) + * @param success Whether or not the argument was parsed successfully + */ + public ArgumentTiming(final long start, final long end, final boolean success) { + this.start = start; + this.end = end; + this.success = success; + } + + /** + * Created a new argument timing instance without an end time + * + * @param start Start time (in nanoseconds) + */ + public ArgumentTiming(final long start) { + this(start, -1, false); + } + + /** + * Created a new argument timing instance + */ + public ArgumentTiming() { + this(-1, -1, false); + } + + /** + * Get the elapsed time + * + * @return Elapsed time (in nanoseconds) + */ + public long getElapsedTime() { + if (this.end == -1) { + throw new IllegalStateException("No end time has been registered"); + } else if (this.start == -1) { + throw new IllegalStateException("No start time has been registered"); + } + return this.end - this.start; + } + + /** + * Set the end time + * + * @param end End time (in nanoseconds) + * @param success Whether or not the argument was parsed successfully + */ + public void setEnd(final long end, final boolean success) { + this.end = end; + this.success = success; + } + + /** + * Set the start time + * + * @param start Start time (in nanoseconds) + */ + public void setStart(final long start) { + this.start = start; + } + + /** + * Check whether or not the value was parsed successfully + * + * @return {@code true} if the value was parsed successfully, {@code false} if not + */ + public boolean wasSuccess() { + return this.success; + } + + } + } diff --git a/cloud-core/src/test/java/com/intellectualsites/commands/CommandPerformanceTest.java b/cloud-core/src/test/java/com/intellectualsites/commands/CommandPerformanceTest.java new file mode 100644 index 00000000..eff575f4 --- /dev/null +++ b/cloud-core/src/test/java/com/intellectualsites/commands/CommandPerformanceTest.java @@ -0,0 +1,91 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package com.intellectualsites.commands; + +import com.intellectualsites.commands.context.CommandContext; +import com.intellectualsites.commands.execution.CommandResult; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openjdk.jmh.results.RunResult; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.Collection; + +final class CommandPerformanceTest { + + private static CommandManager manager; + private static String literalChain; + + @BeforeAll + static void setup() { + manager = new TestCommandManager(); + + final StringBuilder literalBuilder = new StringBuilder("literals"); + + /* Create 100 literals */ + Command.Builder builder = manager.commandBuilder("literals"); + for (int i = 1; i < 101; i++) { + final String literal = Integer.toString(i); + builder = builder.literal(literal); + literalBuilder.append(' ').append(literal); + } + manager.command(builder.build()); + literalChain = literalBuilder.toString(); + + } + + @Test + void testLiterals() { + final CommandResult result = manager.executeCommand(new TestCommandSender(), literalChain).join(); + + long elapsedTime = 0L; + int amount = 0; + for (int i = 0; i < 100000; i++) { + for (final CommandContext.ArgumentTiming argumentTiming : result.getCommandContext().getArgumentTimings().values()) { + elapsedTime += argumentTiming.getElapsedTime(); + amount += 1; + } + } + double averageTime = elapsedTime / (double) amount; + + System.out.printf("Average literal parse time: %fns (%f ms) | %d samples & %d iterations\n", + averageTime, averageTime / 10e6, 101, 100000); + } + + @Test + void testCompleteExecution() throws Exception { + if (System.getProperty("verboseBenchmarks", "false").equalsIgnoreCase("false")) { + return; + } + final Options options = new OptionsBuilder() + .include(ExecutionBenchmark.class.getSimpleName()) + .build(); + final Collection results = new Runner(options).run(); + Assertions.assertFalse(results.isEmpty()); + } + +} diff --git a/cloud-core/src/test/java/com/intellectualsites/commands/ExecutionBenchmark.java b/cloud-core/src/test/java/com/intellectualsites/commands/ExecutionBenchmark.java new file mode 100644 index 00000000..7feaccc7 --- /dev/null +++ b/cloud-core/src/test/java/com/intellectualsites/commands/ExecutionBenchmark.java @@ -0,0 +1,75 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package com.intellectualsites.commands; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +import java.util.concurrent.TimeUnit; + +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +public class ExecutionBenchmark { + + private CommandManager manager; + private String literalChain; + + @Setup(Level.Trial) + public void setup() { + manager = new TestCommandManager(); + + final StringBuilder literalBuilder = new StringBuilder("literals"); + + /* Create 100 literals */ + Command.Builder builder = manager.commandBuilder("literals"); + for (int i = 1; i < 101; i++) { + final String literal = Integer.toString(i); + builder = builder.literal(literal); + literalBuilder.append(' ').append(literal); + } + manager.command(builder.build()); + literalChain = literalBuilder.toString(); + + } + + @TearDown + public void clean() { + } + + @Benchmark + @Fork(3) + public void testCommandParsing() { + manager.executeCommand(new TestCommandSender(), literalChain).join(); + } + +}