Skip to content

Commit 9525fc9

Browse files
committed
perf(location): bound realpath cache with LRU and reset
1 parent f6c1fce commit 9525fc9

2 files changed

Lines changed: 193 additions & 4 deletions

File tree

lua/peekstack/core/location.lua

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,121 @@ local fs = require("peekstack.util.fs")
22
local str = require("peekstack.util.str")
33

44
local M = {}
5+
6+
local REALPATH_CACHE_MAX = 512
7+
8+
---@type table<string, string>
59
local realpath_cache = {}
10+
---@type table<string, { prev: string?, next: string? }>
11+
local realpath_cache_nodes = {}
12+
---@type string?
13+
local realpath_cache_head = nil
14+
---@type string?
15+
local realpath_cache_tail = nil
16+
local realpath_cache_size = 0
17+
18+
---@param key string
19+
local function cache_detach(key)
20+
local node = realpath_cache_nodes[key]
21+
if not node then
22+
return
23+
end
24+
25+
local prev = node.prev
26+
local next = node.next
27+
28+
if prev then
29+
realpath_cache_nodes[prev].next = next
30+
else
31+
realpath_cache_head = next
32+
end
33+
34+
if next then
35+
realpath_cache_nodes[next].prev = prev
36+
else
37+
realpath_cache_tail = prev
38+
end
39+
40+
node.prev = nil
41+
node.next = nil
42+
end
43+
44+
---@param key string
45+
local function cache_append_tail(key)
46+
if not realpath_cache_nodes[key] then
47+
realpath_cache_nodes[key] = {}
48+
end
49+
50+
local node = realpath_cache_nodes[key]
51+
node.prev = realpath_cache_tail
52+
node.next = nil
53+
54+
if realpath_cache_tail then
55+
realpath_cache_nodes[realpath_cache_tail].next = key
56+
else
57+
realpath_cache_head = key
58+
end
59+
realpath_cache_tail = key
60+
end
61+
62+
---@param key string
63+
---@param is_new boolean
64+
local function cache_touch(key, is_new)
65+
if not is_new and realpath_cache_tail == key then
66+
return
67+
end
68+
69+
if not is_new then
70+
cache_detach(key)
71+
else
72+
realpath_cache_size = realpath_cache_size + 1
73+
end
74+
cache_append_tail(key)
75+
end
76+
77+
local function cache_evict_if_needed()
78+
while realpath_cache_size > REALPATH_CACHE_MAX do
79+
local evict_key = realpath_cache_head
80+
if not evict_key then
81+
break
82+
end
83+
cache_detach(evict_key)
84+
realpath_cache_nodes[evict_key] = nil
85+
realpath_cache[evict_key] = nil
86+
realpath_cache_size = realpath_cache_size - 1
87+
end
88+
end
89+
90+
local function cache_clear()
91+
realpath_cache = {}
92+
realpath_cache_nodes = {}
93+
realpath_cache_head = nil
94+
realpath_cache_tail = nil
95+
realpath_cache_size = 0
96+
end
697

798
---@param fname string
899
---@param cache? table<string, string>
9100
---@return string
10101
local function resolve_realpath(fname, cache)
11-
local store = cache or realpath_cache
12-
if store[fname] then
13-
return store[fname]
102+
if cache then
103+
if cache[fname] then
104+
return cache[fname]
105+
end
106+
local resolved = vim.uv.fs_realpath(fname) or fname
107+
cache[fname] = resolved
108+
return resolved
109+
end
110+
111+
if realpath_cache[fname] then
112+
cache_touch(fname, false)
113+
return realpath_cache[fname]
14114
end
115+
15116
local resolved = vim.uv.fs_realpath(fname) or fname
16-
store[fname] = resolved
117+
realpath_cache[fname] = resolved
118+
cache_touch(fname, true)
119+
cache_evict_if_needed()
17120
return resolved
18121
end
19122

@@ -205,4 +308,14 @@ function M.is_same_position(location, uri, line, character, opts)
205308
return true
206309
end
207310

311+
---Reset internal caches (for testing).
312+
function M._reset()
313+
cache_clear()
314+
end
315+
316+
---@return integer
317+
function M._realpath_cache_limit()
318+
return REALPATH_CACHE_MAX
319+
end
320+
208321
return M

tests/location_spec.lua

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,80 @@ describe("location", function()
191191
assert(vim.uv.fs_rmdir(tmpdir))
192192
end)
193193
end)
194+
195+
describe("is_same_position cache", function()
196+
---@param path string
197+
---@return PeekstackLocation
198+
local function location_for(path)
199+
return {
200+
uri = vim.uri_from_fname(path),
201+
range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 0 } },
202+
}
203+
end
204+
205+
after_each(function()
206+
location._reset()
207+
end)
208+
209+
it("clears cached realpath entries via _reset", function()
210+
local original_fs_realpath = vim.uv.fs_realpath
211+
local calls = 0
212+
213+
local ok, err = pcall(function()
214+
vim.uv.fs_realpath = function(path)
215+
calls = calls + 1
216+
return path
217+
end
218+
219+
local path = "/tmp/peekstack-location-reset.lua"
220+
local loc = location_for(path)
221+
222+
assert.is_true(location.is_same_position(loc, loc.uri, 0, 0))
223+
assert.is_true(location.is_same_position(loc, loc.uri, 0, 0))
224+
assert.equals(1, calls)
225+
226+
location._reset()
227+
228+
assert.is_true(location.is_same_position(loc, loc.uri, 0, 0))
229+
assert.equals(2, calls)
230+
end)
231+
232+
vim.uv.fs_realpath = original_fs_realpath
233+
if not ok then
234+
error(err)
235+
end
236+
end)
237+
238+
it("evicts least recently used realpath entries when cache exceeds the limit", function()
239+
local original_fs_realpath = vim.uv.fs_realpath
240+
local calls = 0
241+
242+
local ok, err = pcall(function()
243+
vim.uv.fs_realpath = function(path)
244+
calls = calls + 1
245+
return path
246+
end
247+
248+
local limit = location._realpath_cache_limit()
249+
local oldest = location_for("/tmp/peekstack-location-lru-0.lua")
250+
251+
assert.is_true(location.is_same_position(oldest, oldest.uri, 0, 0))
252+
assert.equals(1, calls)
253+
254+
for idx = 1, limit do
255+
local loc = location_for(string.format("/tmp/peekstack-location-lru-%d.lua", idx))
256+
assert.is_true(location.is_same_position(loc, loc.uri, 0, 0))
257+
end
258+
assert.equals(limit + 1, calls)
259+
260+
assert.is_true(location.is_same_position(oldest, oldest.uri, 0, 0))
261+
assert.equals(limit + 2, calls)
262+
end)
263+
264+
vim.uv.fs_realpath = original_fs_realpath
265+
if not ok then
266+
error(err)
267+
end
268+
end)
269+
end)
194270
end)

0 commit comments

Comments
 (0)