perf: remove clearPending inline operation

This commit is contained in:
littensy 2026-04-25 22:32:53 -07:00
parent 71d155e947
commit b00e6760c0

View file

@ -17,15 +17,10 @@ local shallowPropagate = system.shallowPropagate
local createReactiveSystem = system.createReactiveSystem local createReactiveSystem = system.createReactiveSystem
export type Getter<T> = () -> T export type Getter<T> = () -> T
export type Setter<T> = (update: Update<T>) -> T export type Setter<T> = (update: Update<T>) -> T
export type Atom<T = any> = (...Update<T>) -> T export type Atom<T = any> = (...Update<T>) -> T
export type Update<T> = ((current: T) -> T) | T export type Update<T> = ((current: T) -> T) | T
export type Equals<T> = (current: T, incoming: T) -> boolean export type Equals<T> = (current: T, incoming: T) -> boolean
export type Cleanup = () -> () export type Cleanup = () -> ()
type EffectScopeNode = ReactiveNode & { type EffectScopeNode = ReactiveNode & {
@ -63,7 +58,7 @@ local PENDING = 0b100000 -- Might be dirty, pending verification
local cycle = 0 local cycle = 0
-- Effects runs are queued to prevent redundant executions when multiple -- Effects runs are queued to prevent redundant executions when multiple
-- signals update in the same batch. -- signals update in the same batch.
local queued: { EffectNode? } = {} local queued: { EffectNode } = {}
local queuedLength = 0 local queuedLength = 0
local notifyIndex = 0 local notifyIndex = 0
-- Batch operations group multiple updates together, deferring effect runs -- Batch operations group multiple updates together, deferring effect runs
@ -216,6 +211,7 @@ local function run(effect: EffectNode)
then then
runCleanups(effect.cleanups) runCleanups(effect.cleanups)
-- Cancel the run if the effect was disposed during cleanup
if effect.flags == NONE then if effect.flags == NONE then
return return
end end
@ -236,6 +232,8 @@ local function run(effect: EffectNode)
table.insert(effect.cleanups, wrapUserSpace(result)) table.insert(effect.cleanups, wrapUserSpace(result))
end end
-- If the effect was disposed during execution but before a cleanup was
-- was registered, run the cleanups that would have been missed
if effect.flags == NONE then if effect.flags == NONE then
runCleanups(effect.cleanups) runCleanups(effect.cleanups)
end end
@ -257,18 +255,18 @@ local function flush()
local success, err = pcall(function() local success, err = pcall(function()
while notifyIndex < queuedLength do while notifyIndex < queuedLength do
notifyIndex += 1
local effect = queued[notifyIndex] :: EffectNode local effect = queued[notifyIndex] :: EffectNode
queued[notifyIndex] = nil queued[notifyIndex] = nil
notifyIndex += 1
run(effect) run(effect)
end end
end) end)
-- Skip over any remaining queued effects that were not run due to an error -- Skip over any remaining queued effects that were not run due to an error
while notifyIndex < queuedLength do while notifyIndex < queuedLength do
notifyIndex += 1
local effect = queued[notifyIndex] :: EffectNode local effect = queued[notifyIndex] :: EffectNode
queued[notifyIndex] = nil queued[notifyIndex] = nil
notifyIndex += 1
effect.flags = bit32.bor(effect.flags, bit32.bor(WATCHING, RECURSED)) effect.flags = bit32.bor(effect.flags, bit32.bor(WATCHING, RECURSED))
end end
@ -337,17 +335,6 @@ local function updateSignal(signal: SignalNode): boolean
end end
end end
--[[
Clears the pending flag from a subscriber. Used inside of if conditions to
inline the operation.
@param sub Effect or computed node
@param subFlags Subscriber's current flags
]]
local function clearPending(sub: ReactiveNode, subFlags: ReactiveFlags)
sub.flags = bit32.band(subFlags, bit32.bnot(PENDING))
end
--[[ --[[
Clears all dependencies and subscribers from the effect node. After calling Clears all dependencies and subscribers from the effect node. After calling
this, the node will no longer be subscribed to any dependencies and will be this, the node will no longer be subscribed to any dependencies and will be
@ -359,6 +346,7 @@ local function stopEffect(effect: EffectScopeNode)
purgeDeps(effect) purgeDeps(effect)
-- Unlink from the parent effect, if it exists
local sub = effect.subs local sub = effect.subs
if sub then if sub then
unlink(sub, effect) unlink(sub, effect)
@ -383,15 +371,18 @@ end
correct order. correct order.
]] ]]
local function notify(effect: ReactiveNode) local function notify(effect: ReactiveNode)
local insertIndex = queuedLength local startIndex = queuedLength + 1
local firstInsertedIndex = insertIndex
-- Collect inner effects in a chain -- Collect inner effects in a chain
repeat repeat
queued[insertIndex] = effect :: EffectNode queuedLength += 1
insertIndex += 1 queued[queuedLength] = effect :: EffectNode
-- Mark as not watching to prevent duplicates in the same flush
effect.flags = bit32.band(effect.flags, bit32.bnot(WATCHING)) effect.flags = bit32.band(effect.flags, bit32.bnot(WATCHING))
-- Traverse to the parent effect, if it exists and is not queued, to
-- ensure it gets notified before the child effect
effect = (effect.subs and effect.subs.sub) :: EffectNode effect = (effect.subs and effect.subs.sub) :: EffectNode
if not effect or bit32.band(effect.flags, WATCHING) == 0 then if not effect or bit32.band(effect.flags, WATCHING) == 0 then
@ -399,17 +390,15 @@ local function notify(effect: ReactiveNode)
end end
until false until false
queuedLength = insertIndex local endIndex = queuedLength
-- Reverse the collected effects to ensure outer effects run first -- Reverse the collected effects since they were collected from child to
repeat -- parent, but should be notified from parent to child
insertIndex -= 1 while startIndex < endIndex do
if firstInsertedIndex >= insertIndex then queued[startIndex], queued[endIndex] = queued[endIndex], queued[startIndex]
break startIndex += 1
endIndex -= 1
end end
queued[firstInsertedIndex], queued[insertIndex] = queued[insertIndex], queued[firstInsertedIndex]
firstInsertedIndex += 1
until false
end end
--[[ --[[
@ -555,20 +544,24 @@ local function computed<T>(getter: (previousValue: T?) -> T): Getter<T>
local function computedOper(): T local function computedOper(): T
local nodeFlags = node.flags local nodeFlags = node.flags
if if bit32.btest(nodeFlags, DIRTY) then
bit32.btest(nodeFlags, DIRTY)
or (
bit32.btest(nodeFlags, PENDING)
and (checkDirty(node.deps :: Link, node) or clearPending(node, nodeFlags))
)
then
if updateComputed(node) then if updateComputed(node) then
-- Notify subscribers if the value changed
local subs = node.subs local subs = node.subs
if subs then if subs then
shallowPropagate(subs) shallowPropagate(subs)
end end
end end
elseif bit32.btest(nodeFlags, PENDING) then
if checkDirty(node.deps :: Link, node) then
if updateComputed(node) then
local subs = node.subs
if subs then
shallowPropagate(subs)
end
end
else
node.flags = bit32.band(nodeFlags, bit32.bnot(PENDING))
end
elseif nodeFlags == NONE then elseif nodeFlags == NONE then
-- First access should initialize the computed value -- First access should initialize the computed value
node.flags = bit32.bor(MUTABLE, RECURSED_CHECK) node.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
@ -590,7 +583,7 @@ local function computed<T>(getter: (previousValue: T?) -> T): Getter<T>
link(node, activeSub, cycle) link(node, activeSub, cycle)
end end
return node.value :: T return node.value
end end
return computedOper return computedOper
@ -618,26 +611,30 @@ local function effect(fn: () -> ...Cleanup?): Cleanup
cleanups = {}, cleanups = {},
} }
-- Inner effects are tracked as a dependency of the parent effect, so that
-- they can be automatically cleaned up when the parent effect re-runs, and
-- to ensure the correct execution order when nested effects are notified
if activeSub and (flags.trackInnerEffects or activeSub.flags == NONE) then if activeSub and (flags.trackInnerEffects or activeSub.flags == NONE) then
link(node, activeSub, 0) link(node, activeSub, 0)
end end
-- Start a batch so that any signal updates triggered during the effect -- Start a batch so that the effect can register a cleanup function before
-- execution are queued after the effect registers a cleanup function -- it can receive notifications, ensuring the cleanup always runs
startBatch() startBatch()
local prevSub = setActiveSub(node) local prevSub = setActiveSub(node)
local success, result = pcall(node.fn) local success, result = pcall(node.fn)
activeSub = prevSub activeSub = prevSub
if not success then if success then
endBatch() if result then
error(result, 2)
elseif result then
table.insert(node.cleanups, wrapUserSpace(result)) table.insert(node.cleanups, wrapUserSpace(result))
end end
endBatch() endBatch()
else
endBatch()
error(result, 2)
end
return function() return function()
stopEffect(node) stopEffect(node)
@ -659,6 +656,7 @@ local function effectScope(fn: () -> ...Cleanup?, detached: boolean?): Cleanup
cleanups = {}, cleanups = {},
} }
-- Inner effect scopes are tracked as a dependency of the parent effect
if activeSub and not detached then if activeSub and not detached then
link(node, activeSub, 0) link(node, activeSub, 0)
end end
@ -948,7 +946,7 @@ local function mapped<KI, VI, KO, VO>(getter: Getter<{ [KI]: VI }>, transform: (
end end
local nextValueOut, keyOutOrNil = transform(nextValueIn, keyIn) local nextValueOut, keyOutOrNil = transform(nextValueIn, keyIn)
local nextKeyOut: KO = if keyOutOrNil ~= nil then keyOutOrNil else keyIn :: any local nextKeyOut: KO = if keyOutOrNil ~= nil then keyOutOrNil else keyIn
local prevKeyOut = keyMap[keyIn] local prevKeyOut = keyMap[keyIn]
if prevKeyOut ~= nil and prevKeyOut ~= nextKeyOut then if prevKeyOut ~= nil and prevKeyOut ~= nextKeyOut then