Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Running annotation-based picocli applications without runtime reflection #539

Open
remkop opened this issue Nov 6, 2018 · 9 comments
Open
Labels
theme: codegen An issue or change related to the picocli-codegen module

Comments

@remkop
Copy link
Owner

remkop commented Nov 6, 2018

The annotation processor implemented in #500 can create a CommandSpec model at compile time.
One use case for an annotation processor like this is to generate source code at compile time that would allow picocli-based applications to run without runtime reflection.

This ticket is to explore some ideas for achieving that.

One idea is to generate code (actually modify an existing annotated class) to implement an interface, where a method in this interface generates a CommandSpec model. Picocli needs to be modified to not perform reflection when the user object implements this interface, and instead call the interface method to obtain the CommandSpec.

Example input:

@picocli.codegen.GenerateModel
class App {
    @Option(names = "-x")
    private int x;
    
    public static void main(String[] args) {
        CommandLine cmd = new CommandLine(new App());
        cmd.parseArgs(args);
    }
}

Example output of annotation processor:

// after annotation processing:
class App implements picocli.CommandLine.Model.ICommandSpecFactory {
    @Option(names = "-x")
    private int x;
    
    public static void main(String[] args) {
        CommandLine cmd = new CommandLine(new AppV1());
        cmd.parseArgs(args);
    }
    
    // ICommandSpecFactory implementation (generated)
    public CommandSpec getCommandSpec() {
        CommandSpec result = CommandSpec.wrapWithoutInspection(this);
        result.addOption(OptionSpec.builder("-x").type(int.class)
            .getter(new IGetter() {
                public Object get() {
                    return App.this.x;
                }
            })
            .setter(new ISetter() {
                public Object set(Object newValue) {
                    Object old = App.this.x;
                    App.this.x = (Integer) newValue;
                    return old;
                }
            })
            .build());
            
        // other options
        return result;
    }
}

Lombok does something similar: it inserts accessor code into an existing class for fields annotated with @Getter and @Setter.
The way Lombok does this is by using internal APIs from Javac and the Eclipse compiler.

@remkop remkop modified the milestone: 4.0 Nov 10, 2018
@remkop remkop added the theme: codegen An issue or change related to the picocli-codegen module label Apr 27, 2019
@remkop
Copy link
Owner Author

remkop commented Apr 27, 2019

Current thinking is to generate a subclass that can get/set protected or package-private fields and methods in the annotated superclass. This is a reasonable restriction that is more maintainable than using internal compiler APIs.


Related: #750, #1003

@kristofdho
Copy link

Hi @remkop, is there any progress on this? Do you maybe have an estimation on when you would get around to implementing this?
We are using picocli in combination with GraalVM native-image. We have multiple entrypoints with different sets of supported commands pulled from a shared pool. However with the current config generation, all commands are seen as reachable to the native-image compiler.
With the suggested codegen, automatic reachabillity analysis would correctly remove all unused commands, reducing our final executable size.

@remkop
Copy link
Owner Author

remkop commented Jul 28, 2021

Hi @kristofdho I am not currently working on this and I don't see myself working on this in the near future. So I cannot give any estimate.

However, this message that was posted on the picocli mailing list may be relevant, so let me copy it here:

On Tuesday, July 6, 2021 at 9:14:03 PM UTC+9 [email protected] wrote:

Hi,

Just in case this might interest anyone, I created an annotation processor that takes an existing project that uses PicoCli annotations and generates code that uses the PicoCli API to recreate the same model:

https://github.com/quintesse/jbang/tree/picocli_annoproc

The code isn't finished because it only implements the PicoCli features we were using in our own project (but we're using quite a number). But it might be useful as a starting point for someone who'd like to do something similar.

Also the code quality is PoC-level because I was only trying to see if model creation time would improve using the API (in our case PicoCli setup is 3/4 of our app startup time so finding a way to improve this is pretty important to us). Unfortunately the gains were pretty minimal so I'll not be working on this any further.

The code for the annotation processor can be found in ... drum roll... /annotation.

Cheers!
-Tako
(Jbang contributor)

I currently have very little time to spend on picocli, but if this is something you want to work on, we can look at bringing this to production quality and integrating this into the picocli project somehow.

@kristofdho
Copy link

@remkop Thank you for the quick reply.

For now our best option would probably be writing a feature that programatically registers the required reflection configuration based on reachabillity hooks.

However, could you shed some light on when the reflective lookup calls happen? The CommandLine class is, to put it lightly, quite complex. For what I could follow, it looks like Field references get stored in the CommandLine instance while parsing the annotations. For simply running CommandLine#parseArgs and CommandLine#usage calls after the setup, are there any reflective lookups required?
If that would be the case, we could simply cache the CommandLine instance at build-time and we wouldn't need any configuration at all, removing all setup overhead as well.

@remkop
Copy link
Owner Author

remkop commented Aug 2, 2021

@kristofdho Reflection happens at two times:

  • at initialization time to create the model (the hierarchy of CommandSpec, OptionSpec etc instances)
  • during parsing (parseArgs), after options and positional parameters were matched: the IGetter and ISetter bindings that picocli generates use reflection to set the value of annotated fields or invoke the annotated methods

An annotation processor could avoid both usages: the processor could generate code that creates the model. The IGetter and ISetter for each option and positional parameter could simply set the annotated field value (or invoke the annotated method) programmatically without reflection.

The usage methods does not need to use reflection if the default value for an option can be obtained without calling the IGetter of the option. So, @Option(names = "-a", defaultValue = "abc") would be fine.

It should be possible to reuse a single CommandLine instance, but there are some edge cases (like #1010) where there may be issues.

@mikehearn
Copy link

May I ask a possibly stupid question - has anyone profiled PicoCLI startup time to determine that reflection actually is the problem? I'm a bit disturbed by the comment above saying that even with compile-time generated specs, startup time didn't improve much. Why would that be? Where is the time actually going?

@remkop
Copy link
Owner Author

remkop commented Apr 21, 2022

@mikehearn That is a good question. I personally have not spent time profiling. I believe some others have, notably here: #1377

(...) comment above saying that even with compile-time generated specs, startup time didn't improve much.

Can you link to that comment? I cannot find it.

@mikehearn
Copy link

Also the code quality is PoC-level because I was only trying to see if model creation time would improve using the API (in our case PicoCli setup is 3/4 of our app startup time so finding a way to improve this is pretty important to us). Unfortunately the gains were pretty minimal so I'll not be working on this any further.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
theme: codegen An issue or change related to the picocli-codegen module
Projects
None yet
Development

No branches or pull requests

3 participants