From 1eb874a1060fa0df0e68f68c4575271d728b44ce Mon Sep 17 00:00:00 2001 From: Cayman Date: Mon, 16 Mar 2026 16:03:00 -0400 Subject: [PATCH 1/2] refactor(ssz): introduce TreeViewState, replace ChildNodes Add TreeViewState struct with concrete typed methods for the common state (allocator, pool, root, children_nodes, changed) shared by all chunk-based tree views. Replaces duck-typed ChildNodes anytype dispatch. - BasicPackedChunks, CompositeChunks, BitArray embed TreeViewState - Update all callers (list/array/bit views) to access via .state.* - Delete child_nodes.zig utility Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ssz/tree_view/array_basic.zig | 22 +- src/ssz/tree_view/array_composite.zig | 16 +- src/ssz/tree_view/bit_array.zig | 60 ++---- src/ssz/tree_view/bit_list.zig | 18 +- src/ssz/tree_view/bit_vector.zig | 18 +- src/ssz/tree_view/chunks.zig | 221 +++++++------------- src/ssz/tree_view/list_basic.zig | 60 +++--- src/ssz/tree_view/list_composite.zig | 66 +++--- src/ssz/tree_view/utils/child_nodes.zig | 106 ---------- src/ssz/tree_view/utils/tree_view_state.zig | 123 +++++++++++ 10 files changed, 323 insertions(+), 387 deletions(-) delete mode 100644 src/ssz/tree_view/utils/child_nodes.zig create mode 100644 src/ssz/tree_view/utils/tree_view_state.zig diff --git a/src/ssz/tree_view/array_basic.zig b/src/ssz/tree_view/array_basic.zig index b4ed34a02..9ec2bba61 100644 --- a/src/ssz/tree_view/array_basic.zig +++ b/src/ssz/tree_view/array_basic.zig @@ -73,12 +73,12 @@ pub fn ArrayBasicTreeView(comptime ST: type) type { pub fn hashTreeRootInto(self: *Self, out: *[32]u8) !void { try self.commit(); - out.* = self.chunks.root.getRoot(self.chunks.pool).*; + out.* = self.chunks.state.root.getRoot(self.chunks.state.pool).*; } pub fn hashTreeRoot(self: *Self) !*const [32]u8 { try self.commit(); - return self.chunks.root.getRoot(self.chunks.pool); + return self.chunks.state.root.getRoot(self.chunks.state.pool); } pub fn fromValue(allocator: Allocator, pool: *Node.Pool, value: *const ST.Type) !*Self { @@ -89,11 +89,11 @@ pub fn ArrayBasicTreeView(comptime ST: type) type { pub fn toValue(self: *Self, _: Allocator, out: *ST.Type) !void { try self.commit(); - try ST.tree.toValue(self.chunks.root, self.chunks.pool, out); + try ST.tree.toValue(self.chunks.state.root, self.chunks.state.pool, out); } pub fn getRoot(self: *const Self) Node.Id { - return self.chunks.root; + return self.chunks.state.root; } pub fn get(self: *Self, index: usize) !Element { @@ -119,7 +119,7 @@ pub fn ArrayBasicTreeView(comptime ST: type) type { /// Returns the number of bytes written. pub fn serializeIntoBytes(self: *Self, out: []u8) !usize { try self.commit(); - return try ST.tree.serializeIntoBytes(self.chunks.root, self.chunks.pool, out); + return try ST.tree.serializeIntoBytes(self.chunks.state.root, self.chunks.state.pool, out); } /// Get the serialized size of this tree view. @@ -330,13 +330,13 @@ test "TreeView vector clone(true) does not transfer cache" { defer v.deinit(); _ = try v.get(0); - try std.testing.expect(v.chunks.children_nodes.count() > 0); + try std.testing.expect(v.chunks.state.children_nodes.count() > 0); var cloned_no_cache = try v.clone(.{ .transfer_cache = false }); defer cloned_no_cache.deinit(); - try std.testing.expect(v.chunks.children_nodes.count() > 0); - try std.testing.expectEqual(@as(usize, 0), cloned_no_cache.chunks.children_nodes.count()); + try std.testing.expect(v.chunks.state.children_nodes.count() > 0); + try std.testing.expectEqual(@as(usize, 0), cloned_no_cache.chunks.state.children_nodes.count()); } test "TreeView vector clone(false) transfers cache and clears source" { @@ -354,13 +354,13 @@ test "TreeView vector clone(false) transfers cache and clears source" { defer v.deinit(); _ = try v.get(0); - try std.testing.expect(v.chunks.children_nodes.count() > 0); + try std.testing.expect(v.chunks.state.children_nodes.count() > 0); var cloned = try v.clone(.{}); defer cloned.deinit(); - try std.testing.expectEqual(@as(usize, 0), v.chunks.children_nodes.count()); - try std.testing.expect(cloned.chunks.children_nodes.count() > 0); + try std.testing.expectEqual(@as(usize, 0), v.chunks.state.children_nodes.count()); + try std.testing.expect(cloned.chunks.state.children_nodes.count() > 0); } // Tests ported from TypeScript ssz packages/ssz/test/unit/byType/vector/tree.test.ts diff --git a/src/ssz/tree_view/array_composite.zig b/src/ssz/tree_view/array_composite.zig index a28fc4e92..eb8c86a30 100644 --- a/src/ssz/tree_view/array_composite.zig +++ b/src/ssz/tree_view/array_composite.zig @@ -76,12 +76,12 @@ pub fn ArrayCompositeTreeView(comptime ST: type) type { pub fn hashTreeRootInto(self: *Self, out: *[32]u8) !void { try self.commit(); - out.* = self.chunks.root.getRoot(self.chunks.pool).*; + out.* = self.chunks.state.root.getRoot(self.chunks.state.pool).*; } pub fn hashTreeRoot(self: *Self) !*const [32]u8 { try self.commit(); - return self.chunks.root.getRoot(self.chunks.pool); + return self.chunks.state.root.getRoot(self.chunks.state.pool); } pub fn fromValue(allocator: Allocator, pool: *Node.Pool, value: *const ST.Type) !*Self { @@ -96,14 +96,14 @@ pub fn ArrayCompositeTreeView(comptime ST: type) type { pub fn toValue(self: *Self, allocator: Allocator, out: *ST.Type) !void { try self.commit(); if (comptime isFixedType(ST)) { - try ST.tree.toValue(self.chunks.root, self.chunks.pool, out); + try ST.tree.toValue(self.chunks.state.root, self.chunks.state.pool, out); } else { - try ST.tree.toValue(allocator, self.chunks.root, self.chunks.pool, out); + try ST.tree.toValue(allocator, self.chunks.state.root, self.chunks.state.pool, out); } } pub fn getRoot(self: *const Self) Node.Id { - return self.chunks.root; + return self.chunks.state.root; } pub fn get(self: *Self, index: usize) !Element { @@ -130,7 +130,7 @@ pub fn ArrayCompositeTreeView(comptime ST: type) type { if (index >= length) return error.IndexOutOfBounds; const elem = try self.chunks.get(index); try elem.commit(); - return elem.getRoot().getRoot(self.chunks.pool); + return elem.getRoot().getRoot(self.chunks.state.pool); } pub fn set(self: *Self, index: usize, value: Element) !void { @@ -150,7 +150,7 @@ pub fn ArrayCompositeTreeView(comptime ST: type) type { /// Returns the number of bytes written. pub fn serializeIntoBytes(self: *Self, out: []u8) !usize { try self.commit(); - return try ST.tree.serializeIntoBytes(self.chunks.root, self.chunks.pool, out); + return try ST.tree.serializeIntoBytes(self.chunks.state.root, self.chunks.state.pool, out); } /// Get the serialized size of this tree view. @@ -159,7 +159,7 @@ pub fn ArrayCompositeTreeView(comptime ST: type) type { if (comptime isFixedType(ST)) { return ST.fixed_size; } else { - return ST.tree.serializedSize(self.chunks.root, self.chunks.pool); + return ST.tree.serializedSize(self.chunks.state.root, self.chunks.state.pool); } } }; diff --git a/src/ssz/tree_view/bit_array.zig b/src/ssz/tree_view/bit_array.zig index aefcaff1b..70dec220c 100644 --- a/src/ssz/tree_view/bit_array.zig +++ b/src/ssz/tree_view/bit_array.zig @@ -6,55 +6,35 @@ const Depth = hashing.Depth; const Node = @import("persistent_merkle_tree").Node; const Gindex = @import("persistent_merkle_tree").Gindex; -const ChildNodes = @import("utils/child_nodes.zig").ChildNodes; +const TreeViewState = @import("utils/tree_view_state.zig").TreeViewState; const CloneOpts = @import("utils/clone_opts.zig").CloneOpts; /// Provides common bit array operations for both BitVectorTreeView and BitListTreeView. pub fn BitArray(comptime chunk_depth: Depth) type { return struct { const bits_per_chunk: usize = 256; - allocator: Allocator, - pool: *Node.Pool, - root: Node.Id, - - /// cached nodes for faster access of already-visited children - children_nodes: std.AutoHashMapUnmanaged(Gindex, Node.Id), - - /// whether the corresponding child node/data has changed since the last update of the root - changed: std.AutoArrayHashMapUnmanaged(Gindex, void), + state: TreeViewState, const Self = @This(); pub fn init(self: *Self, allocator: Allocator, pool: *Node.Pool, root: Node.Id) !void { - try pool.ref(root); - errdefer pool.unref(root); - self.* = .{ - .allocator = allocator, - .pool = pool, - .root = root, - .children_nodes = .empty, - .changed = .empty, - }; + try self.state.init(allocator, pool, root); } pub fn clone(self: *Self, opts: CloneOpts, out: *Self) !void { - try ChildNodes.Change.cloneAndTransferCache(Self, self, opts, out); + try self.state.clone(opts, &out.state); } pub fn deinit(self: *Self) void { - self.pool.unref(self.root); - self.clearChildrenNodesCache(); - self.children_nodes.deinit(self.allocator); - self.changed.deinit(self.allocator); + self.state.deinit(); } pub fn commit(self: *Self) !void { - try ChildNodes.Change.commit(self); + try self.state.commitNodes(); } pub fn clearCache(self: *Self) void { - self.clearChildrenNodesCache(); - self.changed.clearRetainingCapacity(); + self.state.clearCache(); } pub fn get(self: *Self, index: usize, len: usize) !bool { @@ -65,8 +45,8 @@ pub fn BitArray(comptime chunk_depth: Depth) type { const byte_in_chunk = bit_in_chunk / 8; const bit_in_byte: u3 = @intCast(bit_in_chunk % 8); - const leaf_node = try self.getChildNode(Gindex.fromDepth(chunk_depth, chunk_index)); - const leaf = leaf_node.getRoot(self.pool); + const leaf_node = try self.state.getChildNode(Gindex.fromDepth(chunk_depth, chunk_index)); + const leaf = leaf_node.getRoot(self.state.pool); const mask = @as(u8, 1) << bit_in_byte; return (leaf[byte_in_chunk] & mask) != 0; } @@ -80,8 +60,8 @@ pub fn BitArray(comptime chunk_depth: Depth) type { const bit_in_byte: u3 = @intCast(bit_in_chunk % 8); const gindex = Gindex.fromDepth(chunk_depth, chunk_index); - const leaf_node = try self.getChildNode(gindex); - var leaf_bytes = leaf_node.getRoot(self.pool).*; + const leaf_node = try self.state.getChildNode(gindex); + var leaf_bytes = leaf_node.getRoot(self.state.pool).*; const mask = @as(u8, 1) << bit_in_byte; if (value) { @@ -90,8 +70,8 @@ pub fn BitArray(comptime chunk_depth: Depth) type { leaf_bytes[byte_in_chunk] &= ~mask; } - const new_leaf = try self.pool.createLeaf(&leaf_bytes); - try self.setChildNode(gindex, new_leaf); + const new_leaf = try self.state.pool.createLeaf(&leaf_bytes); + try self.state.setChildNode(gindex, new_leaf); } pub fn fillBools(self: *Self, values: []bool, len: usize) !void { @@ -103,8 +83,8 @@ pub fn BitArray(comptime chunk_depth: Depth) type { var dest = values; for (0..full_chunks) |chunk_idx| { - const leaf_node = try self.getChildNode(Gindex.fromDepth(chunk_depth, chunk_idx)); - const leaf = leaf_node.getRoot(self.pool); + const leaf_node = try self.state.getChildNode(Gindex.fromDepth(chunk_depth, chunk_idx)); + const leaf = leaf_node.getRoot(self.state.pool); for (leaf) |b| { inline for (0..8) |j| { @@ -115,8 +95,8 @@ pub fn BitArray(comptime chunk_depth: Depth) type { } if (remainder_bits != 0) { - const leaf_node = try self.getChildNode(Gindex.fromDepth(chunk_depth, full_chunks)); - const leaf = leaf_node.getRoot(self.pool); + const leaf_node = try self.state.getChildNode(Gindex.fromDepth(chunk_depth, full_chunks)); + const leaf = leaf_node.getRoot(self.state.pool); const full_bytes = remainder_bits / 8; const tail_bits = remainder_bits % 8; @@ -138,15 +118,15 @@ pub fn BitArray(comptime chunk_depth: Depth) type { } pub fn getChildNode(self: *Self, gindex: Gindex) !Node.Id { - return ChildNodes.getChildNode(self, gindex); + return self.state.getChildNode(gindex); } pub fn setChildNode(self: *Self, gindex: Gindex, node: Node.Id) !void { - try ChildNodes.setChildNode(self, gindex, node); + try self.state.setChildNode(gindex, node); } pub fn clearChildrenNodesCache(self: *Self) void { - ChildNodes.clearChildrenNodesCache(self, self.pool); + self.state.clearChildrenNodesCache(); } }; } diff --git a/src/ssz/tree_view/bit_list.zig b/src/ssz/tree_view/bit_list.zig index 873f5fe46..5f27c28c8 100644 --- a/src/ssz/tree_view/bit_list.zig +++ b/src/ssz/tree_view/bit_list.zig @@ -69,16 +69,16 @@ pub fn BitListTreeView(comptime ST: type) type { pub fn hashTreeRoot(self: *Self, out: *[32]u8) !void { try self.commit(); - out.* = self.data.root.getRoot(self.data.pool).*; + out.* = self.data.state.root.getRoot(self.data.state.pool).*; } pub fn getRoot(self: *const Self) Node.Id { - return self.data.root; + return self.data.state.root; } fn readLength(self: *Self) !usize { const length_node = try self.data.getChildNode(@enumFromInt(3)); - const length_chunk = length_node.getRoot(self.data.pool); + const length_chunk = length_node.getRoot(self.data.state.pool); return std.mem.readInt(usize, length_chunk[0..@sizeOf(usize)], .little); } @@ -167,13 +167,13 @@ test "BitListTreeView clone(true) does not transfer cache" { defer view.deinit(); _ = try view.get(0); - try std.testing.expect(view.data.children_nodes.count() > 0); + try std.testing.expect(view.data.state.children_nodes.count() > 0); var cloned_no_cache = try view.clone(.{ .transfer_cache = false }); defer cloned_no_cache.deinit(); - try std.testing.expect(view.data.children_nodes.count() > 0); - try std.testing.expectEqual(@as(usize, 0), cloned_no_cache.data.children_nodes.count()); + try std.testing.expect(view.data.state.children_nodes.count() > 0); + try std.testing.expectEqual(@as(usize, 0), cloned_no_cache.data.state.children_nodes.count()); } test "BitListTreeView clone(false) transfers cache and clears source" { @@ -193,13 +193,13 @@ test "BitListTreeView clone(false) transfers cache and clears source" { defer view.deinit(); _ = try view.get(0); - try std.testing.expect(view.data.children_nodes.count() > 0); + try std.testing.expect(view.data.state.children_nodes.count() > 0); var cloned = try view.clone(.{}); defer cloned.deinit(); - try std.testing.expectEqual(@as(usize, 0), view.data.children_nodes.count()); - try std.testing.expect(cloned.data.children_nodes.count() > 0); + try std.testing.expectEqual(@as(usize, 0), view.data.state.children_nodes.count()); + try std.testing.expect(cloned.data.state.children_nodes.count() > 0); } test "BitListTreeView clone isolates updates" { diff --git a/src/ssz/tree_view/bit_vector.zig b/src/ssz/tree_view/bit_vector.zig index 362cd11c8..228639d4e 100644 --- a/src/ssz/tree_view/bit_vector.zig +++ b/src/ssz/tree_view/bit_vector.zig @@ -66,11 +66,11 @@ pub fn BitVectorTreeView(comptime ST: type) type { pub fn hashTreeRoot(self: *Self, out: *[32]u8) !void { try self.commit(); - out.* = self.data.root.getRoot(self.data.pool).*; + out.* = self.data.state.root.getRoot(self.data.state.pool).*; } pub fn getRoot(self: *const Self) Node.Id { - return self.data.root; + return self.data.state.root; } pub fn get(self: *Self, index: usize) !Element { @@ -89,7 +89,7 @@ pub fn BitVectorTreeView(comptime ST: type) type { pub fn toValue(self: *Self, _: Allocator, out: *ST.Type) !void { try self.commit(); - try ST.tree.toValue(self.data.root, self.data.pool, out); + try ST.tree.toValue(self.data.state.root, self.data.state.pool, out); } /// Caller must free the returned slice. @@ -164,13 +164,13 @@ test "BitVectorTreeView clone(true) does not transfer cache" { defer view.deinit(); _ = try view.get(0); - try std.testing.expect(view.data.children_nodes.count() > 0); + try std.testing.expect(view.data.state.children_nodes.count() > 0); var cloned_no_cache = try view.clone(.{ .transfer_cache = false }); defer cloned_no_cache.deinit(); - try std.testing.expect(view.data.children_nodes.count() > 0); - try std.testing.expectEqual(@as(usize, 0), cloned_no_cache.data.children_nodes.count()); + try std.testing.expect(view.data.state.children_nodes.count() > 0); + try std.testing.expectEqual(@as(usize, 0), cloned_no_cache.data.state.children_nodes.count()); } test "BitVectorTreeView clone(false) transfers cache and clears source" { @@ -190,13 +190,13 @@ test "BitVectorTreeView clone(false) transfers cache and clears source" { defer view.deinit(); _ = try view.get(0); - try std.testing.expect(view.data.children_nodes.count() > 0); + try std.testing.expect(view.data.state.children_nodes.count() > 0); var cloned = try view.clone(.{}); defer cloned.deinit(); - try std.testing.expectEqual(@as(usize, 0), view.data.children_nodes.count()); - try std.testing.expect(cloned.data.children_nodes.count() > 0); + try std.testing.expectEqual(@as(usize, 0), view.data.state.children_nodes.count()); + try std.testing.expect(cloned.data.state.children_nodes.count() > 0); } test "BitVectorTreeView clone isolates updates" { diff --git a/src/ssz/tree_view/chunks.zig b/src/ssz/tree_view/chunks.zig index 4272eadf0..82cd72df7 100644 --- a/src/ssz/tree_view/chunks.zig +++ b/src/ssz/tree_view/chunks.zig @@ -10,7 +10,7 @@ const Gindex = @import("persistent_merkle_tree").Gindex; const isFixedType = @import("../type/type_kind.zig").isFixedType; const tree_view_root = @import("root.zig"); -const ChildNodes = @import("utils/child_nodes.zig").ChildNodes; +const TreeViewState = @import("utils/tree_view_state.zig").TreeViewState; const CloneOpts = @import("utils/clone_opts.zig").CloneOpts; /// Shared helpers for basic element types packed into chunks. @@ -20,73 +20,44 @@ pub fn BasicPackedChunks( comptime items_per_chunk: usize, ) type { return struct { - allocator: Allocator, - pool: *Node.Pool, - root: Node.Id, - - /// cached nodes for faster access of already-visited children - children_nodes: std.AutoHashMapUnmanaged(Gindex, Node.Id), - - /// whether the corresponding child node/data has changed since the last update of the root - changed: std.AutoArrayHashMapUnmanaged(Gindex, void), + state: TreeViewState, pub const Element = ST.Element.Type; const Self = @This(); pub fn init(self: *Self, allocator: Allocator, pool: *Node.Pool, root: Node.Id) !void { - try pool.ref(root); - errdefer pool.unref(root); - self.* = .{ - .allocator = allocator, - .pool = pool, - .root = root, - .children_nodes = .empty, - .changed = .empty, - }; + try self.state.init(allocator, pool, root); } pub fn clone(self: *Self, opts: CloneOpts, out: *Self) !void { - try ChildNodes.Change.cloneAndTransferCache(Self, self, opts, out); + try self.state.clone(opts, &out.state); } pub fn deinit(self: *Self) void { - self.pool.unref(self.root); - self.clearChildrenNodesCache(); - self.children_nodes.deinit(self.allocator); - self.changed.deinit(self.allocator); + self.state.deinit(); } pub fn commit(self: *Self) !void { - try ChildNodes.Change.commit(self); + try self.state.commitNodes(); } pub fn clearCache(self: *Self) void { - self.clearChildrenNodesCache(); - self.changed.clearRetainingCapacity(); + self.state.clearCache(); } pub fn get(self: *Self, index: usize) !Element { var value: Element = undefined; - const child_node = try self.getChildNode(Gindex.fromDepth(chunk_depth, index / items_per_chunk)); - try ST.Element.tree.toValuePacked(child_node, self.pool, index, &value); + const child_node = try self.state.getChildNode(Gindex.fromDepth(chunk_depth, index / items_per_chunk)); + try ST.Element.tree.toValuePacked(child_node, self.state.pool, index, &value); return value; } pub fn set(self: *Self, index: usize, value: Element) !void { const gindex = Gindex.fromDepth(chunk_depth, index / items_per_chunk); - try self.changed.put(self.allocator, gindex, {}); - const child_node = try self.getChildNode(gindex); - const opt_old_node = try self.children_nodes.fetchPut( - self.allocator, - gindex, - try ST.Element.tree.fromValuePacked(child_node, self.pool, index, &value), - ); - if (opt_old_node) |old_node| { - if (old_node.value.getState(self.pool).getRefCount() == 0) { - self.pool.unref(old_node.value); - } - } + const child_node = try self.state.getChildNode(gindex); + const new_node = try ST.Element.tree.fromValuePacked(child_node, self.state.pool, index, &value); + try self.state.setChildNode(gindex, new_node); } pub fn getAll( @@ -114,11 +85,11 @@ pub fn BasicPackedChunks( try self.populateAllNodes(chunk_count); for (0..len_full_chunks) |chunk_idx| { - const leaf_node = try self.getChildNode(Gindex.fromDepth(chunk_depth, chunk_idx)); + const leaf_node = try self.state.getChildNode(Gindex.fromDepth(chunk_depth, chunk_idx)); for (0..items_per_chunk) |i| { try ST.Element.tree.toValuePacked( leaf_node, - self.pool, + self.state.pool, i, &values[chunk_idx * items_per_chunk + i], ); @@ -126,11 +97,11 @@ pub fn BasicPackedChunks( } if (remainder > 0) { - const leaf_node = try self.getChildNode(Gindex.fromDepth(chunk_depth, len_full_chunks)); + const leaf_node = try self.state.getChildNode(Gindex.fromDepth(chunk_depth, len_full_chunks)); for (0..remainder) |i| { try ST.Element.tree.toValuePacked( leaf_node, - self.pool, + self.state.pool, i, &values[len_full_chunks * items_per_chunk + i], ); @@ -143,14 +114,14 @@ pub fn BasicPackedChunks( fn populateAllNodes(self: *Self, chunk_count: usize) !void { if (chunk_count == 0) return; - const nodes = try self.allocator.alloc(Node.Id, chunk_count); - defer self.allocator.free(nodes); + const nodes = try self.state.allocator.alloc(Node.Id, chunk_count); + defer self.state.allocator.free(nodes); - try self.root.getNodesAtDepth(self.pool, chunk_depth, 0, nodes); + try self.state.root.getNodesAtDepth(self.state.pool, chunk_depth, 0, nodes); for (nodes, 0..) |node, chunk_idx| { const gindex = Gindex.fromDepth(chunk_depth, chunk_idx); - const gop = try self.children_nodes.getOrPut(self.allocator, gindex); + const gop = try self.state.children_nodes.getOrPut(self.state.allocator, gindex); if (!gop.found_existing) { gop.value_ptr.* = node; } @@ -158,23 +129,23 @@ pub fn BasicPackedChunks( } pub fn getChildNode(self: *Self, gindex: Gindex) !Node.Id { - return ChildNodes.getChildNode(self, gindex); + return self.state.getChildNode(gindex); } pub fn setChildNode(self: *Self, gindex: Gindex, node: Node.Id) !void { - try ChildNodes.setChildNode(self, gindex, node); - } - - fn clearChildrenNodesCache(self: *Self) void { - ChildNodes.clearChildrenNodesCache(self, self.pool); + try self.state.setChildNode(gindex, node); } pub fn getLength(self: *Self) !usize { - return try ChildNodes.getLength(self); + const length_node = try self.state.getChildNode(@enumFromInt(3)); + const length_chunk = length_node.getRoot(self.state.pool); + return std.mem.readInt(usize, length_chunk[0..@sizeOf(usize)], .little); } pub fn setLength(self: *Self, length: usize) !void { - try ChildNodes.setLength(self, length); + const length_node = try self.state.pool.createLeafFromUint(@intCast(length)); + errdefer self.state.pool.unref(length_node); + try self.state.setChildNode(@enumFromInt(3), length_node); } }; } @@ -185,128 +156,96 @@ pub fn CompositeChunks( comptime chunk_depth: Depth, ) type { return struct { - allocator: Allocator, - pool: *Node.Pool, - root: Node.Id, - - /// cached nodes for faster access of already-visited children - children_nodes: std.AutoHashMapUnmanaged(Gindex, Node.Id), + state: TreeViewState, /// cached data for faster access of already-visited children children_data: std.AutoHashMapUnmanaged(Gindex, ElementPtr), - /// whether the corresponding child node/data has changed since the last update of the root - changed: std.AutoArrayHashMapUnmanaged(Gindex, void), - const Element = ST.Element.TreeView; pub const ElementPtr = *Element; const Self = @This(); pub fn init(self: *Self, allocator: Allocator, pool: *Node.Pool, root: Node.Id) !void { - try pool.ref(root); - errdefer pool.unref(root); - self.* = .{ - .allocator = allocator, - .pool = pool, - .root = root, - .children_nodes = .empty, - .children_data = .empty, - .changed = .empty, - }; + try self.state.init(allocator, pool, root); + self.children_data = .empty; } pub fn clone(self: *Self, opts: CloneOpts, out: *Self) !void { - try init(out, self.allocator, self.pool, self.root); - if (!opts.transfer_cache) { + try self.state.clone(opts, &out.state); + out.children_data = .empty; return; } - out.children_nodes = self.children_nodes; + // Transfer children_data, removing uncommitted entries. out.children_data = self.children_data; - - // Removing while iterating can invalidate the iterator. - for (self.changed.keys()) |gindex| { - if (out.children_data.fetchRemove(gindex)) |entry| { - entry.value.deinit(); + { + const changed_keys = self.state.changed.keys(); + for (changed_keys) |gindex| { + if (out.children_data.fetchRemove(gindex)) |entry| { + entry.value.deinit(); + } } } + // changed_keys borrow is now out of scope. - // clear self's caches - self.children_nodes = .empty; + // Clone state (transfers children_nodes, clears self caches). + try self.state.clone(opts, &out.state); self.children_data = .empty; - self.changed.clearRetainingCapacity(); } /// Deinitialize the Data and free all associated resources. /// This also deinits all child Data recursively. pub fn deinit(self: *Self) void { - self.pool.unref(self.root); - self.clearChildrenNodesCache(); - self.children_nodes.deinit(self.allocator); + const allocator = self.state.allocator; self.clearChildrenDataCache(); - self.children_data.deinit(self.allocator); - self.changed.deinit(self.allocator); + self.children_data.deinit(allocator); + self.state.deinit(); } pub fn commit(self: *Self) !void { - if (self.changed.count() == 0) { + if (self.state.changed.count() == 0) { return; } - const nodes = try self.allocator.alloc(Node.Id, self.changed.count()); - defer self.allocator.free(nodes); - - const gindices = self.changed.keys(); - Gindex.sortAsc(gindices); - - for (gindices, 0..) |gindex, i| { + // Flush child views into children_nodes so commitNodes can handle them uniformly. + for (self.state.changed.keys()) |gindex| { if (self.children_data.get(gindex)) |child_ptr| { - // TODO: compare with child_nodes to avoid unnecessary rebind try child_ptr.commit(); - nodes[i] = child_ptr.getRoot(); - } else if (self.children_nodes.get(gindex)) |child_node| { - nodes[i] = child_node; - } else { - return error.ChildNotFound; + const gop = try self.state.children_nodes.getOrPut(self.state.allocator, gindex); + gop.value_ptr.* = child_ptr.getRoot(); } } - const new_root = try self.root.setNodesGrouped(self.pool, gindices, nodes); - try self.pool.ref(new_root); - self.pool.unref(self.root); - self.root = new_root; - - self.changed.clearRetainingCapacity(); + try self.state.commitNodes(); } pub fn clearCache(self: *Self) void { - self.clearChildrenNodesCache(); + self.state.clearCache(); self.clearChildrenDataCache(); - self.changed.clearRetainingCapacity(); } pub fn get(self: *Self, index: usize) !ElementPtr { const gindex = Gindex.fromDepth(chunk_depth, index); // Always mark as changed - the child may have been previously cached // via getReadonly() without being tracked in changed. - try self.changed.put(self.allocator, gindex, {}); - const gop = try self.children_data.getOrPut(self.allocator, gindex); + try self.state.changed.put(self.state.allocator, gindex, {}); + const gop = try self.children_data.getOrPut(self.state.allocator, gindex); if (gop.found_existing) { return gop.value_ptr.*; } - const child_node = try self.getChildNode(gindex); - const child_ptr = try Element.init(self.allocator, self.pool, child_node); + const child_node = try self.state.getChildNode(gindex); + const child_ptr = try Element.init(self.state.allocator, self.state.pool, child_node); gop.value_ptr.* = child_ptr; return child_ptr; } pub fn set(self: *Self, index: usize, value: ElementPtr) !void { const gindex = Gindex.fromDepth(chunk_depth, index); - try self.changed.put(self.allocator, gindex, {}); + try self.state.changed.put(self.state.allocator, gindex, {}); const opt_old_data = try self.children_data.fetchPut( - self.allocator, + self.state.allocator, gindex, value, ); @@ -324,10 +263,10 @@ pub fn CompositeChunks( if (self.children_data.get(gindex)) |child_ptr| { return child_ptr; } - const child_node = try self.getChildNode(gindex); - const child_ptr = try Element.init(self.allocator, self.pool, child_node); - try self.children_data.put(self.allocator, gindex, child_ptr); - // Do NOT add to self.changed (read-only) + const child_node = try self.state.getChildNode(gindex); + const child_ptr = try Element.init(self.state.allocator, self.state.pool, child_node); + try self.children_data.put(self.state.allocator, gindex, child_ptr); + // Do NOT add to self.state.changed (read-only) return child_ptr; } @@ -355,9 +294,9 @@ pub fn CompositeChunks( /// Set a child from an SSZ value type. pub fn setValue(self: *Self, index: usize, value: *const Value) !void { - const root = try ST.Element.tree.fromValue(self.pool, value); - errdefer self.pool.unref(root); - const child_view = try Element.init(self.allocator, self.pool, root); + const root = try ST.Element.tree.fromValue(self.state.pool, value); + errdefer self.state.pool.unref(root); + const child_view = try Element.init(self.state.allocator, self.state.pool, root); errdefer child_view.deinit(); try self.set(index, child_view); } @@ -375,25 +314,25 @@ pub fn CompositeChunks( const len = values.len; if (len == 0) return values; - if (self.changed.count() != 0) { + if (self.state.changed.count() != 0) { return error.MustCommitBeforeBulkRead; } const nodes = try allocator.alloc(Node.Id, len); defer allocator.free(nodes); - try self.root.getNodesAtDepth(self.pool, chunk_depth, 0, nodes); + try self.state.root.getNodesAtDepth(self.state.pool, chunk_depth, 0, nodes); for (nodes, 0..) |node, i| { if (comptime @hasDecl(ST.Element, "deinit")) { errdefer { - for (values[0..i]) |*value| { - ST.Element.deinit(allocator, value); + for (values[0..i]) |*v| { + ST.Element.deinit(allocator, v); } } } if (comptime isFixedType(ST.Element)) { - try ST.Element.tree.toValue(node, self.pool, &values[i]); + try ST.Element.tree.toValue(node, self.state.pool, &values[i]); } else { // Initialize value to default before toValue for variable types // (e.g. BitList fields need initialized ArrayListUnmanaged) @@ -402,7 +341,7 @@ pub fn CompositeChunks( } else { values[i] = std.mem.zeroes(Value); } - try ST.Element.tree.toValue(allocator, node, self.pool, &values[i]); + try ST.Element.tree.toValue(allocator, node, self.state.pool, &values[i]); } } @@ -410,23 +349,23 @@ pub fn CompositeChunks( } pub fn getChildNode(self: *Self, gindex: Gindex) !Node.Id { - return ChildNodes.getChildNode(self, gindex); + return self.state.getChildNode(gindex); } pub fn setChildNode(self: *Self, gindex: Gindex, node: Node.Id) !void { - try ChildNodes.setChildNode(self, gindex, node); - } - - fn clearChildrenNodesCache(self: *Self) void { - ChildNodes.clearChildrenNodesCache(self, self.pool); + try self.state.setChildNode(gindex, node); } pub fn getLength(self: *Self) !usize { - return try ChildNodes.getLength(self); + const length_node = try self.state.getChildNode(@enumFromInt(3)); + const length_chunk = length_node.getRoot(self.state.pool); + return std.mem.readInt(usize, length_chunk[0..@sizeOf(usize)], .little); } pub fn setLength(self: *Self, length: usize) !void { - try ChildNodes.setLength(self, length); + const length_node = try self.state.pool.createLeafFromUint(@intCast(length)); + errdefer self.state.pool.unref(length_node); + try self.state.setChildNode(@enumFromInt(3), length_node); } fn clearChildrenDataCache(self: *Self) void { diff --git a/src/ssz/tree_view/list_basic.zig b/src/ssz/tree_view/list_basic.zig index 7a05fcf9d..18b30ca38 100644 --- a/src/ssz/tree_view/list_basic.zig +++ b/src/ssz/tree_view/list_basic.zig @@ -83,12 +83,12 @@ pub fn ListBasicTreeView(comptime ST: type) type { pub fn hashTreeRootInto(self: *Self, out: *[32]u8) !void { try self.commit(); - out.* = self.chunks.root.getRoot(self.chunks.pool).*; + out.* = self.chunks.state.root.getRoot(self.chunks.state.pool).*; } pub fn hashTreeRoot(self: *Self) !*const [32]u8 { try self.commit(); - return self.chunks.root.getRoot(self.chunks.pool); + return self.chunks.state.root.getRoot(self.chunks.state.pool); } pub fn fromValue(allocator: Allocator, pool: *Node.Pool, value: *const ST.Type) !*Self { @@ -99,7 +99,7 @@ pub fn ListBasicTreeView(comptime ST: type) type { pub fn toValue(self: *Self, allocator: Allocator, out: *ST.Type) !void { try self.commit(); - try ST.tree.toValue(allocator, self.chunks.root, self.chunks.pool, out); + try ST.tree.toValue(allocator, self.chunks.state.root, self.chunks.state.pool, out); } pub fn setLength(self: *Self, new_length: usize) !void { @@ -120,8 +120,8 @@ pub fn ListBasicTreeView(comptime ST: type) type { return .{ .tree_view = tree_view, .depth_iterator = Node.DepthIterator.init( - tree_view.chunks.pool, - tree_view.chunks.root, + tree_view.chunks.state.pool, + tree_view.chunks.state.root, ST.chunk_depth + 1, ST.chunkIndex(start_index), ), @@ -138,7 +138,7 @@ pub fn ListBasicTreeView(comptime ST: type) type { try self.depth_iterator.next(); self.elem_node = n; var value: Element = undefined; - try ST.Element.tree.toValuePacked(n, self.tree_view.chunks.pool, elem_index, &value); + try ST.Element.tree.toValuePacked(n, self.tree_view.chunks.state.pool, elem_index, &value); self.elem_index += 1; if (self.elem_index % items_per_chunk == 0) { self.elem_node = null; @@ -148,7 +148,7 @@ pub fn ListBasicTreeView(comptime ST: type) type { }; pub fn getRoot(self: *const Self) Node.Id { - return self.chunks.root; + return self.chunks.state.root; } pub fn length(self: *const Self) !usize { @@ -194,7 +194,7 @@ pub fn ListBasicTreeView(comptime ST: type) type { const list_length = try self.length(); if (list_length == 0 or index >= list_length - 1) { - return try Self.init(self.allocator, self.chunks.pool, self.chunks.root); + return try Self.init(self.allocator, self.chunks.state.pool, self.chunks.state.root); } const new_length = index + 1; @@ -204,52 +204,52 @@ pub fn ListBasicTreeView(comptime ST: type) type { const chunk_index = index / items_per_chunk; const chunk_offset = index % items_per_chunk; - const chunk_node = try Node.Id.getNodeAtDepth(self.chunks.root, self.chunks.pool, chunk_depth, chunk_index); + const chunk_node = try Node.Id.getNodeAtDepth(self.chunks.state.root, self.chunks.state.pool, chunk_depth, chunk_index); - var chunk_bytes = chunk_node.getRoot(self.chunks.pool).*; + var chunk_bytes = chunk_node.getRoot(self.chunks.state.pool).*; const keep_bytes = (chunk_offset + 1) * ST.Element.fixed_size; if (keep_bytes < BYTES_PER_CHUNK) { @memset(chunk_bytes[keep_bytes..], 0); } - var truncated_chunk_node: ?Node.Id = try self.chunks.pool.createLeaf(&chunk_bytes); - defer if (truncated_chunk_node) |id| self.chunks.pool.unref(id); + var truncated_chunk_node: ?Node.Id = try self.chunks.state.pool.createLeaf(&chunk_bytes); + defer if (truncated_chunk_node) |id| self.chunks.state.pool.unref(id); var updated: ?Node.Id = try Node.Id.setNodeAtDepth( - self.chunks.root, - self.chunks.pool, + self.chunks.state.root, + self.chunks.state.pool, chunk_depth, chunk_index, truncated_chunk_node.?, ); - defer if (updated) |id| self.chunks.pool.unref(id); + defer if (updated) |id| self.chunks.state.pool.unref(id); truncated_chunk_node = null; - var new_root: ?Node.Id = try Node.Id.truncateAfterIndex(updated.?, self.chunks.pool, chunk_depth, chunk_index); - defer if (new_root) |id| self.chunks.pool.unref(id); + var new_root: ?Node.Id = try Node.Id.truncateAfterIndex(updated.?, self.chunks.state.pool, chunk_depth, chunk_index); + defer if (new_root) |id| self.chunks.state.pool.unref(id); updated = null; - var length_node: ?Node.Id = try self.chunks.pool.createLeafFromUint(@intCast(new_length)); - defer if (length_node) |id| self.chunks.pool.unref(id); - const root_with_length = try Node.Id.setNode(new_root.?, self.chunks.pool, @enumFromInt(3), length_node.?); - errdefer self.chunks.pool.unref(root_with_length); + var length_node: ?Node.Id = try self.chunks.state.pool.createLeafFromUint(@intCast(new_length)); + defer if (length_node) |id| self.chunks.state.pool.unref(id); + const root_with_length = try Node.Id.setNode(new_root.?, self.chunks.state.pool, @enumFromInt(3), length_node.?); + errdefer self.chunks.state.pool.unref(root_with_length); length_node = null; new_root = null; - return try Self.init(self.allocator, self.chunks.pool, root_with_length); + return try Self.init(self.allocator, self.chunks.state.pool, root_with_length); } /// Serialize the tree view into a provided buffer. /// Returns the number of bytes written. pub fn serializeIntoBytes(self: *Self, out: []u8) !usize { try self.commit(); - return try ST.tree.serializeIntoBytes(self.chunks.root, self.chunks.pool, out); + return try ST.tree.serializeIntoBytes(self.chunks.state.root, self.chunks.state.pool, out); } /// Get the serialized size of this tree view. pub fn serializedSize(self: *Self) !usize { try self.commit(); - return try ST.tree.serializedSize(self.chunks.root, self.chunks.pool); + return try ST.tree.serializedSize(self.chunks.state.root, self.chunks.state.pool); } fn updateListLength(self: *Self) !void { @@ -591,13 +591,13 @@ test "TreeView list basic clone(true) does not transfer cache" { defer view.deinit(); _ = try view.get(0); - try std.testing.expect(view.chunks.children_nodes.count() > 0); + try std.testing.expect(view.chunks.state.children_nodes.count() > 0); var cloned_no_cache = try view.clone(.{ .transfer_cache = false }); defer cloned_no_cache.deinit(); - try std.testing.expect(view.chunks.children_nodes.count() > 0); - try std.testing.expectEqual(@as(usize, 0), cloned_no_cache.chunks.children_nodes.count()); + try std.testing.expect(view.chunks.state.children_nodes.count() > 0); + try std.testing.expectEqual(@as(usize, 0), cloned_no_cache.chunks.state.children_nodes.count()); } test "TreeView list basic clone(false) transfers cache and clears source" { @@ -617,13 +617,13 @@ test "TreeView list basic clone(false) transfers cache and clears source" { defer view.deinit(); _ = try view.get(0); - try std.testing.expect(view.chunks.children_nodes.count() > 0); + try std.testing.expect(view.chunks.state.children_nodes.count() > 0); var cloned = try view.clone(.{}); defer cloned.deinit(); - try std.testing.expectEqual(@as(usize, 0), view.chunks.children_nodes.count()); - try std.testing.expect(cloned.chunks.children_nodes.count() > 0); + try std.testing.expectEqual(@as(usize, 0), view.chunks.state.children_nodes.count()); + try std.testing.expect(cloned.chunks.state.children_nodes.count() > 0); } // Refer to https://github.com/ChainSafe/ssz/blob/7f5580c2ea69f9307300ddb6010a8bc7ce2fc471/packages/ssz/test/unit/byType/listBasic/tree.test.ts#L180-L203 diff --git a/src/ssz/tree_view/list_composite.zig b/src/ssz/tree_view/list_composite.zig index 536400f95..aec5786ef 100644 --- a/src/ssz/tree_view/list_composite.zig +++ b/src/ssz/tree_view/list_composite.zig @@ -84,12 +84,12 @@ pub fn ListCompositeTreeView(comptime ST: type) type { pub fn hashTreeRootInto(self: *Self, out: *[32]u8) !void { try self.commit(); - out.* = self.chunks.root.getRoot(self.chunks.pool).*; + out.* = self.chunks.state.root.getRoot(self.chunks.state.pool).*; } pub fn hashTreeRoot(self: *Self) !*const [32]u8 { try self.commit(); - return self.chunks.root.getRoot(self.chunks.pool); + return self.chunks.state.root.getRoot(self.chunks.state.pool); } pub fn fromValue(allocator: Allocator, pool: *Node.Pool, value: *const ST.Type) !*Self { @@ -100,7 +100,7 @@ pub fn ListCompositeTreeView(comptime ST: type) type { pub fn toValue(self: *Self, allocator: Allocator, out: *ST.Type) !void { try self.commit(); - try ST.tree.toValue(allocator, self.chunks.root, self.chunks.pool, out); + try ST.tree.toValue(allocator, self.chunks.state.root, self.chunks.state.pool, out); } pub fn setLength(self: *Self, new_length: usize) !void { @@ -108,7 +108,7 @@ pub fn ListCompositeTreeView(comptime ST: type) type { } pub fn getRoot(self: *const Self) Node.Id { - return self.chunks.root; + return self.chunks.state.root; } pub fn length(self: *const Self) !usize { @@ -144,7 +144,7 @@ pub fn ListCompositeTreeView(comptime ST: type) type { if (index >= list_length) return error.IndexOutOfBounds; const elem = try self.chunks.get(index); try elem.commit(); - return elem.getRoot().getRoot(self.chunks.pool); + return elem.getRoot().getRoot(self.chunks.state.pool); } pub fn set(self: *Self, index: usize, value: Element) !void { @@ -180,9 +180,9 @@ pub fn ListCompositeTreeView(comptime ST: type) type { /// Push an SSZ value type, creating a TreeView internally. pub fn pushValue(self: *Self, value: *const ST.Element.Type) !void { - const root = try ST.Element.tree.fromValue(self.chunks.pool, value); - errdefer self.chunks.pool.unref(root); - const child_view = try ST.Element.TreeView.init(self.allocator, self.chunks.pool, root); + const root = try ST.Element.tree.fromValue(self.chunks.state.pool, value); + errdefer self.chunks.state.pool.unref(root); + const child_view = try ST.Element.TreeView.init(self.allocator, self.chunks.state.pool, root); errdefer child_view.deinit(); try self.push(child_view); } @@ -200,8 +200,8 @@ pub fn ListCompositeTreeView(comptime ST: type) type { return .{ .tree_view = tree_view, .depth_iterator = Node.DepthIterator.init( - tree_view.chunks.pool, - tree_view.chunks.root, + tree_view.chunks.state.pool, + tree_view.chunks.state.root, chunk_depth, start_index, ), @@ -213,7 +213,7 @@ pub fn ListCompositeTreeView(comptime ST: type) type { const node = try self.depth_iterator.next(); const child_view = try ST.Element.TreeView.init( self.tree_view.allocator, - self.tree_view.chunks.pool, + self.tree_view.chunks.state.pool, node, ); self.elem_index += 1; @@ -224,7 +224,7 @@ pub fn ListCompositeTreeView(comptime ST: type) type { pub fn nextRoot(self: *ReadonlyIterator) !*const [32]u8 { const node = try self.depth_iterator.next(); self.elem_index += 1; - return node.getRoot(self.tree_view.chunks.pool); + return node.getRoot(self.tree_view.chunks.state.pool); } /// Get the next element as an SSZ value type. @@ -233,9 +233,9 @@ pub fn ListCompositeTreeView(comptime ST: type) type { self.elem_index += 1; var value: ST.Element.Type = undefined; if (comptime isFixedType(ST.Element)) { - try ST.Element.tree.toValue(node, self.tree_view.chunks.pool, &value); + try ST.Element.tree.toValue(node, self.tree_view.chunks.state.pool, &value); } else { - try ST.Element.tree.toValue(allocator, node, self.tree_view.chunks.pool, &value); + try ST.Element.tree.toValue(allocator, node, self.tree_view.chunks.state.pool, &value); } return value; } @@ -248,7 +248,7 @@ pub fn ListCompositeTreeView(comptime ST: type) type { const list_length = try self.length(); if (list_length == 0 or index >= list_length - 1) { - return try Self.init(self.allocator, self.chunks.pool, self.chunks.root); + return try Self.init(self.allocator, self.chunks.state.pool, self.chunks.state.root); } const new_length = index + 1; @@ -256,17 +256,17 @@ pub fn ListCompositeTreeView(comptime ST: type) type { return error.LengthOverLimit; } - var chunk_root: ?Node.Id = try Node.Id.truncateAfterIndex(self.chunks.root, self.chunks.pool, chunk_depth, index); - defer if (chunk_root) |id| self.chunks.pool.unref(id); + var chunk_root: ?Node.Id = try Node.Id.truncateAfterIndex(self.chunks.state.root, self.chunks.state.pool, chunk_depth, index); + defer if (chunk_root) |id| self.chunks.state.pool.unref(id); - var length_node: ?Node.Id = try self.chunks.pool.createLeafFromUint(@intCast(new_length)); - defer if (length_node) |id| self.chunks.pool.unref(id); - const root_with_length = try Node.Id.setNode(chunk_root.?, self.chunks.pool, @enumFromInt(3), length_node.?); - errdefer self.chunks.pool.unref(root_with_length); + var length_node: ?Node.Id = try self.chunks.state.pool.createLeafFromUint(@intCast(new_length)); + defer if (length_node) |id| self.chunks.state.pool.unref(id); + const root_with_length = try Node.Id.setNode(chunk_root.?, self.chunks.state.pool, @enumFromInt(3), length_node.?); + errdefer self.chunks.state.pool.unref(root_with_length); length_node = null; chunk_root = null; - return try Self.init(self.allocator, self.chunks.pool, root_with_length); + return try Self.init(self.allocator, self.chunks.state.pool, root_with_length); } /// Return a new view containing all elements from `index` to the end. @@ -276,46 +276,46 @@ pub fn ListCompositeTreeView(comptime ST: type) type { const list_length = try self.length(); if (index == 0) { - return try Self.init(self.allocator, self.chunks.pool, self.chunks.root); + return try Self.init(self.allocator, self.chunks.state.pool, self.chunks.state.root); } const target_length = if (index >= list_length) 0 else list_length - index; var chunk_root: ?Node.Id = null; - defer if (chunk_root) |id| self.chunks.pool.unref(id); + defer if (chunk_root) |id| self.chunks.state.pool.unref(id); if (target_length == 0) { chunk_root = @enumFromInt(base_chunk_depth); } else { const nodes = try self.allocator.alloc(Node.Id, target_length); defer self.allocator.free(nodes); - try self.chunks.root.getNodesAtDepth(self.chunks.pool, chunk_depth, index, nodes); + try self.chunks.state.root.getNodesAtDepth(self.chunks.state.pool, chunk_depth, index, nodes); - chunk_root = try Node.fillWithContents(self.chunks.pool, nodes, base_chunk_depth); + chunk_root = try Node.fillWithContents(self.chunks.state.pool, nodes, base_chunk_depth); } - var length_node: ?Node.Id = try self.chunks.pool.createLeafFromUint(@intCast(target_length)); - defer if (length_node) |id| self.chunks.pool.unref(id); + var length_node: ?Node.Id = try self.chunks.state.pool.createLeafFromUint(@intCast(target_length)); + defer if (length_node) |id| self.chunks.state.pool.unref(id); - const new_root = try self.chunks.pool.createBranch(chunk_root.?, length_node.?); - errdefer self.chunks.pool.unref(new_root); + const new_root = try self.chunks.state.pool.createBranch(chunk_root.?, length_node.?); + errdefer self.chunks.state.pool.unref(new_root); length_node = null; chunk_root = null; - return try Self.init(self.allocator, self.chunks.pool, new_root); + return try Self.init(self.allocator, self.chunks.state.pool, new_root); } /// Serialize the tree view into a provided buffer. /// Returns the number of bytes written. pub fn serializeIntoBytes(self: *Self, out: []u8) !usize { try self.commit(); - return try ST.tree.serializeIntoBytes(self.chunks.root, self.chunks.pool, out); + return try ST.tree.serializeIntoBytes(self.chunks.state.root, self.chunks.state.pool, out); } /// Get the serialized size of this tree view. pub fn serializedSize(self: *Self) !usize { try self.commit(); - return try ST.tree.serializedSize(self.chunks.root, self.chunks.pool); + return try ST.tree.serializedSize(self.chunks.state.root, self.chunks.state.pool); } fn updateListLength(self: *Self) !void { diff --git a/src/ssz/tree_view/utils/child_nodes.zig b/src/ssz/tree_view/utils/child_nodes.zig deleted file mode 100644 index e38ff9863..000000000 --- a/src/ssz/tree_view/utils/child_nodes.zig +++ /dev/null @@ -1,106 +0,0 @@ -const std = @import("std"); -const Node = @import("persistent_merkle_tree").Node; -const Gindex = @import("persistent_merkle_tree").Gindex; -const CloneOpts = @import("clone_opts.zig").CloneOpts; - -/// Common functions for `TreeView`s dealing with `children_nodes`. -pub const ChildNodes = struct { - pub fn getChildNode(self: anytype, gindex: Gindex) !Node.Id { - const gop = try self.children_nodes.getOrPut(self.allocator, gindex); - if (gop.found_existing) { - return gop.value_ptr.*; - } - const child_node = try self.root.getNode(self.pool, gindex); - gop.value_ptr.* = child_node; - return child_node; - } - - pub fn setChildNode(self: anytype, gindex: Gindex, node: Node.Id) !void { - try self.changed.put(self.allocator, gindex, {}); - const opt_old_node = try self.children_nodes.fetchPut( - self.allocator, - gindex, - node, - ); - if (opt_old_node) |old_node| { - // Multiple set() calls before commit() leave our previous temp nodes cached with refcount 0. - // Tree-owned nodes already have a refcount, so skip unref in that case. - if (old_node.value.getState(self.pool).getRefCount() == 0) { - self.pool.unref(old_node.value); - } - } - } - - pub fn clearChildrenNodesCache(self: anytype, pool: *Node.Pool) void { - var value_iter = self.children_nodes.valueIterator(); - while (value_iter.next()) |node_id_ptr| { - const node_id = node_id_ptr.*; - if (node_id.getState(pool).getRefCount() == 0) { - pool.unref(node_id); - } - } - self.children_nodes.clearRetainingCapacity(); - } - - pub fn getLength(self: anytype) !usize { - const length_node = try getChildNode(self, @enumFromInt(3)); - const length_chunk = length_node.getRoot(self.pool); - return std.mem.readInt(usize, length_chunk[0..@sizeOf(usize)], .little); - } - - pub fn setLength(self: anytype, length: usize) !void { - const length_node = try self.pool.createLeafFromUint(@intCast(length)); - errdefer self.pool.unref(length_node); - try self.setChildNode(@enumFromInt(3), length_node); - } - - /// Common functions for TreeViews deal with `children_nodes` + `changed` - pub const Change = struct { - pub fn commit(self: anytype) !void { - if (self.changed.count() == 0) { - return; - } - - const nodes = try self.allocator.alloc(Node.Id, self.changed.count()); - defer self.allocator.free(nodes); - - const gindices = self.changed.keys(); - Gindex.sortAsc(gindices); - - for (gindices, 0..) |gindex, i| { - if (self.children_nodes.get(gindex)) |child_node| { - nodes[i] = child_node; - } else { - return error.ChildNotFound; - } - } - - const new_root = try self.root.setNodesGrouped(self.pool, gindices, nodes); - try self.pool.ref(new_root); - self.pool.unref(self.root); - self.root = new_root; - - self.changed.clearRetainingCapacity(); - } - - pub fn cloneAndTransferCache(comptime T: type, self: *T, opts: CloneOpts, out: *T) !void { - try T.init(out, self.allocator, self.pool, self.root); - - if (!opts.transfer_cache) { - return; - } - - out.children_nodes = self.children_nodes; - - // Removing entries while iterating the same map can invalidate the iterator. - // Remove by traversing `self.changed` keys instead. - for (self.changed.keys()) |gindex| { - _ = out.children_nodes.remove(gindex); - } - - // clear self's caches - self.children_nodes = .empty; - self.changed.clearRetainingCapacity(); - } - }; -}; diff --git a/src/ssz/tree_view/utils/tree_view_state.zig b/src/ssz/tree_view/utils/tree_view_state.zig new file mode 100644 index 000000000..26ac24fee --- /dev/null +++ b/src/ssz/tree_view/utils/tree_view_state.zig @@ -0,0 +1,123 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Node = @import("persistent_merkle_tree").Node; +const Gindex = @import("persistent_merkle_tree").Gindex; +const CloneOpts = @import("clone_opts.zig").CloneOpts; + +/// Common state for tree views that use runtime gindex-based child caching. +/// +/// Used by list, array, bitvector, and bitlist views (chunk-based). +/// NOT used by ContainerTreeView (which uses comptime field-indexed tuples). +pub const TreeViewState = struct { + allocator: Allocator, + pool: *Node.Pool, + root: Node.Id, + + /// cached nodes for faster access of already-visited children + children_nodes: std.AutoHashMapUnmanaged(Gindex, Node.Id), + + /// whether the corresponding child node/data has changed since the last update of the root + changed: std.AutoArrayHashMapUnmanaged(Gindex, void), + + pub fn init(self: *TreeViewState, allocator: Allocator, pool: *Node.Pool, root: Node.Id) !void { + try pool.ref(root); + self.* = .{ + .allocator = allocator, + .pool = pool, + .root = root, + .children_nodes = .empty, + .changed = .empty, + }; + } + + pub fn deinit(self: *TreeViewState) void { + self.clearChildrenNodesCache(); + self.children_nodes.deinit(self.allocator); + self.changed.deinit(self.allocator); + self.pool.unref(self.root); + } + + pub fn getChildNode(self: *TreeViewState, gindex: Gindex) !Node.Id { + const gop = try self.children_nodes.getOrPut(self.allocator, gindex); + if (gop.found_existing) { + return gop.value_ptr.*; + } + const child_node = try self.root.getNode(self.pool, gindex); + gop.value_ptr.* = child_node; + return child_node; + } + + pub fn setChildNode(self: *TreeViewState, gindex: Gindex, node: Node.Id) !void { + try self.changed.put(self.allocator, gindex, {}); + const opt_old_node = try self.children_nodes.fetchPut( + self.allocator, + gindex, + node, + ); + if (opt_old_node) |old_node| { + if (old_node.value.getState(self.pool).getRefCount() == 0) { + self.pool.unref(old_node.value); + } + } + } + + pub fn commitNodes(self: *TreeViewState) !void { + if (self.changed.count() == 0) { + return; + } + + const nodes = try self.allocator.alloc(Node.Id, self.changed.count()); + defer self.allocator.free(nodes); + + const gindices = self.changed.keys(); + Gindex.sortAsc(gindices); + + for (gindices, 0..) |gindex, i| { + if (self.children_nodes.get(gindex)) |child_node| { + nodes[i] = child_node; + } else { + return error.ChildNotFound; + } + } + + const new_root = try self.root.setNodesGrouped(self.pool, gindices, nodes); + try self.pool.ref(new_root); + self.pool.unref(self.root); + self.root = new_root; + + self.changed.clearRetainingCapacity(); + } + + pub fn clearChildrenNodesCache(self: *TreeViewState) void { + var value_iter = self.children_nodes.valueIterator(); + while (value_iter.next()) |node_id_ptr| { + const node_id = node_id_ptr.*; + if (node_id.getState(self.pool).getRefCount() == 0) { + self.pool.unref(node_id); + } + } + self.children_nodes.clearRetainingCapacity(); + } + + pub fn clearCache(self: *TreeViewState) void { + self.clearChildrenNodesCache(); + self.changed.clearRetainingCapacity(); + } + + pub fn clone(self: *TreeViewState, opts: CloneOpts, out: *TreeViewState) !void { + try out.init(self.allocator, self.pool, self.root); + + if (!opts.transfer_cache) { + return; + } + + out.children_nodes = self.children_nodes; + + for (self.changed.keys()) |gindex| { + _ = out.children_nodes.remove(gindex); + } + + self.children_nodes = .empty; + self.changed.clearRetainingCapacity(); + } +}; From 8b3311367de7e1275309858318d8c20d3ae61ef4 Mon Sep 17 00:00:00 2001 From: Cayman Date: Mon, 16 Mar 2026 16:22:17 -0400 Subject: [PATCH 2/2] refactor(ssz): ContainerTreeView uses StaticBitSet for dirty tracking Replaces AutoArrayHashMapUnmanaged(usize, void) with zero-allocation StaticBitSet(N). Field count is comptime-known, so no hashmap needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ssz/tree_view/container.zig | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ssz/tree_view/container.zig b/src/ssz/tree_view/container.zig index 6c23a4c4c..1e6cedd88 100644 --- a/src/ssz/tree_view/container.zig +++ b/src/ssz/tree_view/container.zig @@ -55,7 +55,7 @@ pub fn ContainerTreeView(comptime ST: type) type { /// a tuple of either Optional(Value) for basic type or Optional(ChildTreeView) for composite type child_data: TreeViewData, /// whether the corresponding child node/data has changed since the last update of the root - changed: std.AutoArrayHashMapUnmanaged(usize, void), + changed: std.StaticBitSet(ST.chunk_count), original_nodes: [ST.chunk_count]?Node.Id, pub const SszType = ST; @@ -72,7 +72,7 @@ pub fn ContainerTreeView(comptime ST: type) type { .child_data = .{null} ** ST.chunk_count, .original_nodes = .{null} ** ST.chunk_count, .root = root, - .changed = .empty, + .changed = std.StaticBitSet(ST.chunk_count).initEmpty(), }; return ptr; } @@ -87,7 +87,7 @@ pub fn ContainerTreeView(comptime ST: type) type { ptr.original_nodes = self.original_nodes; inline for (0..ST.fields.len) |i| { - if (self.changed.contains(i)) { + if (self.changed.isSet(i)) { if (ptr.child_data[i]) |child_view_ptr| { if (!comptime isBasicType(ST.fields[i].type)) { @constCast(child_view_ptr).deinit(); @@ -100,7 +100,7 @@ pub fn ContainerTreeView(comptime ST: type) type { // clear self's caches self.child_data = .{null} ** ST.chunk_count; self.original_nodes = .{null} ** ST.chunk_count; - self.changed.clearRetainingCapacity(); + self.changed = std.StaticBitSet(ST.chunk_count).initEmpty(); return ptr; } @@ -124,7 +124,7 @@ pub fn ContainerTreeView(comptime ST: type) type { // these nodes are unref by root self.original_nodes[i] = null; } - self.changed.deinit(self.allocator); + self.changed = std.StaticBitSet(ST.chunk_count).initEmpty(); } pub fn commit(self: *Self) !void { @@ -137,7 +137,7 @@ pub fn ContainerTreeView(comptime ST: type) type { var changed_idx: usize = 0; inline for (ST.fields, 0..) |field, i| { - if (self.changed.get(i) != null) { + if (self.changed.isSet(i)) { const ChildST = ST.getFieldType(field.name); if (comptime isBasicType(ChildST)) { const child_value = self.child_data[i] orelse return error.MissingChildValue; @@ -166,7 +166,7 @@ pub fn ContainerTreeView(comptime ST: type) type { } } - self.changed.clearRetainingCapacity(); + self.changed = std.StaticBitSet(ST.chunk_count).initEmpty(); if (changed_idx == 0) { return; } @@ -235,7 +235,7 @@ pub fn ContainerTreeView(comptime ST: type) type { return child_value; } } else { - try self.changed.put(self.allocator, field_index, {}); + self.changed.set(field_index); const existing_ptr = self.child_data[field_index]; if (existing_ptr) |child_view_ptr| { @@ -278,7 +278,7 @@ pub fn ContainerTreeView(comptime ST: type) type { self.child_data[field_index] = value; } - try self.changed.put(self.allocator, field_index, {}); + self.changed.set(field_index); } /// Serialize the tree view into a provided buffer.