diff options
Diffstat (limited to 'mds/zshgofast.txt')
-rw-r--r-- | mds/zshgofast.txt | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/mds/zshgofast.txt b/mds/zshgofast.txt new file mode 100644 index 0000000..8c0fcf5 --- /dev/null +++ b/mds/zshgofast.txt @@ -0,0 +1,354 @@ +== How to make the zsh prompt faster + +In this post, we are going to take a look at 3 ways of making the zsh +prompt return faster. We will talk about: + +* plugin precompilation +* caching +* async functions + +There are obviously other methods, of course, but you can find them on +almost any other blog talking about zsh prompts so for the sake of +brevity, I’m not going to repeat them(an example: use +https://github.com/Schniz/fnm[fnm] instead of +https://github.com/nvm-sh/nvm[nvm] or why would you not use +https://github.com/jdx/mise[mise]). There is also a faster syntax +highlighter for zsh. so on and so forth. + +Also: + +* Using the color red for all segments in your zsh prompt will make it +go faster because, as everyone should know by now, "`red ones go +faster`" + +Before we start though, a bit of reasoning on why I would be doing the +things the way I am doing them. A prompt written entirely in a faster +language will be faster, yes, but I’m not willing to write a couple of +thousand lines of code to get my current shell prompt(the major issue +here being maintenance throughout the years,i.e. technical debt). My +shell prompt is pretty critical for me so I want to be in complete +control over it which means I am unwilling to use ready-made prompts +with thousands of lines of code which don’t have half the segments that +I need. + +And finally: + +[source,txt] +---- +If you say you care about your zsh prompt's performance then that means that you already have `zmodload zsh/zprof` in your rc file. +---- + +If you talk about performance, then you have to have benchmarks and +profilers. `+zprof+` is built in. + +=== Plugin Precompilation + +zsh can compile zsh scripts using the builtin `+zcompile+` into +wordcode. This will have the effect of having faster parsing. The way we +use this to get a faster prompt is to explicitly ask zsh to compile +certain chunky plugins(think your +https://github.com/zsh-users/zsh-syntax-highlighting[syntax +highlighters] and +https://github.com/zsh-users/zsh-autosuggestions[completion] plugins) +into wordcode. + +For example: + +[source,zsh] +---- +zcompile-many ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions/{zsh-autosuggestions.zsh,src/**/*.zsh} +zcompile-many ~/zsh-async.git/v1.8.5/async.zsh +---- + +You can read more about `+zcompile+` in `+man 1 zshall+`, under +`+SHELL BUILTIN COMMANDS+`, under `+zcompile+`. + +=== Caching + +==== Eval Caching + +First we will talk about evalcaching: caching the results of `+eval+`s. +This is a thing because some heavier/older version managers use this to +inject themselves into your shell and some of them just take too much +time. For those kinds of plugins you can use +https://github.com/mroth/evalcache/[evalcache]. After sourcing the +script, you can use it like so: + +[source,zsh] +---- +_evalcache rbenv init - +---- + +We basically just swap out all instances of `+eval+` with +`+_evalcache+`. + +Admittedly, this is very limited in scope and it has no smart way of +redoing the cache. A function is provided, `+_evalcache_clear+`, which +will clear the cache, which in turn will result in the cache being +regenerated. + +==== Ye Old Result Caching + +You can also just cache some results in a file and then just read from a +file. Now how you update the cache is really up to you. You can use a +user service, you can use cron jobs. You can even run it in the prompt +itself or run the function asynchronously which we will get to in the +next section. + +Below is a simple example of implementing such a cache: + +[source,bash] +---- +caching_function() { + CACHE_OUTPUT="$1" + TIME_CACHE=$2 + + # check if the cache result is too old + if [ $(($(stat --format=%Y "$CACHE_OUTPUT") + TIME_CACHE)) -gt "$(date +%s)" ]; then + # the cache is still valid, we don't need to do anything + : + else + # the cache value is too old + if OUTPUT=$("your function that generates the output goes here"); then + if [ -n "${OUTPUT}" ]; then + echo "${OUTPUT}" >"${CACHE_OUTPUT}" + fi + fi + fi + + cat "${CACHE_OUTPUT}" +} +---- + +Our function has two arguments: + +* The first arg tells it where to store the cache. I keep mine under +`+/tmp+`. +* The second is the amount of time the cache result will be valid for. + +The function will check the last time the file was updated and compare +that using the `+CACHE_TIME+` var and the current time. If the cache +value is new enough, we just return the cache value and we are done. If +the cache is old, we run the function that generates the result, update +the cache and return the new result. + +=== Async Functions + +This is probably the most important one. Again, no surprises here. We +will use a zsh async +library,https://github.com/mafredri/zsh-async[zsh-async], to have some +async segments in our zsh prompt. + +Let’s take this zsh function from my prompt: + +[source,zsh] +---- +docker_compose_running_pwd() { + local cwd="$1" + local list=$(docker compose ls --format json | jq '.[].ConfigFiles' | tr -d '"') + local array=("${(@f)$(echo ${list})}") + for elem in "${array[@]}"; do + if [[ "${cwd}" == $(dirname "${elem}") ]];then + echo "\U1F7E6" + return + else + ; + fi + done +} +---- + +This function prints a blue square if there is a docker compose file +running from the current path we are in and outputs nothing if we are +not. Under normal circumstances this does not require to be an async +segment but as soon as you switch your docker context to a remote docker +host, then you will start to feel the pain. + +We first define a start function that registers an async worker with the +library and then define a callback function that gets called when the +async runner returns: + +[source,zsh] +---- +_async_dcpr_start() { + async_start_worker dcpr_info + async_register_callback dcpr_info _async_dcpr_info_done +} +---- + +The start function has nothing special in it. We just register an async +worker and then register a callback function that will be called when +the runner returns. Please do note that `+async_register_callback+` will +require two arguments, the name of the async runner and the name of our +callback function. Next we will define our callback function: + +[source,zsh] +---- +_async_dcpr_info_done() { + #first part + local job=$1 + local return_code=$2 + local stdout=$3 + local more=$6 + #second part + if [[ $job == '[async]' ]]; then + if [[ $return_code -eq 2 ]]; then + _async_dcpr_start + return + fi + fi + #third part + dcpr_info_msg=$stdout + #fourth part + [[ $more == 1 ]] || set-prompt && zle reset-prompt +} +---- + +The callback functions gets 6 arguments(copied from +https://github.com/mafredri/zsh-async/blob/main/README.md[here]): + +* $1 job name, e.g. the function passed to async_job +* $2 return code +** Returns -1 if return code is missing, this should never happen, if it +does, you have likely run into a bug. Please open a new issue with a +detailed description of what you were doing. +* $3 resulting (stdout) output from job execution +* $4 execution time, floating point e.g. 0.0076138973 seconds +* $5 resulting (stderr) error output from job execution +* $6 has next result in buffer (0 = buffer empty, 1 = yes) +** This means another async job has completed and is pending in the +buffer, it’s very likely that your callback function will be called a +second time (or more) in this execution. It’s generally a good idea to +e.g. delay prompt updates (zle reset-prompt) until the buffer is empty +to prevent strange states in ZLE. + +The function itself is straightforward. I like to rename the shell +arguments so i have to deal with a name a couple of months from the time +of writing the function and not some random numbers. + +Next is the part where we handle the errors. These are the errors +returned by the async runner. I like to call the start function of the +async job again to get it to start again. This is not, generally +speaking, a good idea since if something is broken and a rerun won’t fix +it, you end up in a loop. This is essentially my version of having the +async job "`failing loudly`". + +The third part is where we assign the stdout that our function, +`+docker_compose_running_pwd+`, made to a "`global`" variable. This +variable,`+dcpr_info_msg+`, is the variable that we will use in our +prompt. + +The fourth part is the most important part. The first condition checks +for an empty buffer. If the buffer is not empty, it means we have more +async jobs that have finished and are waiting for their turn, in which +case it will not update the prompt. If, however the prompt buffer is +empty, then we will `+set+` and `+reset+` the prompt to get an updated +prompt displayed when the buffer is empty. + +This is ideal, because, first and foremost, the library’s documentation +tells us to do that to avoid having `+zle+` ending up in a weird state. +The other reason would be to avoid unnecessary zle resets to cut down on +needless flickering and to save us some CPU cycles. + +`+set-prompt+` is a function that I use to set the actual prompt. You +may not need to call that or whatever equivalent that you have. In my +case, I have to do it since my prompt lies on the fancier side of +prompts and a change in the amount of characters in the prompts(because +an async function is providing its output after the initial prompt +display) will need to be taken into account so that’s why i run the +prompt after everything(all the async functions) has returned, so that +the final character lengths are known and correct. + +In the next section, we simply call the init function of the library and +then call our own start function after that: + +[source,zsh] +---- +async_init +_async_dcpr_start +---- + +In this section we add our async job to the precmd hook for zsh so that +our async job runs on the precmd hook on every prompt. The `+precmd+` +hook is executed before the prompt is displayed. Also please do note +that this is where we actually tell the async runner what function to +actually run. Moreover, this is where we pass any arguments that may or +may not be needed by the said function. Do keep in mind that the async +execution environment our function will run in is not the same as the +one your shell prompt will be run in. This means that env vars will not +carry over. In our example our docker compose function cannot get the +current working directory by just accessing the `+$PWD+` env var so we +will have to pass `+$PWD+` to it as a function argument manually. + +[source,zsh] +---- +add-zsh-hook precmd (){ + async_job dcpr_info docker_compose_running_pwd $PWD +} +---- + +This final part serves two purposes. First, it clears the prompt var on +changing a directory, so that we don’t get a wrong result until we get a +new result on a new prompt. Second, this also serves as the definition +for our global var that we will use in the prompt. + +[source,zsh] +---- +add-zsh-hook chpwd() { + dcpr_info_msg= +} +---- + +Here’s everything put together: + +[source,zsh] +---- +docker_compose_running_pwd() { + local cwd="$1" + local list=$(docker compose ls --format json | jq '.[].ConfigFiles' | tr -d '"') + local array=("${(@f)$(echo ${list})}") + for elem in "${array[@]}"; do + if [[ "${cwd}" == $(dirname "${elem}") ]];then + echo "\U1F7E6" + return + else + ; + fi + done +} + +_async_dcpr_start() { + async_start_worker dcpr_info + async_register_callback dcpr_info _async_dcpr_info_done +} + +_async_dcpr_info_done() { + #first part + local job=$1 + local return_code=$2 + local stdout=$3 + local more=$6 + #second part + if [[ $job == '[async]' ]]; then + if [[ $return_code -eq 2 ]]; then + _async_dcpr_start + return + fi + fi + #third part + dcpr_info_msg=$stdout + #fourth part + [[ $more == 1 ]] || set-prompt && zle reset-prompt +} + +async_init +_async_dcpr_start + +add-zsh-hook precmd (){ + async_job dcpr_info docker_compose_running_pwd $PWD +} + +add-zsh-hook chpwd (){ + dcpr_info_msg= +} +---- |