From 9ef8e38aecd98a7c67f3e77492393ff146a016a3 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Mon, 16 Dec 2024 08:55:55 -0800 Subject: [PATCH] Speed up shell startup with evalcache --- zsh/functions/_evalcache | 50 ++++++++++++ zsh/functions/_hub | 160 --------------------------------------- zshrc | 16 ++-- 3 files changed, 60 insertions(+), 166 deletions(-) create mode 100755 zsh/functions/_evalcache delete mode 100644 zsh/functions/_hub diff --git a/zsh/functions/_evalcache b/zsh/functions/_evalcache new file mode 100755 index 0000000..ecbea7c --- /dev/null +++ b/zsh/functions/_evalcache @@ -0,0 +1,50 @@ +# Caches the output of a binary initialization command, to avoid the time to +# execute it in the future. +# +# Usage: _evalcache [NAME=VALUE]... COMMAND [ARG]... + +# default cache directory +export ZSH_EVALCACHE_DIR=${ZSH_EVALCACHE_DIR:-"$HOME/.zsh-evalcache"} + +function _evalcache () { + local cmdHash="nohash" data="$*" name + + # use the first non-variable argument as the name + for name in $@; do + if [ "${name}" = "${name#[A-Za-z_][A-Za-z0-9_]*=}" ]; then + break + fi + done + + # if command is a function, include its definition in data + if typeset -f "${name}" > /dev/null; then + data=${data}$(typeset -f "${name}") + fi + + if builtin command -v md5 > /dev/null; then + cmdHash=$(echo -n "${data}" | md5) + elif builtin command -v md5sum > /dev/null; then + cmdHash=$(echo -n "${data}" | md5sum | cut -d' ' -f1) + fi + + local cacheFile="$ZSH_EVALCACHE_DIR/init-${name##*/}-${cmdHash}.sh" + + if [ "$ZSH_EVALCACHE_DISABLE" = "true" ]; then + eval ${(q)@} + elif [ -s "$cacheFile" ]; then + source "$cacheFile" + else + if type "${name}" > /dev/null; then + echo "evalcache: ${name} initialization not cached, caching output of: $*" >&2 + mkdir -p "$ZSH_EVALCACHE_DIR" + eval ${(q)@} > "$cacheFile" + source "$cacheFile" + else + echo "evalcache: ERROR: ${name} is not installed or in PATH" >&2 + fi + fi +} + +function _evalcache_clear () { + rm -i "$ZSH_EVALCACHE_DIR"/init-*.sh +} diff --git a/zsh/functions/_hub b/zsh/functions/_hub deleted file mode 100644 index c7b2bb2..0000000 --- a/zsh/functions/_hub +++ /dev/null @@ -1,160 +0,0 @@ -#compdef hub - -# Zsh will source this file when attempting to autoload the "_hub" function, -# typically on the first attempt to complete the hub command. We define two new -# setup helper routines (one for the zsh-distributed version, one for the -# git-distributed, bash-based version). Then we redefine the "_hub" function to -# call "_git" after some other interception. -# -# This is pretty fragile, if you think about it. Any number of implementation -# changes in the "_git" scripts could cause problems down the road. It would be -# better if the stock git completions were just a bit more permissive about how -# it allowed third-party commands to be added. - -(( $+functions[__hub_setup_zsh_fns] )) || -__hub_setup_zsh_fns () { - (( $+functions[_git-alias] )) || - _git-alias () { - _arguments \ - '-s[output shell script suitable for eval]' \ - '1::shell:(zsh bash csh)' - } - - (( $+functions[_git-browse] )) || - _git-browse () { - _arguments \ - '-u[output the URL]' \ - '2::subpage:(wiki commits issues)' - } - - (( $+functions[_git-compare] )) || - _git-compare () { - _arguments \ - '-u[output the URL]' \ - ':[start...]end range:' - } - - (( $+functions[_git-create] )) || - _git-create () { - _arguments \ - '::name (REPOSITORY or ORGANIZATION/REPOSITORY):' \ - '-p[make repository private]' \ - '-d[description]:description' \ - '-h[home page]:repository home page URL:_urls' - } - - (( $+functions[_git-fork] )) || - _git-fork () { - _arguments \ - '--no-remote[do not add a remote for the new fork]' - } - - (( $+functions[_git-pull-request] )) || - _git-pull-request () { - _arguments \ - '-f[force (skip check for local commits)]' \ - '-b[base]:base ("branch", "owner\:branch", "owner/repo\:branch"):' \ - '-h[head]:head ("branch", "owner\:branch", "owner/repo\:branch"):' \ - - set1 \ - '-m[message]' \ - '-F[file]' \ - - set2 \ - '-i[issue]:issue number:' \ - - set3 \ - '::issue-url:_urls' - } - - # stash the "real" command for later - functions[_hub_orig_git_commands]=$functions[_git_commands] - - # Replace it with our own wrapper. - declare -f _git_commands >& /dev/null && unfunction _git_commands - _git_commands () { - local ret=1 - # call the original routine - _call_function ret _hub_orig_git_commands - - # Effectively "append" our hub commands to the behavior of the original - # _git_commands function. Using this wrapper function approach ensures - # that we only offer the user the hub subcommands when the user is - # actually trying to complete subcommands. - hub_commands=( - alias:'show shell instructions for wrapping git' - pull-request:'open a pull request on GitHub' - fork:'fork origin repo on GitHub' - create:'create new repo on GitHub for the current project' - browse:'browse the project on GitHub' - compare:'open GitHub compare view' - ci-status:'lookup commit in GitHub Status API' - ) - _describe -t hub-commands 'hub command' hub_commands && ret=0 - - return ret - } -} - -(( $+functions[__hub_setup_bash_fns] )) || -__hub_setup_bash_fns () { - # TODO more bash-style fns needed here to complete subcommand args. They take - # the form "_git_CMD" where "CMD" is something like "pull-request". - - # Duplicate and rename the 'list_all_commands' function - eval "$(declare -f __git_list_all_commands | \ - sed 's/__git_list_all_commands/__git_list_all_commands_without_hub/')" - - # Wrap the 'list_all_commands' function with extra hub commands - __git_list_all_commands() { - cat <<-EOF -alias -pull-request -fork -create -browse -compare -ci-status -EOF - __git_list_all_commands_without_hub - } - - # Ensure cached commands are cleared - __git_all_commands="" -} - -# redefine _hub to a much smaller function in the steady state -_hub () { - # only attempt to intercept the normal "_git" helper functions once - (( $+__hub_func_replacement_done )) || - () { - # At this stage in the shell's execution the "_git" function has not yet - # been autoloaded, so the "_git_commands" or "__git_list_all_commands" - # functions will not be defined. Call it now (with a bogus no-op service - # to prevent premature completion) so that we can wrap them. - if declare -f _git >& /dev/null ; then - _hub_noop () { __hub_zsh_provided=1 } # zsh-provided will call this one - __hub_noop_main () { __hub_git_provided=1 } # git-provided will call this one - local service=hub_noop - _git - unfunction _hub_noop - unfunction __hub_noop_main - service=git - fi - - if (( $__hub_zsh_provided )) ; then - __hub_setup_zsh_fns - elif (( $__hub_git_provided )) ; then - __hub_setup_bash_fns - fi - - __hub_func_replacement_done=1 - } - - # Now perform the actual completion, allowing the "_git" function to call our - # replacement "_git_commands" function as needed. Both versions expect - # service=git or they will call nonexistent routines or end up in an infinite - # loop. - service=git - declare -f _git >& /dev/null && _git -} - -# make sure we actually attempt to complete on the first "tab" from the user -_hub \ No newline at end of file diff --git a/zshrc b/zshrc index 76bd604..c9d2971 100755 --- a/zshrc +++ b/zshrc @@ -13,6 +13,13 @@ function is_interactive() { [ -t 1 ] } [[ -d "$HOME/config/zsh" ]] && ZDOTDIR="$HOME/config/zsh" +fpath=($fpath $ZDOTDIR/functions $ZDOTDIR/completions) +typeset -U fpath + +# Cache the output of commands that take a long time to run even +# though they almost always return the same output. +autoload _evalcache + if [[ -r "$ZDOTDIR/zlocal" ]]; then source "$ZDOTDIR/zlocal" fi @@ -22,13 +29,13 @@ fi # Do this before setting up PATH so ~/bin and similar still have the highest precedence. if command_exists rbenv; then - eval "$(rbenv init -)" + _evalcache rbenv init - fi if command_exists pyenv; then - eval "$(pyenv init -)" + _evalcache pyenv init - fi if command_exists direnv; then - eval "$(direnv hook zsh)" + _evalcache direnv hook zsh fi ### SSH keys @@ -58,9 +65,6 @@ typeset -U path # remove / from word chars export WORDCHARS='*?_-.[]~=&;!#$%^(){}<>' -fpath=($fpath $ZDOTDIR/functions $ZDOTDIR/completions) -typeset -U fpath - cdpath=(~) HOSTNAME=`hostname -s`