diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua
index 60eb49a35..e4203c21c 100644
--- a/plugin/src/App/Components/StringDiffVisualizer/init.lua
+++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua
@@ -9,6 +9,7 @@ local Log = require(Packages.Log)
local Highlighter = require(Packages.Highlighter)
local StringDiff = require(script:FindFirstChild("StringDiff"))
+local Timer = require(Plugin.Timer)
local Theme = require(Plugin.App.Theme)
local CodeLabel = require(Plugin.App.Components.CodeLabel)
@@ -74,6 +75,7 @@ function StringDiffVisualizer:calculateContentSize()
end
function StringDiffVisualizer:calculateDiffLines()
+ Timer.start("StringDiffVisualizer:calculateDiffLines")
local oldString, newString = self.props.oldString, self.props.newString
-- Diff the two texts
@@ -133,6 +135,7 @@ function StringDiffVisualizer:calculateDiffLines()
end
end
+ Timer.stop()
return add, remove
end
diff --git a/plugin/src/App/Components/TableDiffVisualizer/Array.lua b/plugin/src/App/Components/TableDiffVisualizer/Array.lua
index 4fd56f3c7..febd07e97 100644
--- a/plugin/src/App/Components/TableDiffVisualizer/Array.lua
+++ b/plugin/src/App/Components/TableDiffVisualizer/Array.lua
@@ -4,6 +4,7 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
+local Timer = require(Plugin.Timer)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
@@ -21,6 +22,7 @@ function Array:init()
end
function Array:calculateDiff()
+ Timer.start("Array:calculateDiff")
--[[
Find the indexes that are added or removed from the array,
and display them side by side with gaps for the indexes that
@@ -63,6 +65,7 @@ function Array:calculateDiff()
j += 1
end
+ Timer.stop()
return diff
end
diff --git a/plugin/src/App/Components/TableDiffVisualizer/Dictionary.lua b/plugin/src/App/Components/TableDiffVisualizer/Dictionary.lua
index d0c32d963..033956c30 100644
--- a/plugin/src/App/Components/TableDiffVisualizer/Dictionary.lua
+++ b/plugin/src/App/Components/TableDiffVisualizer/Dictionary.lua
@@ -4,6 +4,7 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
+local Timer = require(Plugin.Timer)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
@@ -21,6 +22,7 @@ function Dictionary:init()
end
function Dictionary:calculateDiff()
+ Timer.start("Dictionary:calculateDiff")
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
-- Diff the two tables and find the added keys, removed keys, and changed keys
@@ -59,6 +61,7 @@ function Dictionary:calculateDiff()
return a.key < b.key
end)
+ Timer.stop()
return diff
end
diff --git a/plugin/src/App/StatusPages/Confirming.lua b/plugin/src/App/StatusPages/Confirming.lua
index d3b6baa2e..bd0fd0a13 100644
--- a/plugin/src/App/StatusPages/Confirming.lua
+++ b/plugin/src/App/StatusPages/Confirming.lua
@@ -4,6 +4,8 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
+local Timer = require(Plugin.Timer)
+local PatchTree = require(Plugin.PatchTree)
local Settings = require(Plugin.Settings)
local Theme = require(Plugin.App.Theme)
local TextButton = require(Plugin.App.Components.TextButton)
@@ -23,6 +25,7 @@ function ConfirmingPage:init()
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
self:setState({
+ patchTree = nil,
showingStringDiff = false,
oldString = "",
newString = "",
@@ -30,6 +33,28 @@ function ConfirmingPage:init()
oldTable = {},
newTable = {},
})
+
+ if self.props.confirmData and self.props.confirmData.patch and self.props.confirmData.instanceMap then
+ self:buildPatchTree()
+ end
+end
+
+function ConfirmingPage:didUpdate(prevProps)
+ if prevProps.confirmData ~= self.props.confirmData then
+ self:buildPatchTree()
+ end
+end
+
+function ConfirmingPage:buildPatchTree()
+ Timer.start("ConfirmingPage:buildPatchTree")
+ self:setState({
+ patchTree = PatchTree.build(
+ self.props.confirmData.patch,
+ self.props.confirmData.instanceMap,
+ { "Property", "Current", "Incoming" }
+ ),
+ })
+ Timer.stop()
end
function ConfirmingPage:render()
@@ -61,9 +86,7 @@ function ConfirmingPage:render()
transparency = self.props.transparency,
layoutOrder = 3,
- changeListHeaders = { "Property", "Current", "Incoming" },
- patch = self.props.confirmData.patch,
- instanceMap = self.props.confirmData.instanceMap,
+ patchTree = self.state.patchTree,
showStringDiff = function(oldString: string, newString: string)
self:setState({
diff --git a/plugin/src/App/StatusPages/Settings/Setting.lua b/plugin/src/App/StatusPages/Settings/Setting.lua
index 86adb6fcb..86170ec22 100644
--- a/plugin/src/App/StatusPages/Settings/Setting.lua
+++ b/plugin/src/App/StatusPages/Settings/Setting.lua
@@ -121,8 +121,14 @@ function Setting:render()
BackgroundTransparency = 1,
}, {
Name = e("TextLabel", {
- Text = (if self.props.experimental then '⚠ ' else "")
- .. self.props.name,
+ Text = (
+ if self.props.experimental
+ then '⚠ '
+ elseif
+ self.props.developerDebug
+ then '⚑ ' -- Guru is the only font with the flag emoji
+ else ""
+ ) .. self.props.name,
Font = Enum.Font.GothamBold,
TextSize = 17,
TextColor3 = theme.Setting.NameColor,
@@ -137,8 +143,10 @@ function Setting:render()
}),
Description = e("TextLabel", {
- Text = (if self.props.experimental then '[Experimental] ' else "")
- .. self.props.description,
+ Text = (if self.props.experimental
+ then '[Experimental] '
+ elseif self.props.developerDebug then '[Dev Debug] '
+ else "") .. self.props.description,
Font = Enum.Font.Gotham,
LineHeight = 1.2,
TextSize = 14,
diff --git a/plugin/src/App/StatusPages/Settings/init.lua b/plugin/src/App/StatusPages/Settings/init.lua
index a33eb1b54..c87a9d19f 100644
--- a/plugin/src/App/StatusPages/Settings/init.lua
+++ b/plugin/src/App/StatusPages/Settings/init.lua
@@ -84,148 +84,161 @@ function SettingsPage:render()
return Theme.with(function(theme)
theme = theme.Settings
- return e(ScrollingFrame, {
- size = UDim2.new(1, 0, 1, 0),
- contentSize = self.contentSize,
- transparency = self.props.transparency,
- }, {
+ return Roact.createFragment({
Navbar = e(Navbar, {
onBack = self.props.onBack,
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
-
- ShowNotifications = e(Setting, {
- id = "showNotifications",
- name = "Show Notifications",
- description = "Popup notifications in viewport",
+ Content = e(ScrollingFrame, {
+ size = UDim2.new(1, 0, 1, -47),
+ position = UDim2.new(0, 0, 0, 47),
+ contentSize = self.contentSize,
transparency = self.props.transparency,
- layoutOrder = layoutIncrement(),
- }),
+ }, {
+ ShowNotifications = e(Setting, {
+ id = "showNotifications",
+ name = "Show Notifications",
+ description = "Popup notifications in viewport",
+ transparency = self.props.transparency,
+ layoutOrder = layoutIncrement(),
+ }),
- SyncReminder = e(Setting, {
- id = "syncReminder",
- name = "Sync Reminder",
- description = "Notify to sync when opening a place that has previously been synced",
- transparency = self.props.transparency,
- visible = Settings:getBinding("showNotifications"),
- layoutOrder = layoutIncrement(),
- }),
+ SyncReminder = e(Setting, {
+ id = "syncReminder",
+ name = "Sync Reminder",
+ description = "Notify to sync when opening a place that has previously been synced",
+ transparency = self.props.transparency,
+ visible = Settings:getBinding("showNotifications"),
+ layoutOrder = layoutIncrement(),
+ }),
- ConfirmationBehavior = e(Setting, {
- id = "confirmationBehavior",
- name = "Confirmation Behavior",
- description = "When to prompt for confirmation before syncing",
- transparency = self.props.transparency,
- layoutOrder = layoutIncrement(),
+ ConfirmationBehavior = e(Setting, {
+ id = "confirmationBehavior",
+ name = "Confirmation Behavior",
+ description = "When to prompt for confirmation before syncing",
+ transparency = self.props.transparency,
+ layoutOrder = layoutIncrement(),
- options = confirmationBehaviors,
- }),
+ options = confirmationBehaviors,
+ }),
- LargeChangesConfirmationThreshold = e(Setting, {
- id = "largeChangesConfirmationThreshold",
- name = "Confirmation Threshold",
- description = "How many modified instances to be considered a large change",
- transparency = self.props.transparency,
- layoutOrder = layoutIncrement(),
- visible = Settings:getBinding("confirmationBehavior"):map(function(value)
- return value == "Large Changes"
- end),
- input = e(TextInput, {
- size = UDim2.new(0, 40, 0, 28),
- text = Settings:getBinding("largeChangesConfirmationThreshold"):map(function(value)
- return tostring(value)
+ LargeChangesConfirmationThreshold = e(Setting, {
+ id = "largeChangesConfirmationThreshold",
+ name = "Confirmation Threshold",
+ description = "How many modified instances to be considered a large change",
+ transparency = self.props.transparency,
+ layoutOrder = layoutIncrement(),
+ visible = Settings:getBinding("confirmationBehavior"):map(function(value)
+ return value == "Large Changes"
end),
+ input = e(TextInput, {
+ size = UDim2.new(0, 40, 0, 28),
+ text = Settings:getBinding("largeChangesConfirmationThreshold"):map(function(value)
+ return tostring(value)
+ end),
+ transparency = self.props.transparency,
+ enabled = true,
+ onEntered = function(text)
+ local number = tonumber(string.match(text, "%d+"))
+ if number then
+ Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999))
+ else
+ -- Force text back to last valid value
+ Settings:set(
+ "largeChangesConfirmationThreshold",
+ Settings:get("largeChangesConfirmationThreshold")
+ )
+ end
+ end,
+ }),
+ }),
+
+ PlaySounds = e(Setting, {
+ id = "playSounds",
+ name = "Play Sounds",
+ description = "Toggle sound effects",
transparency = self.props.transparency,
- enabled = true,
- onEntered = function(text)
- local number = tonumber(string.match(text, "%d+"))
- if number then
- Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999))
- else
- -- Force text back to last valid value
- Settings:set(
- "largeChangesConfirmationThreshold",
- Settings:get("largeChangesConfirmationThreshold")
- )
- end
- end,
+ layoutOrder = layoutIncrement(),
}),
- }),
- PlaySounds = e(Setting, {
- id = "playSounds",
- name = "Play Sounds",
- description = "Toggle sound effects",
- transparency = self.props.transparency,
- layoutOrder = layoutIncrement(),
- }),
+ AutoConnectPlaytestServer = e(Setting, {
+ id = "autoConnectPlaytestServer",
+ name = "Auto Connect Playtest Server",
+ description = "Automatically connect game server to Rojo when playtesting while connected in Edit",
+ experimental = true,
+ transparency = self.props.transparency,
+ layoutOrder = layoutIncrement(),
+ }),
- AutoConnectPlaytestServer = e(Setting, {
- id = "autoConnectPlaytestServer",
- name = "Auto Connect Playtest Server",
- description = "Automatically connect game server to Rojo when playtesting while connected in Edit",
- experimental = true,
- transparency = self.props.transparency,
- layoutOrder = layoutIncrement(),
- }),
+ OpenScriptsExternally = e(Setting, {
+ id = "openScriptsExternally",
+ name = "Open Scripts Externally",
+ description = "Attempt to open scripts in an external editor",
+ locked = self.props.syncActive,
+ experimental = true,
+ transparency = self.props.transparency,
+ layoutOrder = layoutIncrement(),
+ }),
- OpenScriptsExternally = e(Setting, {
- id = "openScriptsExternally",
- name = "Open Scripts Externally",
- description = "Attempt to open scripts in an external editor",
- locked = self.props.syncActive,
- experimental = true,
- transparency = self.props.transparency,
- layoutOrder = layoutIncrement(),
- }),
+ TwoWaySync = e(Setting, {
+ id = "twoWaySync",
+ name = "Two-Way Sync",
+ description = "Editing files in Studio will sync them into the filesystem",
+ locked = self.props.syncActive,
+ experimental = true,
+ transparency = self.props.transparency,
+ layoutOrder = layoutIncrement(),
+ }),
- TwoWaySync = e(Setting, {
- id = "twoWaySync",
- name = "Two-Way Sync",
- description = "Editing files in Studio will sync them into the filesystem",
- locked = self.props.syncActive,
- experimental = true,
- transparency = self.props.transparency,
- layoutOrder = layoutIncrement(),
- }),
+ LogLevel = e(Setting, {
+ id = "logLevel",
+ name = "Log Level",
+ description = "Plugin output verbosity level",
+ developerDebug = true,
+ transparency = self.props.transparency,
+ layoutOrder = layoutIncrement(),
- LogLevel = e(Setting, {
- id = "logLevel",
- name = "Log Level",
- description = "Plugin output verbosity level",
- transparency = self.props.transparency,
- layoutOrder = layoutIncrement(),
+ options = invertedLevels,
+ showReset = Settings:getBinding("logLevel"):map(function(value)
+ return value ~= "Info"
+ end),
+ onReset = function()
+ Settings:set("logLevel", "Info")
+ end,
+ }),
- options = invertedLevels,
- showReset = Settings:getBinding("logLevel"):map(function(value)
- return value ~= "Info"
- end),
- onReset = function()
- Settings:set("logLevel", "Info")
- end,
- }),
+ TypecheckingEnabled = e(Setting, {
+ id = "typecheckingEnabled",
+ name = "Typechecking",
+ description = "Toggle typechecking on the API surface",
+ developerDebug = true,
+ transparency = self.props.transparency,
+ layoutOrder = layoutIncrement(),
+ }),
- TypecheckingEnabled = e(Setting, {
- id = "typecheckingEnabled",
- name = "Typechecking",
- description = "Toggle typechecking on the API surface",
- transparency = self.props.transparency,
- layoutOrder = layoutIncrement(),
- }),
+ TimingLogsEnabled = e(Setting, {
+ id = "timingLogsEnabled",
+ name = "Timing Logs",
+ description = "Toggle logging timing of internal actions for benchmarking Rojo performance",
+ developerDebug = true,
+ transparency = self.props.transparency,
+ layoutOrder = layoutIncrement(),
+ }),
- Layout = e("UIListLayout", {
- FillDirection = Enum.FillDirection.Vertical,
- SortOrder = Enum.SortOrder.LayoutOrder,
+ Layout = e("UIListLayout", {
+ FillDirection = Enum.FillDirection.Vertical,
+ SortOrder = Enum.SortOrder.LayoutOrder,
- [Roact.Change.AbsoluteContentSize] = function(object)
- self.setContentSize(object.AbsoluteContentSize)
- end,
- }),
+ [Roact.Change.AbsoluteContentSize] = function(object)
+ self.setContentSize(object.AbsoluteContentSize)
+ end,
+ }),
- Padding = e("UIPadding", {
- PaddingLeft = UDim.new(0, 20),
- PaddingRight = UDim.new(0, 20),
+ Padding = e("UIPadding", {
+ PaddingLeft = UDim.new(0, 20),
+ PaddingRight = UDim.new(0, 20),
+ }),
}),
})
end)
diff --git a/plugin/src/PatchTree.lua b/plugin/src/PatchTree.lua
index e469e2112..d0d99e37d 100644
--- a/plugin/src/PatchTree.lua
+++ b/plugin/src/PatchTree.lua
@@ -11,6 +11,7 @@ local Packages = Rojo.Packages
local Log = require(Packages.Log)
+local Timer = require(Plugin.Timer)
local Types = require(Plugin.Types)
local decodeValue = require(Plugin.Reconciler.decodeValue)
local getProperty = require(Plugin.Reconciler.getProperty)
@@ -122,6 +123,7 @@ end
-- props must contain id, and cannot contain children or parentId
-- other than those three, it can hold anything
function Tree:addNode(parent, props)
+ Timer.start("Tree:addNode")
assert(props.id, "props must contain id")
parent = parent or "ROOT"
@@ -132,6 +134,7 @@ function Tree:addNode(parent, props)
for k, v in props do
node[k] = v
end
+ Timer.stop()
return node
end
@@ -142,22 +145,26 @@ function Tree:addNode(parent, props)
local parentNode = self:getNode(parent)
if not parentNode then
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
+ Timer.stop()
return
end
parentNode.children[node.id] = node
self.idToNode[node.id] = node
+ Timer.stop()
return node
end
-- Given a list of ancestor ids in descending order, builds the nodes for them
-- using the patch and instanceMap info
function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap)
+ Timer.start("Tree:buildAncestryNodes")
-- Build nodes for ancestry by going up the tree
previousId = previousId or "ROOT"
- for _, ancestorId in ancestryIds do
+ for i = #ancestryIds, 1, -1 do
+ local ancestorId = ancestryIds[i]
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
if not value then
Log.warn("Failed to find ancestor object for " .. ancestorId)
@@ -171,6 +178,8 @@ function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, p
})
previousId = ancestorId
end
+
+ Timer.stop()
end
local PatchTree = {}
@@ -178,10 +187,12 @@ local PatchTree = {}
-- Builds a new tree from a patch and instanceMap
-- uses changeListHeaders in node.changeList
function PatchTree.build(patch, instanceMap, changeListHeaders)
+ Timer.start("PatchTree.build")
local tree = Tree.new()
local knownAncestors = {}
+ Timer.start("patch.updated")
for _, change in patch.updated do
local instance = instanceMap.fromIds[change.id]
if not instance then
@@ -189,7 +200,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
end
-- Gather ancestors from existing DOM
- local ancestryIds = {}
+ local ancestryIds, ancestryIndex = {}, 0
local parentObject = instance.Parent
local parentId = instanceMap.fromInstances[parentObject]
local previousId = nil
@@ -200,7 +211,8 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
break
end
- table.insert(ancestryIds, 1, parentId)
+ ancestryIndex += 1
+ ancestryIds[ancestryIndex] = parentId
knownAncestors[parentId] = true
parentObject = parentObject.Parent
parentId = instanceMap.fromInstances[parentObject]
@@ -213,11 +225,38 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
if next(change.changedProperties) or change.changedName then
changeList = {}
- local hintBuffer, i = {}, 0
+ local hintBuffer, hintBufferSize, hintOverflow = table.create(3), 0, 0
+ local changeIndex = 0
local function addProp(prop: string, current: any?, incoming: any?, metadata: any?)
- i += 1
- hintBuffer[i] = prop
- changeList[i] = { prop, current, incoming, metadata }
+ changeIndex += 1
+ changeList[changeIndex] = { prop, current, incoming, metadata }
+
+ if hintBufferSize < 3 then
+ hintBufferSize += 1
+ hintBuffer[hintBufferSize] = prop
+ return
+ end
+
+ -- We only want to have 3 hints
+ -- to keep it deterministic, we sort them alphabetically
+
+ -- Either this prop overflows, or it makes another one move to overflow
+ hintOverflow += 1
+
+ -- Shortcut for the common case
+ if hintBuffer[3] <= prop then
+ -- This prop is below the last hint, no need to insert
+ return
+ end
+
+ -- Find the first available spot
+ for i, hintItem in hintBuffer do
+ if prop < hintItem then
+ -- This prop is before the currently selected hint,
+ -- so take its place and then continue to find a spot for the old hint
+ hintBuffer[i], prop = prop, hintBuffer[i]
+ end
+ end
end
-- Gather the changes
@@ -238,18 +277,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
end
-- Finalize detail values
-
- -- Trim hint to top 3
- table.sort(hintBuffer)
- if #hintBuffer > 3 then
- hintBuffer = {
- hintBuffer[1],
- hintBuffer[2],
- hintBuffer[3],
- i - 3 .. " more",
- }
- end
- hint = table.concat(hintBuffer, ", ")
+ hint = table.concat(hintBuffer, ", ") .. (if hintOverflow == 0 then "" else ", " .. hintOverflow .. " more")
-- Sort changes and add header
table.sort(changeList, function(a, b)
@@ -269,7 +297,9 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
changeList = changeList,
})
end
+ Timer.stop()
+ Timer.start("patch.removed")
for _, idOrInstance in patch.removed do
local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance
if not instance then
@@ -311,7 +341,9 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
instance = instance,
})
end
+ Timer.stop()
+ Timer.start("patch.added")
for id, change in patch.added do
-- Gather ancestors from existing DOM or future additions
local ancestryIds = {}
@@ -350,32 +382,47 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
if next(change.Properties) then
changeList = {}
- local hintBuffer, i = {}, 0
- for prop, incoming in change.Properties do
- i += 1
- hintBuffer[i] = prop
+ local hintBuffer, hintBufferSize, hintOverflow = table.create(3), 0, 0
+ local changeIndex = 0
+ local function addProp(prop: string, incoming: any)
+ changeIndex += 1
+ changeList[changeIndex] = { prop, "N/A", incoming }
- local success, incomingValue = decodeValue(incoming, instanceMap)
- if success then
- table.insert(changeList, { prop, "N/A", incomingValue })
- else
- table.insert(changeList, { prop, "N/A", select(2, next(incoming)) })
+ if hintBufferSize < 3 then
+ hintBufferSize += 1
+ hintBuffer[hintBufferSize] = prop
+ return
end
- end
- -- Finalize detail values
+ -- We only want to have 3 hints
+ -- to keep it deterministic, we sort them alphabetically
+
+ -- Either this prop overflows, or it makes another one move to overflow
+ hintOverflow += 1
- -- Trim hint to top 3
- table.sort(hintBuffer)
- if #hintBuffer > 3 then
- hintBuffer = {
- hintBuffer[1],
- hintBuffer[2],
- hintBuffer[3],
- i - 3 .. " more",
- }
+ -- Shortcut for the common case
+ if hintBuffer[3] <= prop then
+ -- This prop is below the last hint, no need to insert
+ return
+ end
+
+ -- Find the first available spot
+ for i, hintItem in hintBuffer do
+ if prop < hintItem then
+ -- This prop is before the currently selected hint,
+ -- so take its place and then continue to find a spot for the old hint
+ hintBuffer[i], prop = prop, hintBuffer[i]
+ end
+ end
end
- hint = table.concat(hintBuffer, ", ")
+
+ for prop, incoming in change.Properties do
+ local success, incomingValue = decodeValue(incoming, instanceMap)
+ addProp(prop, if success then incomingValue else select(2, next(incoming)))
+ end
+
+ -- Finalize detail values
+ hint = table.concat(hintBuffer, ", ") .. (if hintOverflow == 0 then "" else ", " .. hintOverflow .. " more")
-- Sort changes and add header
table.sort(changeList, function(a, b)
@@ -395,35 +442,27 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
instance = instanceMap.fromIds[id],
})
end
+ Timer.stop()
+ Timer.stop()
return tree
end
--- Creates a deep copy of a tree for immutability purposes in Roact
-function PatchTree.clone(tree)
- if not tree then
- return
- end
-
- local newTree = Tree.new()
- tree:forEach(function(node)
- newTree:addNode(node.parentId, table.clone(node))
- end)
-
- return newTree
-end
-
-- Updates the metadata of a tree with the unapplied patch and currently existing instances
-- Builds a new tree from the data if one isn't provided
-- Always returns a new tree for immutability purposes in Roact
function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
+ Timer.start("PatchTree.updateMetadata")
if tree then
- tree = PatchTree.clone(tree)
+ -- A shallow copy is enough for our purposes here since we really only need a new top-level object
+ -- for immutable comparison checks in Roact
+ tree = table.clone(tree)
else
tree = PatchTree.build(patch, instanceMap)
end
-- Update isWarning metadata
+ Timer.start("isWarning")
for _, failedChange in unappliedPatch.updated do
local node = tree:getNode(failedChange.id)
if not node then
@@ -492,8 +531,10 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name)
end
+ Timer.stop()
-- Update if instances exist
+ Timer.start("instanceAncestry")
tree:forEach(function(node)
if node.instance then
if node.instance.Parent == nil and node.instance ~= game then
@@ -509,7 +550,9 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
end
end
end)
+ Timer.stop()
+ Timer.stop()
return tree
end
diff --git a/plugin/src/Reconciler/init.lua b/plugin/src/Reconciler/init.lua
index 129457827..df7cd92f4 100644
--- a/plugin/src/Reconciler/init.lua
+++ b/plugin/src/Reconciler/init.lua
@@ -3,9 +3,14 @@
and mutating the Roblox DOM.
]]
-local Packages = script.Parent.Parent.Packages
+local Rojo = script:FindFirstAncestor("Rojo")
+local Plugin = Rojo.Plugin
+local Packages = Rojo.Packages
+
local Log = require(Packages.Log)
+local Timer = require(Plugin.Timer)
+
local applyPatch = require(script.applyPatch)
local hydrate = require(script.hydrate)
local diff = require(script.diff)
@@ -57,31 +62,55 @@ function Reconciler:hookPostcommit(callback: (patch: any, instanceMap: any, unap
end
function Reconciler:applyPatch(patch)
+ Timer.start("Reconciler:applyPatch")
+
+ Timer.start("precommitCallbacks")
+ -- Precommit callbacks must be serial in order to obey the contract that
+ -- they execute before commit
for _, callback in self.__precommitCallbacks do
local success, err = pcall(callback, patch, self.__instanceMap)
if not success then
Log.warn("Precommit hook errored: {}", err)
end
end
+ Timer.stop()
+ Timer.start("apply")
local unappliedPatch = applyPatch(self.__instanceMap, patch)
+ Timer.stop()
+ Timer.start("postcommitCallbacks")
+ -- Postcommit callbacks can be called with spawn since regardless of firing order, they are
+ -- guaranteed to be called after the commit
for _, callback in self.__postcommitCallbacks do
- local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
- if not success then
- Log.warn("Postcommit hook errored: {}", err)
- end
+ task.spawn(function()
+ local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
+ if not success then
+ Log.warn("Postcommit hook errored: {}", err)
+ end
+ end)
end
+ Timer.stop()
+
+ Timer.stop()
return unappliedPatch
end
function Reconciler:hydrate(virtualInstances, rootId, rootInstance)
- return hydrate(self.__instanceMap, virtualInstances, rootId, rootInstance)
+ Timer.start("Reconciler:hydrate")
+ local result = hydrate(self.__instanceMap, virtualInstances, rootId, rootInstance)
+ Timer.stop()
+
+ return result
end
function Reconciler:diff(virtualInstances, rootId)
- return diff(self.__instanceMap, virtualInstances, rootId)
+ Timer.start("Reconciler:diff")
+ local success, result = diff(self.__instanceMap, virtualInstances, rootId)
+ Timer.stop()
+
+ return success, result
end
return Reconciler
diff --git a/plugin/src/Settings.lua b/plugin/src/Settings.lua
index b5be22a76..67f13ba21 100644
--- a/plugin/src/Settings.lua
+++ b/plugin/src/Settings.lua
@@ -20,6 +20,7 @@ local defaultSettings = {
playSounds = true,
typecheckingEnabled = false,
logLevel = "Info",
+ timingLogsEnabled = false,
priorEndpoints = {},
}
diff --git a/plugin/src/Timer.lua b/plugin/src/Timer.lua
new file mode 100644
index 000000000..9a5bbbd02
--- /dev/null
+++ b/plugin/src/Timer.lua
@@ -0,0 +1,57 @@
+local Settings = require(script.Parent.Settings)
+
+local clock = os.clock
+
+local Timer = {
+ _entries = {},
+}
+
+function Timer._start(label)
+ local start = clock()
+ if not label then
+ error("[Rojo-Timer] Timer.start: label is required", 2)
+ return
+ end
+
+ table.insert(Timer._entries, { label, start })
+end
+
+function Timer._stop()
+ local stop = clock()
+
+ local entry = table.remove(Timer._entries)
+ if not entry then
+ error("[Rojo-Timer] Timer.stop: no label to stop", 2)
+ return
+ end
+
+ local label = entry[1]
+ if #Timer._entries > 0 then
+ local priorLabels = {}
+ for _, priorEntry in ipairs(Timer._entries) do
+ table.insert(priorLabels, priorEntry[1])
+ end
+ label = table.concat(priorLabels, "/") .. "/" .. label
+ end
+
+ local start = entry[2]
+ local duration = stop - start
+ print(string.format("[Rojo-Timer] %s took %.3f ms", label, duration * 1000))
+end
+
+-- Replace functions with no-op if not in debug mode
+local function no_op() end
+local function setFunctions(enabled)
+ if enabled then
+ Timer.start = Timer._start
+ Timer.stop = Timer._stop
+ else
+ Timer.start = no_op
+ Timer.stop = no_op
+ end
+end
+
+Settings:onChanged("timingLogsEnabled", setFunctions)
+setFunctions(Settings:get("timingLogsEnabled"))
+
+return Timer