From 5e64d92411737a8fe0ea44faebf986944bfea8ea Mon Sep 17 00:00:00 2001 From: monkoose Date: Thu, 22 May 2025 13:00:22 +0300 Subject: [PATCH] perf(runtime): vim.trim for long only whitespace strings --- runtime/lua/vim/shared.lua | 4 +- test/benchmark/trim_spec.lua | 70 ++++++++++++++++++++++++++++++++ test/functional/lua/vim_spec.lua | 4 +- 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 test/benchmark/trim_spec.lua diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index fa66e18ac4..73b7408118 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -801,7 +801,9 @@ end ---@return string String with whitespace removed from its beginning and end function vim.trim(s) vim.validate('s', s, 'string') - return s:match('^%s*(.*%S)') or '' + -- `s:match('^%s*(.*%S)')` is slow for long whitespace strings, + -- so we are forced to split it into two parts to prevent this + return s:gsub('^%s+', ''):match('^.*%S') or '' end --- Escapes magic chars in |lua-pattern|s. diff --git a/test/benchmark/trim_spec.lua b/test/benchmark/trim_spec.lua new file mode 100644 index 0000000000..29b7793c36 --- /dev/null +++ b/test/benchmark/trim_spec.lua @@ -0,0 +1,70 @@ +describe('vim.trim()', function() + --- @param t number[] + local function mean(t) + assert(#t > 0) + local sum = 0 + for _, v in ipairs(t) do + sum = sum + v + end + return sum / #t + end + + --- @param t number[] + local function median(t) + local len = #t + if len % 2 == 0 then + return t[len / 2] + end + return t[(len + 1) / 2] + end + + --- @param f fun(t: number[]): table + local function measure(f, input, N) + local stats = {} ---@type number[] + for _ = 1, N do + local tic = vim.uv.hrtime() + f(input) + local toc = vim.uv.hrtime() + stats[#stats + 1] = (toc - tic) / 1000000 + end + table.sort(stats) + print( + string.format( + '\nN: %d, Min: %0.6f ms, Max: %0.6f ms, Median: %0.6f ms, Mean: %0.6f ms', + N, + math.min(unpack(stats)), + math.max(unpack(stats)), + median(stats), + mean(stats) + ) + ) + end + + local strings = { + ['10000 whitespace characters'] = string.rep(' ', 10000), + ['10000 whitespace characters and one non-whitespace at the end'] = string.rep(' ', 10000) + .. '0', + ['10000 whitespace characters and one non-whitespace at the start'] = '0' + .. string.rep(' ', 10000), + ['10000 non-whitespace characters'] = string.rep('0', 10000), + ['10000 whitespace and one non-whitespace in the middle'] = string.rep(' ', 5000) + .. 'a' + .. string.rep(' ', 5000), + ['10000 whitespace characters surrounded by non-whitespace'] = '0' + .. string.rep(' ', 10000) + .. '0', + ['10000 non-whitespace characters surrounded by whitespace'] = ' ' + .. string.rep('0', 10000) + .. ' ', + } + + --- @type string[] + local string_names = vim.tbl_keys(strings) + table.sort(string_names) + + for _, name in ipairs(string_names) do + it(name, function() + measure(vim.trim, strings[name], 100) + end) + end +end) diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 1cada42403..c599e1aa8a 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -719,10 +719,12 @@ describe('lua stdlib', function() { ' b ', 'b' }, { '\tc', 'c' }, { 'r\n', 'r' }, + { '', '' }, + { ' \t \n', '' }, } for _, q in ipairs(trims) do - assert(q[2], trim(q[1])) + eq(q[2], trim(q[1])) end -- Validates args.