rojo/plugin/rbx_dom_lua/init.lua
Joe d45b1c5b94 Enhance property comparison and caching mechanisms
- Introduced a new function `getClassComparisonKeys` in `rbx_dom_lua/init.lua` to pre-compute and cache comparison keys for classes, improving efficiency in property matching.
- Updated the `Matching` module in `Reconciler/matching.lua` to utilize pre-computed caches for virtual and Studio instances, optimizing the reconciliation process.
- Enhanced handling of excluded properties and types during comparisons, ensuring accurate matching while reducing unnecessary computations.

These changes significantly improve the performance and accuracy of property reconciliation, addressing previous inefficiencies in the matching logic.
2026-02-17 08:07:46 -06:00

253 lines
7.1 KiB
Lua

local database = require(script.database)
local Error = require(script.Error)
local PropertyDescriptor = require(script.PropertyDescriptor)
local EncodedValue = require(script.EncodedValue)
-- Cache for default properties per class (includes inherited defaults).
local defaultPropertiesCache: { [string]: { [string]: any } } = {}
-- Returns a table mapping property names to their encoded default values
-- for a given class, including inherited defaults from parent classes.
-- Subclass overrides take precedence. Results are cached per class.
local function findDefaultProperties(className: string): { [string]: any }
local cached = defaultPropertiesCache[className]
if cached then
return cached
end
local defaults: { [string]: any } = {}
local currentClassName: string? = className
repeat
local currentClass = database.Classes[currentClassName]
if currentClass == nil then
break
end
local classDefaults = currentClass.DefaultProperties
if classDefaults then
for propName, encodedValue in classDefaults do
if defaults[propName] == nil then
defaults[propName] = encodedValue
end
end
end
currentClassName = currentClass.Superclass
until currentClassName == nil
defaultPropertiesCache[className] = defaults
return defaults
end
-- Property names excluded from comparison (handled separately).
local EXCLUDED_PROP_NAMES: { [string]: boolean } = {
Tags = true,
Attributes = true,
}
-- Encoded value type keys that cannot be compared during matching.
local EXCLUDED_ENCODED_TYPES: { [string]: boolean } = {
Ref = true,
UniqueId = true,
SharedString = true,
Region3 = true,
}
-- Scriptabilities that allow reading from Studio instances.
local READABLE_SCRIPTABILITY: { [string]: boolean } = {
ReadWrite = true,
Read = true,
}
-- Cache for class comparison keys (propNames + decoded defaults).
-- Computed once per ClassName, lives for the plugin's lifetime.
local classComparisonKeysCache: { [string]: any } = {}
-- Returns a pre-computed comparison key set for a class:
-- propNames: ordered array of comparable property names
-- propNameSet: O(1) lookup set of the same names
-- defaults: map of propName → decoded native default value
--
-- Filtered to: canonical, scriptable (readable), serializable, and
-- decodable. Excludes Tags, Attributes, Ref, UniqueId, SharedString.
-- All defaults are pre-decoded to native Roblox values.
local function getClassComparisonKeys(className: string)
local cached = classComparisonKeysCache[className]
if cached then
return cached
end
local propNames: { string } = {}
local propNameSet: { [string]: boolean } = {}
local seenProps: { [string]: boolean } = {}
-- Walk the class hierarchy for property metadata
local currentClassName: string? = className
repeat
local currentClass = database.Classes[currentClassName]
if currentClass == nil then
break
end
for propertyName, propertyData in currentClass.Properties do
if seenProps[propertyName] then
continue
end
seenProps[propertyName] = true
if EXCLUDED_PROP_NAMES[propertyName] then
continue
end
if not propertyData.Kind or not propertyData.Kind.Canonical then
continue
end
if not READABLE_SCRIPTABILITY[propertyData.Scriptability] then
continue
end
if propertyData.Kind.Canonical.Serialization == "DoesNotSerialize" then
continue
end
table.insert(propNames, propertyName)
propNameSet[propertyName] = true
end
currentClassName = currentClass.Superclass
until currentClassName == nil
-- Pre-decode defaults for every comparable property
local defaults: { [string]: any } = {}
local encodedDefaults = findDefaultProperties(className)
for _, propName in propNames do
local encoded = encodedDefaults[propName]
if encoded then
local ty = next(encoded)
if not EXCLUDED_ENCODED_TYPES[ty] then
local pcallOk, decodeOk, decoded = pcall(EncodedValue.decode, encoded)
if pcallOk and decodeOk and decoded ~= nil then
defaults[propName] = decoded
end
end
end
end
local result = table.freeze({
propNames = table.freeze(propNames),
propNameSet = table.freeze(propNameSet),
defaults = table.freeze(defaults),
})
classComparisonKeysCache[className] = result
return result
end
local function findClassDescriptor(className)
local classData = database.Classes[className]
if classData == nil then
return nil
end
-- Build a table of all properties, walking up the inheritance chain
local allProperties = {}
local currentClassName = className
repeat
local currentClass = database.Classes[currentClassName]
if currentClass == nil then
break
end
-- Add properties from this class (don't override if already set by subclass)
for propertyName, propertyData in pairs(currentClass.Properties) do
if allProperties[propertyName] == nil then
-- Only include canonical properties (not aliases)
if propertyData.Kind and propertyData.Kind.Canonical then
allProperties[propertyName] = {
name = propertyName,
scriptability = propertyData.Scriptability,
dataType = propertyData.DataType,
serialization = propertyData.Kind.Canonical.Serialization,
}
end
end
end
currentClassName = currentClass.Superclass
until currentClassName == nil
return {
name = classData.Name,
superclass = classData.Superclass,
properties = allProperties,
}
end
local function findCanonicalPropertyDescriptor(className, propertyName)
local currentClassName = className
repeat
local currentClass = database.Classes[currentClassName]
if currentClass == nil then
return currentClass
end
local propertyData = currentClass.Properties[propertyName]
if propertyData ~= nil then
local canonicalData = propertyData.Kind.Canonical
if canonicalData ~= nil then
return PropertyDescriptor.fromRaw(propertyData, currentClassName, propertyName)
end
local aliasData = propertyData.Kind.Alias
if aliasData ~= nil then
return PropertyDescriptor.fromRaw(
currentClass.Properties[aliasData.AliasFor],
currentClassName,
aliasData.AliasFor
)
end
return nil
end
currentClassName = currentClass.Superclass
until currentClassName == nil
return nil
end
local function readProperty(instance, propertyName)
local descriptor = findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
if descriptor == nil then
local fullName = ("%s.%s"):format(instance.className, propertyName)
return false, Error.new(Error.Kind.UnknownProperty, fullName)
end
return descriptor:read(instance)
end
local function writeProperty(instance, propertyName, value)
local descriptor = findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
if descriptor == nil then
local fullName = ("%s.%s"):format(instance.className, propertyName)
return false, Error.new(Error.Kind.UnknownProperty, fullName)
end
return descriptor:write(instance, value)
end
return {
readProperty = readProperty,
writeProperty = writeProperty,
findCanonicalPropertyDescriptor = findCanonicalPropertyDescriptor,
findClassDescriptor = findClassDescriptor,
findDefaultProperties = findDefaultProperties,
getClassComparisonKeys = getClassComparisonKeys,
Error = Error,
EncodedValue = EncodedValue,
}