Files
cobalt/src/cache.zig
T

170 lines
5.0 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
pub const BlockKey = struct {
fileId: u64,
offset: u64,
};
pub const CacheStats = struct {
hits: u64 = 0,
misses: u64 = 0,
pub fn hitRate(self: CacheStats) f64 {
const total = self.hits + self.misses;
if (total == 0) return 0;
return @as(f64, @floatFromInt(self.hits)) / @as(f64, @floatFromInt(total));
}
};
const Node = struct {
key: BlockKey,
bytes: []u8,
prev: ?*Node,
next: ?*Node,
};
pub const BlockCache = struct {
allocator: Allocator,
capacityBytes: usize,
currentBytes: usize,
map: std.AutoHashMap(BlockKey, *Node),
head: ?*Node,
tail: ?*Node,
stats: CacheStats,
pub fn init(allocator: Allocator, capacityBytes: usize) BlockCache {
return .{
.allocator = allocator,
.capacityBytes = capacityBytes,
.currentBytes = 0,
.map = std.AutoHashMap(BlockKey, *Node).init(allocator),
.head = null,
.tail = null,
.stats = .{},
};
}
pub fn deinit(self: *BlockCache) void {
var node = self.head;
while (node) |current| {
const nextNode = current.next;
self.allocator.free(current.bytes);
self.allocator.destroy(current);
node = nextNode;
}
self.map.deinit();
}
pub fn get(self: *BlockCache, key: BlockKey) ?[]const u8 {
if (self.map.get(key)) |node| {
self.detach(node);
self.pushFront(node);
self.stats.hits += 1;
return node.bytes;
}
self.stats.misses += 1;
return null;
}
pub fn put(self: *BlockCache, key: BlockKey, bytes: []const u8) !void {
if (self.map.get(key)) |node| {
self.currentBytes = self.currentBytes - node.bytes.len + bytes.len;
self.allocator.free(node.bytes);
node.bytes = try self.allocator.dupe(u8, bytes);
self.detach(node);
self.pushFront(node);
try self.evict();
return;
}
const node = try self.allocator.create(Node);
errdefer self.allocator.destroy(node);
node.* = .{
.key = key,
.bytes = try self.allocator.dupe(u8, bytes),
.prev = null,
.next = null,
};
errdefer self.allocator.free(node.bytes);
try self.map.put(key, node);
self.currentBytes += node.bytes.len;
self.pushFront(node);
try self.evict();
}
fn evict(self: *BlockCache) !void {
while (self.currentBytes > self.capacityBytes and self.map.count() > 1) {
const victim = self.tail orelse break;
self.detach(victim);
_ = self.map.remove(victim.key);
self.currentBytes -= victim.bytes.len;
self.allocator.free(victim.bytes);
self.allocator.destroy(victim);
}
}
fn detach(self: *BlockCache, node: *Node) void {
if (node.prev) |p| p.next = node.next else self.head = node.next;
if (node.next) |n| n.prev = node.prev else self.tail = node.prev;
node.prev = null;
node.next = null;
}
fn pushFront(self: *BlockCache, node: *Node) void {
node.prev = null;
node.next = self.head;
if (self.head) |h| h.prev = node;
self.head = node;
if (self.tail == null) self.tail = node;
}
};
test "block cache stores and serves" {
const allocator = std.testing.allocator;
var cache = BlockCache.init(allocator, 1024);
defer cache.deinit();
const key = BlockKey{ .fileId = 1, .offset = 0 };
try std.testing.expectEqual(@as(?[]const u8, null), cache.get(key));
try std.testing.expectEqual(@as(u64, 1), cache.stats.misses);
try cache.put(key, "block-bytes");
const got = cache.get(key).?;
try std.testing.expectEqualStrings("block-bytes", got);
try std.testing.expectEqual(@as(u64, 1), cache.stats.hits);
}
test "block cache evicts least recently used" {
const allocator = std.testing.allocator;
var cache = BlockCache.init(allocator, 20);
defer cache.deinit();
const a = BlockKey{ .fileId = 1, .offset = 0 };
const b = BlockKey{ .fileId = 1, .offset = 100 };
const c = BlockKey{ .fileId = 1, .offset = 200 };
try cache.put(a, "0123456789");
try cache.put(b, "0123456789");
_ = cache.get(a);
try cache.put(c, "0123456789");
try std.testing.expect(cache.get(b) == null);
try std.testing.expect(cache.get(a) != null);
try std.testing.expect(cache.get(c) != null);
}
test "block cache keeps most recent under tiny capacity" {
const allocator = std.testing.allocator;
var cache = BlockCache.init(allocator, 1);
defer cache.deinit();
const a = BlockKey{ .fileId = 2, .offset = 0 };
const b = BlockKey{ .fileId = 2, .offset = 50 };
try cache.put(a, "big-payload");
try cache.put(b, "big-payload");
try std.testing.expect(cache.get(a) == null);
try std.testing.expect(cache.get(b) != null);
}