Skip to content

Commit

Permalink
perf(blame): some improvements
Browse files Browse the repository at this point in the history
- Ensure ':Gitsigns blame' utilizes the blame cache.
- Rewrite the blame runner to process output incrementally.
- Make the blame cache more efficient.
- Move the blame processing code to a separate module.
  • Loading branch information
lewis6991 committed Jun 20, 2024
1 parent 89a4dce commit 9cdfcb5
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 244 deletions.
2 changes: 0 additions & 2 deletions doc/gitsigns.txt
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,6 @@ blame_line({opts}, {callback?}) *gitsigns.blame_line()*
Display full commit message with hunk.
• {ignore_whitespace}: (boolean)
Ignore whitespace when running blame.
{rev}: (string)
Revision to blame against.
• {extra_opts}: (string[])
Extra options passed to `git-blame`.

Expand Down
2 changes: 0 additions & 2 deletions lua/gitsigns/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -977,8 +977,6 @@ end
--- Display full commit message with hunk.
--- • {ignore_whitespace}: (boolean)
--- Ignore whitespace when running blame.
--- • {rev}: (string)
--- Revision to blame against.
--- • {extra_opts}: (string[])
--- Extra options passed to `git-blame`.
M.blame_line = async.create(1, function(opts)
Expand Down
7 changes: 2 additions & 5 deletions lua/gitsigns/blame.lua
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,8 @@ M.blame = function()
return
end

local blame = bcache:run_blame(nil, { rev = bcache.git_obj.revision })
if not blame then
dprint('No blame info')
return
end
bcache:get_blame()
local blame = assert(bcache.blame)

-- Save position to align 'scrollbind'
local top = vim.fn.line('w0') + vim.wo.scrolloff
Expand Down
73 changes: 32 additions & 41 deletions lua/gitsigns/cache.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ local sleep = async.wrap(2, function(duration, cb)
vim.defer_fn(cb, duration)
end)

--- @async
--- @private
function CacheEntry:wait_for_hunks()
local loop_protect = 0
Expand All @@ -65,71 +66,61 @@ end
-- If a file contains has up to this amount of lines, then
-- always blame the whole file, otherwise only blame one line
-- at a time.
local BLAME_THRESHOLD_LEN = 1000000
local BLAME_THRESHOLD_LEN = 10000

--- @async
--- @private
--- @param lnum? integer
--- @param opts Gitsigns.BlameOpts
--- @return table<integer,Gitsigns.BlameInfo?>?
--- @param opts? Gitsigns.BlameOpts
--- @return table<integer,Gitsigns.BlameInfo?>
--- @return boolean? full
function CacheEntry:run_blame(lnum, opts)
local bufnr = self.bufnr
local blame_cache --- @type table<integer,Gitsigns.BlameInfo?>?
local blame --- @type table<integer,Gitsigns.BlameInfo?>?
local lnum0 --- @type integer?
repeat
local buftext = util.buf_lines(bufnr, true)
local tick = vim.b[bufnr].changedtick
local lnum0 = #buftext > BLAME_THRESHOLD_LEN and lnum or nil
lnum0 = #buftext > BLAME_THRESHOLD_LEN and lnum or nil
-- TODO(lewis6991): Cancel blame on changedtick
blame_cache = self.git_obj:run_blame(buftext, lnum0, opts)
blame = self.git_obj:run_blame(buftext, lnum0, self.git_obj.revision, opts)
async.scheduler()
if not vim.api.nvim_buf_is_valid(bufnr) then
return
return {}
end
until vim.b[bufnr].changedtick == tick
return blame_cache
return blame, lnum0 == nil
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_committed(file),
filename = file,
}
end

--- @param lnum integer
--- @param opts Gitsigns.BlameOpts
--- If lnum is nil then run blame for the entire buffer.
--- @async
--- @param lnum? integer
--- @param opts? Gitsigns.BlameOpts
--- @return Gitsigns.BlameInfo?
function CacheEntry:get_blame(lnum, opts)
if opts.rev then
local buftext = util.buf_lines(self.bufnr)
return self.git_obj:run_blame(buftext, lnum, opts)[lnum]
end
local blame = self.blame

local blame_cache = self.blame

if not blame_cache or not blame_cache[lnum] then
if not blame or (lnum and not blame[lnum]) then
self:wait_for_hunks()
blame = blame or {}
local Hunks = require('gitsigns.hunks')
if Hunks.find_hunk(lnum, self.hunks) then
if lnum and 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)
local Blame = require('gitsigns.git.blame')
blame[lnum] = Blame.get_blame_nc(self.git_obj.relpath, lnum)
else
-- Refresh cache
blame_cache = self:run_blame(lnum, opts)
-- Refresh/update cache
local b, full = self:run_blame(lnum, opts)
if lnum and not full then
blame[lnum] = b[lnum]
else
blame = b
end
end
self.blame = blame_cache
self.blame = blame
end

if blame_cache then
return blame_cache[lnum]
end
return blame[lnum]
end

function CacheEntry:destroy()
Expand All @@ -139,7 +130,7 @@ function CacheEntry:destroy()
end
end

---@type table<integer,Gitsigns.CacheEntry>
---@type table<integer,Gitsigns.CacheEntry?>
M.cache = {}

--- @param bufnr integer
Expand Down
1 change: 0 additions & 1 deletion lua/gitsigns/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@

--- @class (exact) Gitsigns.BlameOpts
--- @field ignore_whitespace? boolean
--- @field rev? string
--- @field extra_opts? string[]

--- @class (exact) Gitsigns.LineBlameOpts : Gitsigns.BlameOpts
Expand Down
20 changes: 11 additions & 9 deletions lua/gitsigns/diffthis.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ local M = {}
--- @param dbufnr integer
--- @param base string?
local function bufread(bufnr, dbufnr, base)
local bcache = cache[bufnr]
local bcache = assert(cache[bufnr])
base = util.norm_base(base)
local text --- @type string[]
if base == bcache.git_obj.revision then
Expand Down Expand Up @@ -52,8 +52,9 @@ end
--- @param bufnr integer
--- @param dbufnr integer
--- @param base string?
local bufwrite = async.create(3, function(bufnr, dbufnr, base)
local bcache = cache[bufnr]
--- @param _callback? fun()
local bufwrite = async.create(3, function(bufnr, dbufnr, base, _callback)
local bcache = assert(cache[bufnr])
local buftext = util.buf_lines(dbufnr)
base = util.norm_base(base)
bcache.git_obj:stage_lines(buftext)
Expand Down Expand Up @@ -151,7 +152,8 @@ end

--- @param base string?
--- @param opts Gitsigns.DiffthisOpts
M.diffthis = async.create(2, function(base, opts)
--- @param _callback? fun()
M.diffthis = async.create(2, function(base, opts, _callback)
if vim.wo.diff then
return
end
Expand All @@ -176,7 +178,8 @@ end)

--- @param bufnr integer
--- @param base string
M.show = async.create(2, function(bufnr, base)
--- @param _callback? fun()
M.show = async.create(2, function(bufnr, base, _callback)
__FUNC__ = 'show'
local bufname = create_show_buf(bufnr, base)
if not bufname then
Expand Down Expand Up @@ -205,16 +208,15 @@ end

-- This function needs to be throttled as there is a call to vim.ui.input
--- @param bufnr integer
M.update = throttle_by_id(async.create(1, function(bufnr)
--- @param _callback? fun()
M.update = throttle_by_id(async.create(1, function(bufnr, _callback)
if not vim.wo.diff then
return
end

local bcache = cache[bufnr]

-- Note this will be the bufname for the currently set base
-- which are the only ones we want to update
local bufname = bcache:get_rev_bufname()
local bufname = assert(cache[bufnr]):get_rev_bufname()

for _, w in ipairs(api.nvim_list_wins()) do
if api.nvim_win_is_valid(w) then
Expand Down
Loading

0 comments on commit 9cdfcb5

Please sign in to comment.