首页 > 解决方案 > 如何在运行时提高命令性能?

问题描述

我有一个带有使用 picocli 3.9.6 的内部命令行的库。这些命令之一是log命令,它像大多数记录器一样工作,采用日志级别和消息以及其他几个选项。在一些使用该库的应用程序中,它会被大量调用,我们注意到从一次性实现此命令与切换到 picocli 时相比,性能大幅下降。即使日志级别设置为没有任何有趣的事情发生也是如此。两个版本的核心代码相同。

因此,我们怀疑 picocli 正在使用反射来处理每个命令实例。我们怎样才能提高性能?我注意到 picocli 4.x 包含一个注释处理器,但让我们的用户使用 Graal 对我们来说是不现实的。由于注释不会跨实例更改,也许它们可以被缓存?

log命令的代码可以在这里找到:

https://github.com/soartech/jsoar/blob/maven/jsoar-core/src/main/java/org/jsoar/kernel/commands/LogCommand.java

testPerformance在这里添加了一个单元测试:https ://github.com/soartech/jsoar/blob/maven/jsoar-core/src/test/java/org/jsoar/kernel/commands/LogCommandTest.java

在我的机器上运行单元测试会产生约 3 秒的时间。如果我回到提交2bc4d39549eeb4ad69fd45e97f9607475e6426d9(2018 年 10 月 30 日),就在log命令转换为 picocli 之前,并将测试放在那里(你可以用较新的版本替换整个单元测试文件),我得到了 ~ 0.03 秒。

标签: picocli

解决方案


我使用 Java Flight Recorder 查看执行LogCommandTest.

仔细观察,当前的应用程序逻辑重新初始化一个新CommandLine模型,Log每次调用LogCommand. 这是确保每次调用都重置所有值的一种方法,但是当命令被大量调用时,它会变得很昂贵。幸运的是,这不是唯一的方法。

我建议您创建CommandLine一次对象,然后将其重用于所有后续调用。Picocli 旨在以这种方式使用:在解析新的用户输入之前,picocli 会将选项和参数重置为其默认值。

下面的补丁实现了这一点。我专注于 OP,LogCommand因为这就是 OP 的意义所在,但您可能希望对其他经常调用的性能敏感命令应用类似的更改。

我测试了以下内容,发现LogCommandTest.testPerformance我的机器上的测试时间从 5 秒缩短到了 0.5 秒。中的其他测试LogCommandTest仍然通过。

建议的补丁:

Index: jsoar-core/src/main/java/org/jsoar/kernel/commands/LogCommand.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- jsoar-core/src/main/java/org/jsoar/kernel/commands/LogCommand.java  (revision 576ae0a1420177bad69d2f9e2e0d405c74f87ab0)
+++ jsoar-core/src/main/java/org/jsoar/kernel/commands/LogCommand.java  (date 1577052510919)
@@ -23,6 +23,7 @@
 import org.jsoar.util.commands.SoarCommandContext;
 import org.jsoar.util.commands.SoarCommandInterpreter;

+import picocli.CommandLine;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.HelpCommand;
 import picocli.CommandLine.Model.CommandSpec;
@@ -39,17 +40,22 @@
 {
     private final Agent agent;
     private SoarCommandInterpreter interpreter;
+    private Log log;
+    private CommandLine logCommand;

     public LogCommand(Agent agent, SoarCommandInterpreter interpreter)
     {
         this.agent = agent;
         this.interpreter = interpreter;
+        this.log = new Log(agent, interpreter, null);
+        this.logCommand = new CommandLine(log);
     }

     @Override
     public String execute(SoarCommandContext context, String[] args) throws SoarException
     {
-        Utils.parseAndRun(agent, new Log(agent, interpreter, context), args);
+        this.log.context = context;
+        Utils.parseAndRun(agent, logCommand, args);

         return "";
     }
@@ -57,7 +63,7 @@
     @Override
     public Object getCommand()
     {
-        return new Log(agent,interpreter,null);
+        return logCommand;
     }

     @Command(name="log", description="Adjusts logging settings",
@@ -67,7 +73,7 @@
         private final Agent agent;
         private final LogManager logManager;
         private final SoarCommandInterpreter interpreter;
-        private final SoarCommandContext context;
+        private SoarCommandContext context;
         private static String sourceLocationSeparator = ".";

         @Spec
Index: jsoar-core/src/main/java/org/jsoar/kernel/commands/Utils.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- jsoar-core/src/main/java/org/jsoar/kernel/commands/Utils.java   (revision 576ae0a1420177bad69d2f9e2e0d405c74f87ab0)
+++ jsoar-core/src/main/java/org/jsoar/kernel/commands/Utils.java   (date 1577052217242)
@@ -41,10 +41,25 @@

         parseAndRun(command, args, ps);
     }
-    
+
+    /**
+     * Executes the specified command and returns the result.
+     * A command may be a user object or a pre-initialized {@code picocli.CommandLine} object.
+     * For performance-sensitive commands that are invoked often,
+     * it is recommended to pass a pre-initialized CommandLine object instead of the user object.
+     * 
+     * @param command the command to execute; this may be a user object or a pre-initialized {@code picocli.CommandLine} object
+     * @param args the command line arguments (the first arg will be removed from this list)
+     * @param ps the PrintStream to print any command output to
+     * @return the command result
+     * @throws SoarException if the user input was invalid or if a runtime exception occurred
+     *                      while executing the command business logic
+     */
     public static List<Object> parseAndRun(Object command, String[] args, PrintStream ps) throws SoarException {

-        CommandLine commandLine = new CommandLine(command);
+        CommandLine commandLine = command instanceof CommandLine
+                ? (CommandLine) command
+                : new CommandLine(command);

         // The "debug time" command takes a command as a parameter, which can contain options
         // In order to inform picocli that the options are part of the command parameter

推荐阅读