Improved graph.luau performance

This commit is contained in:
Olari Tšernobrovkin 2026-07-07 17:57:51 +03:00
parent 19eaeb424f
commit 0d8abd884d
No known key found for this signature in database
GPG key ID: 3CC0189D1D6A3A22

View file

@ -2,24 +2,25 @@ local flags = require "./flags"
export type SourceNode<T> = { export type SourceNode<T> = {
cache: T, cache: T,
[number]: Node<T> children: { [Node<T>]: true },
} }
export type Node<T> = { export type Node<T> = {
cache: T, cache: T,
effect: ((T) -> T) | false, effect: ((T) -> T) | false,
cleanups: { () -> () } | false, cleanups: { () -> () } | false,
context: { [number]: unknown } | false, context: { [number]: unknown } | false,
owned: { Node<T> } | false, owned: { [Node<T>]: true } | false,
owner: Node<T> | false, owner: Node<T> | false,
parents: { SourceNode<T> }, parents: { SourceNode<T> },
[number]: Node<T> -- children children: { [Node<T>]: true },
} }
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack local scopes = {} :: { [number]: Node<any> } -- scopes stack
local active_nodes = {} :: { [Node<any>]: true } -- nodes present in scopes stack
local function efn(err: string) local function efn(err: string)
local trace = debug.traceback(err, 2) local trace = debug.traceback(err, 2)
@ -31,11 +32,10 @@ local function efn(err: string)
end end
trace ..= "\nsource update stacktrace:" trace ..= "\nsource update stacktrace:"
return trace return trace
end end
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U) local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string | U)
local thread = coroutine.create(xpcall) local thread = coroutine.create(xpcall)
--local function efn(err: string) return debug.traceback(err, 3) end --local function efn(err: string) return debug.traceback(err, 3) end
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg) local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
@ -50,7 +50,7 @@ local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
end end
local function get_scope(): Node<unknown>? local function get_scope(): Node<unknown>?
return scopes[scopes.n] return scopes[#scopes]
end end
local function assert_stable_scope(): Node<unknown> local function assert_stable_scope(): Node<unknown>
@ -67,20 +67,19 @@ local function assert_stable_scope(): Node<unknown>
end end
local function push_child<T>(parent: SourceNode<any>, child: Node<any>) local function push_child<T>(parent: SourceNode<any>, child: Node<any>)
table.insert(parent, child) parent.children[child] = true
table.insert(child.parents, parent) table.insert(child.parents, parent)
end end
local function push_scope<T>(node: Node<T>) local function push_scope<T>(node: Node<T>)
local n = scopes.n + 1 active_nodes[node] = true
scopes.n = n table.insert(scopes, node)
scopes[n] = node
end end
local function pop_scope() local function pop_scope()
local n = scopes.n local len = #scopes
scopes.n = n - 1 active_nodes[scopes[len]] = nil
scopes[n] = nil scopes[len] = nil
end end
local function push_cleanup<T>(node: Node<T>, cleanup: () -> ()) local function push_cleanup<T>(node: Node<T>, cleanup: () -> ())
@ -102,24 +101,16 @@ local function flush_cleanups<T>(node: Node<T>)
end end
end end
local function find_and_swap_pop<T>(t: { T }, v: T)
local i = table.find(t, v) :: number
local n = #t
t[i] = t[n]
t[n] = nil
end
local function unparent<T>(node: Node<T>) local function unparent<T>(node: Node<T>)
local parents = node.parents local parents = node.parents
for _, parent in parents do
for i, parent in parents do parent.children[node] = nil
find_and_swap_pop(parent, node)
parents[i] = nil
end end
table.clear(parents)
end end
local function destroy<T>(node: Node<T>) local function destroy<T>(node: Node<T>)
if flags.strict and table.find(scopes, node) then if flags.strict and active_nodes[node] then
error("attempt to destroy an active scope", 0) error("attempt to destroy an active scope", 0)
end end
@ -127,20 +118,22 @@ local function destroy<T>(node: Node<T>)
unparent(node) unparent(node)
if node.owner then if node.owner then
find_and_swap_pop(node.owner.owned :: { Node<T> }, node) (node.owner.owned :: { [Node<T>]: true })[node] = nil
node.owner = false node.owner = false
end end
if node.owned then if node.owned then
local owned = node.owned for ownedNode, _ in node.owned do
while owned[1] do destroy(owned[1]) end destroy(ownedNode)
end
end end
end end
local function destroy_owned<T>(node: Node<T>) local function destroy_owned<T>(node: Node<T>)
if node.owned then if node.owned then
local owned = node.owned for ownedNode, _ in node.owned do
while owned[1] do destroy(owned[1]) end destroy(ownedNode)
end
end end
end end
@ -148,7 +141,7 @@ local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
local function evaluate_node<T>(node: Node<T>) local function evaluate_node<T>(node: Node<T>)
if flags.strict then if flags.strict then
if table.find(scopes, node) then if active_nodes[node] then
error("a scope, that should rerun due to the update of a source, is already active", 0) error("a scope, that should rerun due to the update of a source, is already active", 0)
end end
@ -197,10 +190,10 @@ end
local function queue_children_for_update<T>(node: SourceNode<T>) local function queue_children_for_update<T>(node: SourceNode<T>)
local i = update_queue.n local i = update_queue.n
while node[1] do for child, _ in node.children do
i += 1 i += 1
update_queue[i] = node[1] update_queue[i] = child
unparent(node[1]) unparent(child)
end end
update_queue.n = i update_queue.n = i
end end
@ -268,13 +261,14 @@ local function create_node<T>(owner: false | Node<any>, effect: false | (T) -> T
owned = false, owned = false,
parents = {}, parents = {},
children = {},
} }
if owner then if owner then
if owner.owned then if owner.owned then
table.insert(owner.owned, node) owner.owned[node] = true
else else
owner.owned = { node } owner.owned = { [node] = true }
end end
end end
@ -282,11 +276,7 @@ local function create_node<T>(owner: false | Node<any>, effect: false | (T) -> T
end end
local function create_source_node<T>(value: T): SourceNode<T> local function create_source_node<T>(value: T): SourceNode<T>
return { cache = value } return { cache = value, children = {} }
end
local function get_children<T>(node: Node<T>): { Node<unknown> }
return { unpack(node) } :: { Node<any> }
end end
local function set_context<T>(node: Node<T>, key: number, value: unknown) local function set_context<T>(node: Node<T>, key: number, value: unknown)
@ -311,11 +301,10 @@ return table.freeze {
push_child = push_child, push_child = push_child,
create_node = create_node, create_node = create_node,
create_source_node = create_source_node, create_source_node = create_source_node,
get_children = get_children,
flush_update_queue = flush_update_queue, flush_update_queue = flush_update_queue,
get_update_queue_length = get_update_queue_length, get_update_queue_length = get_update_queue_length,
set_context = set_context, set_context = set_context,
scopes = scopes, scopes = scopes,
q = update_queue q = update_queue,
} }