obj: add object graph relations implementation using MPSC queue
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
78bc07c69b
commit
ae863c2431
2 changed files with 655 additions and 0 deletions
654
src/graph.zig
Normal file
654
src/graph.zig
Normal file
|
|
@ -0,0 +1,654 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
const Queue = @import("mpsc.zig").Queue;
|
||||||
|
const Pool = @import("mpsc.zig").Pool;
|
||||||
|
|
||||||
|
/// Node in the graph, represents an object that can have parent/child relations
|
||||||
|
const Node = struct {
|
||||||
|
id: u64,
|
||||||
|
next: ?*Node = null, // Used by Pool, otherwise stores next_sibling
|
||||||
|
parent: ?*Node = null,
|
||||||
|
first_child: ?*Node = null,
|
||||||
|
|
||||||
|
/// Returns true if this node has the given node as a child
|
||||||
|
fn hasChild(n: *Node, child_id: u64) bool {
|
||||||
|
var current = n.first_child;
|
||||||
|
while (current) |child| : (current = child.next) {
|
||||||
|
if (child.id == child_id) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a child to this node's children list
|
||||||
|
fn addChild(n: *Node, child: *Node) void {
|
||||||
|
var last_child: ?*Node = null;
|
||||||
|
var current = n.first_child;
|
||||||
|
while (current) |curr| : (current = curr.next) {
|
||||||
|
last_child = curr;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.parent = n;
|
||||||
|
if (last_child) |last| last.next = child else n.first_child = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a child from this node's children list
|
||||||
|
fn removeChild(n: *Node, child_id: u64) void {
|
||||||
|
if (n.first_child) |first| {
|
||||||
|
if (first.id == child_id) {
|
||||||
|
n.first_child = first.next;
|
||||||
|
if (first.parent == n) {
|
||||||
|
first.parent = null;
|
||||||
|
first.next = null; // Clear the next pointer
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prev = first;
|
||||||
|
var current = first.next;
|
||||||
|
while (current) |curr| {
|
||||||
|
if (curr.id == child_id) {
|
||||||
|
prev.next = curr.next;
|
||||||
|
if (curr.parent == n) {
|
||||||
|
curr.parent = null;
|
||||||
|
curr.next = null; // Clear the next pointer
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prev = curr;
|
||||||
|
current = curr.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Error set for graph operations
|
||||||
|
pub const Error = error{
|
||||||
|
OutOfMemory,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read/write/mutate operations to perform on the graph.
|
||||||
|
const Op = union(enum) {
|
||||||
|
/// Add a child to a parent. no-op if already a child.
|
||||||
|
add_child: struct { parent_id: u64, child_id: u64 },
|
||||||
|
|
||||||
|
/// Remove a child from a parent. no-op if not a child.
|
||||||
|
remove_child: struct { parent_id: u64, child_id: u64 },
|
||||||
|
|
||||||
|
/// Set the parent of a child node. no-op if already has this parent.
|
||||||
|
set_parent: struct { child_id: u64, parent_id: u64 },
|
||||||
|
|
||||||
|
/// Remove the parent of a child node. no-op if no parent.
|
||||||
|
remove_parent: struct { child_id: u64 },
|
||||||
|
|
||||||
|
/// Query the children of the given node
|
||||||
|
get_children: struct {
|
||||||
|
node_id: u64,
|
||||||
|
result: *std.ArrayListUnmanaged(u64),
|
||||||
|
err: *?Error,
|
||||||
|
done: *std.atomic.Value(bool),
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Query the parent of a node
|
||||||
|
get_parent: struct {
|
||||||
|
node_id: u64,
|
||||||
|
result: *?u64,
|
||||||
|
done: *std.atomic.Value(bool),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A graph of objects with parent/child/etc relations.
|
||||||
|
///
|
||||||
|
/// Reads/writes/mutations to the graph can be performed by any thread in parallel. This is possible
|
||||||
|
/// without a global mutex locking the entire graph because we represent all interactions with the
|
||||||
|
/// graph as /operations/ enqueued to a lock-free Multi Producer, Single Consumer (MPSC) FIFO queue.
|
||||||
|
///
|
||||||
|
/// When an operation is desired (adding a parent to a child, querying the children or parent of a
|
||||||
|
/// node, etc.) it is enqueued. Then, if the queue contains entries, that thread becomes the
|
||||||
|
/// consumer of the MPSC queue temporarily and processes all pending operations in the queue.
|
||||||
|
///
|
||||||
|
/// The graph uses lock-free pools to manage all nodes internally, eliminating runtime allocations
|
||||||
|
/// during operation processing.
|
||||||
|
pub const Graph = struct {
|
||||||
|
/// Queue of read/write/mutation operations to the graph.
|
||||||
|
queue: Queue(Op),
|
||||||
|
|
||||||
|
/// Pool of nodes for allocation and recycling.
|
||||||
|
nodes: Pool(Node),
|
||||||
|
|
||||||
|
/// Maps node IDs to their struct, protected by a read-write lock
|
||||||
|
id_to_node: struct {
|
||||||
|
map: std.AutoHashMapUnmanaged(u64, *Node) = .{},
|
||||||
|
lock: std.Thread.RwLock = .{},
|
||||||
|
} = .{},
|
||||||
|
|
||||||
|
/// Pool of ArrayLists for query results
|
||||||
|
result_lists: struct {
|
||||||
|
available: std.ArrayListUnmanaged(*std.ArrayListUnmanaged(u64)) = .{},
|
||||||
|
lock: std.Thread.RwLock = .{},
|
||||||
|
} = .{},
|
||||||
|
|
||||||
|
preallocate_result_list_size: u32,
|
||||||
|
|
||||||
|
/// Initialize the graph with the given pre-allocated space for nodes and operations.
|
||||||
|
pub fn init(
|
||||||
|
graph: *Graph,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
preallocate: struct {
|
||||||
|
queue_size: u32,
|
||||||
|
nodes_size: u32,
|
||||||
|
num_result_lists: u32,
|
||||||
|
result_list_size: u32,
|
||||||
|
},
|
||||||
|
) !void {
|
||||||
|
graph.* = .{
|
||||||
|
.queue = undefined,
|
||||||
|
.nodes = undefined,
|
||||||
|
.preallocate_result_list_size = preallocate.result_list_size,
|
||||||
|
};
|
||||||
|
|
||||||
|
try graph.queue.init(allocator, preallocate.queue_size);
|
||||||
|
errdefer graph.queue.deinit(allocator);
|
||||||
|
|
||||||
|
try graph.id_to_node.map.ensureTotalCapacity(allocator, preallocate.nodes_size);
|
||||||
|
errdefer graph.id_to_node.map.deinit(allocator);
|
||||||
|
|
||||||
|
graph.nodes = try Pool(Node).init(allocator, preallocate.nodes_size);
|
||||||
|
errdefer graph.nodes.deinit(allocator);
|
||||||
|
|
||||||
|
// Pre-allocate result lists
|
||||||
|
try graph.result_lists.available.ensureTotalCapacity(allocator, preallocate.num_result_lists);
|
||||||
|
errdefer {
|
||||||
|
for (graph.result_lists.available.items) |list| {
|
||||||
|
list.deinit(allocator);
|
||||||
|
allocator.destroy(list);
|
||||||
|
}
|
||||||
|
graph.result_lists.available.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (0..preallocate.num_result_lists) |_| {
|
||||||
|
var list = try allocator.create(std.ArrayListUnmanaged(u64));
|
||||||
|
errdefer allocator.destroy(list);
|
||||||
|
list.* = .{};
|
||||||
|
try list.ensureTotalCapacity(allocator, preallocate.result_list_size);
|
||||||
|
try graph.result_lists.available.append(allocator, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(graph: *Graph, allocator: std.mem.Allocator) void {
|
||||||
|
for (graph.result_lists.available.items) |list| {
|
||||||
|
list.deinit(allocator);
|
||||||
|
allocator.destroy(list);
|
||||||
|
}
|
||||||
|
graph.result_lists.available.deinit(allocator);
|
||||||
|
graph.id_to_node.map.deinit(allocator);
|
||||||
|
graph.nodes.deinit(allocator);
|
||||||
|
graph.queue.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an existing Node for the given ID, or null if not found
|
||||||
|
fn getNode(graph: *Graph, id: u64) ?*Node {
|
||||||
|
graph.id_to_node.lock.lockShared();
|
||||||
|
defer graph.id_to_node.lock.unlockShared();
|
||||||
|
return graph.id_to_node.map.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to take all queued operations to the graph and, if successful, processes them.
|
||||||
|
///
|
||||||
|
/// A different thread which calls processQueue() may beat us to acquiring all of the queued
|
||||||
|
/// operations, in which case this function may return before they are processed.
|
||||||
|
fn processQueue(graph: *Graph, allocator: std.mem.Allocator) void {
|
||||||
|
if (graph.queue.takeAll()) |nodes| {
|
||||||
|
defer graph.queue.releaseAll(nodes);
|
||||||
|
|
||||||
|
// Process the entire chain of nodes
|
||||||
|
var current: ?*Queue(Op).Node = nodes;
|
||||||
|
while (current) |node| {
|
||||||
|
graph.processOp(allocator, node.value);
|
||||||
|
current = node.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a node has any relationships and if not, removes it from the graph
|
||||||
|
inline fn cleanupIsolatedNode(graph: *Graph, node: *Node) void {
|
||||||
|
if (node.parent != null or node.first_child != null) return;
|
||||||
|
graph.id_to_node.lock.lock();
|
||||||
|
defer graph.id_to_node.lock.unlock();
|
||||||
|
_ = graph.id_to_node.map.remove(node.id);
|
||||||
|
graph.nodes.release(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a single operation to the graph.
|
||||||
|
inline fn processOp(graph: *Graph, allocator: std.mem.Allocator, op: Op) void {
|
||||||
|
switch (op) {
|
||||||
|
.add_child => |data| {
|
||||||
|
const parent = graph.getNode(data.parent_id) orelse return;
|
||||||
|
const new_child = graph.getNode(data.child_id) orelse return;
|
||||||
|
|
||||||
|
if (!parent.hasChild(new_child.id)) parent.addChild(new_child);
|
||||||
|
},
|
||||||
|
|
||||||
|
.remove_child => |data| {
|
||||||
|
const parent = graph.getNode(data.parent_id) orelse return;
|
||||||
|
const child = graph.getNode(data.child_id) orelse return;
|
||||||
|
parent.removeChild(data.child_id);
|
||||||
|
graph.cleanupIsolatedNode(parent);
|
||||||
|
graph.cleanupIsolatedNode(child);
|
||||||
|
},
|
||||||
|
|
||||||
|
.set_parent => |data| {
|
||||||
|
const child = graph.getNode(data.child_id) orelse return;
|
||||||
|
const new_parent = graph.getNode(data.parent_id) orelse return;
|
||||||
|
|
||||||
|
if (child.parent) |old_parent| {
|
||||||
|
if (old_parent == new_parent) return;
|
||||||
|
old_parent.removeChild(child.id);
|
||||||
|
graph.cleanupIsolatedNode(old_parent);
|
||||||
|
}
|
||||||
|
new_parent.addChild(child);
|
||||||
|
},
|
||||||
|
|
||||||
|
.remove_parent => |data| {
|
||||||
|
const child = graph.getNode(data.child_id) orelse return;
|
||||||
|
if (child.parent) |parent| {
|
||||||
|
parent.removeChild(child.id);
|
||||||
|
graph.cleanupIsolatedNode(parent);
|
||||||
|
graph.cleanupIsolatedNode(child);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.get_children => |query| {
|
||||||
|
const node = graph.getNode(query.node_id) orelse {
|
||||||
|
// Instead of just storing done, we return an empty result
|
||||||
|
query.done.store(true, .release);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
var current = node.first_child;
|
||||||
|
while (current) |child| : (current = child.next) {
|
||||||
|
query.result.append(allocator, child.id) catch |err| {
|
||||||
|
query.err.* = err;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
query.done.store(true, .release);
|
||||||
|
},
|
||||||
|
|
||||||
|
.get_parent => |query| {
|
||||||
|
const node = graph.getNode(query.node_id) orelse {
|
||||||
|
query.result.* = null;
|
||||||
|
query.done.store(true, .release);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
query.result.* = if (node.parent) |parent| parent.id else null;
|
||||||
|
query.done.store(true, .release);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// preallocateNodes2 ensures graph.id_to_node contains an entry for the two given IDs.
|
||||||
|
fn preallocateNodes2(graph: *Graph, allocator: std.mem.Allocator, id1: u64, id2: u64) !void {
|
||||||
|
graph.id_to_node.lock.lock();
|
||||||
|
defer graph.id_to_node.lock.unlock();
|
||||||
|
|
||||||
|
// Preallocate first node
|
||||||
|
const result1 = try graph.id_to_node.map.getOrPut(allocator, id1);
|
||||||
|
if (!result1.found_existing) {
|
||||||
|
const node = try graph.nodes.acquire(allocator);
|
||||||
|
node.* = .{ .id = id1 };
|
||||||
|
result1.value_ptr.* = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preallocate second node
|
||||||
|
const result2 = try graph.id_to_node.map.getOrPut(allocator, id2);
|
||||||
|
if (!result2.found_existing) {
|
||||||
|
const node = try graph.nodes.acquire(allocator);
|
||||||
|
node.* = .{ .id = id2 };
|
||||||
|
result2.value_ptr.* = node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addChild(graph: *Graph, allocator: std.mem.Allocator, parent_id: u64, child_id: u64) Error!void {
|
||||||
|
try graph.preallocateNodes2(allocator, parent_id, child_id);
|
||||||
|
|
||||||
|
try graph.queue.push(allocator, .{ .add_child = .{
|
||||||
|
.parent_id = parent_id,
|
||||||
|
.child_id = child_id,
|
||||||
|
} });
|
||||||
|
graph.processQueue(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn removeChild(graph: *Graph, allocator: std.mem.Allocator, parent_id: u64, child_id: u64) Error!void {
|
||||||
|
try graph.preallocateNodes2(allocator, parent_id, child_id);
|
||||||
|
try graph.queue.push(allocator, .{ .remove_child = .{
|
||||||
|
.parent_id = parent_id,
|
||||||
|
.child_id = child_id,
|
||||||
|
} });
|
||||||
|
graph.processQueue(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setParent(graph: *Graph, allocator: std.mem.Allocator, child_id: u64, parent_id: u64) Error!void {
|
||||||
|
try graph.preallocateNodes2(allocator, child_id, parent_id);
|
||||||
|
|
||||||
|
try graph.queue.push(allocator, .{ .set_parent = .{
|
||||||
|
.child_id = child_id,
|
||||||
|
.parent_id = parent_id,
|
||||||
|
} });
|
||||||
|
graph.processQueue(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn removeParent(graph: *Graph, allocator: std.mem.Allocator, child_id: u64) Error!void {
|
||||||
|
try graph.queue.push(allocator, .{ .remove_parent = .{
|
||||||
|
.child_id = child_id,
|
||||||
|
} });
|
||||||
|
graph.processQueue(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Results = struct {
|
||||||
|
// The actual result items. Read-only.
|
||||||
|
items: []const u64,
|
||||||
|
|
||||||
|
// Internal / private fields.
|
||||||
|
internal_list: *std.ArrayListUnmanaged(u64),
|
||||||
|
internal_graph: *Graph,
|
||||||
|
|
||||||
|
// Deinit returns the allocation back to the Graph memory pool for reuse in the future.
|
||||||
|
pub fn deinit(r: Results) void {
|
||||||
|
r.internal_graph.releaseResultList(r.internal_list);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getChildren(graph: *Graph, allocator: std.mem.Allocator, id: u64) Error!Results {
|
||||||
|
const results = try graph.acquireResultList(allocator, graph.preallocate_result_list_size);
|
||||||
|
errdefer graph.releaseResultList(results);
|
||||||
|
|
||||||
|
var done = std.atomic.Value(bool).init(false);
|
||||||
|
var err: ?Error = null;
|
||||||
|
|
||||||
|
try graph.queue.push(allocator, .{ .get_children = .{
|
||||||
|
.node_id = id,
|
||||||
|
.result = results,
|
||||||
|
.err = &err,
|
||||||
|
.done = &done,
|
||||||
|
} });
|
||||||
|
|
||||||
|
while (!done.load(.acquire)) {
|
||||||
|
graph.processQueue(allocator);
|
||||||
|
std.Thread.yield() catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) |e| return e;
|
||||||
|
return Results{
|
||||||
|
.items = results.items,
|
||||||
|
.internal_list = results,
|
||||||
|
.internal_graph = graph,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn acquireResultList(graph: *Graph, allocator: std.mem.Allocator, min_capacity: usize) !*std.ArrayListUnmanaged(u64) {
|
||||||
|
// Try to get an existing list first
|
||||||
|
graph.result_lists.lock.lock();
|
||||||
|
const list = graph.result_lists.available.popOrNull();
|
||||||
|
graph.result_lists.lock.unlock();
|
||||||
|
|
||||||
|
if (list) |l| {
|
||||||
|
errdefer {
|
||||||
|
graph.result_lists.lock.lock();
|
||||||
|
defer graph.result_lists.lock.unlock();
|
||||||
|
graph.result_lists.available.appendAssumeCapacity(l);
|
||||||
|
}
|
||||||
|
try l.ensureTotalCapacity(allocator, min_capacity);
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new result list if needed
|
||||||
|
var new_list = try allocator.create(std.ArrayListUnmanaged(u64));
|
||||||
|
errdefer allocator.destroy(new_list);
|
||||||
|
new_list.* = .{};
|
||||||
|
try new_list.ensureTotalCapacity(allocator, graph.preallocate_result_list_size);
|
||||||
|
return new_list;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn releaseResultList(graph: *Graph, list: *std.ArrayListUnmanaged(u64)) void {
|
||||||
|
list.clearRetainingCapacity();
|
||||||
|
|
||||||
|
graph.result_lists.lock.lock();
|
||||||
|
defer graph.result_lists.lock.unlock();
|
||||||
|
graph.result_lists.available.appendAssumeCapacity(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getParent(graph: *Graph, allocator: std.mem.Allocator, id: u64) Error!?u64 {
|
||||||
|
var result: ?u64 = null;
|
||||||
|
var done = std.atomic.Value(bool).init(false);
|
||||||
|
|
||||||
|
try graph.queue.push(allocator, .{ .get_parent = .{
|
||||||
|
.node_id = id,
|
||||||
|
.result = &result,
|
||||||
|
.done = &done,
|
||||||
|
} });
|
||||||
|
|
||||||
|
while (!done.load(.acquire)) {
|
||||||
|
graph.processQueue(allocator);
|
||||||
|
std.Thread.yield() catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "basic child addition and querying" {
|
||||||
|
const allocator = testing.allocator;
|
||||||
|
var graph: Graph = undefined;
|
||||||
|
try graph.init(allocator, .{ .queue_size = 32, .nodes_size = 32, .num_result_lists = 8, .result_list_size = 8 });
|
||||||
|
defer graph.deinit(allocator);
|
||||||
|
|
||||||
|
try graph.addChild(allocator, 1, 2);
|
||||||
|
|
||||||
|
const results = try graph.getChildren(allocator, 1);
|
||||||
|
defer results.deinit();
|
||||||
|
try testing.expectEqual(results.items.len, 1);
|
||||||
|
try testing.expectEqual(results.items[0], 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "basic parent querying" {
|
||||||
|
const allocator = testing.allocator;
|
||||||
|
var graph: Graph = undefined;
|
||||||
|
try graph.init(allocator, .{ .queue_size = 32, .nodes_size = 32, .num_result_lists = 8, .result_list_size = 8 });
|
||||||
|
defer graph.deinit(allocator);
|
||||||
|
|
||||||
|
try graph.addChild(allocator, 1, 2);
|
||||||
|
const parent = try graph.getParent(allocator, 2);
|
||||||
|
try testing.expectEqual(parent.?, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "child removal" {
|
||||||
|
const allocator = testing.allocator;
|
||||||
|
var graph: Graph = undefined;
|
||||||
|
try graph.init(allocator, .{ .queue_size = 32, .nodes_size = 32, .num_result_lists = 8, .result_list_size = 8 });
|
||||||
|
defer graph.deinit(allocator);
|
||||||
|
|
||||||
|
try graph.addChild(allocator, 1, 2);
|
||||||
|
try graph.removeChild(allocator, 1, 2);
|
||||||
|
|
||||||
|
const results = try graph.getChildren(allocator, 1);
|
||||||
|
defer results.deinit();
|
||||||
|
try testing.expectEqual(results.items.len, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parent setting" {
|
||||||
|
const allocator = testing.allocator;
|
||||||
|
var graph: Graph = undefined;
|
||||||
|
try graph.init(allocator, .{ .queue_size = 32, .nodes_size = 32, .num_result_lists = 8, .result_list_size = 8 });
|
||||||
|
defer graph.deinit(allocator);
|
||||||
|
|
||||||
|
try graph.addChild(allocator, 1, 2);
|
||||||
|
try graph.setParent(allocator, 2, 3); // Move child 2 from parent 1 to parent 3
|
||||||
|
|
||||||
|
const parent = try graph.getParent(allocator, 2);
|
||||||
|
try testing.expectEqual(parent.?, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parent removal" {
|
||||||
|
const allocator = testing.allocator;
|
||||||
|
var graph: Graph = undefined;
|
||||||
|
try graph.init(allocator, .{ .queue_size = 32, .nodes_size = 32, .num_result_lists = 8, .result_list_size = 8 });
|
||||||
|
defer graph.deinit(allocator);
|
||||||
|
|
||||||
|
try graph.addChild(allocator, 1, 2);
|
||||||
|
try graph.removeParent(allocator, 2);
|
||||||
|
|
||||||
|
const parent = try graph.getParent(allocator, 2);
|
||||||
|
try testing.expectEqual(parent, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "graph - idempotent child addition" {
|
||||||
|
const allocator = testing.allocator;
|
||||||
|
var graph: Graph = undefined;
|
||||||
|
try graph.init(allocator, .{
|
||||||
|
.queue_size = 256,
|
||||||
|
.nodes_size = 64,
|
||||||
|
.num_result_lists = 32,
|
||||||
|
.result_list_size = 32,
|
||||||
|
});
|
||||||
|
defer graph.deinit(allocator);
|
||||||
|
|
||||||
|
try graph.addChild(allocator, 1, 2);
|
||||||
|
try graph.addChild(allocator, 1, 2); // Add same child twice
|
||||||
|
|
||||||
|
const results = try graph.getChildren(allocator, 1);
|
||||||
|
defer results.deinit();
|
||||||
|
try testing.expectEqual(results.items.len, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "graph - deep hierarchy and chain operations" {
|
||||||
|
const allocator = testing.allocator;
|
||||||
|
var graph: Graph = undefined;
|
||||||
|
try graph.init(allocator, .{
|
||||||
|
.queue_size = 256,
|
||||||
|
.nodes_size = 64,
|
||||||
|
.num_result_lists = 32,
|
||||||
|
.result_list_size = 32,
|
||||||
|
});
|
||||||
|
defer graph.deinit(allocator);
|
||||||
|
|
||||||
|
try graph.addChild(allocator, 1, 2);
|
||||||
|
try graph.addChild(allocator, 2, 3);
|
||||||
|
try graph.addChild(allocator, 3, 4);
|
||||||
|
try graph.addChild(allocator, 4, 5);
|
||||||
|
|
||||||
|
// Verify chain
|
||||||
|
try testing.expectEqual((try graph.getParent(allocator, 5)).?, 4);
|
||||||
|
try testing.expectEqual((try graph.getParent(allocator, 4)).?, 3);
|
||||||
|
try testing.expectEqual((try graph.getParent(allocator, 3)).?, 2);
|
||||||
|
try testing.expectEqual((try graph.getParent(allocator, 2)).?, 1);
|
||||||
|
|
||||||
|
// Test reparenting middle of chain
|
||||||
|
try graph.setParent(allocator, 3, 1); // Move 3 to be under 1 directly
|
||||||
|
|
||||||
|
// Verify chain was broken correctly
|
||||||
|
try testing.expectEqual((try graph.getParent(allocator, 3)).?, 1);
|
||||||
|
const results = try graph.getChildren(allocator, 2);
|
||||||
|
defer results.deinit();
|
||||||
|
try testing.expectEqual(results.items.len, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "graph - cleanup of isolated nodes" {
|
||||||
|
const allocator = testing.allocator;
|
||||||
|
var graph: Graph = undefined;
|
||||||
|
try graph.init(allocator, .{
|
||||||
|
.queue_size = 256,
|
||||||
|
.nodes_size = 64,
|
||||||
|
.num_result_lists = 32,
|
||||||
|
.result_list_size = 32,
|
||||||
|
});
|
||||||
|
defer graph.deinit(allocator);
|
||||||
|
|
||||||
|
// First verify the initial state
|
||||||
|
try graph.addChild(allocator, 1, 2);
|
||||||
|
try graph.addChild(allocator, 2, 3);
|
||||||
|
|
||||||
|
// Verify initial setup
|
||||||
|
{
|
||||||
|
const results1 = try graph.getChildren(allocator, 1);
|
||||||
|
defer results1.deinit();
|
||||||
|
try testing.expectEqual(results1.items.len, 1);
|
||||||
|
try testing.expectEqual(results1.items[0], 2);
|
||||||
|
|
||||||
|
const results2 = try graph.getChildren(allocator, 2);
|
||||||
|
defer results2.deinit();
|
||||||
|
try testing.expectEqual(results2.items.len, 1);
|
||||||
|
try testing.expectEqual(results2.items[0], 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the parent-child relationship
|
||||||
|
try graph.removeChild(allocator, 1, 2);
|
||||||
|
|
||||||
|
// Node 2 should still exist and have node 3 as its child
|
||||||
|
const node2_children = try graph.getChildren(allocator, 2);
|
||||||
|
defer node2_children.deinit();
|
||||||
|
try testing.expectEqual(node2_children.items.len, 1);
|
||||||
|
try testing.expectEqual(node2_children.items[0], 3);
|
||||||
|
|
||||||
|
// But node 2 should no longer have a parent
|
||||||
|
try testing.expectEqual(try graph.getParent(allocator, 2), null);
|
||||||
|
|
||||||
|
// Node 3 should still have node 2 as its parent
|
||||||
|
try testing.expectEqual((try graph.getParent(allocator, 3)).?, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "graph - edge cases with non-existent nodes" {
|
||||||
|
const allocator = testing.allocator;
|
||||||
|
var graph: Graph = undefined;
|
||||||
|
try graph.init(allocator, .{
|
||||||
|
.queue_size = 256,
|
||||||
|
.nodes_size = 64,
|
||||||
|
.num_result_lists = 32,
|
||||||
|
.result_list_size = 32,
|
||||||
|
});
|
||||||
|
defer graph.deinit(allocator);
|
||||||
|
|
||||||
|
// Test querying non-existent nodes
|
||||||
|
const results = try graph.getChildren(allocator, 99999);
|
||||||
|
defer results.deinit();
|
||||||
|
try testing.expectEqual(results.items.len, 0);
|
||||||
|
try testing.expectEqual(try graph.getParent(allocator, 99999), null);
|
||||||
|
|
||||||
|
// These should not crash
|
||||||
|
try graph.removeChild(allocator, 99999, 1);
|
||||||
|
try graph.removeChild(allocator, 1, 99999);
|
||||||
|
try graph.removeParent(allocator, 99999);
|
||||||
|
|
||||||
|
// Add child to non-existent parent (should create both nodes)
|
||||||
|
try graph.addChild(allocator, 100, 101);
|
||||||
|
const results2 = (try graph.getChildren(allocator, 100));
|
||||||
|
defer results2.deinit();
|
||||||
|
try testing.expectEqual(results2.items.len, 1);
|
||||||
|
try testing.expectEqual(results2.items[0], 101);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "graph - multiple operations consistency" {
|
||||||
|
const allocator = testing.allocator;
|
||||||
|
var graph: Graph = undefined;
|
||||||
|
try graph.init(allocator, .{
|
||||||
|
.queue_size = 256,
|
||||||
|
.nodes_size = 64,
|
||||||
|
.num_result_lists = 32,
|
||||||
|
.result_list_size = 32,
|
||||||
|
});
|
||||||
|
defer graph.deinit(allocator);
|
||||||
|
|
||||||
|
try graph.addChild(allocator, 1, 2);
|
||||||
|
|
||||||
|
try graph.addChild(allocator, 1, 3);
|
||||||
|
try graph.addChild(allocator, 2, 4);
|
||||||
|
try graph.setParent(allocator, 3, 2); // Move 3 under 2
|
||||||
|
try graph.removeParent(allocator, 4); // Remove 4's parent
|
||||||
|
|
||||||
|
const results = try graph.getChildren(allocator, 2);
|
||||||
|
defer results.deinit();
|
||||||
|
try testing.expectEqual(results.items.len, 1);
|
||||||
|
try testing.expectEqual(results.items[0], 3);
|
||||||
|
|
||||||
|
try testing.expectEqual(try graph.getParent(allocator, 4), null);
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,7 @@ test {
|
||||||
_ = testing;
|
_ = testing;
|
||||||
_ = time;
|
_ = time;
|
||||||
_ = @import("mpsc.zig");
|
_ = @import("mpsc.zig");
|
||||||
|
_ = @import("graph.zig");
|
||||||
std.testing.refAllDeclsRecursive(gamemode);
|
std.testing.refAllDeclsRecursive(gamemode);
|
||||||
std.testing.refAllDeclsRecursive(math);
|
std.testing.refAllDeclsRecursive(math);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue