Skip to content

Commit

Permalink
perf: reduce renders, mutate state
Browse files Browse the repository at this point in the history
Misc performance improvements (some inspired by
http://wiki.luajit.org/Numerical-Computing-Performance-Guide)

Major one is diffing the window configs before updating to avoid
unnecessary rendering.

Also switched to mutating existing state in spring rather than creating
a new table which reduces number of allocations (one per field window per
draw).

Should help #63 and potentially #72
  • Loading branch information
rcarriga committed Apr 7, 2022
1 parent 5cfbdb2 commit 7fed925
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 130 deletions.
26 changes: 7 additions & 19 deletions lua/notify/animate/spring.lua
Expand Up @@ -7,24 +7,17 @@ local cos = math.cos
local sqrt = math.sqrt

---@class SpringState
---@field position number | string
---@field goal number | string
---@field position number
---@field velocity number | nil
---@field damping number
---@field frequency number

---@param dt number @Step in time
---@param state SpringState
---@return SpringState
return function(dt, state, settings)
local damping = settings.damping
local angular_freq = settings.frequency * 2 * pi
return function(dt, goal, state, frequency, damping)
local angular_freq = frequency * 2 * pi

local cur_os = state.position
local cur_vel = state.velocity or 0
local goal = state.goal

local offset = cur_os - goal
local offset = state.position - goal
local decay = exp(-dt * damping * angular_freq)

local new_pos
Expand All @@ -43,7 +36,7 @@ return function(dt, state, settings)
* decay
+ goal
new_vel = (i * c * cur_vel - j * (cur_vel * damping + angular_freq * offset)) * decay / c
elseif damping > 1 then -- overdamped
else -- overdamped
local c = sqrt(damping * damping - 1)

local r1 = -angular_freq * (damping - c)
Expand All @@ -58,11 +51,6 @@ return function(dt, state, settings)
new_pos = e1 + e2 + goal
new_pos = r1 * e1 + r2 * e2
end
return {
position = new_pos,
velocity = new_vel,
goal = goal,
damping = state.damping,
frequency = state.frequency,
}
state.position = new_pos
state.velocity = new_vel
end
68 changes: 6 additions & 62 deletions lua/notify/util/init.lua
Expand Up @@ -77,19 +77,13 @@ function M.get_win_config(win)
if not success or not conf.row then
return false, conf
end
for _, field in pairs({ "row", "col" }) do
if type(conf[field]) == "table" then
conf[field] = conf[field][false]
end
if type(conf.row) == "table" then
conf.row = conf.row[false]
end
return success, conf
end

function M.set_win_config(win, conf)
for _, field in pairs({ "height", "width" }) do
conf[field] = math.max(M.round(conf[field]), 1)
if type(conf.col) == "table" then
conf.col = conf.col[false]
end
return (pcall(vim.api.nvim_win_set_config, win, conf))
return success, conf
end

function M.open_win(notif_buf, enter, opts)
Expand All @@ -103,11 +97,7 @@ function M.open_win(notif_buf, enter, opts)
"&winhl",
"Normal:" .. notif_buf.highlights.body .. ",FloatBorder:" .. notif_buf.highlights.border
)
vim.fn.setwinvar(
win,
"&wrap",
0
)
vim.fn.setwinvar(win, "&wrap", 0)
return win
end

Expand All @@ -129,52 +119,6 @@ function M.numbers_to_rgb(colours)
return colour
end

function M.deep_equal(t1, t2, ignore_mt)
local ty1 = type(t1)
local ty2 = type(t2)

if ty1 ~= ty2 then
return false
end
if ty1 ~= "table" then
return t1 == t2
end

local mt = getmetatable(t1)
if not ignore_mt and mt and mt.__eq then
return t1 == t2
end

local checked

for k1, v1 in pairs(t1) do
local v2 = t2[k1]
checked[k1] = true
if v2 == nil or not M.deep_equal(v1, v2, ignore_mt) then
return false
end
end

for k2, _ in pairs(t2) do
if not checked[k2] then
return false
end
end
return true
end

function M.update_configs(updates)
for win, win_updates in pairs(updates) do
local exists, conf = M.get_win_config(win)
if exists then
for _, field in pairs({ "row", "col", "height", "width" }) do
conf[field] = win_updates[field] or conf[field]
end
M.set_win_config(win, conf)
end
end
end

function M.highlight(name, fields)
local fields_string = ""
for field, value in pairs(fields) do
Expand Down
122 changes: 73 additions & 49 deletions lua/notify/windows/init.lua
Expand Up @@ -2,6 +2,8 @@ local config = require("notify.config")
local api = vim.api
local animate = require("notify.animate")
local util = require("notify.util")
local round = util.round
local max = math.max

---@class WindowAnimator
---@field win_states table<number, table<string, SpringState>>
Expand Down Expand Up @@ -57,27 +59,28 @@ function WindowAnimator:push_pending(queue)
win_opts.noautocmd = true
local win = util.open_win(notif_buf, false, win_opts)
self.win_stages[win] = 2
self.win_states[win] = {}
self.notif_bufs[win] = notif_buf
notif_buf:open(win)
queue:pop()
end
end

function WindowAnimator:advance_stages(goals)
local default_complete = function(goal, position)
return goal == util.round(position, 2)
end
for win, _ in pairs(self.win_stages) do
local win_goals = goals[win]
local complete = true
for field, state in pairs(self.win_states[win] or {}) do
if win_goals[field].complete then
complete = win_goals[field].complete(state.position)
else
complete = default_complete(state.goal, state.position)
end
if not complete then
break
local win_state = self.win_states[win]
for field, goal in pairs(win_goals) do
if field ~= "time" then
if goal.complete then
complete = goal.complete(win_state[field].position)
else
complete = goal[1] == round(win_state[field].position, 2)
end
if not complete then
break
end
end
end
if complete and not win_goals.time then
Expand Down Expand Up @@ -121,8 +124,6 @@ function WindowAnimator:remove_win(win)
end

function WindowAnimator:update_states(time, goals)
local updated_states = {}

for win, win_goals in pairs(goals) do
if win_goals.time and not self.timed[win] then
local buf_time = self.notif_bufs[win]:timeout()
Expand All @@ -143,48 +144,53 @@ function WindowAnimator:update_states(time, goals)
end
end

updated_states[win] = self:stage_state(win, win_goals, time)
self:update_win_state(win, win_goals, time)
end

self.win_states = updated_states
end

function WindowAnimator:stage_state(win, goals, time)
local cur_state = self.win_states[win] or {}
function WindowAnimator:update_win_state(win, goals, time)
local cur_state = self.win_states[win]

local exists, win_conf = util.get_win_config(win)
if not exists then
self:remove_win(win)
return
local win_configs = {}

local function win_conf(win_)
if win_configs[win_] then
return win_configs[win_]
end
local exists, conf = util.get_win_config(win_)
if not exists then
self:remove_win(win_)
return
end
win_configs[win_] = conf
return conf
end

local new_state = {}
for field, goal in pairs(goals) do
if field ~= "time" then
local goal_type = type(goal)
-- Handle spring goal
if goal_type == "table" and goal[1] then
local init_state = (
field == "opacity" and self.notif_bufs[win].highlights:get_opacity() or win_conf[field]
)
local cur_field_state = cur_state[field] or {}
new_state[field] = animate.spring(time, {
position = cur_field_state.position or init_state,
velocity = cur_field_state.velocity,
goal = goal[1],
}, {
frequency = goal.frequency or 1,
damping = goal.damping or 1,
})
if not cur_state[field] then
if field == "opacity" then
cur_state[field] = { position = self.notif_bufs[win].highlights:get_opacity() }
else
local conf = win_conf(win)
if not conf then
return
end
cur_state[field] = { position = conf[field] }
end
end
animate.spring(time, goal[1], cur_state[field], goal.frequency or 1, goal.damping or 1)
--- Directly move goal
elseif goal_type ~= "table" then
new_state[field] = { position = goal }
cur_state[field] = { position = goal }
else
print("nvim-notify: Invalid stage goal: " .. vim.inspect(goal))
end
end
end
return new_state
end

function WindowAnimator:get_goals()
Expand Down Expand Up @@ -213,22 +219,40 @@ function WindowAnimator._get_dimensions(notif_buf)
end

function WindowAnimator:apply_updates()
local updates = {}
local updated = false
for win, states in pairs(self.win_states) do
updates[win] = {}
for field, state in pairs(states) do
if field == "opacity" then
self.notif_bufs[win].highlights:set_opacity(state.position)
else
updates[win][field] = state.position
updated = true
if states.opacity then
self.notif_bufs[win].highlights:set_opacity(states.opacity.position)
end
local exists, conf = util.get_win_config(win)
if not exists then
self:remove_win(win)
else
local win_updated = false
local function set_field(field, round_field)
if not states[field] then
return
end
local new_value = round_field and max(round(states[field].position), 1)
or states[field].position
if new_value == conf[field] then
return
end
win_updated = true
conf[field] = new_value
end
set_field("row", false)
set_field("col", false)
set_field("width", true)
set_field("height", true)

if win_updated then
api.nvim_win_set_config(win, conf)
end
end
end
if vim.tbl_isempty(updates) then
return false
end
util.update_configs(updates)
return true
return updated
end

---@return WindowAnimator
Expand Down

0 comments on commit 7fed925

Please sign in to comment.