You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Slow shell performance after running pyenv virtualenv-init #259
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:
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.
$ 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
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.
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?
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.
$ 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.
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.
@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.
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]emore 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 "${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:
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?
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.
Reacted by Rémi Desgrange, Lucas Yamanishi, native-api, Matthieu PETIOT, Michael Shepanski, w biggs, Frans Bol, Alexey Shokov and Mark Feldhousen
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.
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
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:
@cmcginty traced the fork chain in 2018 (pyenv sh-activate → pyenv-sh-activate → pyenv-version-name → pyenv-version-file), identifying the cascade of subprocess invocations that the cache now avoids entirely
@david-drinn confirmed pyenv sh-activate --quiet as the specific bottleneck; @ron-premier corroborated, which focused the fix on caching around that call
@hassec documented that the hook-swap workaround broke $VIRTUAL_ENV (poetry couldn't detect the active env), validating the constraint that the hook must check on every prompt
@FanchenBao proposed making sh-activate execution "more selective" by checking whether the virtualenv changed before forking. That's the core idea behind the cache
@ludocode timed the hook at 80–225ms in 2021, establishing the quantitative baseline that framed the problem
@fredcallaway's 200ms measurement in Feb 2025 kept the issue current
@ariebovenberg's fish workaround using --on-variable PWD informed the fish implementation in the fix
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.
Reacted by Mark Feldhousen, Arie Bovenberg, Gábor E. Gévay, David Drinnan and 13steinj
Activity
[-]Shell slow performance after running pyenv virtualenv-init [/-][+]Slow shell performance after running pyenv virtualenv-init [/+]cmcginty commentedon Apr 30, 2018
This issue is caused by a 100ms delay added to every script executed with
$()command substitution in the code. The entry point ofpyenv sh-activatehas number of sub-commands that get executed with this call. The call trace I'm seeing is:All these calls add up to 1s+ of latency.
My theory is this is related to a virus scanner or carbonblack.
cmcginty commentedon Apr 30, 2018
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.
Even on my unaffected system, the performance is measurably worse when shebang line exists.
Remove shebang lines from bin/ scripts for performance
Remove shebang lines from bin/ scripts for performance
Remove shebang lines from scripts for performance
Remove shebang lines from scripts for performance
Remove shebang lines from scripts for performance
Honghe commentedon May 18, 2018
running into the same problem too, on Ubuntu 16.04.
cmcginty commentedon May 18, 2018
@Honghe Does your laptop run CarbonBlack? That was the root cause of my performance issue.
Honghe commentedon May 18, 2018
@cmcginty no running CarbonBlack ;(
chriscarpenter12 commentedon Jan 2, 2019
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.Baishan commentedon Jan 7, 2019
Experiencing this^ too.
Remove shebang lines from scripts for performance
jtherrmann commentedon Mar 26, 2019
Similar problem on Debian 9 with zsh. Removing
eval "$(pyenv virtualenv-init -)"from~/.zshrcfixes it.42 remaining items
ardeidae commentedon Apr 2, 2024
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-versionfile is found. So I don't understand what this line is for. Does anyone know the answer?hassec commentedon Apr 2, 2024
@ardeidae with the eval command removed cd into the directory that you mention.
Then do
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-versionorpyenvand thus already shows the name of the virutal env without it being activated.ardeidae commentedon Apr 2, 2024
@hassec
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.hassec commentedon Apr 3, 2024
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
pythonit picks the right one becausepyenvresolves 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.
ardeidae commentedon Apr 3, 2024
@hassec
Using
pip freezeto see what is installed returns the correct list of packages.So for me, this uses the right virtual environment without using
eval.13steinj commentedon Apr 4, 2024
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:
pyenv-virtualenv/bin/pyenv-sh-activate
Lines 111 to 117 in f8469a1
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:Which tells me that the actual prefix finding script isn't a large deal. Digging deeper,
get_current_versionsis 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:
pyenv shellSo, the ideal would be
.python_versionfile if you have to. Which meansPYENV_VERSIONis set, skip callingget_current_versionsspawning subshells (that info is already in the env var).pass the versions in to
sh-activate, cache-miss on changed directory (chpwd), set a separate hook such that every timepyenv global/localare 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.
kaddkaka commentedon Dec 8, 2024
Any reactions from devs?
native-api commentedon Dec 8, 2024
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.
kaddkaka commentedon Dec 9, 2024
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.
fredcallaway commentedon Feb 22, 2025
I'm getting a ~200ms delay at each prompt. It is all coming from
pyenv sh-activate.native-api commentedon Mar 4, 2026
pyenv/pyenv#3411 has an example of a profiling technique to find hot spots in Bash code
native-api commentedon Mar 25, 2026
#523
jakelodwick commentedon Mar 25, 2026
This is fixed in v1.3.0, released today. #523 replaces the per-prompt
pyenv sh-activatecall (10+ forks, ~200ms) with a stat-based cache that detects.python-versionchanges without subshells. Prompt overhead drops to ~2ms.This fix drew on contributions across this thread:
pyenv sh-activate→pyenv-sh-activate→pyenv-version-name→pyenv-version-file), identifying the cascade of subprocess invocations that the cache now avoids entirelypyenv sh-activate --quietas the specific bottleneck; @ron-premier corroborated, which focused the fix on caching around that callprecmd→precwdhook swap for zsh, reducing prompt latency 10×; @RemiDesgrange adapted it to the standardchpwdhook, which became the dominant workaround in this thread. That workaround broke onpyenv localchanges within the same directory, because directory mtime doesn't change when a file is edited in place. Drastically speed up precommand hook by caching active version to skip redundantactivatecalls #523 checks file mtimes directly, which covers that case$VIRTUAL_ENV(poetry couldn't detect the active env), validating the constraint that the hook must check on every promptsh-activateexecution "more selective" by checking whether the virtualenv changed before forking. That's the core idea behind the cacheactivatecalls #523get_current_versionsat 40–60ms per call and proposed the architecture closest to Drastically speed up precommand hook by caching active version to skip redundantactivatecalls #523: cache the directory+versions tuple, skipget_current_versionswhenPYENV_VERSIONis set, and invalidate on cache miss rather than tracking every mutation--on-variable PWDinformed the fish implementation in the fixThanks 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.
fix: try to fix slow zsh prompt