# On slow systems, checking the cached .zcompdump file to see if it must be | |
# regenerated adds a noticable delay to zsh startup. This little hack restricts | |
# it to once a day. It should be pasted into your own completion file. | |
# | |
# The globbing is a little complicated here: | |
# - '#q' is an explicit glob qualifier that makes globbing work within zsh's [[ ]] construct. | |
# - 'N' makes the glob pattern evaluate to nothing when it doesn't match (rather than throw a globbing error) | |
# - '.' matches "regular files" | |
# - 'mh+24' matches files (or directories or whatever) that are older than 24 hours. | |
autoload -Uz compinit | |
if [[ -n ${ZDOTDIR}/.zcompdump(#qN.mh+24) ]]; then | |
compinit; | |
else | |
compinit -C; | |
fi; | |
Over time, I've improved this snippet to become something like this:
function {
# http://zsh.sourceforge.net/Doc/Release/Options.html#Scripts-and-Functions
setopt LOCAL_OPTIONS extendedglob
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Glob-Qualifiers
# http://zsh.sourceforge.net/Doc/Release/Conditional-Expressions.html#Conditional-Expressions
if [[ ! -e "${ZSH_COMPDUMP}" || -n "${ZSH_COMPDUMP}"(#qNY1.mh+24) ]]; then
zi compinit > /dev/null
zi cdreplay -q
fi
}
A few notes:
ZSH_COMPDUMP
comes fromohmyzsh
, which I use in addition tozinit
- The
Y1
enables short-circuit mode so that only the first file which matches the rest of the pattern will be returned. - Everything is in an anonymous function so that we can enable
extendedglob
just for the function, and not for the entire shell
autoload -Uz compinit
for dump in ~/.zcompdump(N.mh+24); do
compinit
done
compinit -C
works for me.
Timing with timezsh
:
timezsh() {
shell=${1-$SHELL}
for i in $(seq 1 4); do /usr/bin/time $shell -i -c exit; done
}
Before:
1.33 real 0.80 user 0.52 sys
1.38 real 0.80 user 0.57 sys
1.55 real 0.82 user 0.64 sys
1.50 real 0.81 user 0.63 sys
After:
0.78 real 0.32 user 0.42 sys
0.81 real 0.33 user 0.45 sys
0.96 real 0.37 user 0.55 sys
1.21 real 0.42 user 0.71 sys
It was faster here
Definitive solution?
I am going for
() {
emulate -L zsh
setopt extendedglob
autoload -Uz compinit complist
local zcd=$1 # compdump
local zcdc=$1.zwc # compiled compdump
local zcda=$1.last # last compilation
local zcdl=$1.lock # lock file
local attempts=30
while (( attempts-- > 0 )) && ! ln $zcd $zcdl 2> /dev/null; do sleep 0.1; done
{
if [[ ! -e $zcda || -n $zcda(#qN.mh+24) ]]; then
compinit -i -d $zcd
: > $zcda
else
compinit -C -d $zcd
fi
[[ ! -f $zcdc || $zcd -nt $zcdc ]] && rm -f $zcdc && zcompile $zcd &!
} always {
rm -f $zcdl
}
} $ZSH/run/u/$HOST-$UID/zcompdump
I didn't see a mention what happens when multiple shells are spawned at the same time. Maybe that's the reason we sometime run into empty dump file.
Where exactly do we put this code:
autoload -Uz compinit
for dump in ~/.zcompdump(N.mh+24); do
compinit
done
compinit -C
@medwatt put the snippet somewhere in your .zshrc file, or put it in an external script file and call it from your .zshrc
@vincentbernat I solved the multiple shell problem by using a lockfile and a trap
to remove it. This ensures that only one process is responsible for updating the file. Even though there's still a race condition, it's now almost impossibly small 1
() {
setopt local_options
setopt extendedglob
local zcd=${1}
local zcomp_hours=${2:-24} # how often to regenerate the file
local lock_timeout=${2:-1} # change this if compinit normally takes longer to run
local lockfile=${zcd}.lock
if [ -f ${lockfile} ]; then
if [[ -f ${lockfile}(#qN.mm+${lock_timeout}) ]]; then
(
echo "${lockfile} has been held by $(< ${lockfile}) for longer than ${lock_timeout} minute(s)."
echo "This may indicate a problem with compinit"
) >&2
fi
# Exit if there's a lockfile; another process is handling things
return
else
# Create the lockfile with this shell's PID for debugging
echo $$ > ${lockfile}
# Ensure the lockfile is removed
trap "rm -f ${lockfile}" EXIT
fi
autoload -Uz compinit
if [[ -n ${zcd}(#qN.mh+${zcomp_hours}) ]]; then
# The file is old and needs to be regenerated
compinit
else
# The file is either new or does not exist. Either way, -C will handle it correctly
compinit -C
fi
} ${ZDOTDIR:-$HOME}/.zcompdump
For those who are new to some of this stuff, check out
- https://zsh.sourceforge.io/Doc/Release/Completion-System.html
- For how
compinit
works
- For how
- https://zsh.sourceforge.io/Doc/Release/Expansion.html#Filename-Generation
- For the
(#q.N.....)
syntax, search for "file access qualifier"
- For the
Footnotes
-
A second process would have to check for the file in between 2 (effectively) subsequent commands.
if [ -f ${lockfile} ]
andecho $$ > ${lockfile}
↩
@thefotios my solution is already handling this case (but my message wasn't quite clear on that, so I understand you thought this was not the case). This is the purpose of the ln command (which is atomic, so no race condition).
I commented out these lines in my .zshrc
which sped it up a lot:
if type brew &>/dev/null; then
FPATH=$(brew --prefix)/share/zsh-completions:$FPATH
autoload -Uz compinit
compinit
if
As this comment above pointed out, oh-my-zsh already runs compinit.
Just adding this one thing to .zshenv cut my initial loading time in half.👍
For plugins, though. I use this: https://getantibody.github.io/ "The fastest shell plugin manager."
Plus, here are some additional tips to make initial loading faster if anyone is interested.
https://carlosbecker.com/posts/speeding-up-zsh/
(Man, I didn't realize it's already been 5 years since I came across that site. So I suppose its a bit of an old post, but most of it should still be relevant and helpful)