aboutsummaryrefslogtreecommitdiffstats
path: root/mds/zshgofast.txt
diff options
context:
space:
mode:
Diffstat (limited to 'mds/zshgofast.txt')
-rw-r--r--mds/zshgofast.txt354
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=
+}
+----