Improvements to vim-gitgutter

Over the past few days I’ve made vim-gitgutter:

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:

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:

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:

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:

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:

My new workflow:

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…

Andrew Stewart • 10 January 2014 • vim
You can reach me by email or on Twitter.