Add benchmarking, perf gains, and better settings UI (#850)

This commit is contained in:
boatbomber
2024-02-12 15:58:35 -08:00
committed by GitHub
parent cf25eb0833
commit 8ff064fe28
10 changed files with 380 additions and 197 deletions

View File

@@ -9,6 +9,7 @@ local Log = require(Packages.Log)
local Highlighter = require(Packages.Highlighter) local Highlighter = require(Packages.Highlighter)
local StringDiff = require(script:FindFirstChild("StringDiff")) local StringDiff = require(script:FindFirstChild("StringDiff"))
local Timer = require(Plugin.Timer)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local CodeLabel = require(Plugin.App.Components.CodeLabel) local CodeLabel = require(Plugin.App.Components.CodeLabel)
@@ -74,6 +75,7 @@ function StringDiffVisualizer:calculateContentSize()
end end
function StringDiffVisualizer:calculateDiffLines() function StringDiffVisualizer:calculateDiffLines()
Timer.start("StringDiffVisualizer:calculateDiffLines")
local oldString, newString = self.props.oldString, self.props.newString local oldString, newString = self.props.oldString, self.props.newString
-- Diff the two texts -- Diff the two texts
@@ -133,6 +135,7 @@ function StringDiffVisualizer:calculateDiffLines()
end end
end end
Timer.stop()
return add, remove return add, remove
end end

View File

@@ -4,6 +4,7 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Packages.Roact)
local Timer = require(Plugin.Timer)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
@@ -21,6 +22,7 @@ function Array:init()
end end
function Array:calculateDiff() function Array:calculateDiff()
Timer.start("Array:calculateDiff")
--[[ --[[
Find the indexes that are added or removed from the array, Find the indexes that are added or removed from the array,
and display them side by side with gaps for the indexes that and display them side by side with gaps for the indexes that
@@ -63,6 +65,7 @@ function Array:calculateDiff()
j += 1 j += 1
end end
Timer.stop()
return diff return diff
end end

View File

@@ -4,6 +4,7 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Packages.Roact)
local Timer = require(Plugin.Timer)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
@@ -21,6 +22,7 @@ function Dictionary:init()
end end
function Dictionary:calculateDiff() function Dictionary:calculateDiff()
Timer.start("Dictionary:calculateDiff")
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {} 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 -- 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 return a.key < b.key
end) end)
Timer.stop()
return diff return diff
end end

View File

@@ -4,6 +4,8 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Packages.Roact)
local Timer = require(Plugin.Timer)
local PatchTree = require(Plugin.PatchTree)
local Settings = require(Plugin.Settings) local Settings = require(Plugin.Settings)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local TextButton = require(Plugin.App.Components.TextButton) 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.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
self:setState({ self:setState({
patchTree = nil,
showingStringDiff = false, showingStringDiff = false,
oldString = "", oldString = "",
newString = "", newString = "",
@@ -30,6 +33,28 @@ function ConfirmingPage:init()
oldTable = {}, oldTable = {},
newTable = {}, 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 end
function ConfirmingPage:render() function ConfirmingPage:render()
@@ -61,9 +86,7 @@ function ConfirmingPage:render()
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 3, layoutOrder = 3,
changeListHeaders = { "Property", "Current", "Incoming" }, patchTree = self.state.patchTree,
patch = self.props.confirmData.patch,
instanceMap = self.props.confirmData.instanceMap,
showStringDiff = function(oldString: string, newString: string) showStringDiff = function(oldString: string, newString: string)
self:setState({ self:setState({

View File

@@ -121,8 +121,14 @@ function Setting:render()
BackgroundTransparency = 1, BackgroundTransparency = 1,
}, { }, {
Name = e("TextLabel", { Name = e("TextLabel", {
Text = (if self.props.experimental then '<font color="#FF8E3C">⚠ </font>' else "") Text = (
.. self.props.name, if self.props.experimental
then '<font color="#FF8E3C">⚠ </font>'
elseif
self.props.developerDebug
then '<font family="rbxasset://fonts/families/Guru.json" color="#35B5FF">⚑ </font>' -- Guru is the only font with the flag emoji
else ""
) .. self.props.name,
Font = Enum.Font.GothamBold, Font = Enum.Font.GothamBold,
TextSize = 17, TextSize = 17,
TextColor3 = theme.Setting.NameColor, TextColor3 = theme.Setting.NameColor,
@@ -137,8 +143,10 @@ function Setting:render()
}), }),
Description = e("TextLabel", { Description = e("TextLabel", {
Text = (if self.props.experimental then '<font color="#FF8E3C">[Experimental] </font>' else "") Text = (if self.props.experimental
.. self.props.description, then '<font color="#FF8E3C">[Experimental] </font>'
elseif self.props.developerDebug then '<font color="#35B5FF">[Dev Debug] </font>'
else "") .. self.props.description,
Font = Enum.Font.Gotham, Font = Enum.Font.Gotham,
LineHeight = 1.2, LineHeight = 1.2,
TextSize = 14, TextSize = 14,

View File

@@ -84,148 +84,161 @@ function SettingsPage:render()
return Theme.with(function(theme) return Theme.with(function(theme)
theme = theme.Settings theme = theme.Settings
return e(ScrollingFrame, { return Roact.createFragment({
size = UDim2.new(1, 0, 1, 0),
contentSize = self.contentSize,
transparency = self.props.transparency,
}, {
Navbar = e(Navbar, { Navbar = e(Navbar, {
onBack = self.props.onBack, onBack = self.props.onBack,
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = layoutIncrement(),
}), }),
Content = e(ScrollingFrame, {
ShowNotifications = e(Setting, { size = UDim2.new(1, 0, 1, -47),
id = "showNotifications", position = UDim2.new(0, 0, 0, 47),
name = "Show Notifications", contentSize = self.contentSize,
description = "Popup notifications in viewport",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), }, {
}), ShowNotifications = e(Setting, {
id = "showNotifications",
SyncReminder = e(Setting, { name = "Show Notifications",
id = "syncReminder", description = "Popup notifications in viewport",
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(),
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)
end),
transparency = self.props.transparency, transparency = self.props.transparency,
enabled = true, layoutOrder = layoutIncrement(),
onEntered = function(text) }),
local number = tonumber(string.match(text, "%d+"))
if number then SyncReminder = e(Setting, {
Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999)) id = "syncReminder",
else name = "Sync Reminder",
-- Force text back to last valid value description = "Notify to sync when opening a place that has previously been synced",
Settings:set( transparency = self.props.transparency,
"largeChangesConfirmationThreshold", visible = Settings:getBinding("showNotifications"),
Settings:get("largeChangesConfirmationThreshold") layoutOrder = layoutIncrement(),
) }),
end
ConfirmationBehavior = e(Setting, {
id = "confirmationBehavior",
name = "Confirmation Behavior",
description = "When to prompt for confirmation before syncing",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
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)
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,
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(),
}),
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(),
options = invertedLevels,
showReset = Settings:getBinding("logLevel"):map(function(value)
return value ~= "Info"
end),
onReset = function()
Settings:set("logLevel", "Info")
end, end,
}), }),
}),
PlaySounds = e(Setting, { TypecheckingEnabled = e(Setting, {
id = "playSounds", id = "typecheckingEnabled",
name = "Play Sounds", name = "Typechecking",
description = "Toggle sound effects", description = "Toggle typechecking on the API surface",
transparency = self.props.transparency, developerDebug = true,
layoutOrder = layoutIncrement(), transparency = self.props.transparency,
}), layoutOrder = layoutIncrement(),
}),
AutoConnectPlaytestServer = e(Setting, { TimingLogsEnabled = e(Setting, {
id = "autoConnectPlaytestServer", id = "timingLogsEnabled",
name = "Auto Connect Playtest Server", name = "Timing Logs",
description = "Automatically connect game server to Rojo when playtesting while connected in Edit", description = "Toggle logging timing of internal actions for benchmarking Rojo performance",
experimental = true, developerDebug = true,
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = layoutIncrement(),
}), }),
OpenScriptsExternally = e(Setting, { Layout = e("UIListLayout", {
id = "openScriptsExternally", FillDirection = Enum.FillDirection.Vertical,
name = "Open Scripts Externally", SortOrder = Enum.SortOrder.LayoutOrder,
description = "Attempt to open scripts in an external editor",
locked = self.props.syncActive,
experimental = true,
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
TwoWaySync = e(Setting, { [Roact.Change.AbsoluteContentSize] = function(object)
id = "twoWaySync", self.setContentSize(object.AbsoluteContentSize)
name = "Two-Way Sync", end,
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, { Padding = e("UIPadding", {
id = "logLevel", PaddingLeft = UDim.new(0, 20),
name = "Log Level", PaddingRight = UDim.new(0, 20),
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,
}),
TypecheckingEnabled = e(Setting, {
id = "typecheckingEnabled",
name = "Typechecking",
description = "Toggle typechecking on the API surface",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}), }),
}) })
end) end)

View File

@@ -11,6 +11,7 @@ local Packages = Rojo.Packages
local Log = require(Packages.Log) local Log = require(Packages.Log)
local Timer = require(Plugin.Timer)
local Types = require(Plugin.Types) local Types = require(Plugin.Types)
local decodeValue = require(Plugin.Reconciler.decodeValue) local decodeValue = require(Plugin.Reconciler.decodeValue)
local getProperty = require(Plugin.Reconciler.getProperty) local getProperty = require(Plugin.Reconciler.getProperty)
@@ -122,6 +123,7 @@ end
-- props must contain id, and cannot contain children or parentId -- props must contain id, and cannot contain children or parentId
-- other than those three, it can hold anything -- other than those three, it can hold anything
function Tree:addNode(parent, props) function Tree:addNode(parent, props)
Timer.start("Tree:addNode")
assert(props.id, "props must contain id") assert(props.id, "props must contain id")
parent = parent or "ROOT" parent = parent or "ROOT"
@@ -132,6 +134,7 @@ function Tree:addNode(parent, props)
for k, v in props do for k, v in props do
node[k] = v node[k] = v
end end
Timer.stop()
return node return node
end end
@@ -142,22 +145,26 @@ function Tree:addNode(parent, props)
local parentNode = self:getNode(parent) local parentNode = self:getNode(parent)
if not parentNode then if not parentNode then
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props) Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
Timer.stop()
return return
end end
parentNode.children[node.id] = node parentNode.children[node.id] = node
self.idToNode[node.id] = node self.idToNode[node.id] = node
Timer.stop()
return node return node
end end
-- Given a list of ancestor ids in descending order, builds the nodes for them -- Given a list of ancestor ids in descending order, builds the nodes for them
-- using the patch and instanceMap info -- using the patch and instanceMap info
function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap) function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap)
Timer.start("Tree:buildAncestryNodes")
-- Build nodes for ancestry by going up the tree -- Build nodes for ancestry by going up the tree
previousId = previousId or "ROOT" 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] local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
if not value then if not value then
Log.warn("Failed to find ancestor object for " .. ancestorId) Log.warn("Failed to find ancestor object for " .. ancestorId)
@@ -171,6 +178,8 @@ function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, p
}) })
previousId = ancestorId previousId = ancestorId
end end
Timer.stop()
end end
local PatchTree = {} local PatchTree = {}
@@ -178,10 +187,12 @@ local PatchTree = {}
-- Builds a new tree from a patch and instanceMap -- Builds a new tree from a patch and instanceMap
-- uses changeListHeaders in node.changeList -- uses changeListHeaders in node.changeList
function PatchTree.build(patch, instanceMap, changeListHeaders) function PatchTree.build(patch, instanceMap, changeListHeaders)
Timer.start("PatchTree.build")
local tree = Tree.new() local tree = Tree.new()
local knownAncestors = {} local knownAncestors = {}
Timer.start("patch.updated")
for _, change in patch.updated do for _, change in patch.updated do
local instance = instanceMap.fromIds[change.id] local instance = instanceMap.fromIds[change.id]
if not instance then if not instance then
@@ -189,7 +200,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
end end
-- Gather ancestors from existing DOM -- Gather ancestors from existing DOM
local ancestryIds = {} local ancestryIds, ancestryIndex = {}, 0
local parentObject = instance.Parent local parentObject = instance.Parent
local parentId = instanceMap.fromInstances[parentObject] local parentId = instanceMap.fromInstances[parentObject]
local previousId = nil local previousId = nil
@@ -200,7 +211,8 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
break break
end end
table.insert(ancestryIds, 1, parentId) ancestryIndex += 1
ancestryIds[ancestryIndex] = parentId
knownAncestors[parentId] = true knownAncestors[parentId] = true
parentObject = parentObject.Parent parentObject = parentObject.Parent
parentId = instanceMap.fromInstances[parentObject] parentId = instanceMap.fromInstances[parentObject]
@@ -213,11 +225,38 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
if next(change.changedProperties) or change.changedName then if next(change.changedProperties) or change.changedName then
changeList = {} 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?) local function addProp(prop: string, current: any?, incoming: any?, metadata: any?)
i += 1 changeIndex += 1
hintBuffer[i] = prop changeList[changeIndex] = { prop, current, incoming, metadata }
changeList[i] = { 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 end
-- Gather the changes -- Gather the changes
@@ -238,18 +277,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
end end
-- Finalize detail values -- Finalize detail values
hint = table.concat(hintBuffer, ", ") .. (if hintOverflow == 0 then "" else ", " .. hintOverflow .. " more")
-- 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, ", ")
-- Sort changes and add header -- Sort changes and add header
table.sort(changeList, function(a, b) table.sort(changeList, function(a, b)
@@ -269,7 +297,9 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
changeList = changeList, changeList = changeList,
}) })
end end
Timer.stop()
Timer.start("patch.removed")
for _, idOrInstance in patch.removed do for _, idOrInstance in patch.removed do
local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance
if not instance then if not instance then
@@ -311,7 +341,9 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
instance = instance, instance = instance,
}) })
end end
Timer.stop()
Timer.start("patch.added")
for id, change in patch.added do for id, change in patch.added do
-- Gather ancestors from existing DOM or future additions -- Gather ancestors from existing DOM or future additions
local ancestryIds = {} local ancestryIds = {}
@@ -350,32 +382,47 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
if next(change.Properties) then if next(change.Properties) then
changeList = {} changeList = {}
local hintBuffer, i = {}, 0 local hintBuffer, hintBufferSize, hintOverflow = table.create(3), 0, 0
for prop, incoming in change.Properties do local changeIndex = 0
i += 1 local function addProp(prop: string, incoming: any)
hintBuffer[i] = prop changeIndex += 1
changeList[changeIndex] = { prop, "N/A", incoming }
local success, incomingValue = decodeValue(incoming, instanceMap) if hintBufferSize < 3 then
if success then hintBufferSize += 1
table.insert(changeList, { prop, "N/A", incomingValue }) hintBuffer[hintBufferSize] = prop
else return
table.insert(changeList, { prop, "N/A", select(2, next(incoming)) }) 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
end end
-- Finalize detail values for prop, incoming in change.Properties do
local success, incomingValue = decodeValue(incoming, instanceMap)
-- Trim hint to top 3 addProp(prop, if success then incomingValue else select(2, next(incoming)))
table.sort(hintBuffer)
if #hintBuffer > 3 then
hintBuffer = {
hintBuffer[1],
hintBuffer[2],
hintBuffer[3],
i - 3 .. " more",
}
end end
hint = table.concat(hintBuffer, ", ")
-- Finalize detail values
hint = table.concat(hintBuffer, ", ") .. (if hintOverflow == 0 then "" else ", " .. hintOverflow .. " more")
-- Sort changes and add header -- Sort changes and add header
table.sort(changeList, function(a, b) table.sort(changeList, function(a, b)
@@ -395,35 +442,27 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
instance = instanceMap.fromIds[id], instance = instanceMap.fromIds[id],
}) })
end end
Timer.stop()
Timer.stop()
return tree return tree
end 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 -- 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 -- Builds a new tree from the data if one isn't provided
-- Always returns a new tree for immutability purposes in Roact -- Always returns a new tree for immutability purposes in Roact
function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch) function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
Timer.start("PatchTree.updateMetadata")
if tree then 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 else
tree = PatchTree.build(patch, instanceMap) tree = PatchTree.build(patch, instanceMap)
end end
-- Update isWarning metadata -- Update isWarning metadata
Timer.start("isWarning")
for _, failedChange in unappliedPatch.updated do for _, failedChange in unappliedPatch.updated do
local node = tree:getNode(failedChange.id) local node = tree:getNode(failedChange.id)
if not node then if not node then
@@ -492,8 +531,10 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
node.isWarning = true node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name) Log.trace("Marked node as warning: {} {}", node.id, node.name)
end end
Timer.stop()
-- Update if instances exist -- Update if instances exist
Timer.start("instanceAncestry")
tree:forEach(function(node) tree:forEach(function(node)
if node.instance then if node.instance then
if node.instance.Parent == nil and node.instance ~= game 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 end
end) end)
Timer.stop()
Timer.stop()
return tree return tree
end end

View File

@@ -3,9 +3,14 @@
and mutating the Roblox DOM. 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 Log = require(Packages.Log)
local Timer = require(Plugin.Timer)
local applyPatch = require(script.applyPatch) local applyPatch = require(script.applyPatch)
local hydrate = require(script.hydrate) local hydrate = require(script.hydrate)
local diff = require(script.diff) local diff = require(script.diff)
@@ -57,31 +62,55 @@ function Reconciler:hookPostcommit(callback: (patch: any, instanceMap: any, unap
end end
function Reconciler:applyPatch(patch) 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 for _, callback in self.__precommitCallbacks do
local success, err = pcall(callback, patch, self.__instanceMap) local success, err = pcall(callback, patch, self.__instanceMap)
if not success then if not success then
Log.warn("Precommit hook errored: {}", err) Log.warn("Precommit hook errored: {}", err)
end end
end end
Timer.stop()
Timer.start("apply")
local unappliedPatch = applyPatch(self.__instanceMap, patch) 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 for _, callback in self.__postcommitCallbacks do
local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch) task.spawn(function()
if not success then local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
Log.warn("Postcommit hook errored: {}", err) if not success then
end Log.warn("Postcommit hook errored: {}", err)
end
end)
end end
Timer.stop()
Timer.stop()
return unappliedPatch return unappliedPatch
end end
function Reconciler:hydrate(virtualInstances, rootId, rootInstance) 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 end
function Reconciler:diff(virtualInstances, rootId) 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 end
return Reconciler return Reconciler

View File

@@ -20,6 +20,7 @@ local defaultSettings = {
playSounds = true, playSounds = true,
typecheckingEnabled = false, typecheckingEnabled = false,
logLevel = "Info", logLevel = "Info",
timingLogsEnabled = false,
priorEndpoints = {}, priorEndpoints = {},
} }

57
plugin/src/Timer.lua Normal file
View File

@@ -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