olets / zsh-abbr

The zsh manager for auto-expanding abbreviations, inspired by fish. ~13,000 unique cloners as of May '24, 580+ Homebrew installs 6/23-6/24
https://zsh-abbr.olets.dev
Other
510 stars 18 forks source link

Performance improvements #52

Closed mattmc3 closed 2 years ago

mattmc3 commented 2 years ago

Thank you for your work on this plugin! It's an essential one for me, coming from Fish. That said, it's also by far the slowest Zsh plugin I use and I was wondering if there's a performance tuning effort on the roadmap, if you've identified places where focusing on performance would have the biggest impact, or if you would accept PRs that address performance tuning?

When running /usr/bin/time zsh -i -c exit, my Zsh startup times jump from 0.10 real 0.05 user 0.03 sys to 0.29 real 0.13 user 0.13 sys when adding zsh-abbr. I use about 20 other plugins if that gives some context. Perhaps I'm spoiled by Fish's speed, but anything over .15 can feel really sluggish.

I have had great luck loading this plugin via zsh-defer, which waits until Zle is idle to do the work of loading a plugin, thus getting you to a prompt quicker. My times then drop to 0.05 real 0.02 user 0.02 sys. I think this is similar to what zinit does, but not everyone likes/uses zinit.

An alternative to performance tuning the existing code might be to carve out the core concepts from zsh-defer and implement a mini-defer within zsh-abbr itself to accomplish the same thing without changing any other code. I'm happy to help, but don't want to go down the wrong path here if some thought has already gone into how to achieve better performance from this plugin.

Again, thank you so much for the work on this!


As a reference, this is my topline zprof results:

num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)   47         147.62     3.14   64.41%    140.42     2.99   61.27%  _abbr:util_add
 2)    1          24.53    24.53   10.70%     24.52    24.52   10.70%  _abbr_deprecations:widgets
 3)   48          10.84     0.23    4.73%     10.84     0.23    4.73%  _abbr_no_color
 4)   47         171.99     3.66   75.05%     10.36     0.22    4.52%  _abbr
 5)    1         229.18   229.18  100.00%     10.28    10.28    4.49%  _abbr_init
 6)    1           9.83     9.83    4.29%      9.83     9.83    4.29%  _abbr_job_push:wait_turn
 7)  294           4.42     0.02    1.93%      4.42     0.02    1.93%  _abbr_debugger
 8)    1         177.28   177.28   77.35%      3.07     3.07    1.34%  _abbr_load_user_abbreviations:load
 9)    1           2.88     2.88    1.26%      2.87     2.87    1.25%  _abbr_job_pop
10)   47         151.10     3.21   65.93%      2.82     0.06    1.23%  _abbr:add
11)   47           3.06     0.07    1.34%      2.12     0.05    0.92%  _abbr:util_check_command
12)   47         174.20     3.71   76.01%      1.66     0.04    0.72%  abbr
13)    1           1.42     1.42    0.62%      1.42     1.42    0.62%  colors
14)   47           2.15     0.05    0.94%      1.19     0.03    0.52%  _abbr:util_sync_user
15)    1          11.20    11.20    4.89%      1.03     1.03    0.45%  _abbr_job_push
16)   47           1.43     0.03    0.62%      0.83     0.02    0.36%  _abbr:util_log_unless_quiet
17)    1         177.92   177.92   77.63%      0.49     0.49    0.21%  _abbr_load_user_abbreviations
18)    1          24.87    24.87   10.85%      0.33     0.33    0.14%  _abbr_deprecations
19)    1           0.32     0.32    0.14%      0.31     0.31    0.13%  _abbr_job_push:add_job
20)    1           0.15     0.15    0.06%      0.15     0.15    0.06%  add-zsh-hook
21)    1           0.09     0.09    0.04%      0.07     0.07    0.03%  _abbr_create_files
22)    1           0.08     0.08    0.03%      0.07     0.07    0.03%  _abbr_bind_widgets
23)    1           0.05     0.05    0.02%      0.04     0.04    0.02%  _abbr_add_widgets
24)    1           0.14     0.14    0.06%      0.03     0.03    0.01%  _abbr_load_user_abbreviations:setup
olets commented 2 years ago

Thanks for the issue and kind words!

by far the slowest Zsh plugin I use

To be clear, you're talking about time to first prompt right?

my topline zprof results

Looks like you have 47 abbreviations, is that right?

not everyone likes/uses [zinit].

I expect you bring zinit up because of its appearance in the readme's performance section. As of this I no longer recommend zinit myself.

I have had great luck loading this plugin via zsh-defer I think this is similar to what zinit does don't want to go down the wrong path here

Would you share your zsh-defer implementation? Others might come across this conversation and appreciate it.

I don't see zsh-abbr having built-in async loading. I personally would rather have a delay than have an abbreviation not expand; zsh-defer and other async solutions make it possible for individuals to choose to load it async.

After wrapping up #32 / #49 I don't plan to do major feature development on zsh-abbr any time soon. Without a major overhaul to how zsh-abbr's initialization works, I don't think it'll ever get really fast. (As suggested by your zprof output, it essentially runs each abbr command in the user abbreviations file.) ~I suppose the overhaul could be that the intermediary typeset -p output is saved in a permanent location next to the current user abbreviation file, instead of in temp files, and then initialization reads those arrays and does a single user file sync at the end. Not sure it'd be worth cluttering up the dotfiles with two extra files. If you want to take a crack at that go for it! The branch v5 is the one to work off. As with all open source, can't promise it'll merge — feel free to open a wip PR, or chat about design here. (I won't be on GitHub much in the next month though)~ edit: I no longer like this idea, see further down in the conversation for details

For now I think lazy loading the plugin is going to be your best bet — with zsh-defer, or with a plugin manager that has lazy loading support (from their readmes, at least zcomet, zgenom, znap, and zplug have lazy loading features. I haven't experimented with them).

olets commented 2 years ago

ps tangentially, I'm interested to hear why you left Fish!

mattmc3 commented 2 years ago

To be clear, you're talking about time to first prompt right?

For all intents and purposes, yes, essentially. New terminal tab, delay, prompt, start typing being the workflow.

Looks like you have 47 abbreviations, is that right?

LOL, I didn't really know until you mentioned it - I just pulled in what I used in Fish, but wc -l $ZDOTDIR/abbreviations confirms 47!

I expect you bring zinit up because of its appearance in the readme's performance section

Yes, that and zsh-defer references it too. It's one performance avenue, though you're right - anyone that came to rely too heavily on zinit got burned.

Would you share your zsh-defer implementation

Happy to. Here's is my (perhaps too clever) plugin loading snippet. Plugins that show up before zsh-defer are sourced normally. Ones after will always use zsh-defer.

# github plugins list
plugins=(
  zshzoo/zshrc.d
  romkatv/zsh-defer
  zshzoo/setopts
  zshzoo/history
  zshzoo/keybindings
  zshzoo/zstyle-completions
  zsh-users/zsh-autosuggestions
  zsh-users/zsh-history-substring-search
  mattmc3/zman
  olets/zsh-abbr
  zshzoo/copier
  zshzoo/macos
  zshzoo/prj
  zshzoo/magic-enter
  zshzoo/zfishcmds
  zshzoo/termtitle
  rupa/z
  rummik/zsh-tailf
  peterhurford/up.zsh
  zshzoo/compinit
  zdharma-continuum/fast-syntax-highlighting
)
# clone and source plugins, using zsh-defer if it exists
for repo in $plugins; do
  plugin_dir=$ZDOTDIR/plugins/${repo:t}
  initfile=$plugin_dir/${repo:t}.plugin.zsh
  [[ -d $plugin_dir ]] \
    || git clone --depth 1 --recursive --shallow-submodules https://github.com/$repo $plugin_dir
  if [[ ! -e $initfile ]]; then
    initfiles=($plugin_dir/*.plugin.{z,}sh(N) $plugin_dir/*.{z,}sh(N))
    ln -s "${initfiles[1]}" "$initfile"
  fi
  fpath+=$plugin_dir
  if (( $+functions[zsh-defer] )); then
    zsh-defer source $initfile
  else
    source $initfile
  fi
done

I pretty much have the speed I need with this setup (it's crazy fast!), but since others use plugin managers, they'd probably only get the benefit if zsh-abbr got faster or if their plugin manager has defer.

ps tangentially, I'm interested to hear why you left Fish!

I didn't leave it as much as it's just not available on every system I use, and context switching can be tiring. I (obviously) spend entirely too much time making Zsh behave like Fish so it feels more like home.

If you want to take a crack at that go for it

Performance tuning Zsh isn't my forte, but I may have look. My Zle widget experience is a bit light. Thanks for the reply!

olets commented 2 years ago

Thanks for sharing your setup!

Thought about this some more, and I can't think of a way I'm happy with. Much better than my above idea would be to let users opt out of loading abbreviations from the user abbreviation file during initialization, but that feels like configuration variable bloat with the new risk that users could lose data (in a way the user would be responsible for, but still it makes me uncomfortable). The idea I proposed earlier has the same risk, plus extra files. So I think relying on an outside solution for async is the way to go.

I've released v4.7.0 with two changes inspired by your zprof output: _abbr_no_color is now only called once during initialization, and _abbr_deprecations:widgets is much faster.

mattmc3 commented 2 years ago

Awesome! Thanks for looking into it. If anyone else runs into performance questions in the future, hopefully they'll find this thread and get some ideas. As long as zsh-abbr continues to play nice with zsh-defer, I'm good on my end. Thanks again.

romkatv commented 2 years ago

Relevant to the topic of deferred initialization and zsh startup speed:

zsh-defer implements deferred initialization better than zinit (or any other project I know of) but I still don't recommend it.

Disclaimer: I'm the author of zsh-defer.

mattmc3 commented 2 years ago

@romkatv - is there an initialization speed up strategy you do recommend? The only side effect I've observed from my (albeit very recent and limited) use of zsh-defer is occasionally the first command may lack some plugin features due to not having completed the deferred init. That's the compromise olets rightly calls out being a non-starter for zsh-abbr on the whole, but not one I've in practice had actually hurt my setup. I'd have to type a short abbr really fast for it not to expand.

romkatv commented 2 years ago

If your goal is to reduce first prompt lag, something like instant prompt is the best solution I know of. It's strictly better than zsh-defer and by a large margin.

If you use turbo mode in zinit instead of zsh-defer, the list of problems gets longer:

zsh-defer has one "advantage" over instant prompt: it reduces the time it takes to run zsh -c exit. If that's something you care about, https://github.com/romkatv/you-the-champ is even better at this job and has no other effects.