kakoune: kak starts noticeably slower than neovim

Install kakoune on Arch Linux from the official repo.

Run kak -n — kakoune starts instantly (perfect). Run kak — kakoune starts noticeably slowly, even though I didn’t even create ~/.config/kak/ folder yet. I use neovim with lots of plugins and huge config file, and it starts faster than kak.

Given that I open and close editor many times during the day, it is essential to have the editor start as fast as possible.

I’m not sure how I can provide more information to narrow down the cause of slowness. What does kak do, that kak -n doesn’t?

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 59 (51 by maintainers)

Commits related to this issue

Most upvoted comments

The benefit of modules is also to be able to share code between various scripts. Right now a lot of language scripts have copy-pasted logic to handle auto-indentation and auto-insertion of delimiters, it’s rather fragile and I’m pretty sure some obscure language scripts are just outdated.

This is fixed in master by lazy loading implemented by @laelath in https://github.com/mawww/kakoune/pull/2782, thanks a lot everyone!

The final numbers:

nvim with no config: 25ms

❯ time (repeat 100 nvim -u NONE -c 'quit')
1.33s user 0.87s system 88% cpu 2.490 total

nvim with huge config and lots of plugins: 114ms

❯ time (repeat 100 nvim -c 'quit')
9.20s user 1.61s system 94% cpu 11.390 total

kak -n: 7ms

❯ time (repeat 100 kak -n -e 'quit')
0.35s user 0.35s system 99% cpu 0.703 total

kak with no custom config in v2019.01.20: 70ms

❯ time (repeat 100 kak -e 'quit')
7.11s user 1.91s system 115% cpu 7.812 total

kak with no custom config in master: 20ms

❯ time (repeat 100 kak -e 'quit')
1.77s user 0.72s system 120% cpu 2.060 total

20ms beats neovim’s 25ms, the issue is solved 🙂

In my view, lazy loading is the last resort, I’d much rather ensure that we are quick to load even without.

That said, I have a plan on how lazy loading would work, which is related to the “modules” system that would solve the dependency between script problem.

The idea for the “modules” system is to introduce 2 new commands:

provides or module or similar, that takes a single parameter which is an arbitrary string identifying a module, I expect most bundled .kak files to start with that. That command will stop sourcing the current file if its parameter has already be given to a preceeding provides command.

# File c-family.kak
provides c-family
# following commands will only be executed once, even if this file is sourced many times

requires or similar, which takes a name as a parameter, and loads a file matching that name from the script path (script path being a list of directories in which we might have scripts, like /usr/share/kak/rc and ~/.config/kak/rc).

# File blah.kak
requires c-family.kak # sources /usr/share/kak/rc/c-family.kak, which will stop at the provides line if it was already loaded
# Now we know commands provided by c-family.kak are available

The link with lazy loading is that we can have

hook global BufSetOption filetype=(.*) %{ requires "%val{hook_param_capture_1}.kak" }

There are still problems to solve (for example, sourcing python.kak would add hooks that want to run on BufSetOption filetype=python, and those would not run currently on the file that triggers the loading of python.kak), but thats the general direction I had in mind.

Comments ?

Well, Kakoune doesn’t have to load everything as well. I can imagine we could put source commands into hooks so Kakoune would load language support only when file with specific extension is opened:

hook global BufCreate .*\.go$ %{
    source /usr/share/kak/go.kak
    source /usr/share/kak/go-tools.kak
}

Of course this hook won’t work in Kakoune in current form. This solution would require quite a lot changes in scripts. First - we need a way to check if file was already sourced, second - language initialization should be handled after sourcing (usually language script does it in BufCreate hook, but in this case buffer already exists).

I don’t think it’s hard but it’s a lot of work.

kak -n makes Kakoune skip loading all the included defaults, configuration and syntax highlighting, which is a lot of stuff. We were actually talking about this in #kakoune the other day; it’s hard to pinpoint any specific thing that’s slow, since Kakoune and its scripts do so many things at startup.

One practical thing you can do:

  • Figure out where Arch has installed Kakoune’s default config files. Open Kakoune and type :echo %val{runtime}/autoload<ret> to find out.
  • Create the directory ~/.local/config/kak/autoload/ which will prevent Kakoune from automatically loading anything at startup.
  • Symlink the scripts you actually use from the system-wide autoload directory into your personal autoload directory, so that the ones you don’t use won’t be read at startup and won’t slow things down.

kakoune is indeed plenty fast when started with no config (kak -n), however when starting with a few plugins, it is way more slower than my nvim config with more plugins. 🤔

kakoune with a few plugins:

❯ time (repeat 100 kak -e 'quit')
( repeat 100; do; kak -e 'quit'; done; )  59.99s user 23.15s system 103% cpu 1:20.35 total

nvim with a bigger config and more plugins:

❯ time (repeat 100 nvim -c 'quit')
( repeat 100; do; nvim -c 'quit'; done; )  6.84s user 0.90s system 97% cpu 7.961 total

I guess nvim greatly improved loading speed for heavy configs since 2019

It is remarkable however that kakoune is faster than nvim with no config:

❯ time (repeat 100 kak -n -e 'quit')
( repeat 100; do; kak -n -e 'quit'; done; )  0.21s user 0.19s system 100% cpu 0.400 total
❯ time (repeat 100 nvim -u NONE -c 'quit')
( repeat 100; do; nvim -u NONE -c 'quit'; done; )  1.20s user 0.48s system 85% cpu 1.972 total

I wonder what it would take to improve loading time when using plugins

In my view, lazy loading is the last resort, I’d much rather ensure that we are quick to load even without.

@mawww one issue with even super fast script/module loading is - bloat. Sometimes I’m just editing a git commit, or a text file; I really don’t want to see arbitrary clang completions or rust commands when pressing :.

I’d much prefer to see something like @laelath described above, where the editor loads lang specific stuff only in buffers (or clients) when a file of that kind is open. Loading & caching per session would also keep the bloat problem.

This would work for most of the languages, but c-family.kak has a lot of it’s highlighters added within those top-level evaluate-commands %sh{} blocks.

~If we consider my approach, we could go with only wrapping top-level evaluate-commands %sh{ blocks in hook -once, my observation shows that those are the slowest pieces of code to execute anyway. Having top-level add-highlighters not being lazy loaded is acceptable in my mind, and solves this dependency problem that you mention.~

Realized just after posting that I’m wrong, the issue is totally not solved.

I can convert a few heaviest files to get some estimates, I’m a bit hesitant to convert everything because it will take some time (e.g. I would want to manually test every syntax), and because of whitespace diffs it will quickly become outdated if other PRs will touch the same files.

Can we maybe discuss your concerns first?

Previously you mentioned two things: repeating the same thing in 92 files and creating many new commands that will remain in memory. Now commands are no longer being created, and all these 92 files already have WinSetOption filetype hooks anyway, so I would argue this change will not add any new repetition.

Are you maybe concerned that this change will break a particular syntax? I can start by sending a PR just for that one (or for those few) syntaxes, and then you and I can do some more thorough testing.

I don’t want to rush you, I want you too to be happy and confident with the change, but if we agree to do this, I would ask you to review & merge it quickly to avoid wasted effort and possible conflicts 😛

I see, and just to be clear, I don’t disagree with the modular approach, especially because it brings other benefits as @occivink has mentioned — I would love to see the “modules” solution! But it is a complicated change that you leave as the last resort and don’t plan to do right now, while what I’m proposing is a simple thing for which I can make a PR today and close this ticket for good.

I think we can iteratively improve in this case, once we get to implement modular approach, deleting 6 lines per file is not the most difficult cleanup to do — and on a positive side, we will be able to measure the perf impact of “modules” solution, our goal would be not to decrease the performance comparing to “lazy loading” in its currently proposed form.

Gotcha, thanks for explaining this bit. But I don’t want to mix refactoring scripts into modules with what I’m proposing here, modularization is a much bigger change with different goals.

Let me perhaps ask a different question: do you see any issues with this change? Would you merge it? It is 6 new lines that don’t break anything and improve loading time by 60ms on bash and 10ms on dash.

https://github.com/mawww/kakoune/pull/2255/files?w=1

I’m not sure why everyone is so carefully avoiding the topic of lazy-loading, it gives the great perf boost and is a very simple thing to do.

I made an example for c-family.kak: https://github.com/mawww/kakoune/pull/2255/files?w=1

Do you see any issues with this approach?

This is the whole loading, all the other sourcing of scripts is nested inside this one.

Ah, I see, so in reality c-lang.kak is the slowest script.

What shell is at /bin/sh on your system ? What difference do you get if you make it point to a lightweight shell (dash) ?

It was bash, and dash is giving me a very good perf improvement, thanks for the idea @co-dh!


Time for some perf runs! The results are averaged for 100 executions of kak -e 'quit'.

  1. sh -> bash: 242ms
  2. sh -> bash, removed c-family.kak: 179ms
  3. sh -> dash: 98ms
  4. sh -> dash, removed c-family.kak: 88ms !!!

Woohoo, we are finally beating neovim!


Can we still consider lazy-loading language-specific configs? Still not sure if this could be as simple as wrapping all top-level evaluate-command calls into a WinSetOption filetype hook, or there’s more to it.

Oh, I see. You right, megre operation is blazing fast while autoloading is way slower. Interesting


Looks like autoload is just kak/sh script here: https://github.com/mawww/kakoune/blob/0d838f80a0cc920e64c6a5a969861f83d96967a6/share/kak/kakrc

source command: https://github.com/mawww/kakoune/blob/665d3fa196f4df905ebd682965adf78d80eaf8a8/src/commands.cc#L1234

I did some additional benchmarking:


nvim with no config: 25ms

❯ time (repeat 100 nvim -u NONE -c 'quit')
1.33s user 0.87s system 88% cpu 2.490 total

nvim with huge config and lots of plugins: 114ms

❯ time (repeat 100 nvim -c 'quit')
9.20s user 1.61s system 94% cpu 11.390 total

kak -n: 7ms (awesome result!)

❯ time (repeat 100 kak -n -e 'quit')
0.35s user 0.35s system 99% cpu 0.703 total

kak with no custom config (except whatever ArchLinux package ships): 400ms

❯ time (repeat 100 kak -e 'quit')
31.48s user 12.14s system 108% cpu 40.057 total

So before I even start configuring kak, it already takes almost 4 times longer to load than my fully customized nvim — let’s try to get this number down 😉

:echo %val{runtime}/autoload<ret> outputs: /usr/share/kak /autoload

I don’t have the folder /autoload, but I do have the folder /usr/share/kak present.

Creating a folder ~/.config/kak/autoload does make kak load much faster, as if I provided the -n argument. However, I do want to have all those files loaded 🙂


Neovim is probably fast because it can load configs based on a file type, can we have the same in kakoune? For example, I probably don’t really need the file rc/base/haskell.kak loaded until I actually open a haskell file.


Merging the files together as @TeddyDD suggested does provide a huge benefit, I get the startup time decreased from 400ms down to 176ms (although nvim is still faster even with all custom configs and plugins, again probably because it doesn’t have to load everything on startup).

❯ time (repeat 100 kak -n -e 'source /usr/share/kak/rc/mega.kak;quit')
14.99s user 4.76s system 112% cpu 17.583 total

manually sourcing each file from kakrc is faster than loading same files from autoload. manually loading all default kak files takes ~115ms while when putting them in autoload takes ~170ms. why is that?