mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-10 11:19:45 +00:00
terminal: fix bitmap allocator chunk region sizing (#13240)
BitmapAllocator.layout takes a capacity in bytes, but sized its chunk region as `aligned_cap * chunk_size`, reserving chunk_size times more memory than the bitmaps can ever address. As a result, the grapheme region of every standard page reserved 128 KiB with only 8 KiB reachable, and the string region 64 KiB with only 2 KiB reachable. About ~180 KiB of waste in every 576 KiB page. Results for a standard page: | region | before | after | |---|---|---| | grapheme allocator | 131,136 B | 8,256 B | | string allocator | 65,544 B | 2,056 B | | page total | 589,824 B (576 KiB) | 409,600 B (400 KiB) | 30% less memory for every standard page in every terminal, including the preheated pages in the PageList pool.
This commit is contained in:
@@ -219,7 +219,13 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type {
|
||||
const bitmap_start = 0;
|
||||
const bitmap_end = @sizeOf(u64) * bitmap_count;
|
||||
const chunks_start = alignForward(usize, bitmap_end, @alignOf(u8));
|
||||
const chunks_end = chunks_start + (aligned_cap * chunk_size);
|
||||
|
||||
// The chunks region must be exactly the bytes addressable by
|
||||
// the bitmaps: one chunk per bit of every bitmap. Anything more
|
||||
// is unreachable waste, while anything less would let alloc
|
||||
// hand out memory beyond the region, since init marks every
|
||||
// bitmap bit as free.
|
||||
const chunks_end = chunks_start + (aligned_chunk_count * chunk_size);
|
||||
const total_size = chunks_end;
|
||||
|
||||
return Layout{
|
||||
@@ -452,6 +458,79 @@ test "BitmapAllocator layout" {
|
||||
try testing.expectEqual(@as(usize, 1), layout.bitmap_count);
|
||||
}
|
||||
|
||||
test "BitmapAllocator layout chunks region matches bitmap addressable bytes" {
|
||||
const testing = std.testing;
|
||||
|
||||
// The chunks region must be exactly the bytes addressable by the
|
||||
// bitmaps: one chunk per bit of every bitmap. Prior to this being
|
||||
// fixed, the region was over-reserved by a factor of chunk_size
|
||||
// (~186 KiB of dead space per standard page), while capacities
|
||||
// smaller than one full bitmap were under-reserved, allowing
|
||||
// out-of-bounds allocations.
|
||||
inline for (.{ 1, 2, 4, 16, 32 }) |chunk| {
|
||||
const Alloc = BitmapAllocator(chunk);
|
||||
for ([_]usize{
|
||||
1,
|
||||
chunk,
|
||||
chunk + 1,
|
||||
48,
|
||||
64,
|
||||
512,
|
||||
1024,
|
||||
2048,
|
||||
8192,
|
||||
8193,
|
||||
}) |cap| {
|
||||
const layout = Alloc.layout(cap);
|
||||
const chunks_size = layout.total_size - layout.chunks_start;
|
||||
|
||||
// Reserved == addressable by the bitmaps. This must match
|
||||
// capacityBytes() which is computed from the bitmap count.
|
||||
try testing.expectEqual(
|
||||
layout.bitmap_count * Alloc.bitmap_bit_size * chunk,
|
||||
chunks_size,
|
||||
);
|
||||
|
||||
// We always reserve at least the requested capacity.
|
||||
try testing.expect(chunks_size >= cap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "BitmapAllocator layout small capacity cannot alloc out of bounds" {
|
||||
// Regression test: for capacities smaller than one full bitmap of
|
||||
// chunks, init marks all bitmap bits as free, so alloc will hand out
|
||||
// chunks up to the full bitmap. The layout must reserve that entire
|
||||
// addressable region or those allocations would be out of bounds of
|
||||
// the backing buffer.
|
||||
const Alloc = BitmapAllocator(16);
|
||||
const cap = 48; // 3 chunks, bitmap addresses 64
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const layout = Alloc.layout(cap);
|
||||
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
|
||||
defer alloc.free(buf);
|
||||
|
||||
var bm = Alloc.init(.init(buf), layout);
|
||||
|
||||
// Allocate every chunk the bitmap can hand out and verify each is
|
||||
// fully within the backing buffer. Writing to each allocation lets
|
||||
// the testing allocator catch any out-of-bounds corruption.
|
||||
const buf_start = @intFromPtr(buf.ptr);
|
||||
const buf_end = buf_start + buf.len;
|
||||
var count: usize = 0;
|
||||
while (bm.alloc(u8, buf, 16)) |slice| {
|
||||
try testing.expect(@intFromPtr(slice.ptr) >= buf_start);
|
||||
try testing.expect(@intFromPtr(slice.ptr) + slice.len <= buf_end);
|
||||
@memset(slice, 0xAA);
|
||||
count += 1;
|
||||
} else |err| {
|
||||
try testing.expectEqual(error.OutOfMemory, err);
|
||||
}
|
||||
try testing.expectEqual(Alloc.bitmap_bit_size, count);
|
||||
}
|
||||
|
||||
test "BitmapAllocator alloc sequentially" {
|
||||
const Alloc = BitmapAllocator(4);
|
||||
const cap = 64;
|
||||
|
||||
Reference in New Issue
Block a user