terminal: PageList nodes are pooled individually from the gpa instead of an arena

This commit is contained in:
Mitchell Hashimoto
2026-09-03 13:58:07 -07:00
parent 07bccf7a31
commit efa3c66ed1

View File

@@ -32,14 +32,17 @@ const log = std.log.scoped(.page_list);
const native_freestanding = builtin.os.tag == .freestanding and
!builtin.target.cpu.arch.isWasm();
/// The number of PageList.Nodes we preheat the pool with. A node is
/// a very small struct so we can afford to preheat many, but the exact
/// number is uncertain. Any number too large is wasting memory, any number
/// too small will cause the pool to have to allocate more memory later.
/// This should be set to some reasonable minimum that we expect a terminal
/// window to scroll into quickly.
/// The number of pages we preheat the page pool with. For operating systems
/// that support it, pages are demand-paged (see PagePool) so this only
/// costs us address space. For other operating systems, we don't preheat.
const page_preheat = 4;
/// The number of nodes we preheat the node pool with. Unlike pages, nodes
/// are ordinary heap memory so every idle preheated node costs real
/// memory. A new PageList needs exactly one node for its first page, so
/// we preheat that one and let the pool grow on demand.
const node_preheat = 1;
/// The list of pages in the screen. These are expected to be in order
/// where the first page is the topmost page (scrollback) and the last is
/// the bottommost page (the current active page).
@@ -299,7 +302,14 @@ const Node = struct {
};
/// The memory pool we get page nodes from.
const NodePool = std.heap.memory_pool.Managed(List.Node);
///
/// We don't use a std memory pool here because it is backed by an arena
/// and we end up paying a lot of wasted memory for the growth factor when
/// in practice we don't usually use many nodes.
///
/// We don't need the "untouched" property of our UntouchedPool but
/// this gives us a GPA-allocated pool so we reuse it here.
const NodePool = datastruct.UntouchedPool(List.Node, .of(List.Node));
/// The standard page capacity that we use as a starting point for
/// all pages. This is chosen as a sane default that fits most terminal
@@ -356,7 +366,7 @@ pub const MemoryPool = struct {
page_alloc: Allocator,
preheat: usize,
) Allocator.Error!MemoryPool {
var node_pool = try NodePool.initCapacity(gen_alloc, preheat);
var node_pool = try NodePool.initCapacity(gen_alloc, gen_alloc, node_preheat);
errdefer node_pool.deinit();
var page_pool = try PagePool.initCapacity(gen_alloc, page_alloc, preheat);
errdefer page_pool.deinit();
@@ -899,18 +909,19 @@ fn verifyIntegrity(self: *const PageList) IntegrityError!void {
/// Release every page in the list during a teardown walk (deinit,
/// reset, or an errdefer unwinding a partially built list): heap-owned
/// pages go back to the page allocator and pool-owned pages back to the
/// page pool. The nodes themselves are not touched; they live in the
/// node pool.
/// pages go back to the page allocator, pool-owned pages back to the
/// page pool, and the nodes back to the node pool's free list.
fn releasePages(pool: *MemoryPool, list: List) void {
const page_alloc = pool.pages.allocator;
var it = list.first;
while (it) |node| : (it = node.next) {
while (it) |node| {
it = node.next;
const page = node.restore(.discard);
switch (node.owned) {
.pool => releasePoolPage(pool, page),
.heap => page_alloc.free(page.memory),
}
pool.nodes.destroy(node);
}
}
@@ -943,8 +954,7 @@ pub fn deinit(self: *PageList) void {
// Always deallocate our hashmap.
self.tracked_pins.deinit(self.pool.alloc);
// Release every page. We don't need to deallocate the list or
// nodes because they all reside in the pool.
// Release every page and node back to the pools, then free the pools.
releasePages(&self.pool, self.pages);
self.pool.deinit();
}