如何在运行时提高命令性能?

时间:2019-12-19 18:37:45

标签: picocli

我有一个带有内部命令行的库,该库使用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

运行单元测试在我的机器上产生〜3s的时间。如果我返回提交2bc4d39549eeb4ad69fd45e97f9607475e6426d9(2018年10月30日),那是在log命令转换为picocli之前,然后将测试放入其中(您可以将整个单元测试文件替换为新版本),我得到的时间约为0.03秒。

1 个答案:

答案 0 :(得分:1)

我使用Java Flight Recorder来查看执行LogCommandTest时的热点,实际上,最上面显示的实际上是picocli从注释中构建模型。

仔细观察,当前的应用程序逻辑每次调用CommandLine时都会使用新的Log实例重新初始化新的LogCommand模型。这是确保每次调用都重置所有值的一种方法,但是当多次调用该命令时,结果却很昂贵。幸运的是,这不是唯一的方法。

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

以下补丁可实现此目的。我专注于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