Skip to content

Commit

Permalink
Debugging hooks (#351)
Browse files Browse the repository at this point in the history
  • Loading branch information
dphfox authored Jul 27, 2024
1 parent b8edd8e commit b2999f9
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 24 deletions.
70 changes: 70 additions & 0 deletions src/ExternalDebug.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
Abstraction layer between Fusion internals and external debuggers, allowing
for deep introspection using function hooks.
Unlike `External`, attaching a debugger is optional, and all debugger
functions are expected to be infallible and non-blocking.
]]

local Package = script.Parent
local Types = require(Package.Types)

local currentProvider: Types.ExternalDebugger? = nil
local lastUpdateStep = 0

local Debugger = {}

--[[
Swaps to a new debugger.
Returns the old debugger, so it can be used again later.
]]
function Debugger.setDebugger(
newProvider: Types.ExternalDebugger?
): Types.ExternalDebugger?
local oldProvider = currentProvider
if oldProvider ~= nil then
oldProvider.stopDebugging()
end
currentProvider = newProvider
if newProvider ~= nil then
newProvider.startDebugging()
end
return oldProvider
end

--[[
Called at the earliest moment after a scope is created or removed from the
scope pool, but not before the scope has finished being prepared by the
library, so that debuggers can register its existence and track changes
to the scope over time.
]]
function Debugger.trackScope(
scope: Types.Scope<unknown>
): ()
if currentProvider == nil then
return
end
currentProvider.trackScope(scope)
end

--[[
Called at the final moment before a scope is poisoned or added to the scope
pool, after all cleanup tasks have completed, so that debuggers can erase
the scope from internal trackers. Note that, due to scope pooling and user
code, never assume that this correlates with garbage collection events.
]]
function Debugger.untrackScope(
scope: Types.Scope<unknown>
): ()
if currentProvider == nil then
return
end
currentProvider.trackScope(scope)
end

return Debugger
30 changes: 10 additions & 20 deletions src/Memory/deriveScope.luau
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,18 @@ local task = nil -- Disable usage of Roblox's task scheduler
--[[
Creates an empty scope with the same metatables as the original scope. Used
for preserving access to constructors when creating inner scopes.
This is the public version of the function, which implements external
debugging hooks.
]]
local Package = script.Parent.Parent
local Types = require(Package.Types)
local merge = require(Package.Utility.merge)
local scopePool = require(Package.Memory.scopePool)
local ExternalDebug = require(Package.ExternalDebug)
local deriveScopeImpl = require(Package.Memory.deriveScopeImpl)

-- This return type is technically a lie, but it's required for useful type
-- checking behaviour.
local function deriveScope<T>(
existing: Types.Scope<T>,
methods: {[unknown]: unknown}?,
...: {[unknown]: unknown}
): any
local metatable = getmetatable(existing)
if methods ~= nil then
metatable = table.clone(metatable)
metatable.__index = merge("first", table.clone(metatable.__index), methods, ...)
end
return setmetatable(
scopePool.reuseAny() :: any or {},
metatable
)
local function deriveScope(...)
local scope = deriveScopeImpl(...)
ExternalDebug.trackScope(scope)
return scope
end

return (deriveScope :: any) :: Types.DeriveScopeConstructor
return deriveScope
37 changes: 37 additions & 0 deletions src/Memory/deriveScopeImpl.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
Creates an empty scope with the same metatables as the original scope. Used
for preserving access to constructors when creating inner scopes.
This is the internal version of the function, which does not implement
external debugging hooks.
]]
local Package = script.Parent.Parent
local Types = require(Package.Types)
local merge = require(Package.Utility.merge)
local scopePool = require(Package.Memory.scopePool)

-- This return type is technically a lie, but it's required for useful type
-- checking behaviour.
local function deriveScopeImpl<T>(
existing: Types.Scope<T>,
methods: {[unknown]: unknown}?,
...: {[unknown]: unknown}
): any
local metatable = getmetatable(existing)
if methods ~= nil then
metatable = table.clone(metatable)
metatable.__index = merge("first", table.clone(metatable.__index), methods, ...)
end
local scope = setmetatable(
scopePool.reuseAny() :: any or {},
metatable
)
return scope
end

return (deriveScopeImpl :: any) :: Types.DeriveScopeConstructor
6 changes: 4 additions & 2 deletions src/Memory/innerScope.luau
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ local task = nil -- Disable usage of Roblox's task scheduler
]]
local Package = script.Parent.Parent
local Types = require(Package.Types)
local deriveScope = require(Package.Memory.deriveScope)
local ExternalDebug = require(Package.ExternalDebug)
local deriveScopeImpl = require(Package.Memory.deriveScopeImpl)

local function innerScope<T>(
existing: Types.Scope<T>,
...: {[unknown]: unknown}
): any
local new = deriveScope(existing, ...)
local new = deriveScopeImpl(existing, ...)
table.insert(existing, new)
table.insert(
new,
Expand All @@ -26,6 +27,7 @@ local function innerScope<T>(
end
end
)
ExternalDebug.trackScope(new)
return new
end

Expand Down
3 changes: 3 additions & 0 deletions src/Memory/scopePool.luau
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ local task = nil -- Disable usage of Roblox's task scheduler

local Package = script.Parent.Parent
local Types = require(Package.Types)
local ExternalDebug = require(Package.ExternalDebug)

local MAX_POOL_SIZE = 16 -- TODO: need to test what an ideal number for this is

Expand All @@ -16,6 +17,7 @@ return {
scope: Types.Scope<S>
): Types.Scope<S>?
if next(scope) == nil then
ExternalDebug.untrackScope(scope)
if poolSize < MAX_POOL_SIZE then
poolSize += 1
pool[poolSize] = scope
Expand All @@ -28,6 +30,7 @@ return {
clearAndGive = function(
scope: Types.Scope<unknown>
)
ExternalDebug.untrackScope(scope)
if poolSize < MAX_POOL_SIZE then
table.clear(scope)
poolSize += 1
Expand Down
7 changes: 5 additions & 2 deletions src/Memory/scoped.luau
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ local task = nil -- Disable usage of Roblox's task scheduler

local Package = script.Parent.Parent
local Types = require(Package.Types)
local ExternalDebug = require(Package.ExternalDebug)
local merge = require(Package.Utility.merge)
local scopePool = require(Package.Memory.scopePool)

local function scoped(
...: {[unknown]: unknown}
): any
return setmetatable(
local scope = setmetatable(
scopePool.reuseAny() :: any or {},
{__index = merge("none", {}, ...)}
)
) :: any
ExternalDebug.trackScope(scope)
return scope
end

return (scoped :: any) :: Types.ScopedConstructor
12 changes: 12 additions & 0 deletions src/Types.luau
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,16 @@ export type ExternalProvider = {
stopScheduler: () -> ()
}

export type ExternalDebugger = {
startDebugging: () -> (),
stopDebugging: () -> (),

trackScope: (
scope: Scope<unknown>
) -> (),
untrackScope: (
scope: Scope<unknown>
) -> ()
}

return nil

0 comments on commit b2999f9

Please sign in to comment.