Skip to content

Commit 48e32cd

Browse files
committed
perf(pool): pool performance was not on par with the rest of the modules
1 parent 5bbefc6 commit 48e32cd

24 files changed

Lines changed: 1459 additions & 463 deletions

README.md

Lines changed: 112 additions & 75 deletions
Large diffs are not rendered by default.

benchmarks/pool_perf.lua

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
local helpers = require("script.test_utils")
2+
local Picker = require("fuzzy.picker")
3+
local Pool = require("fuzzy.pool")
4+
local M = {}
5+
6+
local sizes = {}
7+
8+
local query = "kqzv"
9+
local match_every = 997
10+
local noise = "abcdefghijlmnoprstuwxy"
11+
12+
local function now_ms()
13+
return vim.loop.hrtime() / 1e6
14+
end
15+
16+
local function build_random_sizes(min_size, max_size, count)
17+
local out = {}
18+
for i = 1, count do
19+
out[i] = math.random(min_size, max_size)
20+
end
21+
return out
22+
end
23+
24+
local function log_line(label, values, sink)
25+
local line = label .. " " .. table.concat(values, " ")
26+
print(line)
27+
if sink then
28+
sink[#sink + 1] = line
29+
end
30+
end
31+
32+
local function reset_pool()
33+
if Pool.prune_timer and not vim.uv.is_closing(Pool.prune_timer) then
34+
Pool.prune_timer:stop()
35+
end
36+
Pool.tables = {}
37+
Pool.used = {}
38+
Pool.meta = {}
39+
end
40+
41+
local function enable_pool_trace(sink)
42+
local orig_obtain = Pool.obtain
43+
local orig_return = Pool._return
44+
local orig_trace = Pool.trace
45+
local obtains = {}
46+
local returns = {}
47+
local events = {}
48+
local fresh = 0
49+
50+
Pool.obtain = function(size)
51+
if #Pool.tables == 0 then
52+
fresh = fresh + 1
53+
end
54+
local info = debug.getinfo(2, "Sl")
55+
local key = string.format("%s:%s", info.short_src or "unknown", info.currentline or 0)
56+
obtains[key] = (obtains[key] or 0) + 1
57+
return orig_obtain(size)
58+
end
59+
60+
Pool._return = function(tbl)
61+
local info = debug.getinfo(2, "Sl")
62+
local key = string.format("%s:%s", info.short_src or "unknown", info.currentline or 0)
63+
returns[key] = (returns[key] or 0) + 1
64+
return orig_return(tbl)
65+
end
66+
67+
Pool.trace = function(event, data)
68+
local key = string.format(
69+
"%s requested=%s actual=%s target=%s tables=%s max_tables=%s",
70+
event,
71+
tostring(data.requested),
72+
tostring(data.actual),
73+
tostring(data.target),
74+
tostring(data.tables),
75+
tostring(data.max_tables)
76+
)
77+
events[key] = (events[key] or 0) + 1
78+
if orig_trace then
79+
orig_trace(event, data)
80+
end
81+
end
82+
83+
local function dump(label, data)
84+
local keys = {}
85+
for key, _ in pairs(data) do
86+
keys[#keys + 1] = key
87+
end
88+
table.sort(keys)
89+
log_line(label, { "count=" .. tostring(#keys) }, sink)
90+
for _, key in ipairs(keys) do
91+
log_line(" " .. key, { "hits=" .. tostring(data[key]) }, sink)
92+
end
93+
end
94+
95+
return function()
96+
log_line("pool_fresh_allocs", { "count=" .. tostring(fresh) }, sink)
97+
dump("pool_obtain_sites", obtains)
98+
dump("pool_return_sites", returns)
99+
dump("pool_events", events)
100+
Pool.obtain = orig_obtain
101+
Pool._return = orig_return
102+
Pool.trace = orig_trace
103+
end
104+
end
105+
106+
local function build_line(i)
107+
local base = noise .. noise .. noise
108+
if i % match_every == 0 then
109+
if i % 3 == 0 then
110+
return query .. "_" .. base .. "_" .. i
111+
elseif i % 3 == 1 then
112+
return base:sub(1, 12) .. "_" .. query .. "_" .. base:sub(13) .. "_" .. i
113+
else
114+
return base:sub(1, 20) .. "_" .. i .. "_" .. query
115+
end
116+
end
117+
return base:sub(1, 20) .. "_" .. i .. "_" .. base:sub(21)
118+
end
119+
120+
local function build_entries(size)
121+
local entries = {}
122+
for i = 1, size do
123+
entries[i] = build_line(i)
124+
end
125+
return entries
126+
end
127+
128+
local function build_awk_args(size)
129+
local program = table.concat({
130+
"BEGIN{",
131+
"for(i=1;i<=n;i++){",
132+
"base=noise noise noise;",
133+
"if (i%" .. match_every .. "==0){",
134+
"if (i%3==0) line=pat \"_\" base \"_\" i;",
135+
"else if (i%3==1) line=substr(base,1,12) \"_\" pat \"_\" substr(base,13) \"_\" i;",
136+
"else line=substr(base,1,20) \"_\" i \"_\" pat;",
137+
"} else {",
138+
"line=substr(base,1,20) \"_\" i \"_\" substr(base,21);",
139+
"}",
140+
"print line;",
141+
"}",
142+
"}",
143+
})
144+
return {
145+
"-v", "n=" .. size,
146+
"-v", "pat=" .. query,
147+
"-v", "noise=" .. noise,
148+
program,
149+
}
150+
end
151+
152+
local function open_picker(opts)
153+
local picker = Picker.new(opts)
154+
picker:open()
155+
return picker
156+
end
157+
158+
local function wait_ready(picker, mode)
159+
if mode == "command" then
160+
helpers.wait_for_stream(picker, 600000)
161+
else
162+
helpers.wait_for_entries(picker, 600000)
163+
end
164+
end
165+
166+
local function run_query(picker, size)
167+
local t0 = now_ms()
168+
helpers.type_query(picker, query)
169+
local results = helpers.wait_for_match(picker, 600000)
170+
local t1 = now_ms()
171+
local count = results and results[1] and #results[1] or 0
172+
return t1 - t0, count
173+
end
174+
175+
local function close_picker(picker)
176+
local t0 = now_ms()
177+
picker:close()
178+
helpers.wait_for_picker_closed(picker, 600000)
179+
local t1 = now_ms()
180+
return t1 - t0
181+
end
182+
183+
local function run_picker_cycles(mode, size, runs, sink)
184+
local picker
185+
if mode == "command" then
186+
picker = open_picker({
187+
content = "awk",
188+
context = {
189+
args = build_awk_args(size),
190+
},
191+
preview = false,
192+
prompt_debounce = 0,
193+
match_timer = 75,
194+
match_step = 75000,
195+
stream_step = 150000,
196+
})
197+
else
198+
local entries = build_entries(size)
199+
picker = open_picker({
200+
content = entries,
201+
preview = false,
202+
prompt_debounce = 0,
203+
match_timer = 75,
204+
match_step = 75000,
205+
stream_step = 150000,
206+
})
207+
end
208+
209+
wait_ready(picker, mode)
210+
for run = 1, runs do
211+
local t_query, count = run_query(picker, size)
212+
local t_close = close_picker(picker)
213+
local t_open = now_ms()
214+
picker:open()
215+
wait_ready(picker, mode)
216+
local t_ready = now_ms() - t_open
217+
log_line("cycle", {
218+
"mode=" .. mode,
219+
"size=" .. size,
220+
"run=" .. run,
221+
string.format("query_ms=%.2f", t_query),
222+
string.format("close_ms=%.2f", t_close),
223+
string.format("open_ready_ms=%.2f", t_ready),
224+
"matches=" .. count,
225+
"pool_tables=" .. tostring(#Pool.tables),
226+
}, sink)
227+
end
228+
picker:close()
229+
helpers.reset_state()
230+
collectgarbage()
231+
end
232+
233+
function M.run()
234+
local output = {}
235+
output[#output + 1] = "pool_perf benchmark"
236+
output[#output + 1] = "query=" .. query .. " match_every=" .. match_every
237+
reset_pool()
238+
output[#output + 1] = "pool_start tables=" .. tostring(#Pool.tables)
239+
240+
math.randomseed(os.time())
241+
sizes = build_random_sizes(Pool.prime_min, Pool.prime_max, 16)
242+
local trace_done = enable_pool_trace(output)
243+
for _, size in ipairs(sizes) do
244+
log_line("bench", { "size=" .. size }, output)
245+
local runs = math.random(1, 6)
246+
run_picker_cycles("command", size, runs, output)
247+
run_picker_cycles("list", size, runs, output)
248+
end
249+
trace_done()
250+
251+
local out_path = "/tmp/fuzzymatch_pool_bench.log"
252+
vim.fn.writefile(output, out_path)
253+
print("wrote " .. out_path)
254+
end
255+
256+
return M

lua/fuzzy/async.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@ local utils = require("fuzzy.utils")
55
--- @field private callbacks table List of callback functions to be invoked after completion
66
--- @field private running boolean Indicates whether the Async job is still running
77
--- @field private thread thread Coroutine object executing the async function
8+
--- @field trace? fun(event: string, data: table) Optional debug hook for async lifecycle events
89
local Async = {}
910
Async.__index = Async
1011

12+
local function trace(event, data)
13+
if Async.trace then
14+
Async.trace(event, data)
15+
end
16+
end
17+
1118
--- Creates a new Async object wrapping the given function in a coroutine.
1219
--- @param fn function Function to run in the coroutine context
1320
--- @return Async
@@ -16,6 +23,7 @@ function Async.new(fn)
1623
self.callbacks = {}
1724
self.running = true
1825
self.thread = coroutine.create(fn)
26+
trace("async_new", {})
1927
return self
2028
end
2129

@@ -60,6 +68,7 @@ function Async:_done(result, reason)
6068
self.result = result
6169
self.reason = reason
6270
end
71+
trace("async_done", { reason = reason })
6372
if not self.reason or self.reason ~= "abort" then
6473
for _, callback in ipairs(self.callbacks) do
6574
utils.safe_call(callback, result, reason)

lua/fuzzy/init.lua

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@ function M.setup(opts)
1717
},
1818
scheduler = {
1919
async_budget = 1 * 1e6,
20+
trace = nil,
2021
},
2122
pool = {
2223
max_idle = 5 * 60 * 1000,
2324
prune_interval = 30 * 1000,
2425
max_tables = nil,
25-
prime_sizes = { 1024, 2048, 4096, 8192, 16384 },
26+
prime_min = 16384,
27+
prime_max = 524288,
28+
prime_chunk = 8192,
2629
},
2730
registry = {
2831
max_idle = 5 * 60 * 1000,
2932
prune_interval = 30 * 1000,
33+
trace = nil,
3034
},
3135
})
3236

@@ -66,7 +70,6 @@ function M.setup(opts)
6670
vim.api.nvim_set_hl(0, "SelectLineHighlight", { link = "Normal", default = false })
6771
vim.api.nvim_set_hl(0, "SelectDecoratorDefault", { link = "Normal", default = false })
6872

69-
Pool.prime(pool_config.prime_sizes or {})
7073
end
7174

7275
return M

0 commit comments

Comments
 (0)