Skip to content

Commit

Permalink
Add first take at building a full list of all refs
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron committed Oct 17, 2024
1 parent dc588bb commit 7b0b344
Show file tree
Hide file tree
Showing 17 changed files with 936 additions and 169 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ benchmarks/*.brs
isolate-*
v8*.log
*.cpuprofile
roku_modules
8 changes: 8 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
"${workspaceFolder}/**",
"!**/node_modules/**"
]
},
{
"name": "Debug test-project",
"type": "brightscript",
"request": "launch",
"host": "${promptForHost}",
"rootDir": "${workspaceFolder}/out/dist",
"preLaunchTask": "build-test-app"
}
]
}
6 changes: 3 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"**/.DS_Store": true,
"**/node_modules": true,
"**/.nyc_output": true,
"**/coverage": true,
"**/dist": true
"**/coverage": true
},
"search.exclude": {
"**/node_modules": true,
Expand All @@ -20,5 +19,6 @@
"typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": true,
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false,
"files.trimTrailingWhitespace": true,
"typescript.tsdk": "node_modules\\typescript\\lib"
"typescript.tsdk": "node_modules\\typescript\\lib",
"brightscript.bsdk": "1.0.0-alpha.39"
}
9 changes: 9 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
"kind": "test",
"isDefault": true
}
},
{
"label": "build-test-app",
"type": "shell",
"command": "cd test-project && npx bsc",
"group": {
"kind": "test",
"isDefault": true
}
}
]
}
9 changes: 8 additions & 1 deletion lib/bsconfig.json
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
{}
{
"files": [
"manifest",
"source/**/*.*",
"components/**/*.*",
"images/**/*.*"
]
}
94 changes: 94 additions & 0 deletions lib/components/Reftracker.bs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import "pkg:/source/roku_modules/promises/promises.brs"
import "pkg:/source/reftrackerLib.bs"

' typecast m as ReftrackerM
interface ReftrackerM
nodesByKeypath as ifAssociativeArray
keypathsByNodeReftrackerId as ifAssociativeArray
allNodes as roSGNode[]
nodeQueue as NodeQueueItem[]
top as roSGNodeReftracker
end interface

function init()
'build several parallel lookups for nodes to simplify discoverability later

' a lookup of nodes by their keypath
m.nodesByKeypath = {}
'a lookup of all keypaths for a node indexed by the node's reftrackerId
m.keypathsByNodeReftrackerId = {}
'a flat list of all nodes discovered in this run
m.allNodes = []

m.top.runId = reftracker.internal.getRandomUUID()
end function

function discover()
'seed the list of nodes with all roots (should be a good starting point)
for each root in m.top.getRoots() as roSGnode[]
registerNodeRef(`root:<${root.subtype()}>`, root)
end for

'process the nodes one-by-one
promises.onThen(processNextNode(), function(result)
print "done processing nodes"
end function)
end function

'Register a reference to a node so we can process it later. This
function registerNodeRef(keypath as string, node as roSGNode)
reftrackerId = reftracker.internal.getReftrackerId(node)

'store a reference to the node by its keypath
m.nodesByKeypath[keypath] = node

isNewNode = m.keypathsByNodeReftrackerId[reftrackerId] = invalid

if isNewNode
'build a new array to store all the keypaths for this node
m.keypathsByNodeReftrackerId[reftrackerId] = []
'if this is the first time we've seen this node, store it in our list of all nodes
m.allNodes.push(node)

'register this node for future evaluation
m.nodeQueue.push({
keypath: keypath,
node: node
})
end if
m.keypathsByNodeReftrackerId[reftrackerId].push(keypath)
end function

function processNextNode()
'if we have no more nodes, we are done!
if m.nodeQueue.count() = 0
return promises.resolve(invalid)
end if

nodeQueueItem = m.nodeQueue.pop()

return promises.chain(promises.resolve(true), nodeQueueItem).then(function(result, queueItem as NodeQueueItem)
'if this node supports reftracker functionality, process the node's internal `m`
if (nodeQueueItem.node as dynamic).reftrackerEnabled then
return nodeQueueItem.node@.reftracker_internal_execute({ command: "discover", reftracker: m.top })
end if
end function).then(function(result, nodeQueueItem as NodeQueueItem)
'add all public fields to a list of stuff to work on
for each field in nodeQueueItem.node.getFields()
value = nodeQueueItem.node[field]
reftracker.internal.registerWorkItem(m.top, `${nodeQueueItem.keypath}.${field}`, value)
end for

'now process this data (it will run async and process in chunks until all are finished)
return reftracker.internal.processWorkItems(m.top)

end function).then(function(result, nodeQueueItem as NodeQueueItem)

return processNextNode()
end function).toPromise()
end function

interface NodeQueueItem
keypath as string
node as roSGNode
end interface
8 changes: 8 additions & 0 deletions lib/components/Reftracker.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<component name="Reftracker" extends="Group">
<interface>
<function name="discover" />
<function name="registerNodeRef" />
<field id="runId" type="string" />
</interface>
<script type="text/brightscript" uri="pkg:/components/Reftracker.bs" />
</component>
5 changes: 0 additions & 5 deletions lib/source/reftracker.bs

This file was deleted.

173 changes: 173 additions & 0 deletions lib/source/reftrackerLib.bs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import "pkg:/source/roku_modules/promises/promises.brs"

'typecast m as mm

interface mm
reftrackers as roSGNodeReftracker[]
processedItems as roAssociativeArray
__deviceInfo as roDeviceInfo
end interface


namespace reftracker

' Find all references to a SceneGraph node by its ID
function findNodeById(id as string)
'build the reftracker
tracker = createObject("roSGNode", "Reftracker")
'store a reference to it so it doesn't get lost
reftracker.internal.registerReftracker(tracker)
'run it
tracker@.discover()
end function
end namespace


namespace reftracker.internal

' This is a generic function injected into every component as a callfunc, to be a single point of contact for all commands.
'@param {ExecuteOptions} options
function execute(options)
if options.command = "discover"
registerWorkItem(options.reftracker, options.keypath + ".m", m)
return processWorkItems(options.reftracker)
end if
end function

function getWorkQueue(tracker as roSGNodeReftracker) as WorkQueueItem[]
key = `__workQueue_${tracker.id}`
if m[key] = invalid
m[key] = []
end if
return m[key]
end function

' register some item to be processed later
function registerWorkItem(tracker as roSGNodeReftracker, keypath as string, item as dynamic)
if item <> invalid
workQueue = getWorkQueue(tracker)

workQueue.push({
keypath: keypath
item: item
})
end if
end function

function processWorkItems(tracker as roSGNodeReftracker) as roSGNodepromises_Promise
startTime = createObject("roTimeSpan")

workQueue = getWorkQueue(tracker)

'if we have work, and we've been running for less than 500ms, process another item
while workQueue[0] <> invalid and startTime.TotalMilliseconds() < 500
item = workQueue.Shift()

itemType = type(item)

'if this is a node, push register it with the reftracker to be processed later
if itemType = "roSGNode"
tracker@.registerNodeRef(item)
else if itemType = "roArray" and not isProcessed(item)
'if this is an array, register each item in the array
for i = 0 to (item as ifArray).Count() - 1
registerWorkItem(tracker, `${item.keypath}.${i}`, item[i])
end for
else if itemType = "roAssociativeArray" and not isProcessed(item)
'if this is an associative array, register each item in the array
for each key in item
registerWorkItem(tracker, `${item.keypath}.${key}`, item[key])
end for
else
'all other data types can be ignored, since they don't include references to other nodes
end if
end while

'we have no more work, return a resolved promise
if workQueue[0] = invalid
return promises.resolve(true)
end if

'small delay, then process work again
return promises.chain(reftracker.internal.delay(.01)).then(function(result, tracker)
'process work items again
return processWorkItems(tracker)
end function, tracker).toPromise()
end function

function isProcessed(item as dynamic)
id = getReftrackerId(item)
'if we've processed this item, it'll have its id in the processedItems array
return m.processedItems[id] = invalid
end function

'Ensure this node has a unique reftracker id so we can use it for lookups and comparisons
function getReftrackerId(item as dynamic)
key = "reftracker_id"
itemType = type(item)
if itemType = "roSGNode" and item[key] = invalid then
item.addField(key, "string", reftracker.internal.getRandomUUID())
else if itemType = "roAssociativeArray" and item[key] = invalid
item[key] = reftracker.internal.getRandomUUID()
else if itemType = "roArray" and item[(item as roArray).Count() - 2] <> "reftracker_id"
item.push("reftracker_id")
item.push(reftracker.internal.getRandomUUID())
end if
end function

function registerReftracker(finder as roSGNodeRefTracker)
if m.reftrackers = invalid
m.reftrackers = []
end if
m.reftrackers.push(finder)
end function

function getDeviceObject() as object
if m.__deviceInfo <> invalid then return m.__deviceInfo
m.__deviceInfo = createObject("roDeviceInfo")
return m.__deviceInfo
end function

' Get a promise that resolves after a given duration
sub delay(duration = 0.0001 as float)
timer = createObject("roSGNode", "Timer")
timer.duration = duration
timer.repeat = false
timer.id = "__delay_" + getRandomUUID()

m[timer.id] = {
timer: timer
promise: promises.create()
}

timer.observeFieldScoped("fire", (sub(event as object)
delayId = event.getNode()
options = m[delayId]
promise = options.promise
promises.resolve(promise, invalid)
m[delayId].unobserveFieldScoped("fire")
m.delete(delayId)
end sub).toStr().mid(10))

timer.control = "start"
end sub

function getRandomUUID()
return getDeviceObject().getRandomUUID()
end function

function isNode(value as dynamic, subtype = "" as string)
return type(value) = "roSGNode" and (subType = "" or value.isSubtype(subType))
end function

interface ExecuteOptions
command as string
keypath as string
reftracker as roSGNodeReftracker
end interface

interface WorkQueueItem
keypath as string
item as dynamic
end interface
end namespace
Loading

0 comments on commit 7b0b344

Please sign in to comment.