From 227900d81c394e6462fa9b14708516fd97299efb Mon Sep 17 00:00:00 2001 From: Oscar Wallberg Date: Wed, 15 Apr 2026 21:16:48 +0200 Subject: [PATCH] refactor(util): reuse timer handles in Debouncer --- lua/git.lua | 15 ++-------- lua/pack.lua | 16 ++--------- lua/util.lua | 77 ++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 63 insertions(+), 45 deletions(-) diff --git a/lua/git.lua b/lua/git.lua index 373db9e..44992ee 100644 --- a/lua/git.lua +++ b/lua/git.lua @@ -1,4 +1,3 @@ -local log = require("log") local util = require("util") local HIGHLIGHTS = { @@ -97,21 +96,13 @@ local Repo = {} Repo.__index = Repo function Repo:start_watcher() - local watcher, err_msg, err_name = vim.uv.new_fs_event() - if not watcher then - log.error( - "Failed to create fs event watcher: %s (%s)", - err_msg, - err_name - ) - return - end - watcher:start(self.gitdir, {}, function(err, filename) + local watcher = assert(vim.uv.new_fs_event()) + assert(watcher:start(self.gitdir, {}, function(err, filename) if err or (filename ~= "index" and filename ~= "HEAD") then return end self.refresh:call() - end) + end)) self.watcher = watcher end diff --git a/lua/pack.lua b/lua/pack.lua index 45dc620..f43d099 100644 --- a/lua/pack.lua +++ b/lua/pack.lua @@ -187,17 +187,7 @@ function M.watch() local plugins_dir = vim.fs.joinpath(config_dir, "plugins") - local err_msg, err_name - watcher, err_msg, err_name = vim.uv.new_fs_event() - if not watcher then - log.error( - "Failed to create fs event watcher: %s (%s)", - err_msg, - err_name - ) - return - end - + watcher = assert(vim.uv.new_fs_event()) reload = util.debounce(function(filename) local path = vim.fs.joinpath(plugins_dir, filename) if not vim.uv.fs_stat(path) then @@ -211,7 +201,7 @@ function M.watch() end end, 100) - watcher:start(plugins_dir, {}, function(err, filename) + assert(watcher:start(plugins_dir, {}, function(err, filename) if err then log.error("Watch error: %s", err) return @@ -220,7 +210,7 @@ function M.watch() return end reload:call(filename) - end) + end)) end function M.unwatch() diff --git a/lua/util.lua b/lua/util.lua index 7513b5b..5c3de14 100644 --- a/lua/util.lua +++ b/lua/util.lua @@ -295,10 +295,16 @@ end local NIL_KEY = {} +---@class ow.Util.Debouncer.Slot +---@field timer uv.uv_timer_t +---@field cb function +---@field id any +---@field args table + ---@class ow.Util.Debouncer ---@field private _fn fun(id: any, ...) ---@field private _delay integer ----@field private _timers table +---@field private _slots table local Debouncer = {} Debouncer.__index = Debouncer @@ -306,42 +312,73 @@ Debouncer.__index = Debouncer ---@param delay integer ---@return ow.Util.Debouncer function Debouncer.new(fn, delay) - return setmetatable({ _fn = fn, _delay = delay, _timers = {} }, Debouncer) + return setmetatable({ _fn = fn, _delay = delay, _slots = {} }, Debouncer) +end + +---@param slot ow.Util.Debouncer.Slot +local function dispose(slot) + if not slot.timer:is_closing() then + slot.timer:stop() + slot.timer:close() + end end ---@param id? any ---@param ... any function Debouncer:call(id, ...) local key = id == nil and NIL_KEY or id + local slot = self._slots[key] local args = vim.F.pack_len(...) - self:cancel(id) - self._timers[key] = vim.defer_fn(function() - self._timers[key] = nil - self._fn(id, vim.F.unpack_len(args)) - end, self._delay) -end - ----@param timer uv.uv_timer_t -local function dispose(timer) - timer:stop() - timer:close() + if not slot then + -- cb lives on the slot so restart (the else branch below) can hand + -- the same closure back to timer:start, avoiding a fresh + -- schedule_wrap allocation per call. It reads id/args from the + -- slot (not upvalues) so restarts pick up the latest args. + local timer = assert(vim.uv.new_timer()) + ---@type ow.Util.Debouncer.Slot + local new_slot + new_slot = { + timer = timer, + id = id, + args = args, + cb = vim.schedule_wrap(function() + -- Slot may have been cancelled between libuv firing and + -- this scheduled callback running. Identity-check before + -- firing. + if self._slots[key] ~= new_slot then + return + end + self._slots[key] = nil + dispose(new_slot) + self._fn(new_slot.id, vim.F.unpack_len(new_slot.args)) + end), + } + self._slots[key] = new_slot + slot = new_slot + else + slot.id = id + slot.args = args + end + -- uv_timer_start on an already-active timer restarts it with the new + -- timeout, reusing the same handle (no per-call allocation). + assert(slot.timer:start(self._delay, 0, slot.cb)) end ---@param id? any function Debouncer:cancel(id) local key = id == nil and NIL_KEY or id - local timer = self._timers[key] - if timer then - dispose(timer) - self._timers[key] = nil + local slot = self._slots[key] + if slot then + self._slots[key] = nil + dispose(slot) end end function Debouncer:cancel_all() - for _, t in pairs(self._timers) do - dispose(t) + for _, slot in pairs(self._slots) do + dispose(slot) end - self._timers = {} + self._slots = {} end Util.Debouncer = Debouncer