Skip to content

Commit 507f90b

Browse files
authored
perf(metrics): Implement row metrics with FFI for improved memory efficiency (#59)
The `CsvView.Metrics.Row` and `CsvView.Metrics.Field` structures are now defined using FFI in a new dedicated module, `lua/csvview/metrics_row.lua`. This change aims to reduce memory footprint and potentially improve performance when handling large CSV files.
1 parent ddccade commit 507f90b

File tree

2 files changed

+333
-206
lines changed

2 files changed

+333
-206
lines changed

lua/csvview/metrics.lua

Lines changed: 56 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,5 @@
1-
local ffi = require("ffi")
21
local nop = function() end
3-
4-
--- @class CsvView.Metrics.Field
5-
--- @field offset integer
6-
--- @field len integer
7-
--- @field display_width integer
8-
--- @field is_number boolean
9-
--- FFI struct definition for memory efficiency
10-
ffi.cdef([[
11-
typedef struct {
12-
int32_t offset;
13-
int32_t len;
14-
int32_t display_width;
15-
bool is_number;
16-
} csvview_field_t;
17-
]])
18-
19-
-----------------------------------------------------------------------------
20-
-- Row types
21-
-----------------------------------------------------------------------------
22-
23-
--- @class CsvView.Metrics.RowMeta
24-
local CsvViewMetricsRow = {}
25-
CsvViewMetricsRow.__index = CsvViewMetricsRow
26-
27-
--- @class CsvView.Metrics.CommentRow: CsvView.Metrics.RowMeta
28-
--- @field type "comment"
29-
30-
--- @class CsvView.Metrics.MultilineStartRow: CsvView.Metrics.RowMeta
31-
--- @field type "multiline_start"
32-
--- @field _fields CsvView.Metrics.Field[]
33-
--- @field end_loffset integer -- relative end line offset
34-
--- @field terminated boolean -- whether the row is terminated, if false, parser reached lookahead limit
35-
36-
---
37-
--- @class CsvView.Metrics.MultilineContinuationRow: CsvView.Metrics.RowMeta
38-
--- @field type "multiline_continuation"
39-
---
40-
---
41-
--- @field _fields CsvView.Metrics.Field[]
42-
---
43-
--- example:
44-
--- abc,def,"gh <--- type="multiline_start", end_loffset=4
45-
--- i",jkl,"m <--- type="multiline_continuation", start_loffset=1, start_field_offset=1, skipped_ncol=2
46-
--- n <--- type="multiline_continuation", start_loffset=2, start_field_offset=1, skipped_ncol=2
47-
--- o <--- type="multiline_continuation", start_loffset=3, start_field_offset=2, skipped_ncol=4
48-
--- p" <--- type="multiline_continuation", start_loffset=4, start_field_offset=3, skipped_ncol=4
49-
--- @field start_loffset integer -- relative start line offset
50-
--- @field end_loffset integer -- relative end line offset
51-
--- @field start_field_offset integer -- relative start field offset
52-
--- @field skipped_ncol integer -- column number that was skipped in the continuation row
53-
--- @field terminated boolean -- whether the row is terminated, if false, parser reached lookahead limit
54-
55-
--- @class CsvView.Metrics.SinglelineRow: CsvView.Metrics.RowMeta
56-
--- @field type "singleline"
57-
--- @field _fields CsvView.Metrics.Field[] -- empty for comment rows
58-
59-
--- @alias CsvView.Metrics.Row
60-
--- | CsvView.Metrics.CommentRow
61-
--- | CsvView.Metrics.MultilineStartRow
62-
--- | CsvView.Metrics.MultilineContinuationRow
63-
--- | CsvView.Metrics.SinglelineRow
2+
local CsvViewMetricsRow = require("csvview.metrics_row")
643

654
-----------------------------------------------------------------------------
665
-- Metrics class
@@ -275,56 +214,88 @@ local function construct_rows(lnum, is_comment, parsed_fields, parsed_endlnum, t
275214
end
276215

277216
if parsed_endlnum == lnum then -- Single line row
278-
local row = CsvViewMetricsRow.new_single_row({})
217+
local row_fields = {} ---@type CsvView.Metrics.Field[]
279218
for _, field in ipairs(parsed_fields) do
280219
local field_text = field.text
281220
assert(type(field_text) == "string")
282221

283222
local width = vim.fn.strdisplaywidth(field_text)
284-
row:append(field.start_pos - 1, #field.text, width, tonumber(field.text) ~= nil)
223+
table.insert(row_fields, {
224+
offset = field.start_pos - 1,
225+
len = #field_text,
226+
display_width = width,
227+
is_number = tonumber(field_text) ~= nil,
228+
})
285229
end
286-
return { row }
230+
return { CsvViewMetricsRow.new_single_row(row_fields) }
287231
end
288232

289233
-- Multi-line row
290-
local start_row = CsvViewMetricsRow.new_multiline_start_row({}, parsed_endlnum - lnum, terminated)
291-
local index = 1
292-
local rows = { start_row } --- @type CsvView.Metrics.Row[]
234+
local total_rows = parsed_endlnum - lnum + 1
235+
local row_fields = {} --- @type table<integer, CsvView.Metrics.Field[]>
236+
local row_skipped_ncol = {} --- @type table<integer, integer>
237+
238+
-- Initialize field arrays for each row
239+
for i = 1, total_rows do
240+
row_fields[i] = {}
241+
row_skipped_ncol[i] = 0
242+
end
243+
244+
-- First pass: distribute fields to rows and calculate skipped columns
245+
local current_row_index = 1
293246
for field_index, field in ipairs(parsed_fields) do
294247
local field_text = field.text
295248

296249
if type(field_text) == "table" then
297250
-- Multi-line field
298-
local field_start_lnum = index
299251
for i, text in ipairs(field_text) do
300252
-- first line starts at field.start_pos, others are 0
301253
local offset = i == 1 and field.start_pos - 1 or 0
302254
local width = vim.fn.strdisplaywidth(text)
303-
rows[index]:append(offset, #text, width, false)
255+
table.insert(row_fields[current_row_index], {
256+
offset = offset,
257+
len = #text,
258+
display_width = width,
259+
is_number = false,
260+
})
261+
262+
-- Set skipped columns for continuation rows
263+
if i > 1 and row_skipped_ncol[current_row_index] == 0 then
264+
row_skipped_ncol[current_row_index] = field_index - 1
265+
end
304266

305-
-- Add next row
267+
-- Move to next row if not the last line of this field
306268
if i ~= #field_text then
307-
index = index + 1
308-
rows[index] = CsvViewMetricsRow.new_multiline_continuation_row(
309-
{},
310-
index - 1, -- relative start line offset
311-
parsed_endlnum - lnum - index + 1, -- relative end line offset
312-
index - field_start_lnum, -- relative start field offset
313-
field_index - 1,
314-
terminated
315-
)
269+
current_row_index = current_row_index + 1
316270
end
317271
end
318272
else
319273
-- Single-line field
320-
rows[index]:append(
321-
field.start_pos - 1,
322-
#field.text,
323-
vim.fn.strdisplaywidth(field_text),
324-
tonumber(field.text) ~= nil
274+
table.insert(row_fields[current_row_index], {
275+
offset = field.start_pos - 1,
276+
len = #field.text,
277+
display_width = vim.fn.strdisplaywidth(field_text),
278+
is_number = tonumber(field.text) ~= nil,
279+
})
280+
end
281+
end
282+
283+
-- Second pass: create rows with all fields initialized
284+
local rows = {} --- @type CsvView.Metrics.Row[]
285+
for i = 1, total_rows do
286+
if i == 1 then
287+
rows[i] = CsvViewMetricsRow.new_multiline_start_row(parsed_endlnum - lnum, terminated, row_fields[i])
288+
else
289+
rows[i] = CsvViewMetricsRow.new_multiline_continuation_row(
290+
i - 1, -- relative start line offset
291+
parsed_endlnum - lnum - i + 1, -- relative end line offset
292+
row_skipped_ncol[i],
293+
terminated,
294+
row_fields[i]
325295
)
326296
end
327297
end
298+
328299
return rows
329300
end
330301

@@ -685,125 +656,4 @@ end
685656
-- Row functions
686657
----------------------------------------------------
687658

688-
--- Iterate over fields in the row
689-
---@param row CsvView.Metrics.Row
690-
---@return fun():integer?,CsvView.Metrics.Field?
691-
function CsvViewMetricsRow.iter(row)
692-
local i = 0
693-
return function()
694-
if not row._fields then
695-
return nil, nil
696-
end
697-
698-
i = i + 1
699-
if i > #row._fields then
700-
return nil, nil
701-
end
702-
703-
if row.type == "multiline_continuation" then
704-
return row.skipped_ncol + i, row._fields[i]
705-
else
706-
return i, row._fields[i]
707-
end
708-
end
709-
end
710-
711-
--- Append field to the row
712-
---@param row CsvView.Metrics.Row
713-
---@param offset integer
714-
---@param len integer
715-
---@param display_width integer
716-
---@param is_number boolean
717-
function CsvViewMetricsRow.append(row, offset, len, display_width, is_number)
718-
---@diagnostic disable-next-line: assign-type-mismatch
719-
local field = ffi.new("csvview_field_t") --- @type CsvView.Metrics.Field
720-
field.offset = offset
721-
field.len = len
722-
field.display_width = display_width
723-
field.is_number = is_number
724-
table.insert(row._fields, field)
725-
end
726-
727-
--- Get field by column index
728-
--- @param row CsvView.Metrics.Row
729-
--- @param col_idx integer 1-indexed column index
730-
--- @return CsvView.Metrics.Field?
731-
function CsvViewMetricsRow.field(row, col_idx)
732-
if row.type == "comment" then
733-
return nil
734-
end
735-
736-
if row.type == "multiline_continuation" then
737-
col_idx = col_idx - row.skipped_ncol
738-
end
739-
return row._fields[col_idx]
740-
end
741-
742-
--- Get the number of fields in the row
743-
--- @param row CsvView.Metrics.Row
744-
--- @return integer
745-
function CsvViewMetricsRow.field_count(row)
746-
return row._fields and #row._fields or 0
747-
end
748-
749-
--- Create a new CommentRow instance
750-
--- @return CsvView.Metrics.CommentRow
751-
function CsvViewMetricsRow.new_comment_row()
752-
local obj = {}
753-
obj.type = "comment"
754-
return setmetatable(obj, CsvViewMetricsRow) --- @type CsvView.Metrics.CommentRow
755-
end
756-
757-
--- Create a new SinglelineRow instance
758-
---@param fields CsvView.Metrics.Field[] fields in the row
759-
---@return CsvView.Metrics.SinglelineRow
760-
function CsvViewMetricsRow.new_single_row(fields)
761-
local obj = {}
762-
obj.type = "singleline"
763-
obj._fields = fields
764-
return setmetatable(obj, CsvViewMetricsRow) --- @type CsvView.Metrics.SinglelineRow
765-
end
766-
767-
--- Create a new MultilineStartRow instance
768-
--- @param fields CsvView.Metrics.Field[] fields in the row
769-
--- @param end_loffset integer relative end line offset
770-
--- @param terminated boolean whether the row is terminated, if false, parser reached lookahead limit
771-
--- @return CsvView.Metrics.MultilineStartRow
772-
function CsvViewMetricsRow.new_multiline_start_row(fields, end_loffset, terminated)
773-
local obj = {}
774-
obj.type = "multiline_start"
775-
obj._fields = fields or {}
776-
obj.end_loffset = end_loffset
777-
obj.terminated = terminated
778-
779-
return setmetatable(obj, CsvViewMetricsRow) --- @type CsvView.Metrics.MultilineStartRow
780-
end
781-
782-
--- Create a new MultilineContinuationRow instance
783-
--- @param fields CsvView.Metrics.Field[] fields in the row
784-
--- @param start_loffset integer relative start line offset
785-
--- @param end_loffset integer relative end line offset
786-
--- @param start_field_offset integer relative start field offset
787-
--- @param skipped_ncol integer column number that was skipped in the continuation row
788-
--- @param terminated boolean whether the row is terminated, if false, parser reached lookahead limit
789-
--- @return CsvView.Metrics.MultilineContinuationRow
790-
function CsvViewMetricsRow.new_multiline_continuation_row(
791-
fields,
792-
start_loffset,
793-
end_loffset,
794-
start_field_offset,
795-
skipped_ncol,
796-
terminated
797-
)
798-
local obj = {}
799-
obj.type = "multiline_continuation"
800-
obj._fields = fields
801-
obj.start_loffset = start_loffset
802-
obj.end_loffset = end_loffset
803-
obj.start_field_offset = start_field_offset
804-
obj.skipped_ncol = skipped_ncol
805-
obj.terminated = terminated
806-
return setmetatable(obj, CsvViewMetricsRow) --- @type CsvView.Metrics.MultilineContinuationRow
807-
end
808-
809659
return CsvViewMetrics

0 commit comments

Comments
 (0)