Skip to content

Commit

Permalink
perf(blame): better cache invalidation
Browse files Browse the repository at this point in the history
The blame cache is now maintained in the CacheEntry object
and invalidated incrementally on buffer updates.

In addition git-blame is bypassed if the cursor line is within a hunk.
  • Loading branch information
lewis6991 committed Sep 24, 2023
1 parent 9bec6e1 commit bfa1bc2
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 96 deletions.
33 changes: 17 additions & 16 deletions lua/gitsigns/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ M.stage_hunk = mk_repeatable(async.void(function(range, opts)

table.insert(bcache.staged_diffs, hunk)

bcache:invalidate()
bcache:invalidate(true)
update(bufnr)
end))

Expand Down Expand Up @@ -371,7 +371,7 @@ M.undo_stage_hunk = async.void(function()
end

bcache.git_obj:stage_hunks({ hunk }, true)
bcache:invalidate()
bcache:invalidate(true)
update(bufnr)
end)

Expand All @@ -389,7 +389,7 @@ M.stage_buffer = async.void(function()

-- Only process files with existing hunks
local hunks = bcache.hunks
if #hunks == 0 then
if not hunks or #hunks == 0 then
print('No unstaged changes in file to stage')
return
end
Expand All @@ -405,7 +405,7 @@ M.stage_buffer = async.void(function()
table.insert(bcache.staged_diffs, hunk)
end

bcache:invalidate()
bcache:invalidate(true)
update(bufnr)
end)

Expand All @@ -432,7 +432,7 @@ M.reset_buffer_index = async.void(function()

bcache.git_obj:unstage_file()

bcache:invalidate()
bcache:invalidate(true)
update(bufnr)
end)

Expand Down Expand Up @@ -893,17 +893,16 @@ M.blame_line = async.void(function(opts)
end, 1000)

async.scheduler_if_buf_valid()
local buftext = util.buf_lines(bufnr)
local fileformat = vim.bo[bufnr].fileformat
local lnum = api.nvim_win_get_cursor(0)[1]
local results = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace)
local result = bcache:get_blame(lnum, opts)
pcall(function()
loading:close()
end)

assert(results and results[lnum])
assert(result)

local result = util.convert_blame_info(results[lnum])
result = util.convert_blame_info(result)

local is_committed = result.sha and tonumber('0x' .. result.sha) ~= 0

Expand Down Expand Up @@ -934,10 +933,12 @@ C.blame_line = function(args, _)
M.blame_line(args)
end

local function update_buf_base(buf, bcache, base)
---@param bcache Gitsigns.CacheEntry
---@param base string?
local function update_buf_base(bcache, base)
bcache.base = base
bcache:invalidate()
update(buf)
bcache:invalidate(true)
update(bcache.bufnr)
end

--- Change the base revision to diff against. If {base} is not
Expand Down Expand Up @@ -978,8 +979,8 @@ M.change_base = async.void(function(base, global)
if global then
config.base = base

for bufnr, bcache in pairs(cache) do
update_buf_base(bufnr, bcache, base)
for _, bcache in pairs(cache) do
update_buf_base(bcache, base)
end
else
local bufnr = current_buf()
Expand All @@ -988,7 +989,7 @@ M.change_base = async.void(function(base, global)
return
end

update_buf_base(bufnr, bcache, base)
update_buf_base(bcache, base)
end
end)

Expand Down Expand Up @@ -1317,7 +1318,7 @@ M.refresh = async.void(function()
require('gitsigns.highlight').setup_highlights()
require('gitsigns.current_line_blame').setup()
for k, v in pairs(cache) do
v:invalidate()
v:invalidate(true)
manager.update(k)
end
end)
Expand Down
4 changes: 2 additions & 2 deletions lua/gitsigns/attach.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ local hl = require('gitsigns.highlight')

local gs_cache = require('gitsigns.cache')
local cache = gs_cache.cache
local CacheEntry = gs_cache.CacheEntry
local Status = require('gitsigns.status')

local gs_config = require('gitsigns.config')
Expand Down Expand Up @@ -111,6 +110,7 @@ end
--- @param bufnr integer
local function on_reload(_, bufnr)
local __FUNC__ = 'on_reload'
cache[bufnr]:invalidate()
dprint('Reload')
manager.update_debounced(bufnr)
end
Expand Down Expand Up @@ -336,7 +336,7 @@ local attach_throttled = throttle_by_id(function(cbuf, ctx, aucmd)
return
end

cache[cbuf] = CacheEntry.new({
cache[cbuf] = gs_cache.new({
bufnr = cbuf,
base = ctx and ctx.base or config.base,
file = file,
Expand Down
94 changes: 85 additions & 9 deletions lua/gitsigns/cache.lua
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
local async = require('gitsigns.async')
local config = require('gitsigns.config').config
local util = require('gitsigns.util')

local M = {
CacheEntry = {},
}

-- Timer object watching the gitdir

--- @class Gitsigns.CacheEntry
--- @class (exact) Gitsigns.CacheEntry
--- @field bufnr integer
--- @field file string
--- @field base? string
--- @field compare_text? string[]
--- @field hunks Gitsigns.Hunk.Hunk[]
--- @field hunks? Gitsigns.Hunk.Hunk[]
--- @field force_next_update? boolean
---
--- @field compare_text_head? string[]
--- @field hunks_staged? Gitsigns.Hunk.Hunk[]
---
--- @field staged_diffs Gitsigns.Hunk.Hunk[]
--- @field staged_diffs? Gitsigns.Hunk.Hunk[]
--- @field gitdir_watcher? uv.uv_fs_event_t
--- @field git_obj Gitsigns.GitObj
--- @field commit? string
--- @field blame? table<integer,Gitsigns.BlameInfo?>
local CacheEntry = M.CacheEntry

function CacheEntry:get_compare_rev(base)
Expand All @@ -47,20 +48,95 @@ function CacheEntry:get_rev_bufname(rev)
return string.format('gitsigns://%s/%s:%s', self.git_obj.repo.gitdir, rev, self.git_obj.relpath)
end

function CacheEntry:invalidate()
self.compare_text = nil
self.compare_text_head = nil
--- Invalidate any state dependent on the buffer content.
--- If 'all' is passed, then invalidate everything.
--- @param all? boolean
function CacheEntry:invalidate(all)
self.hunks = nil
self.hunks_staged = nil
self.blame = nil
if all then
-- The below doesn't need to be invalidated
-- if the buffer changes
self.compare_text = nil
self.compare_text_head = nil
end
end

--- @param o Gitsigns.CacheEntry
--- @return Gitsigns.CacheEntry
function CacheEntry.new(o)
function M.new(o)
o.staged_diffs = o.staged_diffs or {}
return setmetatable(o, { __index = CacheEntry })
end

local sleep = async.wrap(function(duration, cb)
vim.defer_fn(cb, duration)
end, 2)

--- @private
function CacheEntry:wait_for_hunks()
local loop_protect = 0
while not self.hunks and loop_protect < 10 do
loop_protect = loop_protect + 1
sleep(100)
end
end

--- @private
--- @param opts Gitsigns.CurrentLineBlameOpts
--- @return table<integer,Gitsigns.BlameInfo?>?
function CacheEntry:run_blame(opts)
local blame_cache --- @type table<integer,Gitsigns.BlameInfo?>?
repeat
local buftext = util.buf_lines(self.bufnr)
local tick = vim.b[self.bufnr].changedtick
-- TODO(lewis6991): Cancel blame on changedtick
blame_cache = self.git_obj:run_blame(buftext, nil, opts.ignore_whitespace)
async.scheduler_if_buf_valid(self.bufnr)
until vim.b[self.bufnr].changedtick == tick
return blame_cache
end

--- @param file string
--- @param lnum integer
--- @return Gitsigns.BlameInfo
local function get_blame_nc(file, lnum)
local Git = require('gitsigns.git')

return {
orig_lnum = 0,
final_lnum = lnum,
commit = Git.not_commited(file),
filename = file,
}
end

--- @param lnum integer
--- @param opts Gitsigns.CurrentLineBlameOpts
--- @return Gitsigns.BlameInfo?
function CacheEntry:get_blame(lnum, opts)
local blame_cache = self.blame

if not blame_cache or not blame_cache[lnum] then
self:wait_for_hunks()
local Hunks = require('gitsigns.hunks')
if Hunks.find_hunk(lnum, self.hunks) then
--- Bypass running blame (which can be expensive) if we know lnum is in a hunk
blame_cache = blame_cache or {}
blame_cache[lnum] = get_blame_nc(self.git_obj.relpath, lnum)
else
-- Refresh cache
blame_cache = self:run_blame(opts)
end
self.blame = blame_cache
end

if blame_cache then
return blame_cache[lnum]
end
end

function CacheEntry:destroy()
local w = self.gitdir_watcher
if w and not w:is_closing() then
Expand Down
9 changes: 0 additions & 9 deletions lua/gitsigns/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@
--- @field trouble boolean
--- -- Undocumented
--- @field _refresh_staged_on_update boolean
--- @field _blame_cache boolean
--- @field _threaded_diff boolean
--- @field _inline2 boolean
--- @field _extmark_signs boolean
Expand Down Expand Up @@ -760,14 +759,6 @@ M.schema = {
]],
},

_blame_cache = {
type = 'boolean',
default = true,
description = [[
Cache blame results for current_line_blame
]],
},

_threaded_diff = {
type = 'boolean',
default = true,
Expand Down
41 changes: 1 addition & 40 deletions lua/gitsigns/current_line_blame.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,6 @@ local function reset(bufnr)
vim.b[bufnr].gitsigns_blame_line_dict = nil
end

--- @class (exact) Gitsigns.BlameCache
--- @field cache Gitsigns.BlameInfo[]?
--- @field tick integer

--- @type table<integer,Gitsigns.BlameCache>
local blame_cache = {}

--- @param fmt string
--- @param name string
--- @param info Gitsigns.BlameInfoPublic
Expand All @@ -48,36 +41,6 @@ local function flatten_virt_text(virt_text)
return table.concat(res)
end

--- @param bufnr integer
--- @param lnum integer
--- @param opts Gitsigns.CurrentLineBlameOpts
--- @return Gitsigns.BlameInfo?
local function run_blame(bufnr, lnum, opts)
-- init and invalidate
local tick = vim.b[bufnr].changedtick
if not blame_cache[bufnr] or blame_cache[bufnr].tick ~= tick then
blame_cache[bufnr] = { tick = tick }
end

local result = blame_cache[bufnr].cache

if result then
return result[lnum]
end

local buftext = util.buf_lines(bufnr)
local bcache = cache[bufnr]
result = bcache.git_obj:run_blame(buftext, nil, opts.ignore_whitespace)

if not result then
return
end

blame_cache[bufnr].cache = result

return result[lnum]
end

--- @param winid integer
--- @return integer
local function win_width(winid)
Expand Down Expand Up @@ -203,14 +166,12 @@ local function update0(bufnr)

local opts = config.current_line_blame_opts

local blame_info = run_blame(bufnr, lnum, opts)
local blame_info = bcache:get_blame(lnum, opts)

if not blame_info then
return
end

async.scheduler_if_buf_valid(bufnr)

if lnum ~= get_lnum(winid) then
-- Cursor has moved during events; abort and tr-trigger another update
update0(bufnr)
Expand Down
Loading

0 comments on commit bfa1bc2

Please sign in to comment.