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

Increase performance of typer autocompletion by breaking out subcommand functions into their own module #77

Merged
merged 2 commits into from
Mar 25, 2023

Conversation

MathiasSven
Copy link
Contributor

Fixes #76

The main problem seemed to be loading manim. By separating the commands from the modules where manim needs to be loaded, it is possible to delay loading it until a command is actually executed, since the auto-completion only relied on those command functions being registered by typer.

While the auto-completion is still a bit slow, it is a significant improvement (3x), and as a bonus because manim isn't loaded the Manim Community v0.16.0.post0 log doesn't clutter the output of git-sim --show-completion SHELL.

Previous:

slow.mp4

With PR:

faster.mp4

@initialcommit-io
Copy link
Contributor

@MathiasSven Hmm when I test on Windows in Git-Bash (fish shell) or MacOS using Fish shell or Zsh but I don't see any autocompletion happening. Is this something that needs to be configured in Typer application?

I tried the following in Windows Git-Bash and MacOS terminal using Fish Shell / Zsh:

  1. Type git-sim followed by TAB TAB, and it just traverses the files in the current directory

  2. Ran git-sim --show-completion SHELL and that gives printed output:

Jack@...> git-sim --show-completion fish
complete --command git-sim --no-files --arguments "(env _GIT_SIM_COMPLETE=complete_fish _TYPER_COMPLETE_FISH_ACTION=get-args _TYPER_COMPLETE_ARGS=(commandline -cp) git-sim)" --condition "env _GIT_SIM_COMPLETE=complete_fish _TYPER_COMPLETE_FISH_ACTION=is-args _TYPER_COMPLETE_ARGS=(commandline -cp) git-sim"

@MathiasSven
Copy link
Contributor Author

You need to source it in order to take effect, or use the --install-completion which will just add it to your bashrc or fish and zsh equivalent.

To try it out on fish you can do

  1. git-sim --show-completion fish > git-sim.fish
  2. source git-sim.fish
  3. git-sim followed by TAB TAB

@initialcommit-io
Copy link
Contributor

@MathiasSven Gotcha that worked and I was able to test in Windows powershell/git-bash fish and MacOs Fish. FYI I don't see the manim entry in MacOS or Windows...

Would another option be to conditionally import manim only when the program is executed with a subcommand like "git-sim log", and ignore the manim import if Typer is just doing its completion thing? Something like:

if (git-subcommand option is present):
    import manim as m

If possible, I prefer to keep the Typer command functions in the corresponding subcommand modules for ease of code organization/editing/creating new subcommands.

Note: It looks like in Windows gitbash/Fish it's adding a \r to the completions, so we may need to add some Typer config for that in the application as now it must just be using whatever the default is. Looks like we can add some custom config to strip the completion entries. Do you want to try adding that as a part of this PR?

Also, looks like your PR is based on the main branch which is a bit outdated from the dev branch in which I've been working on a new release. Specifically there are several more subcommand files (switch, checkout, fetch, pull, push, clone) that I'll be releasing soon in dev that don't yet exist in main.

Can you apply your changes to the latest version of the dev branch instead, include any mods to the new subcommands as a part of your change, and resubmit the PR for that to the dev branch?

@MathiasSven
Copy link
Contributor Author

Would another option be to conditionally import manim only when the program is executed with a subcommand like "git-sim log", and ignore the manim import if Typer is just doing its completion thing? Something like:

That could work, taking git_sim.add as an example, the way typer registers the command is by running it through app.command(), as you do in your __main__.py. By keeping add alongside Add:

# ...
import manim as m
# ...
from git_sim.animations import handle_animations
from git_sim.git_sim_base_command import GitSimBaseCommand
from git_sim.settings import settings

class Add(GitSimBaseCommand):
    ...

def add(
    files: List[str] = typer.Argument(
        default=None,
        help="The names of one or more files to add to Git's staging area",
    )
):
    settings.hide_first_tag = True
    scene = Add(files=files)
    handle_animations(scene=scene)

__main__.py will need to load this whole file in order to get to the add function, and evaluating any of

  • from git_sim.animations import handle_animations
  • from git_sim.git_sim_base_command import GitSimBaseCommand
  • import manim as m

Will result in slow down as all of them load import manim as m... I played around a little with making Add and all other command classes use manim as an instance attribute, so instead of m.Arrow, it would be self.m.Arrow (so it would only be imported once instantiated), however because all these classes are subclasses of GitSimBaseCommand, which in turn is a subclass of m.MovingCameraScene, manim would still be loaded.

With your suggestion, which also seems to be the way it was worked around here, all modules would have to be mostly indented by one block inside an if statement, it should work about the same, so it would be a matter of preference...

# _utils.py
import sys
import os

def should_define(command: str) -> bool:
    return _cli_is_invoking_command(command=command) or _autocomplete_is_resolving_command(command=command)

def _cli_is_invoking_command(command: str) -> bool:
    return command in sys.argv

def _autocomplete_is_resolving_command(command: str) -> bool:
    return command in os.environ.get("_TYPER_COMPLETE_ARGS", "")
# add.py
# ...
from git_sim._utils import should_define

if should_define('add'):
    import manim as m
    # ...
    from git_sim.animations import handle_animations
    from git_sim.git_sim_base_command import GitSimBaseCommand
    from git_sim.settings import settings

    class Add(GitSimBaseCommand):
        ...

def add(
    files: List[str] = typer.Argument(
        default=None,
        help="The names of one or more files to add to Git's staging area",
    )
):
    settings.hide_first_tag = True
    scene = Add(files=files)
    handle_animations(scene=scene)

Taking some inspiration from the mentioned issue

@initialcommit-io
Copy link
Contributor

@MathiasSven Thanks a lot for this research and all the details, I don't think it's an ideal solution unfortunately...

Going back to your original PR, I just notice that the new commands.py has stuff like this:

def add(
    files: List[str] = typer.Argument(
        default=None,
        help="The names of one or more files to add to Git's staging area",
    )
):
    from git_sim.add import Add

    settings.hide_first_tag = True
    scene = Add(files=files)
    handle_animations(scene=scene)

This actually includes an import back to Add, (as other other functions do too), so wouldn't this also require Manim to be imported recursively since Add inherits from GitSimBaseCommand?

Signed-off-by: Mathias Sven <mathiassven2@hotmail.com>
@MathiasSven
Copy link
Contributor Author

Yes however that only runs once add is called, typer doesn't call the function for autocompletion, it only needs to see the function to inspect its args like typer.Argument and type annotations such as List[str] in order to do autocompletion.

Signed-off-by: Mathias Sven <mathiassven2@hotmail.com>
@MathiasSven
Copy link
Contributor Author

MathiasSven commented Mar 25, 2023

I also just pushed a small commit to also delay the git import in __main__, finally making it read sub 300ms second for just running git-sim.

manim & git

1.284 <module>  __main__.py:1
├─ 0.993 <module>  add.py:1
│  ├─ 0.917 <module>  manim/__init__.py:1
│  │     [2692 frames hidden]  manim, IPython, prompt_toolkit, wcwid...
│  └─ 0.076 <module>  animations.py:1
│     └─ 0.063 <module>  settings.py:1
│        └─ 0.040 [self]  None
├─ 0.124 <module>  typer/__init__.py:1
│     [454 frames hidden]  typer, rich, attr, <built-in>, copy, ...
├─ 0.075 <module>  git/__init__.py:1
│     [225 frames hidden]  git, gitdb, hashlib, <built-in>, stru...
└─ 0.067 Typer.__call__  typer/main.py:307
      [270 frames hidden]  typer, click, rich, <built-in>, typin...

git

0.360 <module>  __main__.py:1
├─ 0.123 <module>  typer/__init__.py:1
│     [448 frames hidden]  typer, rich, attr, <built-in>, datacl...
├─ 0.074 <module>  git/__init__.py:1
│     [241 frames hidden]  git, gitdb, hashlib, <built-in>, stru...
├─ 0.073 <module>  commands.py:1
│  └─ 0.073 <module>  settings.py:1
│     ├─ 0.044 [self]  None
│     ├─ 0.007 <module>  pickle.py:1
│     │     [4 frames hidden]  pickle, <built-in>
│     ├─ 0.006 inner  typing.py:306
│     │     [27 frames hidden]  typing, <built-in>, typing_extensions
│     └─ 0.005 stat  None
│           [2 frames hidden]  <built-in>
├─ 0.070 Typer.__call__  typer/main.py:307
│     [309 frames hidden]  typer, click, rich, <built-in>, typin...
├─ 0.011 <module>  pathlib.py:1
│     [10 frames hidden]  pathlib, ntpath, <built-in>
└─ 0.006 [self]  None

Without both

0.291 <module>  __main__.py:1
├─ 0.126 <module>  typer/__init__.py:1
│     [452 frames hidden]  typer, rich, attr, copy, <built-in>, ...
├─ 0.077 <module>  commands.py:1
│  └─ 0.076 <module>  settings.py:1
│     ├─ 0.038 [self]  None
│     ├─ 0.011 <module>  pickle.py:1
│     │     [11 frames hidden]  pickle, struct, <built-in>
│     ├─ 0.006 inner  typing.py:306
│     │     [22 frames hidden]  typing, <built-in>, typing_extensions
│     ├─ 0.005 stat  None
│     │     [2 frames hidden]  <built-in>
│     └─ 0.003 str.rpartition  None
│           [2 frames hidden]  <built-in>
├─ 0.069 Typer.__call__  typer/main.py:307
│     [287 frames hidden]  typer, click, rich, <built-in>, typin...
├─ 0.012 <module>  pathlib.py:1
│     [8 frames hidden]  pathlib, ntpath, <built-in>
└─ 0.003 [self]  None

Realistically, this is all a bit silly (I still don't think this is a hack), auto completions should be generated once and then accessed statically, it would be nice if typer had a mechanism for this, but for now in order to keep auto-completion you would have to maintain startup time as lean as possible... If you think that is out of scope, that is understandable, though as you can see manim was definitely the biggest offender

@initialcommit-io initialcommit-io changed the title Separate commands from manim modules Increase performance of typer autocompletion by breaking out subcommand functions into their own module Mar 25, 2023
@initialcommit-io initialcommit-io merged commit ac2bb32 into initialcommit-com:dev Mar 25, 2023
@initialcommit-io initialcommit-io added the enhancement New feature or request label Mar 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Auto-completion is extremely slow
2 participants