--!strict --[[* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow ]] local Packages = script.Parent.Parent.Parent.Parent.Parent local LuauPolyfill = require(Packages.LuauPolyfill) local Array = LuauPolyfill.Array local Map = LuauPolyfill.Map local console = LuauPolyfill.console type Map = LuauPolyfill.Map type Array = LuauPolyfill.Array local exports = {} --[[* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow ]] local constantsModule = require(script.Parent.Parent.Parent.Parent.constants) local __DEBUG__ = constantsModule.__DEBUG__ local TREE_OPERATION_ADD = constantsModule.TREE_OPERATION_ADD local TREE_OPERATION_REMOVE = constantsModule.TREE_OPERATION_REMOVE local TREE_OPERATION_REORDER_CHILDREN = constantsModule.TREE_OPERATION_REORDER_CHILDREN local TREE_OPERATION_UPDATE_TREE_BASE_DURATION = constantsModule.TREE_OPERATION_UPDATE_TREE_BASE_DURATION local devtoolsTypes = require(script.Parent.Parent.Parent.types) type ProfilerStore = devtoolsTypes.ProfilerStore local ElementTypeRoot = require(script.Parent.Parent.Parent.Parent.types).ElementTypeRoot local typesModule = require(script.Parent.Parent.Parent.Parent.types) type ElementType = typesModule.ElementType local Profiler_typesModule = require(script.Parent.types) type CommitTree = Profiler_typesModule.CommitTree type CommitTreeNode = Profiler_typesModule.CommitTreeNode type ProfilingDataForRootFrontend = Profiler_typesModule.ProfilingDataForRootFrontend type ProfilingDataFrontend = Profiler_typesModule.ProfilingDataFrontend local function debug_(methodName, ...: any) if __DEBUG__ then print("[CommitTreeBuilder]", methodName, ...) end end local function __printTree(commitTree: CommitTree) if __DEBUG__ then local nodes, rootID = commitTree.nodes, commitTree.rootID console.group("__printTree()") local queue = { rootID, 0 } -- ROBLOX TODO Luau? if length check > 0, remove() nil-ability could be removed while #queue > 0 do local id = table.remove(queue, 1) :: number local depth = table.remove(queue, 1) :: number local node = nodes:get(id) -- ROBLOX FIXME Luau: need to understand error() narrows node nil-ability if node == nil then error( string.format( 'Could not find node with id "%s" in commit tree', tostring(id) ) ) end console.log( string.format( "%s%s:%s %s (%s)", string.rep("\u{2022}", depth), tostring((node :: CommitTreeNode).id), tostring((node :: CommitTreeNode).displayName or ""), if (node :: CommitTreeNode).key then string.format( 'key:"%s"', tostring((node :: CommitTreeNode).key) ) else "", tostring((node :: CommitTreeNode).treeBaseDuration) ) ) Array.forEach((node :: CommitTreeNode).children, function(childID) Array.concat(queue, { childID, depth + 1 }) end) end console.groupEnd() end end local function updateTree(commitTree: CommitTree, operations: Array): CommitTree -- Clone the original tree so edits don't affect it. local nodes = Map.new(commitTree.nodes) -- Clone nodes before mutating them so edits don't affect the original. local function getClonedNode(id: number): CommitTreeNode local clonedNode = table.clone((nodes:get(id) :: any) :: CommitTreeNode) nodes:set(id, clonedNode) return clonedNode end local i = 3 -- Skip rendererID and currentRootID local function POSTFIX_INCREMENT() local x = i i += 1 return x end local id: number = (nil :: any) :: number -- Reassemble the string table. local stringTable: Array = { -- ROBLOX deviation: element 1 corresponds to empty string, this is why key is "" instead of nil in snapshots "", -- ID = 0 corresponds to the null string. } local stringTableSize = operations[POSTFIX_INCREMENT()] local stringTableEnd = i + stringTableSize while i < stringTableEnd do -- ROBLOX deviation: don't binary encode strings, so store string directly rather than length -- local nextLength = operations[POSTFIX_INCREMENT()] -- local nextString = utfDecodeString(Array.slice(operations, i, i + nextLength)) local nextString = operations[POSTFIX_INCREMENT()] table.insert(stringTable, nextString) -- ROBLOX deviation: don't binary encode strings, so no need to move pointer -- i = i + nextLength end while i <= #operations do local operation = operations[POSTFIX_INCREMENT()] if operation == TREE_OPERATION_ADD then id = operations[POSTFIX_INCREMENT()] local type_ = (operations[POSTFIX_INCREMENT()] :: any) :: ElementType if nodes:has(id) then error( "Commit tree already contains fiber " .. tostring(id) .. ". This is a bug in React DevTools." ) end if type_ == ElementTypeRoot then i += 2 -- supportsProfiling flag and hasOwnerMetadata flag if __DEBUG__ then debug_("Add", ("new root fiber %s"):format(tostring(id))) end local node: CommitTreeNode = { children = {}, displayName = nil, hocDisplayNames = nil, id = id, key = nil, parentID = 0, treeBaseDuration = 0, -- This will be updated by a subsequent operation type = type_, } nodes:set(id, node) else local parentID = operations[POSTFIX_INCREMENT()] i += 1 -- skip ownerID local displayNameStringID = operations[POSTFIX_INCREMENT()] local displayName = stringTable[displayNameStringID + 1] local keyStringID = operations[POSTFIX_INCREMENT()] local key = stringTable[keyStringID + 1] -- 1 indexed stringtable if __DEBUG__ then debug_( "Add", ("fiber %s (%s) as child of %s"):format( tostring(id), tostring(displayName or "null"), tostring(parentID) ) ) end local parentNode = getClonedNode(parentID) parentNode.children = Array.concat(parentNode.children, id) local node: CommitTreeNode = { children = {}, displayName = displayName, hocDisplayNames = nil, id = id, key = key, parentID = parentID, treeBaseDuration = 0, -- This will be updated by a subsequent operation type = type_, } nodes:set(id, node) end elseif operation == TREE_OPERATION_REMOVE then local removeLength = operations[POSTFIX_INCREMENT()] for _ = 1, removeLength do id = operations[POSTFIX_INCREMENT()] if not nodes:has(id) then error( "Commit tree does not contain fiber " .. tostring(id) .. ". This is a bug in React DevTools." ) end local node = getClonedNode(id) local parentID = node.parentID nodes:delete(id) if not nodes:has(parentID) then -- No-op else local parentNode = getClonedNode(parentID) if __DEBUG__ then debug_( "Remove", ("fiber %s from parent %s"):format( tostring(id), tostring(parentID) ) ) end parentNode.children = Array.filter( parentNode.children, function(childID) return childID ~= id end ) end end elseif operation == TREE_OPERATION_REORDER_CHILDREN then id = operations[POSTFIX_INCREMENT()] local numChildren = operations[POSTFIX_INCREMENT()] local children = ( Array.slice(operations, i, i + numChildren) :: any ) :: Array i += numChildren if __DEBUG__ then debug_( "Re-order", ("fiber %s children %s"):format( tostring(id), tostring(Array.join(children, ",")) ) ) end local node = getClonedNode(id) -- ROBLOX FIXME Luau: this cast shouldn't be necessary node.children = Array.from(children) :: Array elseif operation == TREE_OPERATION_UPDATE_TREE_BASE_DURATION then id = operations[POSTFIX_INCREMENT()] local node = getClonedNode(id) node.treeBaseDuration = operations[POSTFIX_INCREMENT()] / 1000 -- Convert microseconds back to milliseconds; if __DEBUG__ then debug_( "Update", ("fiber %s treeBaseDuration to %s"):format( tostring(id), tostring(node.treeBaseDuration) ) ) end else error( string.format( "Unsupported Bridge operation %s at operation index %d", tostring(operation), i ) ) end end return { nodes = nodes, rootID = commitTree.rootID } end local function recursivelyInitializeTree( id: number, parentID: number, nodes: Map, dataForRoot: ProfilingDataForRootFrontend ): () local node = dataForRoot.snapshots:get(id) if node ~= nil then nodes:set(id, { id = id, children = node.children, displayName = node.displayName, hocDisplayNames = node.hocDisplayNames, key = node.key, parentID = parentID, treeBaseDuration = ( dataForRoot.initialTreeBaseDurations:get(id) :: any ) :: number, type = node.type, }) for _, childID in node.children do recursivelyInitializeTree(childID, id, nodes, dataForRoot) end end end local rootToCommitTreeMap: Map> = Map.new() local function getCommitTree(ref: { commitIndex: number, profilerStore: ProfilerStore, rootID: number, }): CommitTree local commitIndex, profilerStore, rootID = ref.commitIndex, ref.profilerStore, ref.rootID if not rootToCommitTreeMap:has(rootID) then rootToCommitTreeMap:set(rootID, {}) end local commitTrees = (rootToCommitTreeMap:get(rootID) :: any) :: Array if commitIndex <= #commitTrees then return commitTrees[commitIndex] end local profilingData = profilerStore:profilingData() -- ROBLOX FIXME Luau: need to understand error() means profilingData gets nil-ability stripped. needs type states. if profilingData == nil then error("No profiling data available") end local dataForRoot = (profilingData :: ProfilingDataFrontend).dataForRoots:get(rootID) -- ROBLOX FIXME Luau: need to understand error() means profilingData gets nil-ability stripped. needs type states. if dataForRoot == nil then error( string.format('Could not find profiling data for root "%s"', tostring(rootID)) ) end local operations = (dataForRoot :: ProfilingDataForRootFrontend).operations -- Commits are generated sequentially and cached. -- If this is the very first commit, start with the cached snapshot and apply the first mutation. -- Otherwise load (or generate) the previous commit and append a mutation to it. if commitIndex == 1 then local nodes = Map.new() -- Construct the initial tree. recursivelyInitializeTree( rootID, 0, nodes, dataForRoot :: ProfilingDataForRootFrontend ) -- Mutate the tree if operations ~= nil and commitIndex <= #operations then local commitTree = updateTree({ nodes = nodes, rootID = rootID }, operations[commitIndex]) if __DEBUG__ then __printTree(commitTree) end table.insert(commitTrees, commitTree) return commitTree end else local previousCommitTree = getCommitTree({ commitIndex = commitIndex - 1, profilerStore = profilerStore, rootID = rootID, }) if operations ~= nil and commitIndex <= #operations then local commitTree = updateTree(previousCommitTree, operations[commitIndex]) if __DEBUG__ then __printTree(commitTree) end table.insert(commitTrees, commitTree) return commitTree end end error( string.format( 'getCommitTree(): Unable to reconstruct tree for root "%s" and commit %s', tostring(rootID), tostring(commitIndex) ) ) end exports.getCommitTree = getCommitTree local function invalidateCommitTrees(): any? return rootToCommitTreeMap:clear() end exports.invalidateCommitTrees = invalidateCommitTrees -- DEBUG return exports