mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
add LRU
This commit is contained in:
234
src/lru.zig
Normal file
234
src/lru.zig
Normal file
@ -0,0 +1,234 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Create a HashMap for a key type that can be autoamtically hashed.
|
||||
/// If you want finer-grained control, use HashMap directly.
|
||||
pub fn AutoHashMap(comptime K: type, comptime V: type) type {
|
||||
return HashMap(
|
||||
K,
|
||||
V,
|
||||
std.hash_map.AutoContext(K),
|
||||
std.hash_map.default_max_load_percentage,
|
||||
);
|
||||
}
|
||||
|
||||
/// HashMap implementation that supports least-recently-used eviction.
|
||||
///
|
||||
/// Note: This is a really elementary CS101 version of an LRU right now.
|
||||
/// This is done initially to get something working. Once we have it working,
|
||||
/// we can benchmark and improve if this ends up being a source of slowness.
|
||||
pub fn HashMap(
|
||||
comptime K: type,
|
||||
comptime V: type,
|
||||
comptime Context: type,
|
||||
comptime max_load_percentage: u64,
|
||||
) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
const Map = std.HashMapUnmanaged(K, *Queue.Node, Context, max_load_percentage);
|
||||
const Queue = std.TailQueue(KV);
|
||||
|
||||
/// Map to maintain our entries.
|
||||
map: Map,
|
||||
|
||||
/// Queue to maintain LRU order.
|
||||
queue: Queue,
|
||||
|
||||
/// The capacity of our map. If this capacity is reached, cache
|
||||
/// misses will begin evicting entries.
|
||||
capacity: Map.Size,
|
||||
|
||||
pub const KV = struct {
|
||||
key: K,
|
||||
value: V,
|
||||
};
|
||||
|
||||
/// The result of a getOrPut operation.
|
||||
pub const GetOrPutResult = struct {
|
||||
/// The entry that was retrieved. If found_existing is false,
|
||||
/// then this is a pointer to allocated space to store a V.
|
||||
/// If found_existing is true, the pointer value is valid, but
|
||||
/// can be overwritten.
|
||||
value_ptr: *V,
|
||||
|
||||
/// Whether an existing value was found or not.
|
||||
found_existing: bool,
|
||||
|
||||
/// If another entry had to be evicted to make space for this
|
||||
/// put operation, then this is the value that was evicted.
|
||||
evicted: ?KV,
|
||||
};
|
||||
|
||||
pub fn init(capacity: Map.Size) Self {
|
||||
return .{
|
||||
.map = .{},
|
||||
.queue = .{},
|
||||
.capacity = capacity,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, alloc: Allocator) void {
|
||||
// Important: use our queue as a source of truth for dealloc
|
||||
// because we might keep items in the queue around that aren't
|
||||
// present in our LRU anymore to prevent future allocations.
|
||||
var it = self.queue.first;
|
||||
while (it) |node| {
|
||||
it = node.next;
|
||||
alloc.destroy(node);
|
||||
}
|
||||
|
||||
self.map.deinit(alloc);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Get or put a value for a key. See GetOrPutResult on how to check
|
||||
/// if an existing value was found, if an existing value was evicted,
|
||||
/// etc.
|
||||
pub fn getOrPut(self: *Self, allocator: Allocator, key: K) Allocator.Error!GetOrPutResult {
|
||||
if (@sizeOf(Context) != 0)
|
||||
@compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getOrPutContext instead.");
|
||||
return self.getOrPutContext(allocator, key, undefined);
|
||||
}
|
||||
|
||||
/// See getOrPut
|
||||
pub fn getOrPutContext(
|
||||
self: *Self,
|
||||
alloc: Allocator,
|
||||
key: K,
|
||||
ctx: Context,
|
||||
) Allocator.Error!GetOrPutResult {
|
||||
const map_gop = try self.map.getOrPutContext(alloc, key, ctx);
|
||||
if (map_gop.found_existing) {
|
||||
// Move to end to mark as most recently used
|
||||
self.queue.remove(map_gop.value_ptr.*);
|
||||
self.queue.append(map_gop.value_ptr.*);
|
||||
|
||||
return GetOrPutResult{
|
||||
.found_existing = true,
|
||||
.value_ptr = &map_gop.value_ptr.*.data.value,
|
||||
.evicted = null,
|
||||
};
|
||||
}
|
||||
errdefer _ = self.map.remove(key);
|
||||
|
||||
// We're evicting if our map insertion increased our capacity.
|
||||
const evict = self.map.count() > self.capacity;
|
||||
|
||||
// Get our node. If we're not evicting then we allocate a new
|
||||
// node. If we are evicting then we avoid allocation by just
|
||||
// reusing the node we would've evicted.
|
||||
var node = if (!evict) try alloc.create(Queue.Node) else node: {
|
||||
// Our first node is the least recently used.
|
||||
var least_used = self.queue.first.?;
|
||||
|
||||
// Move our least recently used to the end to make
|
||||
// it the most recently used.
|
||||
self.queue.remove(least_used);
|
||||
|
||||
// Remove the least used from the map
|
||||
_ = self.map.remove(least_used.data.key);
|
||||
|
||||
break :node least_used;
|
||||
};
|
||||
errdefer if (!evict) alloc.destroy(node);
|
||||
|
||||
// Store our node in the map.
|
||||
map_gop.value_ptr.* = node;
|
||||
|
||||
// Mark the node as most recently used
|
||||
self.queue.append(node);
|
||||
|
||||
// Set our key
|
||||
node.data.key = key;
|
||||
|
||||
return GetOrPutResult{
|
||||
.found_existing = map_gop.found_existing,
|
||||
.value_ptr = &node.data.value,
|
||||
.evicted = if (!evict) null else node.data,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get a value for a key.
|
||||
pub fn get(self: *Self, key: K) ?V {
|
||||
if (@sizeOf(Context) != 0) {
|
||||
@compileError("getContext must be used.");
|
||||
}
|
||||
return self.getContext(key, undefined);
|
||||
}
|
||||
|
||||
/// See get
|
||||
pub fn getContext(self: *Self, key: K, ctx: Context) ?V {
|
||||
const node = self.map.getContext(key, ctx) orelse return null;
|
||||
return node.data.value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "getOrPut" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const Map = AutoHashMap(u32, u8);
|
||||
var m = Map.init(2);
|
||||
defer m.deinit(alloc);
|
||||
|
||||
// Insert cap values, should be hits
|
||||
{
|
||||
const gop = try m.getOrPut(alloc, 1);
|
||||
try testing.expect(!gop.found_existing);
|
||||
try testing.expect(gop.evicted == null);
|
||||
gop.value_ptr.* = 1;
|
||||
}
|
||||
{
|
||||
const gop = try m.getOrPut(alloc, 2);
|
||||
try testing.expect(!gop.found_existing);
|
||||
try testing.expect(gop.evicted == null);
|
||||
gop.value_ptr.* = 2;
|
||||
}
|
||||
|
||||
// 1 is LRU
|
||||
try testing.expect((try m.getOrPut(alloc, 1)).found_existing);
|
||||
try testing.expect((try m.getOrPut(alloc, 2)).found_existing);
|
||||
|
||||
// Next should evict
|
||||
{
|
||||
const gop = try m.getOrPut(alloc, 3);
|
||||
try testing.expect(!gop.found_existing);
|
||||
try testing.expect(gop.evicted != null);
|
||||
try testing.expect(gop.evicted.?.value == 1);
|
||||
gop.value_ptr.* = 3;
|
||||
}
|
||||
|
||||
// Currently: 2 is LRU, let's make 3 LRU
|
||||
try testing.expect((try m.getOrPut(alloc, 2)).found_existing);
|
||||
|
||||
// Next should evict
|
||||
{
|
||||
const gop = try m.getOrPut(alloc, 4);
|
||||
try testing.expect(!gop.found_existing);
|
||||
try testing.expect(gop.evicted != null);
|
||||
try testing.expect(gop.evicted.?.value == 3);
|
||||
gop.value_ptr.* = 4;
|
||||
}
|
||||
}
|
||||
|
||||
test "get" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const Map = AutoHashMap(u32, u8);
|
||||
var m = Map.init(2);
|
||||
defer m.deinit(alloc);
|
||||
|
||||
// Insert cap values, should be hits
|
||||
{
|
||||
const gop = try m.getOrPut(alloc, 1);
|
||||
try testing.expect(!gop.found_existing);
|
||||
try testing.expect(gop.evicted == null);
|
||||
gop.value_ptr.* = 1;
|
||||
}
|
||||
|
||||
try testing.expect(m.get(1) != null);
|
||||
try testing.expect(m.get(1).? == 1);
|
||||
try testing.expect(m.get(2) == null);
|
||||
}
|
@ -119,4 +119,5 @@ test {
|
||||
// TODO
|
||||
_ = @import("config.zig");
|
||||
_ = @import("cli_args.zig");
|
||||
_ = @import("lru.zig");
|
||||
}
|
||||
|
Reference in New Issue
Block a user