Improvements to vim-gitgutter
Over the past few days I’ve made vim-gitgutter:
- significantly faster;
- able to stage/revert individual hunks.
It’s well worth upgrading.
The backstory
After an initial flurry of vim-gitgutter development, I was snowed under with work that actually pays the bills. Lots of people gave me good feedback during this time. The top request: make it faster.
I wanted it to be faster too.
Fortunately the start of the year was a good time to return to vim-gitgutter.
Speed
Profiling and reviewing the code showed two main sources of slowness:
- The plugin was running too often, doing redundant work.
- Updating signs was alarmingly slow.
Redundant work
I want vim-gitgutter’s signs to be as accurate as possible, which means running it whenever something might have changed inside or outside vim. However it was running too often.
For example it would run on the CursorHold
event. If you then moved your cursor without changing anything, the plugin would run again on that CursorHold
event – even though nothing had changed.
Another redundancy cropped up on TabEnter
. When you enter a tab, vim-gitgutter runs over all the visible buffers to ensure you’re looking at up to date signs. However after TabEnter
fires, BufEnter
fires for the active buffer in the new tab. At which point vim-gitgutter would run against that buffer all over again. And shortly afterward CursorHold
would fire, running the plugin against the buffer a third time. One might call that suboptimal.
I fixed the TabEnter
/BufEnter
redundancy with a simple guard. And I fixed the CursorHold
redundancy by checking for fresh changes.
Now the plugin runs as frequently as it needs to, but no more.
Updating signs
Initially I took a simplest-thing-that-works approach to updating a buffer’s signs:
- Remove all the existing signs (excluding other plugins').
- Add all the vim-gitgutter signs.
The code was simple. Surprisingly it was slow.
Profiling showed that sign place
and sign unplace
are glacial: several users measured 7ms to add or remove a sign. So for 200 unstaged changes the plugin would remove 200 signs and add them straight back again, taking 7ms x (200 signs to remove + 200 signs to add) = 2.8s. Yikes.
This surprised me since sign place
and sign unplace
are native VimL functions. And being native VimL functions, there’s nothing I can do in a plugin to speed them up.
It was time to revisit the sign-update algorithm. Here’s what vim-gitgutter does now to update a buffer’s signs:
- Remove obsolete signs, i.e. each sign whose line is no longer changed.
- Upsert the current changed lines' signs.
Of course we skip over other plugins' signs while doing this.
How does our example fare with the new algorithm? Time to update signs = 7ms x (0 obsolete signs to remove + 0 signs to upsert) = 0ms. As it should be.
Taken together, these various improvements have made vim-gitgutter very snappy. The one remaining optimisation is when:
- vim-gitgutter is your only vim plugin that uses signs;
- you
git add <file>
outside vim.
In this situation vim-gitgutter could remove all the file’s obsolete signs with a single call to sign unplace
instead of one call per sign. It all helps.
Staging and reverting individual hunks
I didn’t implement this earlier because I didn’t think it would be that useful. How wrong I was: I’ve been using it constantly.
My old workflow:
- [Vim] Make a bunch of changes to a file.
- Change to Terminal.
- [Terminal] Review and stage changes interactively (
git add -p
). Often flip back to Vim to see more context around the change, then back to the terminal. - [Terminal] Commit.
- Change back to Vim.
My new workflow:
- [Vim] Make a bunch of changes to a file.
- [Vim] Jump from one hunk to the next (
]h
), staging the ones I want with\hs
. - Change to Terminal.
- [Terminal] Commit.
- Change back to Vim.
I never thought staging individual changes in the Terminal was slow; but doing it all within Vim turns out to be far quicker.
And it’s very nice to be able to revert a hunk in Vim with a quick \hr
.
Overall
I’m pretty chuffed with these changes. I’ve got a nagging feeling there’s still redundant work which could be eliminated though…