perf(blame): better cache invalidation
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.
This commit is contained in:
parent
9bec6e1ef5
commit
bdeba1cec3
|
@ -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))
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -432,7 +432,7 @@ M.reset_buffer_index = async.void(function()
|
|||
|
||||
bcache.git_obj:unstage_file()
|
||||
|
||||
bcache:invalidate()
|
||||
bcache:invalidate(true)
|
||||
update(bufnr)
|
||||
end)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
|
|
@ -8,8 +8,6 @@ local subprocess = require('gitsigns.subprocess')
|
|||
local gs_config = require('gitsigns.config')
|
||||
local config = gs_config.config
|
||||
|
||||
local gs_hunks = require('gitsigns.hunks')
|
||||
|
||||
local uv = vim.loop
|
||||
local startswith = vim.startswith
|
||||
|
||||
|
@ -569,11 +567,30 @@ end
|
|||
|
||||
local NOT_COMMITTED = {
|
||||
author = 'Not Committed Yet',
|
||||
['author_mail'] = '<not.committed.yet>',
|
||||
author_mail = '<not.committed.yet>',
|
||||
committer = 'Not Committed Yet',
|
||||
['committer_mail'] = '<not.committed.yet>',
|
||||
committer_mail = '<not.committed.yet>',
|
||||
}
|
||||
|
||||
--- @param file string
|
||||
--- @return Gitsigns.CommitInfo
|
||||
function M.not_commited(file)
|
||||
local time = os.time()
|
||||
return {
|
||||
sha = string.rep('0', 40),
|
||||
abbrev_sha = string.rep('0', 8),
|
||||
author = 'Not Committed Yet',
|
||||
author_mail = '<not.committed.yet>',
|
||||
author_tz = '+0000',
|
||||
author_time = time,
|
||||
committer = 'Not Committed Yet',
|
||||
committer_time = time,
|
||||
committer_mail = '<not.committed.yet>',
|
||||
committer_tz = '+0000',
|
||||
summary = 'Version of ' .. file,
|
||||
}
|
||||
end
|
||||
|
||||
---@param x any
|
||||
---@return integer
|
||||
local function asinteger(x)
|
||||
|
@ -585,11 +602,22 @@ end
|
|||
--- @param ignore_whitespace? boolean
|
||||
--- @return table<integer,Gitsigns.BlameInfo?>?
|
||||
function Obj:run_blame(lines, lnum, ignore_whitespace)
|
||||
local ret = {} --- @type table<integer,Gitsigns.BlameInfo>
|
||||
|
||||
if not self.object_name or self.repo.abbrev_head == '' then
|
||||
-- As we support attaching to untracked files we need to return something if
|
||||
-- the file isn't isn't tracked in git.
|
||||
-- If abbrev_head is empty, then assume the repo has no commits
|
||||
return NOT_COMMITTED
|
||||
local commit = M.not_commited(self.file)
|
||||
for i in ipairs(lines) do
|
||||
ret[i] = {
|
||||
orig_lnum = 0,
|
||||
final_lnum = i,
|
||||
commit = commit,
|
||||
filename = self.file,
|
||||
}
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local args = { 'blame', '--contents', '-', '--incremental' }
|
||||
|
@ -619,7 +647,6 @@ function Obj:run_blame(lines, lnum, ignore_whitespace)
|
|||
return
|
||||
end
|
||||
|
||||
local ret = {} --- @type Gitsigns.BlameInfo[]
|
||||
local commits = {} --- @type table<string,Gitsigns.CommitInfo>
|
||||
local i = 1
|
||||
|
||||
|
@ -660,6 +687,9 @@ function Obj:run_blame(lines, lnum, ignore_whitespace)
|
|||
local l = get()
|
||||
local key, value = l:match('^([^%s]+) (.*)')
|
||||
if key then
|
||||
if vim.endswith(key, '_time') then
|
||||
value = tonumber(value)
|
||||
end
|
||||
key = key:gsub('%-', '_') --- @type string
|
||||
commit[key] = value
|
||||
else
|
||||
|
@ -749,6 +779,8 @@ end
|
|||
function Obj.stage_hunks(self, hunks, invert)
|
||||
ensure_file_in_index(self)
|
||||
|
||||
local gs_hunks = require('gitsigns.hunks')
|
||||
|
||||
local patch = gs_hunks.create_patch(self.relpath, hunks, self.mode_bits, invert)
|
||||
|
||||
if not self.i_crlf and self.w_crlf then
|
||||
|
|
|
@ -75,6 +75,28 @@ local function apply_win_signs(bufnr, top, bot, clear)
|
|||
end
|
||||
end
|
||||
|
||||
--- @param blame table<integer,Gitsigns.BlameInfo?>?
|
||||
--- @param first integer
|
||||
--- @param last_orig integer
|
||||
--- @param last_new integer
|
||||
local function on_lines_blame(blame, first, last_orig, last_new)
|
||||
if not blame then
|
||||
return
|
||||
end
|
||||
|
||||
if last_new ~= last_orig then
|
||||
if last_new < last_orig then
|
||||
util.list_remove(blame, last_new, last_orig)
|
||||
else
|
||||
util.list_insert(blame, last_orig, last_new)
|
||||
end
|
||||
end
|
||||
|
||||
for i = math.min(first + 1, last_new), math.max(first + 1, last_new) do
|
||||
blame[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- @param buf integer
|
||||
--- @param first integer
|
||||
--- @param last_orig integer
|
||||
|
@ -87,6 +109,8 @@ function M.on_lines(buf, first, last_orig, last_new)
|
|||
return true
|
||||
end
|
||||
|
||||
on_lines_blame(bcache.blame, first, last_orig, last_new)
|
||||
|
||||
signs_normal:on_lines(buf, first, last_orig, last_new)
|
||||
if signs_staged then
|
||||
signs_staged:on_lines(buf, first, last_orig, last_new)
|
||||
|
|
|
@ -191,19 +191,6 @@ function M.get_relative_time(timestamp)
|
|||
end
|
||||
end
|
||||
|
||||
--- @generic T
|
||||
--- @param x T[]
|
||||
--- @return T[]
|
||||
function M.copy_array(x)
|
||||
local r = {}
|
||||
--- @diagnostic disable-next-line:no-unknown
|
||||
for i, e in ipairs(x) do
|
||||
--- @diagnostic disable-next-line:no-unknown
|
||||
r[i] = e
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
--- Strip '\r' from the EOL of each line only if all lines end with '\r'
|
||||
--- @param xs0 string[]
|
||||
--- @return string[]
|
||||
|
@ -304,4 +291,49 @@ function M.convert_blame_info(x)
|
|||
return ret
|
||||
end
|
||||
|
||||
--- Efficiently remove items from middle of a list a list.
|
||||
---
|
||||
--- Calling table.remove() in a loop will re-index the tail of the table on
|
||||
--- every iteration, instead this function will re-index the table exactly
|
||||
--- once.
|
||||
---
|
||||
--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
|
||||
---
|
||||
---@param t any[]
|
||||
---@param first integer
|
||||
---@param last integer
|
||||
function M.list_remove(t, first, last)
|
||||
local n = #t
|
||||
for i = 0, n - first do
|
||||
t[first + i] = t[last + 1 + i]
|
||||
t[last + 1 + i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Efficiently insert items into the middle of a list.
|
||||
---
|
||||
--- Calling table.insert() in a loop will re-index the tail of the table on
|
||||
--- every iteration, instead this function will re-index the table exactly
|
||||
--- once.
|
||||
---
|
||||
--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
|
||||
---
|
||||
---@param t any[]
|
||||
---@param first integer
|
||||
---@param last integer
|
||||
---@param v any
|
||||
function M.list_insert(t, first, last, v)
|
||||
local n = #t
|
||||
|
||||
-- Shift table forward
|
||||
for i = n - first, 0, -1 do
|
||||
t[last + 1 + i] = t[first + i]
|
||||
end
|
||||
|
||||
-- Fill in new values
|
||||
for i = first, last do
|
||||
t[i] = v
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
@ -87,7 +87,7 @@ local handler = debounce_trailing(
|
|||
buf_check(bufnr)
|
||||
end
|
||||
|
||||
cache[bufnr]:invalidate()
|
||||
cache[bufnr]:invalidate(true)
|
||||
|
||||
require('gitsigns.manager').update(bufnr)
|
||||
end),
|
||||
|
|
Loading…
Reference in New Issue