Skip to content

Commit fcd908b

Browse files
feat(perf): pre-compute conceal information a single time per parse cycle
## Details Our current logic for figuring out what is concealed relies on calling the treesitter get_captures_at_pos API repeatedly. Under the hood this call is doing lots of repeated work iterating over the same queries. Instead store the concealed nodes for a buffer in some table. Then use this much smaller subset of nodes when computing concealed widths. Depending on the concents of a buffer this improves performance somewhere between 3x & 10x, nice!
1 parent 15d8e02 commit fcd908b

File tree

9 files changed

+111
-54
lines changed

9 files changed

+111
-54
lines changed

Diff for: benches/medium_spec.lua

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ describe('medium.md', function()
3939
end)
4040
eq(3000, #util.get_actual_marks())
4141

42-
truthy(start_time > 400 and start_time < 500, 'expected start time (400, 500)')
43-
truthy(refresh_time > 400 and refresh_time < 500, 'expected refresh time (400, 500)')
42+
truthy(start_time > 25 and start_time < 75, 'expected start time (25, 75)')
43+
truthy(refresh_time > 25 and refresh_time < 50, 'expected refresh time (25, 50)')
4444
truthy(move_time > 1 and move_time < 5, 'expected move time (1, 5)')
4545
end)
4646
end)

Diff for: doc/todo.md

+21-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,26 @@
22

33
- See if there is a stable way to align table cells according to delimiter
44
alignment info.
5-
- Add a performance test suite to measure improvements, relative value on
6-
same hardware should still be useful.
75
- Figure out how to display the many configuration options & impact
86
- Potentially change LuaRocks icon dependency to [mini.icons](https://luarocks.org/modules/neorocks/mini.icons)
7+
- Force non-markdown handlers to run first in `ui.lua`. Cache information
8+
at buffer level from inline parser to avoid computing it:
9+
10+
```lua
11+
-- Parse marks
12+
local marks = {}
13+
-- Parse markdown after all other nodes to take advantage of state
14+
local markdown_roots = {}
15+
parser:for_each_tree(function(tree, language_tree)
16+
local language = language_tree:lang()
17+
if language == 'markdown' then
18+
table.insert(markdown_roots, tree:root())
19+
else
20+
vim.list_extend(marks, M.parse_tree(buf, language, tree:root()))
21+
end
22+
end)
23+
for _, root in ipairs(markdown_roots) do
24+
vim.list_extend(marks, M.parse_tree(buf, 'markdown', root))
25+
end
26+
return marks
27+
```

Diff for: lua/render-markdown/colors.lua

+6-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
---@class render.md.ColorCache
2-
local cache = {
3-
---@type string[]
4-
highlights = {},
5-
}
1+
---@type string[]
2+
local cache = {}
63

74
---@class render.md.Colors
85
local M = {}
@@ -65,7 +62,7 @@ end
6562
---@return string
6663
function M.combine(foreground, background)
6764
local name = string.format('%s_%s_%s', M.prefix, foreground, background)
68-
if not vim.tbl_contains(cache.highlights, name) then
65+
if not vim.tbl_contains(cache, name) then
6966
local fg = M.get_hl(foreground)
7067
local bg = M.get_hl(background)
7168
vim.api.nvim_set_hl(0, name, {
@@ -76,7 +73,7 @@ function M.combine(foreground, background)
7673
---@diagnostic disable-next-line: undefined-field
7774
ctermbg = bg.ctermbg,
7875
})
79-
table.insert(cache.highlights, name)
76+
table.insert(cache, name)
8077
end
8178
return name
8279
end
@@ -85,7 +82,7 @@ end
8582
---@return string
8683
function M.inverse(highlight)
8784
local name = string.format('%s_Inverse_%s', M.prefix, highlight)
88-
if not vim.tbl_contains(cache.highlights, name) then
85+
if not vim.tbl_contains(cache, name) then
8986
local hl = M.get_hl(highlight)
9087
vim.api.nvim_set_hl(0, name, {
9188
fg = hl.bg,
@@ -95,7 +92,7 @@ function M.inverse(highlight)
9592
---@diagnostic disable-next-line: undefined-field
9693
ctermfg = hl.ctermbg,
9794
})
98-
table.insert(cache.highlights, name)
95+
table.insert(cache, name)
9996
end
10097
return name
10198
end

Diff for: lua/render-markdown/handler/latex.lua

+4-7
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ local logger = require('render-markdown.logger')
22
local state = require('render-markdown.state')
33
local ts = require('render-markdown.ts')
44

5-
---@class render.md.LatexCache
6-
local cache = {
7-
---@type table<string, string[]>
8-
expressions = {},
9-
}
5+
---@type table<string, string[]>
6+
local cache = {}
107

118
---@class render.md.handler.Latex: render.md.Handler
129
local M = {}
@@ -27,7 +24,7 @@ function M.parse(root, buf)
2724
local info = ts.info(root, buf)
2825
logger.debug_node_info('latex', info)
2926

30-
local expressions = cache.expressions[info.text]
27+
local expressions = cache[info.text]
3128
if expressions == nil then
3229
local raw_expression = vim.fn.system(latex.converter, info.text)
3330
expressions = vim.split(raw_expression, '\n', { plain = true, trimempty = true })
@@ -37,7 +34,7 @@ function M.parse(root, buf)
3734
for _ = 1, latex.bottom_pad do
3835
table.insert(expressions, '')
3936
end
40-
cache.expressions[info.text] = expressions
37+
cache[info.text] = expressions
4138
end
4239

4340
local latex_lines = vim.tbl_map(function(expression)

Diff for: lua/render-markdown/manager.lua

+6-9
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ local state = require('render-markdown.state')
22
local ui = require('render-markdown.ui')
33
local util = require('render-markdown.util')
44

5-
---@class render.md.manager.Data
6-
local data = {
7-
---@type integer[]
8-
buffers = {},
9-
}
5+
---@type integer[]
6+
local buffers = {}
107

118
---@class render.md.Manager
129
local M = {}
@@ -30,7 +27,7 @@ function M.setup()
3027
callback = function()
3128
for _, win in ipairs(vim.v.event.windows) do
3229
local buf = util.win_to_buf(win)
33-
if vim.tbl_contains(data.buffers, buf) then
30+
if vim.tbl_contains(buffers, buf) then
3431
ui.schedule_render(buf, true)
3532
end
3633
end
@@ -43,7 +40,7 @@ function M.set_all(enabled)
4340
-- Attempt to attach current buffer in case this is from a lazy load
4441
M.attach(vim.api.nvim_get_current_buf())
4542
state.enabled = enabled
46-
for _, buf in ipairs(data.buffers) do
43+
for _, buf in ipairs(buffers) do
4744
ui.schedule_render(buf, true)
4845
end
4946
end
@@ -61,10 +58,10 @@ function M.attach(buf)
6158
if util.file_size_mb(buf) > config.max_file_size then
6259
return
6360
end
64-
if vim.tbl_contains(data.buffers, buf) then
61+
if vim.tbl_contains(buffers, buf) then
6562
return
6663
end
67-
table.insert(data.buffers, buf)
64+
table.insert(buffers, buf)
6865
-- Events that do not imply modifications to buffer so can avoid re-parsing
6966
-- This relies on the ui parsing the buffer anyway if it is the first time it is seen
7067
vim.api.nvim_create_autocmd({ 'BufWinEnter', 'BufLeave' }, {

Diff for: lua/render-markdown/request.lua

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
local util = require('render-markdown.util')
2+
3+
---@class render.md.RequestCache
4+
local cache = {
5+
---@type table<integer, TSNode[]>
6+
conceal = {},
7+
}
8+
9+
---@class render.md.Request
10+
local M = {}
11+
12+
---@param buf integer
13+
function M.reset_buf(buf)
14+
cache.conceal[buf] = {}
15+
end
16+
17+
---@param buf integer
18+
---@param parser vim.treesitter.LanguageTree
19+
function M.compute_conceal(buf, parser)
20+
if util.get_win(util.buf_to_win(buf), 'conceallevel') == 0 then
21+
cache.conceal[buf] = {}
22+
return
23+
end
24+
25+
local nodes = {}
26+
parser:for_each_tree(function(tree, language_tree)
27+
local language = language_tree:lang()
28+
if vim.tbl_contains({ 'markdown', 'markdown_inline' }, language) then
29+
local query = vim.treesitter.query.get(language, 'highlights')
30+
if query ~= nil then
31+
for _, node, metadata in query:iter_captures(tree:root(), buf, 0, -1) do
32+
if metadata.conceal ~= nil then
33+
table.insert(nodes, node)
34+
end
35+
end
36+
end
37+
end
38+
end)
39+
cache.conceal[buf] = nodes
40+
end
41+
42+
---@param buf integer
43+
---@param row integer
44+
---@param col integer
45+
---@return boolean
46+
function M.concealed(buf, row, col)
47+
for _, node in ipairs(cache.conceal[buf]) do
48+
if vim.treesitter.is_in_node_range(node, row, col) then
49+
return true
50+
end
51+
end
52+
return false
53+
end
54+
55+
return M

Diff for: lua/render-markdown/state.lua

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
local util = require('render-markdown.util')
22

3-
---@class render.md.StateCache
4-
local cache = {
5-
---@type table<integer, render.md.BufferConfig>
6-
configs = {},
7-
}
3+
---@type table<integer, render.md.BufferConfig>
4+
local configs = {}
85

96
---@class render.md.State
107
---@field private config render.md.Config
@@ -24,7 +21,7 @@ local M = {}
2421
---@param user_config? render.md.UserConfig
2522
function M.setup(default_config, user_config)
2623
-- Reset cache to pickup config changes
27-
cache.configs = {}
24+
configs = {}
2825

2926
-- Create top level config from default & user
3027
local config = vim.deepcopy(vim.tbl_deep_extend('force', default_config, user_config or {}), true)
@@ -46,7 +43,7 @@ end
4643
---@param buf integer
4744
---@return render.md.BufferConfig
4845
M.get_config = function(buf)
49-
local config = cache.configs[buf]
46+
local config = configs[buf]
5047
if config == nil then
5148
local buftype = util.get_buf(buf, 'buftype')
5249
local buftype_config = M.config.overrides.buftype[buftype]
@@ -55,7 +52,7 @@ M.get_config = function(buf)
5552
else
5653
config = M.default_buffer_config()
5754
end
58-
cache.configs[buf] = config
55+
configs[buf] = config
5956
end
6057
return config
6158
end

Diff for: lua/render-markdown/ts.lua

+3-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
local request = require('render-markdown.request')
12
local str = require('render-markdown.str')
2-
local util = require('render-markdown.util')
33

44
---@param node TSNode
55
---@return boolean
@@ -110,18 +110,12 @@ end
110110
---@param info render.md.NodeInfo
111111
---@return integer
112112
function M.concealed(buf, info)
113-
if util.get_win(util.buf_to_win(buf), 'conceallevel') == 0 then
114-
return 0
115-
end
116113
local result = 0
117114
local col = info.start_col
118115
for _, index in ipairs(vim.fn.str2list(info.text)) do
119116
local ch = vim.fn.nr2char(index)
120-
local captures = vim.treesitter.get_captures_at_pos(buf, info.start_row, col)
121-
for _, capture in ipairs(captures) do
122-
if capture.metadata.conceal ~= nil then
123-
result = result + str.width(ch)
124-
end
117+
if request.concealed(buf, info.start_row, col) then
118+
result = result + str.width(ch)
125119
end
126120
col = col + #ch
127121
end

Diff for: lua/render-markdown/ui.lua

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local logger = require('render-markdown.logger')
2+
local request = require('render-markdown.request')
23
local state = require('render-markdown.state')
34
local util = require('render-markdown.util')
45

@@ -9,11 +10,8 @@ local builtin_handlers = {
910
latex = require('render-markdown.handler.latex'),
1011
}
1112

12-
---@class render.md.UiCache
13-
local cache = {
14-
---@type table<integer, render.md.Mark[]>
15-
marks = {},
16-
}
13+
---@type table<integer, render.md.Mark[]>
14+
local cache = {}
1715

1816
---@class render.md.Ui
1917
local M = {}
@@ -22,7 +20,7 @@ local M = {}
2220
M.namespace = vim.api.nvim_create_namespace('render-markdown.nvim')
2321

2422
function M.invalidate_cache()
25-
cache.marks = {}
23+
cache = {}
2624
end
2725

2826
---@param buf integer
@@ -59,13 +57,13 @@ function M.render(buf, mode, parse)
5957
end
6058

6159
-- Re-compute marks, needed if missing or between text changes
62-
local marks = cache.marks[buf]
60+
local marks = cache[buf]
6361
local parsed = marks == nil or parse
6462
if parsed then
6563
logger.start()
6664
marks = M.parse_buffer(buf)
6765
logger.flush()
68-
cache.marks[buf] = marks
66+
cache[buf] = marks
6967
end
7068

7169
local row = util.cursor_row(buf)
@@ -110,6 +108,9 @@ function M.parse_buffer(buf)
110108
if not parser:is_valid() then
111109
parser:parse(true)
112110
end
111+
-- Pre-compute conceal information after reseting buffer cache
112+
request.reset_buf(buf)
113+
request.compute_conceal(buf, parser)
113114
-- Parse marks
114115
local marks = {}
115116
parser:for_each_tree(function(tree, language_tree)

0 commit comments

Comments
 (0)