Skip to content

Slow shell performance after running pyenv virtualenv-init  #259

Closed

Description

@cmcginty

It looks like there is some issue on my system that is causing very poor runtime performance after running pyenv virtualenv-init. Related to #132

I think the issue is with pyenv verision-name. What steps can I take to debug this further?

Activity

  1. changed the title [-]Shell slow performance after running pyenv virtualenv-init [/-] [+]Slow shell performance after running pyenv virtualenv-init [/+] on Apr 20, 2018
  2. cmcginty commented on Apr 30, 2018

    @cmcginty
    Author

    This issue is caused by a 100ms delay added to every script executed with $()command substitution in the code. The entry point of pyenv sh-activate has number of sub-commands that get executed with this call. The call trace I'm seeing is:

    pyenv sh-activate
    -> pyenv-sh-activate                +100ms
      -> pyenv-version-name             +100ms
        -> pyenv-version-file           +100ms
        -> pyenv-version-file-read      +100ms
        -> pyenv-hooks                  +100ms
    -> pyenv-virtualenv-prefix          +100ms
      -> pyenv-version-name             +100ms
        -> pyenv-version-file           +100ms
        -> pyenv-version-file-read      +100ms
        -> pyenv-hooks                  +100ms
    

    All these calls add up to 1s+ of latency.

    My theory is this is related to a virus scanner or carbonblack.

  3. cmcginty commented on Apr 30, 2018

    @cmcginty
    Author

    More progress. I can reproduce outside of pyenv now. 😄
    I wrote a shell script that creates and calls another script. If the script has a shebang line in it, then the execution is horrendous.

    #!/bin/bash
    SUB_SCRIPT=$(mktemp)
    if [[ $1 == "--bash" ]]; then
      echo "#!/bin/bash" > $SUB_SCRIPT
    fi
    echo "exit" >> $SUB_SCRIPT
    chmod +x $SUB_SCRIPT
    for X in $(seq 100); do
      $($SUB_SCRIPT)
    done
    rm $SUB_SCRIPT
    $ time ./test.sh
    ./test.sh  0.05s user 0.08s system 37% cpu 0.341 total
    
    $ time ./test.sh --bash
    ./test.sh --bash  0.24s user 0.23s system 3% cpu 11.869 total
    

    Even on my unaffected system, the performance is measurably worse when shebang line exists.

    $ time ./test.sh
    ./test.sh  0.07s user 0.08s system 94% cpu 0.156 total
    
    $ time ./test.sh --bash
    ./test.sh --bash  0.15s user 0.16s system 87% cpu 0.351 total
    
  4. added 5 commits that reference this issue on May 1, 2018
    957d719
    ee38ee3
    e354eb4
    7e32b2d
    1189c8d
  5. Honghe commented on May 18, 2018

    @Honghe

    running into the same problem too, on Ubuntu 16.04.

  6. cmcginty commented on May 18, 2018

    @cmcginty
    Author

    @Honghe Does your laptop run CarbonBlack? That was the root cause of my performance issue.

  7. Honghe commented on May 18, 2018

    @Honghe

    @cmcginty no running CarbonBlack ;(

  8. chriscarpenter12 commented on Jan 2, 2019

    @chriscarpenter12

    Came here looking for a solution too. Added this to my zsh config and saw terrible performance. On Ubuntu 18.04. Removing this eval "$(pyenv virtualenv-init -)" removes the slowness.

  9. Baishan commented on Jan 7, 2019

    @Baishan

    Experiencing this^ too.

  10. jtherrmann commented on Mar 26, 2019

    @jtherrmann

    Similar problem on Debian 9 with zsh. Removing eval "$(pyenv virtualenv-init -)" from ~/.zshrc fixes it.

  11. 42 remaining items

  12. ardeidae commented on Apr 2, 2024

    @ardeidae

    I am confused. I removed the eval "$(pyenv virtualenv-init -)" line from my .zshrc and my virtual env is still automatically activated when a .python-version file is found. So I don't understand what this line is for. Does anyone know the answer?

  13. hassec commented on Apr 2, 2024

    @hassec

    @ardeidae with the eval command removed cd into the directory that you mention.
    Then do

    echo $VIRTUAL_ENV
    pyenv activate $(/bin/cat .python-version)
    echo $VIRTUAL_ENV
    

    I expect the first echo won't show anything and the 2nd will print the virtualenv name.
    What you are seeing is likely that your prompt checks for .python-version or pyenv and thus already shows the name of the virutal env without it being activated.

  14. ardeidae commented on Apr 2, 2024

    @ardeidae

    @hassec

    $ mkdir test && cd test
    
    $ pyenv versions
    system
    3.11.0
    * 3.12.1 (set by /Users/me/.pyenv/version)
    
    $ pyenv virtualenv 3.11.0 test
    
    $ pyenv local test
    
    $ cat .python-version
    test
    
    $ echo $VIRTUAL_ENV
    
    
    $ pyenv versions
    system
    3.11.0
    3.11.0/envs/test
    3.12.1
    * test --> /Users/me/.pyenv/versions/3.11.0/envs/test (set by /Users/me/test/.python-version)
    
    $ pyenv activate $(/bin/cat .python-version)
    
    (test) $ echo $VIRTUAL_ENV
    /Users/me/.pyenv/versions/3.11.0/envs/test
    

    The only benefit I see is the "test" string added in the prompt, but without the eval, it seems the used virtual environment is the expected one.

    If I use pyenv activate test, I also see this string displayed in the prompt.

  15. hassec commented on Apr 3, 2024

    @hassec

    I don't think I know all the details around this issue, but I believe that the virtualenv is not actually activated. When you call python it picks the right one because pyenv resolves it with its shim mechanism.

    But it's technically not equivalent to actually having activated the env as shown by the absence of $VIRTUAL_ENV.

    If e.g. starting neovim in that directory, it won't pick the python from within that environment. And I anticipate there are similar other corner cases, but this issue isn't the right place to discuss this further, I think.

  16. ardeidae commented on Apr 3, 2024

    @ardeidae

    @hassec
    Using pip freeze to see what is installed returns the correct list of packages.
    So for me, this uses the right virtual environment without using eval.

  17. 13steinj commented on Apr 4, 2024

    @13steinj

    In my case, the root problem is that pytenv sh-active is slow and it is executed each time _pyenv_virtualenv_hook is called. And since the hook is called each time we issue a command (or switch directory, if we change precmd to chpwd), it makes all actions on the shell sluggish.

    To speed up the shell, we need to make the execution of pytenv sh-activ[at]e more selective. Below is my work-around on this issue.

    Emphasis mine, just doing this blind obscures the problem, not solves it. The solution is to actually improve the performance of pyenv-sh-activate (or rather the hook, that currently calls it). Most of the negative performance appears to be caused by recursive call into the pyenv shell function / command to call other pyenv[-virtualenv] scripts as well as pyenv libexec utilities.

    So, for the sake of a fresh look at this investigative process:

    If we can assume that we know what's a libexec and what isn't, and that we don't have to depend on the idea of someone else overriding pyenv or this plugin's behavior, a fix can be seen as follows:

    • Remove unnecessary string -> value evaluation (rely on exit codes)
    • determine the script bindir at the top of the script, use it directly.
    • if no venv names are provided on the command line, no need to check the python prefix twice. ref:
      if ! pyenv-virtualenv-prefix "${venv}" 1>/dev/null 2>&1; then
      # fallback to virtualenv of current version
      [ -n "${current_versions}" ] || get_current_versions
      new_venv="${current_versions%/envs/*}/envs/${venv}"
      if pyenv-virtualenv-prefix "${new_venv}" 1>/dev/null 2>&1; then
      venv="${new_venv}"
      versions[0]="${new_venv}"
      • If getting 3.12.2 as a local/global/shell version, we won't find a venv at 3.12.2/envs/3.12.2.
      • If getting some_venv_name as a local/global/shell version, we won't find a venv at some_venv_name/envs/some_venv_name
      • This shaves off 20-40 millis.

    The first check still takes 20-40 millis. Can we do any better? Littering the scripts with date +%s%N | cut -b1-13 1>&2 (and timing this under zsh to take ~2.5ms, so, count of output x 2.5 => time spent on the date calls themselves) shows me this pattern:

    image

    Which tells me that the actual prefix finding script isn't a large deal. Digging deeper, get_current_versions is 40-60ms (incredibly variable), each call to the prefix versions script is ~ 20ms. Not much point in optimizing out the other call then. Can we do anything about get_current_versions?

    Logically speaking, the answer is yes-- it's effectively the result of https://github.com/pyenv/pyenv/blob/5b4d5a32d343dcae5e7b3f1a09850312f89ba868/libexec/pyenv-version-name. The current versions (assuming no other plugins) can change under the result of the following commands:

    • a changed directory
    • the relevant .python_version file changing (if done manually or directly by some plugin, screw that)
    • pyenv shell

    So, the ideal would be

    • don't check venv prefixes twice unnecessarily
      • have the singular call be a bash function in the sh-activate script rather than it's own script? sourcing is relatively fast by my benchmarks, so might want to move it to libexec and source that in the bin/ script.
    • call the scripts directly rather than via indirection
    • don't go back and forth between strings and values unnecessarily
    • only read the .python_version file if you have to. Which means
      • if PYENV_VERSION is set, skip calling get_current_versions spawning subshells (that info is already in the env var).
      • cache the result of get_current_versions somehow. Maybe change the hook to evaluate on chpwd, get the versions, cache the dir + versions tuple,
        pass the versions in to sh-activate, cache-miss on changed directory (chpwd), set a separate hook such that every time pyenv global/local are called, the versions cache gets cleared?

    I'll try to implement this ideal over the next weekend (only focusing on zsh and by extension that the changes are not magic, bash , sorry) on a branch; and this should shave off the majority (>90ms/120ms per call) by my estimate.

  18. kaddkaka commented on Dec 8, 2024

    @kaddkaka

    Any reactions from devs?

  19. native-api commented on Dec 8, 2024

    @native-api
    Member

    Any reactions from devs?

    If you hover the reaction icon under the last post, you'll see mine in the list. We'll happily accept a PR.

    To do anything ourselves, we need a way to reproduce the issue or a debug trace with timestamps from someone who experiences it.

  20. kaddkaka commented on Dec 9, 2024

    @kaddkaka

    Thanks, I understand.

    My situation: just doing the pyenv virtualen init in my bashrc on Ubuntu 24.04 caused my prompt to appear very slow in wezterm terminal. To notice I held my enter key down for 5 seconds in the terminal and got several empty lines between each prompt.

  21. fredcallaway commented on Feb 22, 2025

    @fredcallaway

    I'm getting a ~200ms delay at each prompt. It is all coming from pyenv sh-activate.

    ❯ time pyenv sh-activate
    pyenv-virtualenv: version `3.11.11' is not a virtualenv
    false
    pyenv sh-activate  0.05s user 0.11s system 81% cpu 0.200 total
    
    ~
    ❯ time pyenv sh-activate
    pyenv-virtualenv: version `3.11.11' is not a virtualenv
    false
    pyenv sh-activate  0.05s user 0.11s system 80% cpu 0.196 total
    
    ~
    ❯ time pyenv sh-activate
    pyenv-virtualenv: version `3.11.11' is not a virtualenv
    false
    pyenv sh-activate  0.05s user 0.11s system 80% cpu 0.199 total
    
  22. native-api commented on Mar 4, 2026

    @native-api
    Member

    pyenv/pyenv#3411 has an example of a profiling technique to find hot spots in Bash code

  23. native-api commented on Mar 25, 2026

    @native-api
    Member
  24. jakelodwick commented on Mar 25, 2026

    @jakelodwick
    Contributor

    This is fixed in v1.3.0, released today. #523 replaces the per-prompt pyenv sh-activate call (10+ forks, ~200ms) with a stat-based cache that detects .python-version changes without subshells. Prompt overhead drops to ~2ms.

    This fix drew on contributions across this thread:

    Thanks to @native-api for the thorough review and for shaping the design from the start. The constraint he set in #456 ("make it more intelligent and not do any unnecessary work") is what the whole cache is built on.

  25. added a commit that references this issue on Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions