mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-09 12:51:45 -04:00
Merge pull request #3627 from lightpanda-io/recycling-allocator
mem: replace slab allocator
This commit is contained in:
5 files changed
+290
-891
No files matched your search
@@ -0,0 +1,267 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Allocator for the Factory. Almost everything allocated here lives until the
|
||||
// child arena goes: DOM nodes are never destroyed. The frees that do happen
|
||||
// are mostly GC-finalized wrappers (collections, iterators), in batches.
|
||||
//
|
||||
// So an allocation is a plain allocation from the child (an arena), and a freed
|
||||
// slot goes on the free list for its class, to be handed to the next
|
||||
// allocation of that class. The free lists are only looked at once something
|
||||
// has been freed. Nothing is ever returned to the child.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Alignment = std.mem.Alignment;
|
||||
|
||||
const RecyclingAllocator = @This();
|
||||
|
||||
child_allocator: Allocator,
|
||||
|
||||
// Slots currently sitting on a free list. While 0, alloc skips the lookup.
|
||||
free_slots: usize = 0,
|
||||
free_lists: std.array_hash_map.Custom(Class, ?[*]u8, Class.Context, false) = .empty,
|
||||
|
||||
// Every slot can hold the free-list link. Alignments below usize's share a
|
||||
// class, e.g. a 34-byte string and a 40-byte node recycle each other.
|
||||
const Class = packed struct(u64) {
|
||||
alignment: Alignment,
|
||||
size: std.meta.Int(.unsigned, 64 - @bitSizeOf(Alignment)),
|
||||
|
||||
fn of(len: usize, alignment: Alignment) Class {
|
||||
const class_alignment = Alignment.max(alignment, .of(usize));
|
||||
return .{
|
||||
.size = @intCast(class_alignment.forward(@max(len, @sizeOf(usize)))),
|
||||
.alignment = class_alignment,
|
||||
};
|
||||
}
|
||||
|
||||
const Context = struct {
|
||||
pub fn hash(_: Context, class: Class) u32 {
|
||||
// A few dozen classes per page: a multiply spreads them well enough.
|
||||
return @truncate((@as(u64, @bitCast(class)) *% 0x9e3779b97f4a7c15) >> 32);
|
||||
}
|
||||
|
||||
pub fn eql(_: Context, a: Class, b: Class, _: usize) bool {
|
||||
return a == b;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const Link = *align(1) ?[*]u8;
|
||||
|
||||
pub fn init(child_allocator: Allocator) RecyclingAllocator {
|
||||
return .{ .child_allocator = child_allocator };
|
||||
}
|
||||
|
||||
pub fn allocator(self: *RecyclingAllocator) Allocator {
|
||||
return .{
|
||||
.ptr = self,
|
||||
.vtable = &.{
|
||||
.alloc = alloc,
|
||||
.free = free,
|
||||
.remap = Allocator.noRemap,
|
||||
.resize = Allocator.noResize,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 {
|
||||
const self: *RecyclingAllocator = @ptrCast(@alignCast(ctx));
|
||||
const class = Class.of(len, alignment);
|
||||
|
||||
if (self.free_slots > 0) {
|
||||
if (self.free_lists.getPtr(class)) |head| {
|
||||
if (head.*) |slot| {
|
||||
head.* = @as(Link, @ptrCast(slot)).*;
|
||||
self.free_slots -= 1;
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self.child_allocator.rawAlloc(class.size, class.alignment, ret_addr);
|
||||
}
|
||||
|
||||
fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, _: usize) void {
|
||||
const self: *RecyclingAllocator = @ptrCast(@alignCast(ctx));
|
||||
const class = Class.of(memory.len, alignment);
|
||||
|
||||
// On OOM the slot just isn't recycled; the child still owns it.
|
||||
const gop = self.free_lists.getOrPut(self.child_allocator, class) catch return;
|
||||
if (gop.found_existing == false) {
|
||||
gop.value_ptr.* = null;
|
||||
}
|
||||
|
||||
const slot = memory.ptr;
|
||||
if (comptime lp.IS_DEBUG) {
|
||||
// Make a use-after-free read garbage rather than the old object.
|
||||
@memset(slot[0..class.size], undefined);
|
||||
}
|
||||
@as(Link, @ptrCast(slot)).* = gop.value_ptr.*;
|
||||
gop.value_ptr.* = slot;
|
||||
self.free_slots += 1;
|
||||
}
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
const TestAllocator = struct {
|
||||
arena: std.heap.ArenaAllocator,
|
||||
recycling: RecyclingAllocator,
|
||||
|
||||
fn init(self: *TestAllocator) void {
|
||||
self.arena = .init(testing.allocator);
|
||||
self.recycling = .init(self.arena.allocator());
|
||||
}
|
||||
|
||||
fn deinit(self: *TestAllocator) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
test "RecyclingAllocator: allocation without frees never touches the free lists" {
|
||||
var t: TestAllocator = undefined;
|
||||
t.init();
|
||||
defer t.deinit();
|
||||
const recycler = t.recycling.allocator();
|
||||
|
||||
for (0..100) |i| {
|
||||
const ptr = try recycler.alloc(u8, 24 + i);
|
||||
@memset(ptr, 42);
|
||||
}
|
||||
|
||||
try testing.expectEqual(0, t.recycling.free_lists.count());
|
||||
}
|
||||
|
||||
test "RecyclingAllocator: reuses freed memory of the same class" {
|
||||
var t: TestAllocator = undefined;
|
||||
t.init();
|
||||
defer t.deinit();
|
||||
const recycler = t.recycling.allocator();
|
||||
|
||||
const ptr1 = try recycler.alloc(u8, 64);
|
||||
recycler.free(ptr1);
|
||||
|
||||
const ptr2 = try recycler.alloc(u8, 64);
|
||||
try testing.expect(ptr1.ptr == ptr2.ptr);
|
||||
|
||||
// free list is LIFO
|
||||
const a = try recycler.alloc(u8, 64);
|
||||
const b = try recycler.alloc(u8, 64);
|
||||
recycler.free(a);
|
||||
recycler.free(b);
|
||||
try testing.expectEqual(2, t.recycling.free_slots);
|
||||
try testing.expect(b.ptr == (try recycler.alloc(u8, 64)).ptr);
|
||||
try testing.expect(a.ptr == (try recycler.alloc(u8, 64)).ptr);
|
||||
try testing.expectEqual(0, t.recycling.free_slots);
|
||||
}
|
||||
|
||||
test "RecyclingAllocator: different classes don't interfere" {
|
||||
var t: TestAllocator = undefined;
|
||||
t.init();
|
||||
defer t.deinit();
|
||||
const recycler = t.recycling.allocator();
|
||||
|
||||
const ptr_64 = try recycler.alloc(u8, 64);
|
||||
recycler.free(ptr_64);
|
||||
|
||||
const ptr_128 = try recycler.alloc(u8, 128);
|
||||
try testing.expect(ptr_64.ptr != ptr_128.ptr);
|
||||
|
||||
try testing.expect(ptr_64.ptr == (try recycler.alloc(u8, 64)).ptr);
|
||||
}
|
||||
|
||||
test "RecyclingAllocator: sizes share a class after rounding" {
|
||||
var t: TestAllocator = undefined;
|
||||
t.init();
|
||||
defer t.deinit();
|
||||
const recycler = t.recycling.allocator();
|
||||
|
||||
// 34 and 40 bytes both round to 40, even at alignment 1
|
||||
const small = try recycler.alloc(u8, 34);
|
||||
recycler.free(small);
|
||||
const ptr = try recycler.alignedAlloc(u8, .@"8", 40);
|
||||
try testing.expect(small.ptr == ptr.ptr);
|
||||
|
||||
// anything smaller than a pointer still fits the free-list link
|
||||
const tiny = try recycler.alloc(u8, 1);
|
||||
recycler.free(tiny);
|
||||
try testing.expect(tiny.ptr == (try recycler.alloc(u8, 3)).ptr);
|
||||
|
||||
// u64 and u32 share the 8-byte class
|
||||
const int64 = try recycler.create(u64);
|
||||
recycler.destroy(int64);
|
||||
try testing.expectEqual(@intFromPtr(int64), @intFromPtr(try recycler.create(u32)));
|
||||
}
|
||||
|
||||
test "RecyclingAllocator: never hands out a slot with weaker alignment" {
|
||||
var t: TestAllocator = undefined;
|
||||
t.init();
|
||||
defer t.deinit();
|
||||
const recycler = t.recycling.allocator();
|
||||
|
||||
const ptr8 = try recycler.alignedAlloc(u8, .@"8", 64);
|
||||
recycler.free(ptr8);
|
||||
|
||||
const ptr32 = try recycler.alignedAlloc(u8, .@"32", 64);
|
||||
try testing.expect(std.mem.isAligned(@intFromPtr(ptr32.ptr), 32));
|
||||
try testing.expect(ptr8.ptr != ptr32.ptr);
|
||||
}
|
||||
|
||||
test "RecyclingAllocator: no resize support" {
|
||||
var t: TestAllocator = undefined;
|
||||
t.init();
|
||||
defer t.deinit();
|
||||
const recycler = t.recycling.allocator();
|
||||
|
||||
const slice = try recycler.alloc(u8, 100);
|
||||
try testing.expect(!recycler.resize(slice, 90));
|
||||
try testing.expect(!recycler.resize(slice, 200));
|
||||
}
|
||||
|
||||
test "RecyclingAllocator: stress" {
|
||||
var t: TestAllocator = undefined;
|
||||
t.init();
|
||||
defer t.deinit();
|
||||
const recycler = t.recycling.allocator();
|
||||
|
||||
var prng = std.Random.DefaultPrng.init(0);
|
||||
const random = prng.random();
|
||||
|
||||
var live: std.ArrayList([]u8) = .empty;
|
||||
defer live.deinit(testing.allocator);
|
||||
|
||||
var frees: usize = 0;
|
||||
for (0..2000) |_| {
|
||||
if (random.boolean() and live.items.len > 0) {
|
||||
const slice = live.swapRemove(random.uintLessThan(usize, live.items.len));
|
||||
// still holds what we wrote: the slot wasn't handed out twice
|
||||
for (slice) |b| try testing.expectEqual(@as(u8, @truncate(slice.len)), b);
|
||||
recycler.free(slice);
|
||||
frees += 1;
|
||||
} else {
|
||||
const slice = try recycler.alloc(u8, random.uintAtMost(usize, 256) + 1);
|
||||
@memset(slice, @truncate(slice.len));
|
||||
try live.append(testing.allocator, slice);
|
||||
}
|
||||
}
|
||||
|
||||
// some frees were handed back out
|
||||
try testing.expect(t.recycling.free_slots < frees);
|
||||
}
|
||||
+21
-21
@@ -21,7 +21,7 @@ const lp = @import("lightpanda");
|
||||
|
||||
const reflect = @import("reflect.zig");
|
||||
|
||||
const SlabAllocator = @import("../slab.zig").SlabAllocator;
|
||||
const RecyclingAllocator = @import("../RecyclingAllocator.zig");
|
||||
|
||||
const Page = @import("Page.zig");
|
||||
const Frame = @import("Frame.zig");
|
||||
@@ -52,7 +52,7 @@ const Factory = @This();
|
||||
|
||||
_page: *Page,
|
||||
_arena: Allocator,
|
||||
_slab: SlabAllocator,
|
||||
_recycling: RecyclingAllocator,
|
||||
_documents: std.ArrayList(u32) = .empty, // ids of the documents _we_ created
|
||||
_document_registry: *DocumentRegistry, // &browser.documents
|
||||
|
||||
@@ -60,7 +60,7 @@ pub fn init(page: *Page, arena: Allocator, document_registry: *DocumentRegistry)
|
||||
return .{
|
||||
._page = page,
|
||||
._arena = arena,
|
||||
._slab = SlabAllocator.init(arena, 128),
|
||||
._recycling = .init(arena),
|
||||
._document_registry = document_registry,
|
||||
};
|
||||
}
|
||||
@@ -72,7 +72,7 @@ pub fn deinit(self: *Factory) void {
|
||||
}
|
||||
|
||||
pub fn storageAllocator(self: *Factory) Allocator {
|
||||
return self._slab.allocator();
|
||||
return self._recycling.allocator();
|
||||
}
|
||||
|
||||
fn registerDocument(self: *Factory, doc: *Document) !u32 {
|
||||
@@ -83,7 +83,7 @@ fn registerDocument(self: *Factory, doc: *Document) !u32 {
|
||||
|
||||
// this is a root object
|
||||
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
return self.eventTargetWithAllocator(self._slab.allocator(), child);
|
||||
return self.eventTargetWithAllocator(self._recycling.allocator(), child);
|
||||
}
|
||||
|
||||
pub fn eventTargetWithAllocator(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {
|
||||
@@ -324,7 +324,7 @@ pub fn abstractRange(_: *const Factory, arena: *lp.Arena, child: anytype, frame:
|
||||
}
|
||||
|
||||
pub fn domRect(self: *Factory, rect: DOMRectReadOnly.Data) !*DOMRect {
|
||||
const chain = try PrototypeChain(&.{ DOMRectReadOnly, DOMRect }).allocate(self._slab.allocator());
|
||||
const chain = try PrototypeChain(&.{ DOMRectReadOnly, DOMRect }).allocate(self._recycling.allocator());
|
||||
|
||||
const base = chain.get(0);
|
||||
base.* = .{
|
||||
@@ -341,7 +341,7 @@ pub fn domRect(self: *Factory, rect: DOMRectReadOnly.Data) !*DOMRect {
|
||||
|
||||
pub fn node(self: *Factory, owner: *const Document, child: anytype) !*@TypeOf(child) {
|
||||
comptime assert(@TypeOf(child) != Document);
|
||||
const allocator = self._slab.allocator();
|
||||
const allocator = self._recycling.allocator();
|
||||
return try AutoPrototypeChain(
|
||||
&.{ EventTarget, Node, @TypeOf(child) },
|
||||
).createOwned(allocator, owner._index, child);
|
||||
@@ -363,7 +363,7 @@ pub fn genericDocument(self: *Factory, opts: DocumentOpts) !*Document {
|
||||
// the start.
|
||||
fn documentChain(self: *Factory, comptime types: []const type, opts: DocumentOpts) !PrototypeChain(types) {
|
||||
comptime assert(types[1] == Node and types[2] == Document);
|
||||
const chain = try PrototypeChain(types).allocate(self._slab.allocator());
|
||||
const chain = try PrototypeChain(types).allocate(self._recycling.allocator());
|
||||
const doc = chain.get(2);
|
||||
const index = try self.registerDocument(doc);
|
||||
|
||||
@@ -391,7 +391,7 @@ pub fn cdataNode(self: *Factory, owner: *const Document, cd: Node.CData, leaf: a
|
||||
const types = comptime prototypeTypes(@TypeOf(leaf));
|
||||
comptime assert(types[0] == EventTarget and types[1] == Node and types[2] == Node.CData);
|
||||
|
||||
const chain = try PrototypeChain(types).allocate(self._slab.allocator());
|
||||
const chain = try PrototypeChain(types).allocate(self._recycling.allocator());
|
||||
chain.setRoot();
|
||||
chain.setMiddle(1);
|
||||
chain.get(1)._owner = owner._index;
|
||||
@@ -475,7 +475,7 @@ fn hasStoredProto(comptime T: type) bool {
|
||||
// any field that must point at another chain member, patching the latter on
|
||||
// the result.
|
||||
pub fn chained(self: *Factory, values: anytype) !*ChainedLeaf(@TypeOf(values)) {
|
||||
return chainedWithAllocator(self._slab.allocator(), values);
|
||||
return chainedWithAllocator(self._recycling.allocator(), values);
|
||||
}
|
||||
|
||||
pub fn chainedWithAllocator(allocator: Allocator, values: anytype) !*ChainedLeaf(@TypeOf(values)) {
|
||||
@@ -516,28 +516,28 @@ pub fn document(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
}
|
||||
|
||||
pub fn documentFragment(self: *Factory, owner: *const Document, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
const allocator = self._recycling.allocator();
|
||||
return try AutoPrototypeChain(
|
||||
&.{ EventTarget, Node, Node.DocumentFragment, @TypeOf(child) },
|
||||
).createOwned(allocator, owner._index, child);
|
||||
}
|
||||
|
||||
pub fn element(self: *Factory, owner: *const Document, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
const allocator = self._recycling.allocator();
|
||||
return try AutoPrototypeChain(
|
||||
&.{ EventTarget, Node, Element, @TypeOf(child) },
|
||||
).createOwned(allocator, owner._index, child);
|
||||
}
|
||||
|
||||
pub fn htmlElement(self: *Factory, owner: *const Document, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
const allocator = self._recycling.allocator();
|
||||
return try AutoPrototypeChain(
|
||||
&.{ EventTarget, Node, Element, Element.Html, @TypeOf(child) },
|
||||
).createOwned(allocator, owner._index, child);
|
||||
}
|
||||
|
||||
pub fn htmlMediaElement(self: *Factory, owner: *const Document, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
const allocator = self._recycling.allocator();
|
||||
return try AutoPrototypeChain(
|
||||
&.{ EventTarget, Node, Element, Element.Html, Element.Html.Media, @TypeOf(child) },
|
||||
).createOwned(allocator, owner._index, child);
|
||||
@@ -545,7 +545,7 @@ pub fn htmlMediaElement(self: *Factory, owner: *const Document, child: anytype)
|
||||
|
||||
pub fn svgElement(self: *Factory, owner: *const Document, tag_name: []const u8, child: anytype) !*@TypeOf(child) {
|
||||
const types = comptime svgPrototypeTypes(@TypeOf(child));
|
||||
const chain = try PrototypeChain(types).allocate(self._slab.allocator());
|
||||
const chain = try PrototypeChain(types).allocate(self._recycling.allocator());
|
||||
|
||||
chain.setRoot();
|
||||
inline for (1..types.len - 1) |i| {
|
||||
@@ -583,17 +583,17 @@ pub fn xhrEventTarget(_: *const Factory, allocator: Allocator, child: anytype) !
|
||||
pub fn idbOpenRequest(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
return try AutoPrototypeChain(
|
||||
&.{ EventTarget, IDBRequest, @TypeOf(child) },
|
||||
).create(self._slab.allocator(), child);
|
||||
).create(self._recycling.allocator(), child);
|
||||
}
|
||||
|
||||
pub fn taskSignal(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
return try AutoPrototypeChain(
|
||||
&.{ EventTarget, AbortSignal, @TypeOf(child) },
|
||||
).create(self._slab.allocator(), child);
|
||||
).create(self._recycling.allocator(), child);
|
||||
}
|
||||
|
||||
pub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
const allocator = self._recycling.allocator();
|
||||
const TextTrackCue = @import("webapi/media/TextTrackCue.zig");
|
||||
|
||||
return try AutoPrototypeChain(
|
||||
@@ -623,7 +623,7 @@ pub fn destroy(self: *Factory, value: anytype) void {
|
||||
}
|
||||
|
||||
pub fn destroyStandalone(self: *Factory, value: anytype) void {
|
||||
const allocator = self._slab.allocator();
|
||||
const allocator = self._recycling.allocator();
|
||||
allocator.destroy(value);
|
||||
}
|
||||
|
||||
@@ -634,7 +634,7 @@ fn destroyChain(
|
||||
old_align: std.mem.Alignment,
|
||||
) void {
|
||||
const S = reflect.Struct(@TypeOf(value));
|
||||
const allocator = self._slab.allocator();
|
||||
const allocator = self._recycling.allocator();
|
||||
|
||||
// aligns the old size to the alignment of this element
|
||||
const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));
|
||||
@@ -656,7 +656,7 @@ fn destroyChain(
|
||||
}
|
||||
|
||||
fn createT(self: *Factory, comptime T: type) !*T {
|
||||
const allocator = self._slab.allocator();
|
||||
const allocator = self._recycling.allocator();
|
||||
return try allocator.create(T);
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ _http_owner: HttpClient.Owner,
|
||||
// List of active live ranges (for mutation updates per DOM spec)
|
||||
_live_ranges: std.DoublyLinkedList = .{},
|
||||
// Live NodeIterators for the DOM pre-removing steps. Iterators are
|
||||
// slab-allocated (frame lifetime) and never unlinked.
|
||||
// factory-allocated (frame lifetime) and never unlinked.
|
||||
_live_node_iterators: std.DoublyLinkedList = .{},
|
||||
|
||||
// List of open BroadcastChannels, used to route postMessage between same-named
|
||||
@@ -453,12 +453,6 @@ pub fn deinit(self: *Frame) void {
|
||||
|
||||
if (comptime lp.IS_DEBUG) {
|
||||
log.debug(.frame, "frame.deinit", .{ .url = self.url, .type = self._type });
|
||||
|
||||
// Uncomment if you want slab statistics to print.
|
||||
// const stats = self._factory._slab.getStats(self.arena) catch unreachable;
|
||||
// var buffer: [256]u8 = undefined;
|
||||
// var stream = std.Io.File.stderr().writerStreaming(lp.io, &buffer).interface;
|
||||
// stats.print(&stream) catch unreachable;
|
||||
}
|
||||
|
||||
self._parse_state.deinit(self);
|
||||
|
||||
@@ -53,7 +53,7 @@ _arena: *lp.Arena,
|
||||
_rc: lp.RC = .{},
|
||||
_items: std.ArrayList(*DataTransferItem) = .empty,
|
||||
_item_list: *DataTransferItemList,
|
||||
// FileList lives on the factory slab and is frame-tracked, so each File ref it
|
||||
// FileList lives on the factory allocator and is frame-tracked, so each File ref it
|
||||
// holds is released at frame teardown (same path as `<input type=file>`).
|
||||
_files: *FileList,
|
||||
_drop_effect: []const u8 = "none",
|
||||
|
||||
-862
@@ -1,862 +0,0 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Alignment = std.mem.Alignment;
|
||||
|
||||
const Slab = struct {
|
||||
alignment: Alignment,
|
||||
item_size: usize,
|
||||
max_slot_count: usize,
|
||||
|
||||
bitset: std.bit_set.DynamicBitSetUnmanaged,
|
||||
chunks: std.ArrayList([]u8),
|
||||
|
||||
pub fn init(
|
||||
allocator: Allocator,
|
||||
alignment: Alignment,
|
||||
item_size: usize,
|
||||
max_slot_count: usize,
|
||||
) !Slab {
|
||||
return .{
|
||||
.alignment = alignment,
|
||||
.item_size = item_size,
|
||||
.bitset = try .initFull(allocator, 0),
|
||||
.chunks = .empty,
|
||||
.max_slot_count = max_slot_count,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Slab, allocator: Allocator) void {
|
||||
self.bitset.deinit(allocator);
|
||||
|
||||
for (self.chunks.items) |chunk| {
|
||||
allocator.rawFree(chunk, self.alignment, @returnAddress());
|
||||
}
|
||||
|
||||
self.chunks.deinit(allocator);
|
||||
}
|
||||
|
||||
inline fn calculateChunkSize(self: *Slab, chunk_index: usize) usize {
|
||||
const safe_index: u6 = @intCast(@min(std.math.maxInt(u6), chunk_index));
|
||||
const exponential = @as(usize, 1) << safe_index;
|
||||
return @min(exponential, self.max_slot_count);
|
||||
}
|
||||
|
||||
inline fn toBitsetIndex(self: *Slab, chunk_index: usize, slot_index: usize) usize {
|
||||
var offset: usize = 0;
|
||||
for (0..chunk_index) |i| {
|
||||
const chunk_size = self.calculateChunkSize(i);
|
||||
offset += chunk_size;
|
||||
}
|
||||
return offset + slot_index;
|
||||
}
|
||||
|
||||
inline fn toChunkAndSlotIndices(self: *Slab, bitset_index: usize) struct { usize, usize } {
|
||||
var offset: usize = 0;
|
||||
var chunk_index: usize = 0;
|
||||
|
||||
while (chunk_index < self.chunks.items.len) : (chunk_index += 1) {
|
||||
const chunk_size = self.calculateChunkSize(chunk_index);
|
||||
if (bitset_index < offset + chunk_size) {
|
||||
return .{ chunk_index, bitset_index - offset };
|
||||
}
|
||||
|
||||
offset += chunk_size;
|
||||
}
|
||||
|
||||
unreachable;
|
||||
}
|
||||
|
||||
fn alloc(self: *Slab, allocator: Allocator) ![]u8 {
|
||||
if (self.bitset.findFirstSet()) |index| {
|
||||
const chunk_index, const slot_index = self.toChunkAndSlotIndices(index);
|
||||
|
||||
// if we have a free slot
|
||||
self.bitset.unset(index);
|
||||
|
||||
const chunk = self.chunks.items[chunk_index];
|
||||
const offset = slot_index * self.item_size;
|
||||
return chunk.ptr[offset..][0..self.item_size];
|
||||
} else {
|
||||
const old_capacity = self.bitset.bit_length;
|
||||
|
||||
// if we have don't have a free slot
|
||||
try self.allocateChunk(allocator);
|
||||
|
||||
const first_slot_index = old_capacity;
|
||||
self.bitset.unset(first_slot_index);
|
||||
|
||||
const new_chunk = self.chunks.items[self.chunks.items.len - 1];
|
||||
return new_chunk.ptr[0..self.item_size];
|
||||
}
|
||||
}
|
||||
|
||||
fn free(self: *Slab, ptr: [*]u8) void {
|
||||
const addr = @intFromPtr(ptr);
|
||||
|
||||
for (self.chunks.items, 0..) |chunk, i| {
|
||||
const chunk_start = @intFromPtr(chunk.ptr);
|
||||
const chunk_end = chunk_start + chunk.len;
|
||||
|
||||
if (addr >= chunk_start and addr < chunk_end) {
|
||||
const offset = addr - chunk_start;
|
||||
const slot_index = offset / self.item_size;
|
||||
|
||||
const bitset_index = self.toBitsetIndex(i, slot_index);
|
||||
assert(!self.bitset.isSet(bitset_index));
|
||||
|
||||
self.bitset.set(bitset_index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
unreachable;
|
||||
}
|
||||
|
||||
fn allocateChunk(self: *Slab, allocator: Allocator) !void {
|
||||
const next_chunk_size = self.calculateChunkSize(self.chunks.items.len);
|
||||
const chunk_len = self.item_size * next_chunk_size;
|
||||
|
||||
const chunk_ptr = allocator.rawAlloc(
|
||||
chunk_len,
|
||||
self.alignment,
|
||||
@returnAddress(),
|
||||
) orelse return error.FailedChildAllocation;
|
||||
|
||||
const chunk = chunk_ptr[0..chunk_len];
|
||||
try self.chunks.append(allocator, chunk);
|
||||
|
||||
const new_capacity = self.bitset.bit_length + next_chunk_size;
|
||||
try self.bitset.resize(allocator, new_capacity, true);
|
||||
}
|
||||
|
||||
const Stats = struct {
|
||||
key: SlabKey,
|
||||
item_size: usize,
|
||||
chunk_count: usize,
|
||||
total_slots: usize,
|
||||
slots_in_use: usize,
|
||||
slots_free: usize,
|
||||
bytes_allocated: usize,
|
||||
bytes_in_use: usize,
|
||||
bytes_free: usize,
|
||||
utilization_ratio: f64,
|
||||
};
|
||||
|
||||
fn getStats(self: *const Slab, key: SlabKey) Stats {
|
||||
const total_slots = self.bitset.bit_length;
|
||||
const free_slots = self.bitset.count();
|
||||
const used_slots = total_slots - free_slots;
|
||||
const bytes_allocated = total_slots * self.item_size;
|
||||
const bytes_in_use = used_slots * self.item_size;
|
||||
|
||||
const utilization_ratio = if (bytes_allocated > 0)
|
||||
@as(f64, @floatFromInt(bytes_in_use)) / @as(f64, @floatFromInt(bytes_allocated))
|
||||
else
|
||||
0.0;
|
||||
|
||||
return .{
|
||||
.key = key,
|
||||
.item_size = self.item_size,
|
||||
.chunk_count = self.chunks.items.len,
|
||||
.total_slots = total_slots,
|
||||
.slots_in_use = used_slots,
|
||||
.slots_free = free_slots,
|
||||
.bytes_allocated = bytes_allocated,
|
||||
.bytes_in_use = bytes_in_use,
|
||||
.bytes_free = free_slots * self.item_size,
|
||||
.utilization_ratio = utilization_ratio,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const SlabKey = struct {
|
||||
size: usize,
|
||||
alignment: Alignment,
|
||||
};
|
||||
|
||||
pub const SlabAllocator = struct {
|
||||
const Self = @This();
|
||||
|
||||
child_allocator: Allocator,
|
||||
max_slot_count: usize,
|
||||
|
||||
slabs: std.ArrayHashMapUnmanaged(SlabKey, Slab, struct {
|
||||
const Context = @This();
|
||||
|
||||
pub fn hash(_: Context, key: SlabKey) u32 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
std.hash.autoHash(&hasher, key.size);
|
||||
std.hash.autoHash(&hasher, key.alignment);
|
||||
return @truncate(hasher.final());
|
||||
}
|
||||
|
||||
pub fn eql(_: Context, a: SlabKey, b: SlabKey, _: usize) bool {
|
||||
return a.size == b.size and a.alignment == b.alignment;
|
||||
}
|
||||
}, false) = .empty,
|
||||
|
||||
pub fn init(child_allocator: Allocator, max_slot_count: usize) Self {
|
||||
assert(std.math.isPowerOfTwo(max_slot_count));
|
||||
|
||||
return .{
|
||||
.child_allocator = child_allocator,
|
||||
.slabs = .empty,
|
||||
.max_slot_count = max_slot_count,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
for (self.slabs.values()) |*slab| {
|
||||
slab.deinit(self.child_allocator);
|
||||
}
|
||||
|
||||
self.slabs.deinit(self.child_allocator);
|
||||
}
|
||||
|
||||
const ResetKind = enum {
|
||||
/// Free all chunks and release all memory.
|
||||
clear,
|
||||
/// Keep all chunks, reset trees to reuse memory.
|
||||
retain_capacity,
|
||||
};
|
||||
|
||||
/// This clears all of the stored memory, freeing the currently used chunks.
|
||||
pub fn reset(self: *Self, kind: ResetKind) void {
|
||||
switch (kind) {
|
||||
.clear => {
|
||||
for (self.slabs.values()) |*slab| {
|
||||
for (slab.chunks.items) |chunk| {
|
||||
self.child_allocator.free(chunk);
|
||||
}
|
||||
|
||||
slab.chunks.clearAndFree(self.child_allocator);
|
||||
slab.bitset.deinit(self.child_allocator);
|
||||
}
|
||||
|
||||
self.slabs.clearAndFree(self.child_allocator);
|
||||
},
|
||||
.retain_capacity => {
|
||||
for (self.slabs.values()) |*slab| {
|
||||
slab.bitset.setAll();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const Stats = struct {
|
||||
total_allocated_bytes: usize,
|
||||
bytes_in_use: usize,
|
||||
bytes_free: usize,
|
||||
slab_count: usize,
|
||||
total_chunks: usize,
|
||||
total_slots: usize,
|
||||
slots_in_use: usize,
|
||||
slots_free: usize,
|
||||
fragmentation_ratio: f64,
|
||||
utilization_ratio: f64,
|
||||
slabs: []const Slab.Stats,
|
||||
|
||||
pub fn print(self: *const Stats, stream: *std.Io.Writer) !void {
|
||||
try stream.print("\n", .{});
|
||||
try stream.print("\n=== Slab Allocator Statistics ===\n", .{});
|
||||
try stream.print("Overall Memory:\n", .{});
|
||||
try stream.print(" Total allocated: {} bytes ({d:.2} MB)\n", .{
|
||||
self.total_allocated_bytes,
|
||||
@as(f64, @floatFromInt(self.total_allocated_bytes)) / 1_048_576.0,
|
||||
});
|
||||
try stream.print(" In use: {} bytes ({d:.2} MB)\n", .{
|
||||
self.bytes_in_use,
|
||||
@as(f64, @floatFromInt(self.bytes_in_use)) / 1_048_576.0,
|
||||
});
|
||||
try stream.print(" Free: {} bytes ({d:.2} MB)\n", .{
|
||||
self.bytes_free,
|
||||
@as(f64, @floatFromInt(self.bytes_free)) / 1_048_576.0,
|
||||
});
|
||||
|
||||
try stream.print("\nOverall Structure:\n", .{});
|
||||
try stream.print(" Slab Count: {}\n", .{self.slab_count});
|
||||
try stream.print(" Total chunks: {}\n", .{self.total_chunks});
|
||||
try stream.print(" Total slots: {}\n", .{self.total_slots});
|
||||
try stream.print(" Slots in use: {}\n", .{self.slots_in_use});
|
||||
try stream.print(" Slots free: {}\n", .{self.slots_free});
|
||||
|
||||
try stream.print("\nOverall Efficiency:\n", .{});
|
||||
try stream.print(" Utilization: {d:.1}%\n", .{self.utilization_ratio * 100.0});
|
||||
try stream.print(" Fragmentation: {d:.1}%\n", .{self.fragmentation_ratio * 100.0});
|
||||
|
||||
if (self.slabs.len > 0) {
|
||||
try stream.print("\nPer-Slab Breakdown:\n", .{});
|
||||
try stream.print(
|
||||
" {s:>5} | {s:>4} | {s:>6} | {s:>6} | {s:>6} | {s:>10} | {s:>6}\n",
|
||||
.{ "Size", "Algn", "Chunks", "Slots", "InUse", "Bytes", "Util%" },
|
||||
);
|
||||
try stream.print(
|
||||
" {s:-<5}-+-{s:-<4}-+-{s:-<6}-+-{s:-<6}-+-{s:-<6}-+-{s:-<10}-+-{s:-<6}\n",
|
||||
.{ "", "", "", "", "", "", "" },
|
||||
);
|
||||
|
||||
for (self.slabs) |slab| {
|
||||
try stream.print(" {d:5} | {d:4} | {d:6} | {d:6} | {d:6} | {d:10} | {d:5.1}%\n", .{
|
||||
slab.key.size,
|
||||
@intFromEnum(slab.key.alignment),
|
||||
slab.chunk_count,
|
||||
slab.total_slots,
|
||||
slab.slots_in_use,
|
||||
slab.bytes_allocated,
|
||||
slab.utilization_ratio * 100.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn getStats(self: *Self, a: std.mem.Allocator) !Stats {
|
||||
var slab_stats: std.ArrayList(Slab.Stats) = try .initCapacity(a, self.slabs.entries.len);
|
||||
errdefer slab_stats.deinit(a);
|
||||
|
||||
var stats = Stats{
|
||||
.total_allocated_bytes = 0,
|
||||
.bytes_in_use = 0,
|
||||
.bytes_free = 0,
|
||||
.slab_count = self.slabs.count(),
|
||||
.total_chunks = 0,
|
||||
.total_slots = 0,
|
||||
.slots_in_use = 0,
|
||||
.slots_free = 0,
|
||||
.fragmentation_ratio = 0.0,
|
||||
.utilization_ratio = 0.0,
|
||||
.slabs = &.{},
|
||||
};
|
||||
|
||||
var it = self.slabs.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const key = entry.key_ptr.*;
|
||||
const slab = entry.value_ptr;
|
||||
const slab_stat = slab.getStats(key);
|
||||
|
||||
slab_stats.appendAssumeCapacity(slab_stat);
|
||||
|
||||
stats.total_allocated_bytes += slab_stat.bytes_allocated;
|
||||
stats.bytes_in_use += slab_stat.bytes_in_use;
|
||||
stats.bytes_free += slab_stat.bytes_free;
|
||||
stats.total_chunks += slab_stat.chunk_count;
|
||||
stats.total_slots += slab_stat.total_slots;
|
||||
stats.slots_in_use += slab_stat.slots_in_use;
|
||||
stats.slots_free += slab_stat.slots_free;
|
||||
}
|
||||
|
||||
if (stats.total_allocated_bytes > 0) {
|
||||
stats.fragmentation_ratio = @as(f64, @floatFromInt(stats.bytes_free)) /
|
||||
@as(f64, @floatFromInt(stats.total_allocated_bytes));
|
||||
stats.utilization_ratio = @as(f64, @floatFromInt(stats.bytes_in_use)) /
|
||||
@as(f64, @floatFromInt(stats.total_allocated_bytes));
|
||||
}
|
||||
|
||||
stats.slabs = try slab_stats.toOwnedSlice(a);
|
||||
return stats;
|
||||
}
|
||||
|
||||
const vtable = Allocator.VTable{
|
||||
.alloc = alloc,
|
||||
.free = free,
|
||||
.remap = Allocator.noRemap,
|
||||
.resize = Allocator.noResize,
|
||||
};
|
||||
|
||||
pub fn allocator(self: *Self) Allocator {
|
||||
return .{
|
||||
.ptr = self,
|
||||
.vtable = &vtable,
|
||||
};
|
||||
}
|
||||
|
||||
fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
_ = ret_addr;
|
||||
|
||||
const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits());
|
||||
|
||||
const list_gop = self.slabs.getOrPut(
|
||||
self.child_allocator,
|
||||
SlabKey{ .size = aligned_len, .alignment = alignment },
|
||||
) catch return null;
|
||||
|
||||
if (!list_gop.found_existing) {
|
||||
list_gop.value_ptr.* = Slab.init(
|
||||
self.child_allocator,
|
||||
alignment,
|
||||
aligned_len,
|
||||
self.max_slot_count,
|
||||
) catch return null;
|
||||
}
|
||||
|
||||
const list = list_gop.value_ptr;
|
||||
const buf = list.alloc(self.child_allocator) catch return null;
|
||||
return buf[0..len].ptr;
|
||||
}
|
||||
|
||||
fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
_ = ret_addr;
|
||||
|
||||
const ptr = memory.ptr;
|
||||
const len = memory.len;
|
||||
const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits());
|
||||
const list = self.slabs.getPtr(.{ .size = aligned_len, .alignment = alignment }).?;
|
||||
list.free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
const TestSlabAllocator = SlabAllocator;
|
||||
|
||||
test "slab allocator - basic allocation and free" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Allocate some memory
|
||||
const ptr1 = try allocator.alloc(u8, 100);
|
||||
try testing.expect(ptr1.len == 100);
|
||||
|
||||
// Write to it to ensure it's valid
|
||||
@memset(ptr1, 42);
|
||||
try testing.expectEqual(@as(u8, 42), ptr1[50]);
|
||||
|
||||
// Free it
|
||||
allocator.free(ptr1);
|
||||
}
|
||||
|
||||
test "slab allocator - multiple allocations" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
const ptr1 = try allocator.alloc(u8, 64);
|
||||
const ptr2 = try allocator.alloc(u8, 128);
|
||||
const ptr3 = try allocator.alloc(u8, 256);
|
||||
|
||||
// Ensure they don't overlap
|
||||
const addr1 = @intFromPtr(ptr1.ptr);
|
||||
const addr2 = @intFromPtr(ptr2.ptr);
|
||||
const addr3 = @intFromPtr(ptr3.ptr);
|
||||
|
||||
try testing.expect(addr1 + 64 <= addr2 or addr2 + 128 <= addr1);
|
||||
try testing.expect(addr2 + 128 <= addr3 or addr3 + 256 <= addr2);
|
||||
|
||||
allocator.free(ptr1);
|
||||
allocator.free(ptr2);
|
||||
allocator.free(ptr3);
|
||||
}
|
||||
|
||||
test "slab allocator - no coalescing (different size classes)" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Allocate two blocks of same size
|
||||
const ptr1 = try allocator.alloc(u8, 128);
|
||||
const ptr2 = try allocator.alloc(u8, 128);
|
||||
|
||||
// Free them (no coalescing in slab allocator)
|
||||
allocator.free(ptr1);
|
||||
allocator.free(ptr2);
|
||||
|
||||
// Can't allocate larger block from these freed 128-byte blocks
|
||||
const ptr3 = try allocator.alloc(u8, 256);
|
||||
|
||||
// ptr3 will be from a different size class, not coalesced from ptr1+ptr2
|
||||
const addr1 = @intFromPtr(ptr1.ptr);
|
||||
const addr3 = @intFromPtr(ptr3.ptr);
|
||||
|
||||
// They should NOT be adjacent (different size classes)
|
||||
try testing.expect(addr3 < addr1 or addr3 >= addr1 + 256);
|
||||
|
||||
allocator.free(ptr3);
|
||||
}
|
||||
|
||||
test "slab allocator - reuse freed memory" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
const ptr1 = try allocator.alloc(u8, 64);
|
||||
const addr1 = @intFromPtr(ptr1.ptr);
|
||||
allocator.free(ptr1);
|
||||
|
||||
// Allocate same size, should reuse from same slab
|
||||
const ptr2 = try allocator.alloc(u8, 64);
|
||||
const addr2 = @intFromPtr(ptr2.ptr);
|
||||
|
||||
try testing.expectEqual(addr1, addr2);
|
||||
allocator.free(ptr2);
|
||||
}
|
||||
|
||||
test "slab allocator - multiple size classes" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Allocate various sizes - each creates a new slab
|
||||
var ptrs: [10][]u8 = undefined;
|
||||
const sizes = [_]usize{ 24, 40, 64, 88, 128, 144, 200, 256, 512, 1000 };
|
||||
|
||||
for (&ptrs, sizes) |*ptr, size| {
|
||||
ptr.* = try allocator.alloc(u8, size);
|
||||
@memset(ptr.*, 0xFF);
|
||||
}
|
||||
|
||||
// Should have created multiple slabs
|
||||
try testing.expect(slab_alloc.slabs.count() >= 10);
|
||||
|
||||
// Free all
|
||||
for (ptrs) |ptr| {
|
||||
allocator.free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
test "slab allocator - various sizes" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Test different sizes (not limited to powers of 2!)
|
||||
const sizes = [_]usize{ 8, 16, 24, 32, 40, 64, 88, 128, 144, 256 };
|
||||
|
||||
for (sizes) |size| {
|
||||
const ptr = try allocator.alloc(u8, size);
|
||||
try testing.expect(ptr.len == size);
|
||||
@memset(ptr, @intCast(size & 0xFF));
|
||||
allocator.free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
test "slab allocator - exact sizes (no rounding)" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Odd sizes stay exact (unlike buddy which rounds to power of 2)
|
||||
const ptr1 = try allocator.alloc(u8, 100);
|
||||
const ptr2 = try allocator.alloc(u8, 200);
|
||||
const ptr3 = try allocator.alloc(u8, 50);
|
||||
|
||||
// Exact sizes!
|
||||
try testing.expect(ptr1.len == 100);
|
||||
try testing.expect(ptr2.len == 200);
|
||||
try testing.expect(ptr3.len == 50);
|
||||
|
||||
allocator.free(ptr1);
|
||||
allocator.free(ptr2);
|
||||
allocator.free(ptr3);
|
||||
}
|
||||
|
||||
test "slab allocator - chunk allocation" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Allocate many items of same size to force multiple chunks
|
||||
var ptrs: [100][]u8 = undefined;
|
||||
for (&ptrs) |*ptr| {
|
||||
ptr.* = try allocator.alloc(u8, 64);
|
||||
}
|
||||
|
||||
// Should have allocated multiple chunks (32 items per chunk)
|
||||
const slab = slab_alloc.slabs.getPtr(.{ .size = 64, .alignment = Alignment.@"1" }).?;
|
||||
try testing.expect(slab.chunks.items.len > 1);
|
||||
|
||||
// Free all
|
||||
for (ptrs) |ptr| {
|
||||
allocator.free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
test "slab allocator - reset with retain_capacity" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Allocate some memory
|
||||
const ptr1 = try allocator.alloc(u8, 128);
|
||||
const ptr2 = try allocator.alloc(u8, 256);
|
||||
_ = ptr1;
|
||||
_ = ptr2;
|
||||
|
||||
const slabs_before = slab_alloc.slabs.count();
|
||||
const slab_128 = slab_alloc.slabs.getPtr(.{ .size = 128, .alignment = Alignment.@"1" }).?;
|
||||
const chunks_before = slab_128.chunks.items.len;
|
||||
|
||||
// Reset but keep chunks
|
||||
slab_alloc.reset(.retain_capacity);
|
||||
|
||||
try testing.expectEqual(slabs_before, slab_alloc.slabs.count());
|
||||
try testing.expectEqual(chunks_before, slab_128.chunks.items.len);
|
||||
|
||||
// Should be able to allocate again
|
||||
const ptr3 = try allocator.alloc(u8, 512);
|
||||
allocator.free(ptr3);
|
||||
}
|
||||
|
||||
test "slab allocator - reset with clear" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Allocate some memory
|
||||
const ptr1 = try allocator.alloc(u8, 128);
|
||||
_ = ptr1;
|
||||
|
||||
try testing.expect(slab_alloc.slabs.count() > 0);
|
||||
|
||||
// Reset and free everything
|
||||
slab_alloc.reset(.clear);
|
||||
|
||||
try testing.expectEqual(@as(usize, 0), slab_alloc.slabs.count());
|
||||
|
||||
// Should still work after reset
|
||||
const ptr2 = try allocator.alloc(u8, 256);
|
||||
allocator.free(ptr2);
|
||||
}
|
||||
|
||||
test "slab allocator - stress test" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
var prng = std.Random.DefaultPrng.init(0);
|
||||
const random = prng.random();
|
||||
|
||||
var ptrs: std.ArrayList([]u8) = .empty;
|
||||
|
||||
defer {
|
||||
for (ptrs.items) |ptr| {
|
||||
allocator.free(ptr);
|
||||
}
|
||||
ptrs.deinit(allocator);
|
||||
}
|
||||
|
||||
// Random allocations and frees
|
||||
var i: usize = 0;
|
||||
while (i < 100) : (i += 1) {
|
||||
if (random.boolean() and ptrs.items.len > 0) {
|
||||
// Free a random allocation
|
||||
const index = random.uintLessThan(usize, ptrs.items.len);
|
||||
allocator.free(ptrs.swapRemove(index));
|
||||
} else {
|
||||
// Allocate random size (8 to 512)
|
||||
const size = random.uintAtMost(usize, 504) + 8;
|
||||
const ptr = try allocator.alloc(u8, size);
|
||||
try ptrs.append(allocator, ptr);
|
||||
|
||||
// Write to ensure it's valid
|
||||
@memset(ptr, @intCast(i & 0xFF));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "slab allocator - alignment" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
const ptr1 = try allocator.create(u64);
|
||||
const ptr2 = try allocator.create(u32);
|
||||
const ptr3 = try allocator.create([100]u8);
|
||||
|
||||
allocator.destroy(ptr1);
|
||||
allocator.destroy(ptr2);
|
||||
allocator.destroy(ptr3);
|
||||
}
|
||||
|
||||
test "slab allocator - no resize support" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
const slice = try allocator.alloc(u8, 100);
|
||||
@memset(slice, 42);
|
||||
|
||||
// Resize should fail (not supported)
|
||||
try testing.expect(!allocator.resize(slice, 90));
|
||||
try testing.expect(!allocator.resize(slice, 200));
|
||||
|
||||
allocator.free(slice);
|
||||
}
|
||||
|
||||
test "slab allocator - fragmentation pattern" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Allocate 10 items
|
||||
var items: [10][]u8 = undefined;
|
||||
for (&items) |*item| {
|
||||
item.* = try allocator.alloc(u8, 64);
|
||||
@memset(item.*, 0xFF);
|
||||
}
|
||||
|
||||
// Free every other one
|
||||
allocator.free(items[0]);
|
||||
allocator.free(items[2]);
|
||||
allocator.free(items[4]);
|
||||
allocator.free(items[6]);
|
||||
allocator.free(items[8]);
|
||||
|
||||
// Allocate new items - should reuse freed slots
|
||||
const new1 = try allocator.alloc(u8, 64);
|
||||
const new2 = try allocator.alloc(u8, 64);
|
||||
const new3 = try allocator.alloc(u8, 64);
|
||||
|
||||
// Should get some of the freed slots back
|
||||
const addrs = [_]usize{
|
||||
@intFromPtr(items[0].ptr),
|
||||
@intFromPtr(items[2].ptr),
|
||||
@intFromPtr(items[4].ptr),
|
||||
@intFromPtr(items[6].ptr),
|
||||
@intFromPtr(items[8].ptr),
|
||||
};
|
||||
|
||||
const new1_addr = @intFromPtr(new1.ptr);
|
||||
var found = false;
|
||||
for (addrs) |addr| {
|
||||
if (new1_addr == addr) found = true;
|
||||
}
|
||||
try testing.expect(found);
|
||||
|
||||
// Cleanup
|
||||
allocator.free(items[1]);
|
||||
allocator.free(items[3]);
|
||||
allocator.free(items[5]);
|
||||
allocator.free(items[7]);
|
||||
allocator.free(items[9]);
|
||||
allocator.free(new1);
|
||||
allocator.free(new2);
|
||||
allocator.free(new3);
|
||||
}
|
||||
|
||||
test "slab allocator - many small allocations" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Allocate 1000 small items
|
||||
var ptrs: std.ArrayList([]u8) = .empty;
|
||||
defer {
|
||||
for (ptrs.items) |ptr| {
|
||||
allocator.free(ptr);
|
||||
}
|
||||
ptrs.deinit(allocator);
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < 1000) : (i += 1) {
|
||||
const ptr = try allocator.alloc(u8, 24);
|
||||
try ptrs.append(allocator, ptr);
|
||||
}
|
||||
|
||||
// Should have created multiple chunks
|
||||
const slab = slab_alloc.slabs.getPtr(.{ .size = 24, .alignment = Alignment.@"1" }).?;
|
||||
try testing.expect(slab.chunks.items.len > 1);
|
||||
}
|
||||
|
||||
test "slab allocator - zero waste for exact sizes" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// These sizes have zero internal fragmentation (unlike buddy)
|
||||
const sizes = [_]usize{ 24, 40, 56, 88, 144, 152, 184, 232, 648 };
|
||||
|
||||
for (sizes) |size| {
|
||||
const ptr = try allocator.alloc(u8, size);
|
||||
|
||||
// Exact size returned!
|
||||
try testing.expectEqual(size, ptr.len);
|
||||
|
||||
@memset(ptr, 0xFF);
|
||||
allocator.free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
test "slab allocator - different size classes don't interfere" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Allocate size 64
|
||||
const ptr_64 = try allocator.alloc(u8, 64);
|
||||
const addr_64 = @intFromPtr(ptr_64.ptr);
|
||||
allocator.free(ptr_64);
|
||||
|
||||
// Allocate size 128 - should NOT reuse size-64 slot
|
||||
const ptr_128 = try allocator.alloc(u8, 128);
|
||||
const addr_128 = @intFromPtr(ptr_128.ptr);
|
||||
|
||||
try testing.expect(addr_64 != addr_128);
|
||||
|
||||
// Allocate size 64 again - SHOULD reuse original slot
|
||||
const ptr_64_again = try allocator.alloc(u8, 64);
|
||||
const addr_64_again = @intFromPtr(ptr_64_again.ptr);
|
||||
|
||||
try testing.expectEqual(addr_64, addr_64_again);
|
||||
|
||||
allocator.free(ptr_128);
|
||||
allocator.free(ptr_64_again);
|
||||
}
|
||||
|
||||
test "slab allocator - 16-byte alignment" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
// Request 16-byte aligned memory
|
||||
const ptr = try allocator.alignedAlloc(u8, .@"16", 152);
|
||||
defer allocator.free(ptr);
|
||||
|
||||
// Verify alignment
|
||||
const addr = @intFromPtr(ptr.ptr);
|
||||
try testing.expect(addr % 16 == 0);
|
||||
|
||||
// Make sure we can use it
|
||||
@memset(ptr, 0xFF);
|
||||
}
|
||||
|
||||
test "slab allocator - various alignments" {
|
||||
var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);
|
||||
defer slab_alloc.deinit();
|
||||
|
||||
const allocator = slab_alloc.allocator();
|
||||
|
||||
const alignments = [_]std.mem.Alignment{ .@"1", .@"2", .@"4", .@"8", .@"16" };
|
||||
|
||||
inline for (alignments) |alignment| {
|
||||
const ptr = try allocator.alignedAlloc(u8, alignment, 100);
|
||||
defer allocator.free(ptr);
|
||||
|
||||
const addr = @intFromPtr(ptr.ptr);
|
||||
const align_value = alignment.toByteUnits();
|
||||
try testing.expect(addr % align_value == 0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user