terminal: fix bitmap allocator word boundaries

Bitmap allocations of at most 64 chunks only searched within a single
bitmap word. Contiguous free space crossing a word boundary was therefore
invisible and could force an unnecessary page growth. Larger requests
could also advance past the final bitmap before returning out of memory.

Search adjacent words for small cross-boundary runs and guard the final
word in the large-allocation path. Add boundary regressions and a
randomized oracle that verifies both placement and bitmap mutation.
This commit is contained in:
Mitchell Hashimoto
2026-07-11 16:37:58 -07:00
parent a887df42c5
commit dcfaa92553

View File

@@ -282,6 +282,12 @@ fn findFreeChunks(bitmaps: []u64, n: usize) ?usize {
rem -= 64;
}
// The prefix may consume the final bitmap while leaving up to
// one bitmap of chunks still required. The loop above only
// checks bounds when more than one full bitmap remains, so guard
// the final partial-or-full bitmap separately.
if (i >= bitmaps.len) return null;
// If the number of available chunks at the start of this bitmap
// is less than the remaining required, we have to try again.
if (@ctz(~bitmaps[i]) < rem) continue;
@@ -317,18 +323,34 @@ fn findFreeChunks(bitmaps: []u64, n: usize) ?usize {
var shifted: u64 = bitmap.*;
for (1..n) |i| shifted &= bitmap.* >> @intCast(i);
// If we have zero then we have no matches
if (shifted == 0) continue;
if (shifted != 0) {
// Trailing zeroes gets us the index of the first bit index with at
// least `n` sequential 1s. In the example above, that would be `4`.
const bit = @ctz(shifted);
// Trailing zeroes gets us the index of the first bit index with at
// least `n` sequential 1s. In the example above, that would be `4`.
const bit = @ctz(shifted);
// Calculate the mask so we can mark it as used.
const mask = (@as(u64, std.math.maxInt(u64)) >> @intCast(64 - n)) << @intCast(bit);
bitmap.* ^= mask;
// Calculate the mask so we can mark it as used
const mask = (@as(u64, std.math.maxInt(u64)) >> @intCast(64 - n)) << @intCast(bit);
bitmap.* ^= mask;
return (idx * 64) + bit;
}
return (idx * 64) + bit;
// A run of at most 64 chunks can also straddle two bitmap words.
// The single-word search above cannot see it. Since there was no
// in-word match, the free suffix is necessarily shorter than n.
if (idx + 1 < bitmaps.len) {
const suffix = @clz(~bitmap.*);
if (suffix > 0) {
const next_prefix = @ctz(~bitmaps[idx + 1]);
if (suffix + next_prefix >= n) {
const start_bit = 64 - suffix;
const rem = n - suffix;
bitmap.* ^= @as(u64, std.math.maxInt(u64)) << @intCast(start_bit);
bitmaps[idx + 1] ^= @as(u64, std.math.maxInt(u64)) >> @intCast(64 - rem);
return idx * 64 + start_bit;
}
}
}
}
return null;
@@ -385,6 +407,30 @@ test "findFreeChunks exactly 64 chunks" {
try testing.expectEqual(@as(usize, 0), idx);
}
test "findFreeChunks small allocation across bitmap boundary" {
const testing = std.testing;
var bitmaps = [_]u64{
@as(u64, 1) << 63,
0b1,
};
const idx = findFreeChunks(&bitmaps, 2).?;
try testing.expectEqual(@as(usize, 63), idx);
try testing.expectEqualSlices(u64, &.{ 0, 0 }, &bitmaps);
}
test "findFreeChunks 64 chunks across bitmap boundary" {
const testing = std.testing;
var bitmaps = [_]u64{
@as(u64, std.math.maxInt(u64)) << 32,
@as(u64, std.math.maxInt(u64)) >> 32,
};
const idx = findFreeChunks(&bitmaps, 64).?;
try testing.expectEqual(@as(usize, 32), idx);
try testing.expectEqualSlices(u64, &.{ 0, 0 }, &bitmaps);
}
test "findFreeChunks larger than 64 chunks" {
const testing = std.testing;
@@ -404,6 +450,14 @@ test "findFreeChunks larger than 64 chunks" {
try testing.expectEqual(@as(usize, 0), idx);
}
test "findFreeChunks larger than available bitmap" {
const testing = std.testing;
var bitmaps = [_]u64{std.math.maxInt(u64)};
try testing.expectEqual(null, findFreeChunks(&bitmaps, 65));
try testing.expectEqual(std.math.maxInt(u64), bitmaps[0]);
}
test "findFreeChunks larger than 64 chunks not at beginning" {
const testing = std.testing;
@@ -447,6 +501,44 @@ test "findFreeChunks larger than 64 chunks exact" {
try testing.expectEqual(@as(usize, 0), idx);
}
test "findFreeChunks random operations against bit oracle" {
const testing = std.testing;
const Oracle = struct {
fn find(bitmaps: []const u64, n: usize) ?usize {
var run: usize = 0;
for (0..bitmaps.len * 64) |i| {
const free = bitmaps[i / 64] &
(@as(u64, 1) << @intCast(i % 64)) != 0;
if (free) {
run += 1;
if (run == n) return i + 1 - n;
} else {
run = 0;
}
}
return null;
}
};
var prng = std.Random.DefaultPrng.init(0xB17);
const random = prng.random();
for (0..10_000) |_| {
var bitmaps: [4]u64 = undefined;
for (&bitmaps) |*bitmap| bitmap.* = random.int(u64);
var expected = bitmaps;
const n = random.intRangeAtMost(usize, 1, 96);
const expected_idx = Oracle.find(&bitmaps, n);
if (expected_idx) |start| {
for (start..start + n) |i| {
expected[i / 64] &= ~(@as(u64, 1) << @intCast(i % 64));
}
}
try testing.expectEqual(expected_idx, findFreeChunks(&bitmaps, n));
try testing.expectEqualSlices(u64, &expected, &bitmaps);
}
}
test "BitmapAllocator layout" {
const Alloc = BitmapAllocator(4);
const cap = 64 * 4;