mirror of
https://github.com/aphrody-labs/opentui.git
synced 2026-10-13 21:08:53 +00:00
Merge branch 'hardening/PERF' into hardening/integration
This commit is contained in:
4 files changed
+31
-23
No files matched your search
@@ -2329,6 +2329,7 @@ pub const OptimizedBuffer = struct {
|
||||
const total_line_count = text_buffer.lineCount();
|
||||
|
||||
const line_info = view.getCachedLineInfo();
|
||||
const selection = view.getSelection();
|
||||
var document_cell_offset: u32 = if (firstVisibleLine < line_info.line_start_cols.len)
|
||||
line_info.line_start_cols[firstVisibleLine]
|
||||
else
|
||||
@@ -2415,6 +2416,7 @@ pub const OptimizedBuffer = struct {
|
||||
|
||||
var grapheme_bytes: []const u8 = undefined;
|
||||
var cluster_width_cols: u32 = undefined;
|
||||
var ascii = false;
|
||||
|
||||
if (at_special) {
|
||||
const g = render_clusters[special_idx];
|
||||
@@ -2428,12 +2430,10 @@ pub const OptimizedBuffer = struct {
|
||||
const cp_len = std.unicode.utf8ByteSequenceLength(chunk_bytes[byte_offset]) catch 1;
|
||||
const next_byte_offset = @min(byte_offset + cp_len, byte_end);
|
||||
grapheme_bytes = chunk_bytes[byte_offset..next_byte_offset];
|
||||
// Sparse metadata also omits zero-width control characters.
|
||||
// Sparse metadata omits printable ASCII and zero-width control characters.
|
||||
// Printable ASCII is one cell under every width method.
|
||||
cluster_width_cols = if (isPrintableAscii(grapheme_bytes))
|
||||
1
|
||||
else
|
||||
utf8.getWidthAt(grapheme_bytes, 0, text_buffer.tabWidth(), text_buffer.widthMethod());
|
||||
ascii = isPrintableAscii(grapheme_bytes);
|
||||
cluster_width_cols = if (ascii) 1 else utf8.getWidthAt(grapheme_bytes, 0, text_buffer.tabWidth(), text_buffer.widthMethod());
|
||||
byte_offset = next_byte_offset;
|
||||
}
|
||||
// Only a glyph that this iteration draws, or a zero-width cluster, sets it again.
|
||||
@@ -2471,7 +2471,7 @@ pub const OptimizedBuffer = struct {
|
||||
}
|
||||
|
||||
const is_tab = grapheme_bytes.len == 1 and grapheme_bytes[0] == '\t';
|
||||
const is_blank = !isPrintableGlyph(grapheme_bytes);
|
||||
const is_blank = !ascii and !isPrintableGlyph(grapheme_bytes);
|
||||
if (!is_blank and !self.isPointInScissor(currentX, currentY)) {
|
||||
document_cell_offset += cluster_width_cols;
|
||||
currentX += @as(i32, @intCast(cluster_width_cols));
|
||||
@@ -2602,7 +2602,7 @@ pub const OptimizedBuffer = struct {
|
||||
|
||||
var cell_idx: u32 = 0;
|
||||
while (cell_idx < cluster_width_cols) : (cell_idx += 1) {
|
||||
if (view.getSelection()) |sel| {
|
||||
if (selection) |sel| {
|
||||
const isSelected = selection_offset + cell_idx >= sel.start and selection_offset + cell_idx < sel.end;
|
||||
if (isSelected) {
|
||||
if (sel.bgColor) |selBg| {
|
||||
@@ -2709,8 +2709,7 @@ pub const OptimizedBuffer = struct {
|
||||
makeCell(char, fg, drawBg, drawAttributes),
|
||||
);
|
||||
}
|
||||
} else if (cluster_width_cols == 1 and isPrintableAscii(grapheme_bytes) and
|
||||
opacity == 1.0 and ansi.alpha(drawBg) == 0 and
|
||||
} else if (ascii and opacity == 1.0 and ansi.alpha(drawBg) == 0 and
|
||||
self.trySetTransparentTextCellFast(
|
||||
self.coordsToIndex(@intCast(currentX), @intCast(currentY)),
|
||||
grapheme_bytes[0],
|
||||
|
||||
@@ -11,15 +11,17 @@ const TextBuffer = text_buffer.UnifiedTextBuffer;
|
||||
const TextBufferView = text_buffer_view.UnifiedTextBufferView;
|
||||
const RGBA = text_buffer.RGBA;
|
||||
|
||||
test "TextBufferView first layout retains reusable word metadata" {
|
||||
test "TextBufferView reuses word metadata across widths and layout capacity across edits" {
|
||||
var pools = TestPools.init(std.testing.allocator);
|
||||
defer pools.deinit();
|
||||
|
||||
const text = "alpha \u{754c}abc e\u{301} words\n" ** 100;
|
||||
const tb = try TextBuffer.init(std.testing.allocator, &pools.graphemes, &pools.links, .unicode);
|
||||
defer tb.deinit();
|
||||
const view = try TextBufferView.init(std.testing.allocator, tb);
|
||||
var tracking = std.testing.FailingAllocator.init(std.testing.allocator, .{});
|
||||
const view = try TextBufferView.init(tracking.allocator(), tb);
|
||||
defer view.deinit();
|
||||
try tb.setText("alpha \u{754c}abc e\u{301} words\n" ** 100);
|
||||
try tb.setText(text);
|
||||
view.setWrapMode(.word);
|
||||
view.setWrapWidth(80);
|
||||
_ = view.getVirtualLines();
|
||||
@@ -30,6 +32,12 @@ test "TextBufferView first layout retains reusable word metadata" {
|
||||
_ = view.getVirtualLines();
|
||||
try std.testing.expectEqual(ptr, view.word_layout.layouts.items.ptr);
|
||||
try std.testing.expectEqual(capacity, view.word_layout.arena.queryCapacity());
|
||||
try tb.setText(text); // the first edit merges each grown arena into one block
|
||||
_ = view.getVirtualLines();
|
||||
tracking.fail_index = tracking.alloc_index;
|
||||
try tb.setText(text);
|
||||
try std.testing.expectEqual(@as(u32, 101), view.getVirtualLineCount());
|
||||
try std.testing.expectEqual(@as(usize, 100), view.word_layout.layouts.items.len);
|
||||
}
|
||||
|
||||
test "TextBufferView fragmented ASCII measurement streams complete words without scratch allocation" {
|
||||
|
||||
@@ -33,7 +33,7 @@ test "TextBuffer CJK layout cache does not retain replaced dense metadata" {
|
||||
}
|
||||
const final_bytes = tracking.allocated_bytes - tracking.freed_bytes;
|
||||
// Persistent rope nodes may grow, but obsolete per-character layouts must not.
|
||||
try std.testing.expect(final_bytes - initial_bytes < 1024 * 1024);
|
||||
try std.testing.expect(final_bytes < initial_bytes + 1024 * 1024);
|
||||
|
||||
const original_chunk = tb.rope().get(1).?.asText().?;
|
||||
const cached_ptr = original_chunk.getCachedLayoutInfo(2, .unicode).?.cjk_breaks.ptr;
|
||||
|
||||
@@ -206,8 +206,8 @@ const WordLayoutStorage = struct {
|
||||
arena: std.heap.ArenaAllocator,
|
||||
layouts: std.ArrayListUnmanaged(utf8.ChunkLayoutInfo) = .empty,
|
||||
|
||||
fn reset(self: *WordLayoutStorage) void {
|
||||
_ = self.arena.reset(.free_all);
|
||||
fn reset(self: *WordLayoutStorage, mode: std.heap.ArenaAllocator.ResetMode) void {
|
||||
_ = self.arena.reset(mode);
|
||||
self.layouts = .empty;
|
||||
}
|
||||
};
|
||||
@@ -377,10 +377,9 @@ pub const UnifiedTextBufferView = struct {
|
||||
while (self.measure_dependents) |dependent| dependent.setMeasureTarget(.none) catch unreachable;
|
||||
self.original_text_buffer.unregisterView(self.view_id);
|
||||
self.view_id = std.math.maxInt(u32);
|
||||
// Virtual lines are rebuilt with free_all; retaining their old bytes would
|
||||
// consume the idle budget without avoiding an allocation on the next paint.
|
||||
// The retired-text pool counts arena capacity, so pooled views keep none.
|
||||
_ = self.virtual_lines_arena.reset(.free_all);
|
||||
self.word_layout.reset();
|
||||
self.word_layout.reset(.free_all);
|
||||
}
|
||||
|
||||
pub fn reinitStorage(self: *Self) TextBufferViewError!void {
|
||||
@@ -501,8 +500,10 @@ pub const UnifiedTextBufferView = struct {
|
||||
const buffer_dirty = self.text_buffer.isViewDirty(self.view_id);
|
||||
if (!self.virtual_lines_dirty and !buffer_dirty) return;
|
||||
|
||||
if (buffer_dirty or self.wrap_mode != .word or self.wrap_width == null) self.word_layout.reset();
|
||||
self.resetVirtualLineStorage(if (!buffer_dirty and self.wrap_mode == .word) .retain_capacity else .free_all);
|
||||
// Word rewraps keep everything, as on main; other relayouts reuse capacity bounded by the text.
|
||||
const keep: std.heap.ArenaAllocator.ResetMode = if (!buffer_dirty and self.wrap_mode == .word) .retain_capacity else .{ .retain_with_limit = 1024 *| @as(usize, self.text_buffer.getLineCount()) +| 16 *| @as(usize, self.text_buffer.getByteSize()) };
|
||||
if (buffer_dirty or self.wrap_mode != .word or self.wrap_width == null) self.word_layout.reset(if (self.wrap_mode == .word) keep else .free_all);
|
||||
self.resetVirtualLineStorage(keep);
|
||||
const virtual_allocator = self.virtual_lines_arena.allocator();
|
||||
|
||||
// Create output structure for the generic function
|
||||
@@ -527,7 +528,7 @@ pub const UnifiedTextBufferView = struct {
|
||||
// Builders append to parallel arrays; discard partial output as a unit
|
||||
// and remain dirty so the next access can retry cleanly.
|
||||
self.resetVirtualLineStorage(.free_all);
|
||||
self.word_layout.reset();
|
||||
self.word_layout.reset(.free_all);
|
||||
self.virtual_lines_dirty = true;
|
||||
return;
|
||||
}
|
||||
@@ -772,7 +773,7 @@ pub const UnifiedTextBufferView = struct {
|
||||
|
||||
pub fn switchToBuffer(self: *Self, buffer: *UnifiedTextBuffer) void {
|
||||
if (self.text_buffer != buffer) self.cached_measure_count = 0;
|
||||
self.word_layout.reset();
|
||||
self.word_layout.reset(.free_all);
|
||||
self.text_buffer = buffer;
|
||||
self.virtual_lines_dirty = true;
|
||||
}
|
||||
@@ -2132,7 +2133,7 @@ pub const UnifiedTextBufferView = struct {
|
||||
};
|
||||
if (!saved) {
|
||||
// Optional reuse must not prevent a streaming layout on OOM.
|
||||
storage.reset();
|
||||
storage.reset(.free_all);
|
||||
wctx.word_layout = null;
|
||||
layout = null;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user