mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
- Updated log messages across `ApiContext`, `ServeSession`, `Reconciler`, and `App` to use consistent formatting for timing information, replacing placeholder syntax with `string.format` for improved clarity. - Enhanced trace logs to provide more precise execution time measurements during various operations, including connection, reading, writing, and patch application processes. These changes aim to standardize logging practices and improve the readability of timing information in the application.
362 lines
12 KiB
Lua
362 lines
12 KiB
Lua
--[[
|
|
Apply a patch to the DOM. Returns any portions of the patch that weren't
|
|
possible to apply.
|
|
|
|
Patches can come from the server or be generated by the client.
|
|
]]
|
|
|
|
local Packages = script.Parent.Parent.Parent.Packages
|
|
local Log = require(Packages.Log)
|
|
|
|
local Config = require(script.Parent.Parent.Config)
|
|
local PatchSet = require(script.Parent.Parent.PatchSet)
|
|
local Types = require(script.Parent.Parent.Types)
|
|
local invariant = require(script.Parent.Parent.invariant)
|
|
|
|
local decodeValue = require(script.Parent.decodeValue)
|
|
local reify = require(script.Parent.reify)
|
|
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
|
|
local setProperty = require(script.Parent.setProperty)
|
|
|
|
local function applyPatch(instanceMap, patch)
|
|
local applyPatchClock = os.clock()
|
|
|
|
-- Tracks any portions of the patch that could not be applied to the DOM.
|
|
local unappliedPatch = PatchSet.newEmpty()
|
|
|
|
-- Contains a list of all of the ref properties that we'll need to assign.
|
|
-- It is imperative that refs are assigned after all instances are created
|
|
-- to ensure that referents can be mapped to instances correctly.
|
|
local deferredRefs = {}
|
|
|
|
local addCount = 0
|
|
for _ in pairs(patch.added) do
|
|
addCount += 1
|
|
end
|
|
if #patch.removed > 0 or addCount > 0 or #patch.updated > 0 then
|
|
Log.info(
|
|
"Applying patch to Studio: {} removals, {} additions, {} updates",
|
|
#patch.removed,
|
|
addCount,
|
|
#patch.updated
|
|
)
|
|
end
|
|
|
|
Log.trace(
|
|
"[TIMING] applyPatch() starting: {} removals, {} additions, {} updates",
|
|
#patch.removed,
|
|
addCount,
|
|
#patch.updated
|
|
)
|
|
|
|
local removalClock = os.clock()
|
|
for _, removedIdOrInstance in ipairs(patch.removed) do
|
|
-- Skip auto-created instances that shouldn't be synced
|
|
local instance = if Types.RbxId(removedIdOrInstance)
|
|
then instanceMap.fromIds[removedIdOrInstance]
|
|
else removedIdOrInstance
|
|
if instance and Config.ignoredClassNames[instance.ClassName] then
|
|
continue
|
|
end
|
|
|
|
-- Log the removal (info level for major operations)
|
|
local instancePath = if instance then instance:GetFullName() else tostring(removedIdOrInstance)
|
|
local instanceClass = if instance then instance.ClassName else "unknown"
|
|
Log.info("[Studio] Remove: {} ({})", instancePath, instanceClass)
|
|
|
|
local removeInstanceSuccess = pcall(function()
|
|
if Types.RbxId(removedIdOrInstance) then
|
|
instanceMap:destroyId(removedIdOrInstance)
|
|
else
|
|
instanceMap:destroyInstance(removedIdOrInstance)
|
|
end
|
|
end)
|
|
if not removeInstanceSuccess then
|
|
Log.warn("[Studio] Remove failed: {}", instancePath)
|
|
table.insert(unappliedPatch.removed, removedIdOrInstance)
|
|
end
|
|
end
|
|
|
|
Log.trace(
|
|
"[TIMING] applyPatch() removals completed ({} ms)",
|
|
string.format("%.1f", (os.clock() - removalClock) * 1000)
|
|
)
|
|
|
|
-- Detect echoed additions from DescendantAdded handler.
|
|
-- When the plugin pre-inserts instances with temp IDs and the server
|
|
-- broadcasts the same instances with real Refs, remap the InstanceMap
|
|
-- entries rather than creating duplicates via reify.
|
|
-- Only remap IDs explicitly tracked as scripts-only temp IDs to avoid
|
|
-- false positives with legitimate same-named additions.
|
|
if next(instanceMap.scriptsOnlyTempIds) then
|
|
local remapProgress = true
|
|
while remapProgress do
|
|
remapProgress = false
|
|
for id, virtualInstance in pairs(patch.added) do
|
|
local parentInstance = instanceMap.fromIds[virtualInstance.Parent]
|
|
if not parentInstance then
|
|
continue
|
|
end
|
|
for _, child in parentInstance:GetChildren() do
|
|
if child.Name == virtualInstance.Name and child.ClassName == virtualInstance.ClassName then
|
|
local existingId = instanceMap.fromInstances[child]
|
|
if existingId and existingId ~= id and instanceMap.scriptsOnlyTempIds[existingId] then
|
|
Log.trace("Echo remap: {} -> {} for {}", existingId, id, child:GetFullName())
|
|
instanceMap.scriptsOnlyTempIds[existingId] = nil
|
|
instanceMap:removeId(existingId)
|
|
instanceMap:insert(id, child)
|
|
patch.added[id] = nil
|
|
remapProgress = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local additionClock = os.clock()
|
|
Log.trace("[TIMING] applyPatch() additions starting")
|
|
for id, virtualInstance in pairs(patch.added) do
|
|
if instanceMap.fromIds[id] ~= nil then
|
|
-- This instance already exists. We might've already added it in a
|
|
-- previous iteration of this loop, or maybe this patch was not
|
|
-- supposed to list this instance.
|
|
--
|
|
-- It's probably fine, right?
|
|
continue
|
|
end
|
|
|
|
-- Find the first ancestor of this instance that is marked for an
|
|
-- addition.
|
|
--
|
|
-- This helps us make sure we only reify each instance once, and we
|
|
-- start from the top.
|
|
while patch.added[virtualInstance.Parent] ~= nil do
|
|
id = virtualInstance.Parent
|
|
virtualInstance = patch.added[id]
|
|
end
|
|
|
|
local parentInstance = instanceMap.fromIds[virtualInstance.Parent]
|
|
|
|
if parentInstance == nil then
|
|
-- This would be peculiar. If you create an instance with no
|
|
-- parent, were you supposed to create it at all?
|
|
invariant(
|
|
"Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}",
|
|
id,
|
|
virtualInstance.Parent,
|
|
instanceMap
|
|
)
|
|
end
|
|
|
|
-- Log the addition (info level for major operations)
|
|
local parentPath = parentInstance:GetFullName()
|
|
Log.info("[Studio] Add: {}.{} ({})", parentPath, virtualInstance.Name, virtualInstance.ClassName)
|
|
|
|
local failedToReify = reifyInstance(deferredRefs, instanceMap, patch.added, id, parentInstance)
|
|
|
|
if not PatchSet.isEmpty(failedToReify) then
|
|
Log.warn("[Studio] Add failed: {}.{}", parentPath, virtualInstance.Name)
|
|
Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
|
|
PatchSet.assign(unappliedPatch, failedToReify)
|
|
end
|
|
end
|
|
|
|
Log.trace(
|
|
"[TIMING] applyPatch() additions completed ({} ms)",
|
|
string.format("%.1f", (os.clock() - additionClock) * 1000)
|
|
)
|
|
|
|
local updateClock = os.clock()
|
|
Log.trace("[TIMING] applyPatch() updates starting ({} updates)", #patch.updated)
|
|
for _, update in ipairs(patch.updated) do
|
|
local instance = instanceMap.fromIds[update.id]
|
|
|
|
if instance == nil then
|
|
-- We can't update an instance that doesn't exist.
|
|
Log.warn("[Studio] Update failed: Instance with ID {} not found", update.id)
|
|
table.insert(unappliedPatch.updated, update)
|
|
continue
|
|
end
|
|
|
|
local instancePath = instance:GetFullName()
|
|
local propList = {}
|
|
if update.changedName then
|
|
table.insert(propList, "Name")
|
|
end
|
|
if update.changedClassName then
|
|
table.insert(propList, "ClassName")
|
|
end
|
|
if update.changedProperties then
|
|
for propName, _ in pairs(update.changedProperties) do
|
|
table.insert(propList, propName)
|
|
end
|
|
end
|
|
Log.info("[Studio] Update: {} - {}", instancePath, table.concat(propList, ", "))
|
|
|
|
-- Pause updates on this instance to avoid picking up our changes when
|
|
-- two-way sync is enabled.
|
|
instanceMap:pauseInstance(instance)
|
|
|
|
-- Track any part of this update that could not be applied.
|
|
local unappliedUpdate = {
|
|
id = update.id,
|
|
changedProperties = {},
|
|
}
|
|
local partiallyApplied = false
|
|
|
|
-- If the instance's className changed, we have a bumpy ride ahead while
|
|
-- we recreate this instance and move all of its children into the new
|
|
-- version atomically...ish.
|
|
if update.changedClassName ~= nil then
|
|
-- If the instance's name also changed, we'll do it here, since this
|
|
-- branch will skip the rest of the loop iteration.
|
|
local newName = update.changedName or instance.Name
|
|
|
|
-- TODO: When changing between instances that have similar sets of
|
|
-- properties, like between an ImageLabel and an ImageButton, we
|
|
-- should preserve all of the properties that are shared between the
|
|
-- two classes unless they're changed as part of this patch. This is
|
|
-- similar to how "class changer" Studio plugins work.
|
|
--
|
|
-- For now, we'll only apply properties that are mentioned in this
|
|
-- update. Patches with changedClassName set only occur in specific
|
|
-- circumstances, usually between Folder and ModuleScript instances.
|
|
-- While this may result in some issues, like not preserving the
|
|
-- "Archived" property, a robust solution is sufficiently
|
|
-- complicated that we're pushing it off for now.
|
|
local newProperties = update.changedProperties
|
|
|
|
-- If the instance's ClassName changed, we'll kick into reify to
|
|
-- create this instance. We'll handle moving all of children between
|
|
-- the instances after the new one is created.
|
|
local mockVirtualInstance = {
|
|
Id = update.id,
|
|
Name = newName,
|
|
ClassName = update.changedClassName,
|
|
Properties = newProperties,
|
|
Children = {},
|
|
}
|
|
|
|
local mockAdded = {
|
|
[update.id] = mockVirtualInstance,
|
|
}
|
|
|
|
local failedToReify = reifyInstance(deferredRefs, instanceMap, mockAdded, update.id, instance.Parent)
|
|
|
|
local newInstance = instanceMap.fromIds[update.id]
|
|
|
|
-- Some parts of reify may have failed, but this is not necessarily
|
|
-- critical. If the instance wasn't recreated or has the wrong Name,
|
|
-- we'll consider our attempt a failure.
|
|
if instance == newInstance or newInstance.Name ~= newName then
|
|
table.insert(unappliedPatch.updated, update)
|
|
continue
|
|
end
|
|
|
|
-- Here are the non-critical failures. We know that the instance
|
|
-- succeeded in creating and that assigning Name did not fail, but
|
|
-- other property assignments might've failed.
|
|
if not PatchSet.isEmpty(failedToReify) then
|
|
PatchSet.assign(unappliedPatch, failedToReify)
|
|
end
|
|
|
|
-- Watch out, this is the scary part! Move all of the children of
|
|
-- instance into newInstance.
|
|
--
|
|
-- TODO: If this fails part way through, should we move everything
|
|
-- back? For now, we assume that moving things will not fail.
|
|
for _, child in ipairs(instance:GetChildren()) do
|
|
child.Parent = newInstance
|
|
end
|
|
|
|
-- See you later, original instance.
|
|
|
|
-- Because the user might want to Undo this change, we cannot use Destroy
|
|
-- since that locks that parent and prevents ChangeHistoryService from
|
|
-- ever bringing it back. Instead, we parent to nil.
|
|
|
|
-- TODO: Can this fail? Some kinds of instance may not appreciate
|
|
-- being reparented, like services.
|
|
instance.Parent = nil
|
|
|
|
-- This completes your rebuilding a plane mid-flight safety
|
|
-- instruction. Please sit back, relax, and enjoy your flight.
|
|
continue
|
|
end
|
|
|
|
if update.changedName ~= nil then
|
|
local setNameSuccess = pcall(function()
|
|
instance.Name = update.changedName
|
|
end)
|
|
if not setNameSuccess then
|
|
unappliedUpdate.changedName = update.changedName
|
|
partiallyApplied = true
|
|
end
|
|
end
|
|
|
|
if update.changedMetadata ~= nil then
|
|
-- TODO: Support changing metadata. This will become necessary when
|
|
-- Rojo persistently tracks metadata for each instance in order to
|
|
-- remove extra instances.
|
|
unappliedUpdate.changedMetadata = update.changedMetadata
|
|
partiallyApplied = true
|
|
end
|
|
|
|
if update.changedProperties ~= nil then
|
|
for propertyName, propertyValue in pairs(update.changedProperties) do
|
|
-- Because refs may refer to instances that we haven't constructed yet,
|
|
-- we defer applying any ref properties until all instances are created.
|
|
if next(propertyValue) == "Ref" then
|
|
table.insert(deferredRefs, {
|
|
id = update.id,
|
|
instance = instance,
|
|
propertyName = propertyName,
|
|
virtualValue = propertyValue,
|
|
})
|
|
continue
|
|
end
|
|
|
|
local decodeSuccess, decodedValue = decodeValue(propertyValue, instanceMap)
|
|
if not decodeSuccess then
|
|
unappliedUpdate.changedProperties[propertyName] = propertyValue
|
|
partiallyApplied = true
|
|
continue
|
|
end
|
|
|
|
local setPropertySuccess = setProperty(instance, propertyName, decodedValue)
|
|
if not setPropertySuccess then
|
|
unappliedUpdate.changedProperties[propertyName] = propertyValue
|
|
partiallyApplied = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if partiallyApplied then
|
|
table.insert(unappliedPatch.updated, unappliedUpdate)
|
|
end
|
|
end
|
|
|
|
Log.trace(
|
|
"[TIMING] applyPatch() updates completed ({} ms)",
|
|
string.format("%.1f", (os.clock() - updateClock) * 1000)
|
|
)
|
|
|
|
local refsClock = os.clock()
|
|
Log.trace("[TIMING] applyPatch() deferred refs starting ({} refs)", #deferredRefs)
|
|
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
|
Log.trace(
|
|
"[TIMING] applyPatch() deferred refs completed ({} ms)",
|
|
string.format("%.1f", (os.clock() - refsClock) * 1000)
|
|
)
|
|
|
|
Log.trace(
|
|
"[TIMING] applyPatch() total completed ({} ms)",
|
|
string.format("%.1f", (os.clock() - applyPatchClock) * 1000)
|
|
)
|
|
|
|
return unappliedPatch
|
|
end
|
|
|
|
return applyPatch
|