From d7df0d985051b1a5d70e5d9e3ba0cd9de5ddac14 Mon Sep 17 00:00:00 2001 From: ernisto Date: Tue, 7 Jul 2026 15:38:09 -0300 Subject: [PATCH] perf(push_child): defer unparent until evaluate, keeping unchanged ordered parents --- src/graph.luau | 53 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/src/graph.luau b/src/graph.luau index fd22489..ce18acf 100644 --- a/src/graph.luau +++ b/src/graph.luau @@ -1,5 +1,8 @@ local flags = require "./flags" +local TEMPORALLY_UNPARENTED = -1 +type TEMPORALLY_UNPARENTED = number + export type SourceNode = { cache: T, [number]: Node @@ -15,6 +18,7 @@ export type Node = { owned: { Node } | false, owner: Node | false, + pushing_parent_index: number | TEMPORALLY_UNPARENTED, parents: { SourceNode }, [number]: Node -- children } @@ -66,11 +70,6 @@ local function assert_stable_scope(): Node return scope end -local function push_child(parent: SourceNode, child: Node) - table.insert(parent, child) - table.insert(child.parents, parent) -end - local function push_scope(node: Node) local n = scopes.n + 1 scopes.n = n @@ -109,7 +108,22 @@ local function find_and_swap_pop(t: { T }, v: T) t[n] = nil end +local function push_child(parent: SourceNode, child: Node) + local parent_index = child.pushing_parent_index + local parents = child.parents + + child.pushing_parent_index = parent_index + 1 + local previous_parent = parents[parent_index] + + if previous_parent == parent then return end + parents[parent_index] = parent + + if previous_parent then find_and_swap_pop(previous_parent, child) end + table.insert(parent, child) +end + local function unparent(node: Node) + if node.pushing_parent_index == TEMPORALLY_UNPARENTED then return end local parents = node.parents for i, parent in parents do @@ -146,6 +160,14 @@ end local update_queue = { n = 0 } :: { n: number, [number]: Node } +local function unparent_unuseds(node: Node) + local parents = node.parents + for i = node.pushing_parent_index+1, #parents do + find_and_swap_pop(parents[i], node) + parents[i] = nil + end +end + local function evaluate_node(node: Node) if flags.strict then if table.find(scopes, node) then @@ -159,10 +181,13 @@ local function evaluate_node(node: Node) flush_cleanups(node) destroy_owned(node) + node.pushing_parent_index = 1 push_scope(node) local ok, new_value = ycall(node.effect :: (T) -> T, cur_value) + pop_scope() + unparent_unuseds(node) if not ok then table.clear(update_queue) @@ -179,10 +204,13 @@ local function evaluate_node(node: Node) flush_cleanups(node) destroy_owned(node) + node.pushing_parent_index = 1 push_scope(node) local ok, new_value = pcall(node.effect :: (T) -> T, node.cache) + pop_scope() + unparent_unuseds(node) if not ok then table.clear(update_queue) @@ -196,13 +224,15 @@ local function evaluate_node(node: Node) end local function queue_children_for_update(node: SourceNode) - local i = update_queue.n - while node[1] do - i += 1 - update_queue[i] = node[1] - unparent(node[1]) + local queue_length = update_queue.n + for j = 1, #node do + if node[j].pushing_parent_index == TEMPORALLY_UNPARENTED then continue end + node[j].pushing_parent_index = TEMPORALLY_UNPARENTED + + queue_length += 1 + update_queue[queue_length] = node[j] end - update_queue.n = i + update_queue.n = queue_length end local function get_update_queue_length() @@ -267,6 +297,7 @@ local function create_node(owner: false | Node, effect: false | (T) -> T owner = owner, owned = false, + pushing_parent_index = 1, parents = {}, }