This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,6 @@
{
"lint": {
"SameLineStatement": "fatal",
"FunctionUnused": "fatal"
}
}
@@ -0,0 +1,218 @@
--[[
Filename: BarGraph.lua
Written by: dbanks
Description: A simple bar graph.
--]]
--[[ Services ]]--
local CoreGuiService = game:GetService('CoreGui')
--[[ Globals ]]--
local BarZIndex = 1
local LineZIndex = 2
--[[ Modules ]]--
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
--[[ Classes ]]--
local BarGraphClass = {}
BarGraphClass.__index = BarGraphClass
function BarGraphClass.new(showExtras)
local self = {}
setmetatable(self, BarGraphClass)
self._barFrame = Instance.new("Frame")
self._barFrame.Name = "PS_BarFrame"
self._barFrame.BackgroundTransparency = 1.0
self._lineFrame = Instance.new("Frame")
self._lineFrame.Name = "PS_LineFrame"
self._lineFrame.BackgroundTransparency = 1.0
self._showExtras = showExtras
-- All of the values we are showing in the bar graph, in order.
self._values = {}
self._bars = {}
-- Average of these values.
self._average = 0
-- Suggested max for these values.
self._target = 0
if self._showExtras then
self:_addGraphTarget()
self:_addGraphAverage()
end
return self
end
function BarGraphClass:SetZIndex(zIndex)
self._barFrame.ZIndex = zIndex
self._lineFrame.ZIndex = zIndex + 1
if self._showExtras then
self._targetLine.ZIndex = self._lineFrame.ZIndex
self._averageLine.ZIndex = self._lineFrame.ZIndex
end
end
function BarGraphClass:PlaceInParent(parent, size, position)
self._barFrame.Position = position
self._barFrame.Size = size
self._barFrame.Parent = parent
self._lineFrame.Position = position
self._lineFrame.Size = size
self._lineFrame.Parent = parent
end
function BarGraphClass:SetAxisMax(axisMax)
self._axisMax = axisMax
end
function BarGraphClass:SetValues(values)
self._values = values
end
function BarGraphClass:SetAverage(average)
self._average = average
end
function BarGraphClass:SetTarget(target)
-- Set the target value, move corresponding graph line
-- accordingly (if present).
self._target = target
self:_moveGraphTarget()
end
function BarGraphClass:_updateBarCount(newBarCount)
-- Make sure we have exactly this number of bars in _bars.
-- Reuse old ones if possible.
-- If we have more old ones than we need, delete them.
local newBars = {}
local currentBarCount = table.getn(self._bars)
for i = 1, currentBarCount, 1 do
if (i <= currentBarCount) then
table.insert(newBars, self._bars[i])
else
self._bars[i].Destroy()
end
end
for i = currentBarCount + 1, newBarCount, 1 do
table.insert(newBars, self:_makeNthBar(i))
end
self._bars = newBars
end
function BarGraphClass:Render()
local numValues = table.getn(self._values)
self:_updateBarCount(numValues)
for i, value in ipairs(self._values) do
self:_updateBar(i, value, numValues)
end
if self._showExtras then
self:_moveGraphAverage()
end
end
function BarGraphClass:_addGraphTarget()
-- Add the line used to mark target value.
local line = Instance.new("ImageLabel")
line.Name = "TargetLine"
line.Size = UDim2.new(1, 0, 0, StatsUtils.GraphTargetLineInnerThickness)
line.Image = 'rbxasset://textures/ui/PerformanceStats/TargetLine.png'
line.BackgroundTransparency = 1
line.Parent = self._lineFrame
line.ZIndex = self._lineFrame.ZIndex
line.BorderSizePixel = 0
line.Changed:connect(function()
self:_updateTargetLineImageSize()
end)
self._targetLine = line
self:_updateTargetLineImageSize()
end
function BarGraphClass:_updateTargetLineImageSize()
self._targetLine.ImageRectSize = self._targetLine.AbsoluteSize
end
function BarGraphClass:_addGraphAverage()
-- Add the line used to mark average of current values.
local line = Instance.new("Frame")
line.Name = "AverageLine"
line.Size = UDim2.new(1, 0, 0, StatsUtils.GraphAverageLineInnerThickness)
line.Parent = self._lineFrame
line.ZIndex = self._lineFrame.ZIndex
StatsUtils.StyleAverageLine(line)
self._averageLine = line
end
function BarGraphClass:_moveGraphTarget()
-- Update position of graph target line, if present.
if self._targetLine == nil then
return
end
self._targetLine.Position = UDim2.new(0,
0, (
self._axisMax - self._target)/self._axisMax,
-StatsUtils.GraphTargetLineInnerThickness/2)
end
function BarGraphClass:_moveGraphAverage()
-- Update position of graph average line, if present.
if self._averageLine == nil then
return
end
-- Never let it go above axis max.
local adjustedAverage = math.min(self._average, self._axisMax)
self._averageLine.Position = UDim2.new(0,
0,
(self._axisMax - adjustedAverage)/self._axisMax,
-StatsUtils.GraphAverageLineTotalThickness/2)
end
function BarGraphClass:_makeNthBar(index)
-- Make the nth bar in the bar graph.
local realIndex = index-1
local bar = Instance.new("Frame")
bar.Name = string.format("Bar_%d", realIndex)
bar.Parent = self._barFrame
bar.ZIndex = self._barFrame.ZIndex
bar.BorderSizePixel = 0
return bar
end
function BarGraphClass:_updateBar(i, value, numValues)
-- Update nth bar in graph: size, position, and color.
local bar = self._bars[i]
local realIndex = i-1
-- Don't let it go off the chart.
local clampedValue = math.max(0, math.min(value, self._axisMax))
bar.Position = UDim2.new(realIndex/numValues, 0,
(self._axisMax - clampedValue)/self._axisMax, 0)
bar.Size = UDim2.new(1/numValues, 0,
clampedValue/self._axisMax, 0)
bar.BackgroundColor3 = StatsUtils.GetColorForValue(value, self._target)
end
return BarGraphClass
@@ -0,0 +1,346 @@
--[[
Filename: BaseMemoryAnalyzer.lua
Written by: dbanks
Description: Base class for a widget that displays data about memory usage.
--]]
--[[ Globals ]]--
local RowHeight = 20
local ValueFrameWidth = 100
local StdRowColor3 = Color3.new(0.35, 0.55, 0.35)
local AltRowColor3 = Color3.new(0.15, 0.35, 0.15)
local RowLabelTextColor3 = Color3.new(1, 1, 1)
local RowLabelBorderColor3 = Color3.new(1, 1, 1)
local ButtonBorderColor3 = Color3.new(1, 1, 1)
local RowLabelBorderWidth = 1
local ButtonBorderWidth = 1
local IndentSize = RowHeight
local ButtonSize = IndentSize - 4
local ButtonFrameUDim2Position = UDim2.new(0, 0, 0, 0)
local ButtonUDim2Position = UDim2.new(1, -(IndentSize + ButtonSize)/2, 0, (IndentSize - ButtonSize)/2)
local ButtonUDim2Size = UDim2.new(0, ButtonSize, 0, ButtonSize)
local ValueUDim2Position = UDim2.new(1, -ValueFrameWidth, 0, 0)
local ValueUDim2Size = UDim2.new(0, ValueFrameWidth, 0, RowHeight)
--[[ Services ]]--
local CoreGuiService = game:GetService('CoreGui')
--[[ Helper functions ]]--
local function __StyleAndSizeButton(button)
button.TextXAlignment = Enum.TextXAlignment.Center
button.TextColor3 = RowLabelTextColor3
button.BackgroundTransparency = 1
button.BorderColor3 = ButtonBorderColor3
button.BorderSizePixel = ButtonBorderWidth
button.Position = ButtonUDim2Position
button.Size = ButtonUDim2Size
end
local function __StyleAndSizeLabel(label)
label.TextXAlignment = Enum.TextXAlignment.Left
label.TextColor3 = RowLabelTextColor3
label.BackgroundTransparency = 1
label.Position = UDim2.new(0, 5, 0, 0)
label.Size = UDim2.new(1, -10, 1, 0)
end
local function __StyleRowCellFrame(labelFrame)
labelFrame.BorderColor3 = RowLabelBorderColor3
labelFrame.BorderSizePixel = RowLabelBorderWidth
labelFrame.BackgroundTransparency = 0.6
end
--[[ Classes ]]--
--////////////////////////////////////
--
-- MemoryAnalyzerRowClass
-- A single row in the table.
--
--////////////////////////////////////
local MemoryAnalyzerRowClass = {}
MemoryAnalyzerRowClass.__index = MemoryAnalyzerRowClass
function MemoryAnalyzerRowClass.new(treeViewItem)
local self = {}
setmetatable(self, MemoryAnalyzerRowClass)
self._treeViewItem = treeViewItem
self._expanded = true
self._expansionToggleCallback = nil
-- The gui widget for the row
self._frame = Instance.new("Frame")
self._frame.Name = "MemoryAnalyzerRowClassFrame"
self._frame.BackgroundTransparency = 1
-- The button
self._buttonFrame = Instance.new("Frame")
self._buttonFrame.Name = "ButtonFrame"
self._buttonFrame.Parent = self._frame
self._buttonFrame.Position = ButtonFrameUDim2Position
local buttonFrameWidth = (1 + treeViewItem:getStackDepth()) * IndentSize
self._buttonFrame.Size = UDim2.new(0, buttonFrameWidth, 0, RowHeight)
__StyleRowCellFrame(self._buttonFrame)
self._button = Instance.new("TextButton")
self._button.Name = "Button"
self._button.Parent = self._buttonFrame
__StyleAndSizeButton(self._button)
self._button.MouseButton1Click:connect(function()
self:__toggleExpansion()
end)
self:__updateButtonState()
-- The label
self._labelFrame = Instance.new("Frame")
self._labelFrame.Name = "LabelFrame"
self._labelFrame.Parent = self._frame
-- From the left edge of button frame to right edge of value frame.
self._labelFrame.Position = UDim2.new(0, buttonFrameWidth, 0, 0)
self._labelFrame.Size = UDim2.new(1, -buttonFrameWidth - ValueFrameWidth, 0, RowHeight)
__StyleRowCellFrame(self._labelFrame)
self._labelTextLabel = Instance.new("TextLabel")
self._labelTextLabel.Name = "Label"
self._labelTextLabel.Parent = self._labelFrame
__StyleAndSizeLabel(self._labelTextLabel)
self._labelTextLabel.Text = treeViewItem:getLabel()
-- The value
self._valueFrame = Instance.new("Frame")
self._valueFrame.Name = "ValueFrame"
self._valueFrame.Parent = self._frame
self._valueFrame.Position = ValueUDim2Position
self._valueFrame.Size = ValueUDim2Size
__StyleRowCellFrame(self._valueFrame)
self._valueTextLabel = Instance.new("TextLabel")
self._valueTextLabel.Name = "Value"
self._valueTextLabel.Parent = self._valueFrame
__StyleAndSizeLabel(self._valueTextLabel)
self._valueHasBeenNonZero = false
self:updateValue()
return self
end
function MemoryAnalyzerRowClass:isExpanded()
return self._expanded
end
function MemoryAnalyzerRowClass:valueHasBeenNonZero()
return self._valueHasBeenNonZero
end
function MemoryAnalyzerRowClass:setExpansionToggleCallback(callback)
self._expansionToggleCallback = callback
end
function MemoryAnalyzerRowClass:__toggleExpansion()
self._expanded = not self._expanded
self:__updateButtonState()
if (self._expansionToggleCallback ~= nil) then
self._expansionToggleCallback()
end
end
function MemoryAnalyzerRowClass:__updateButtonState()
local children = self._treeViewItem:getChildren()
if #children == 0 then
self._button.Visible = false
else
self._button.Visible = true
if (self._expanded) then
self._button.Text = "-"
else
self._button.Text = "+"
end
end
end
function MemoryAnalyzerRowClass:updateValue()
local value = self._treeViewItem:getValue()
-- We know we're only dealing with positive numbers.
-- And if I'm only going to display 3 significant digits (see below) I'm really
-- asking if this is >= 0.001
if (value >= 0.001) then
self._valueHasBeenNonZero = true
end
self._valueTextLabel.Text = string.format("%.3f", self._treeViewItem:getValue())
end
function MemoryAnalyzerRowClass:setZIndex(zIndex)
self._frame.ZIndex = zIndex
self._labelTextLabel.ZIndex = zIndex
self._valueTextLabel.ZIndex = zIndex
end
function MemoryAnalyzerRowClass:setRowValue(value)
self._valueTextLabel.Text = string.format("%.3f", value)
end
function MemoryAnalyzerRowClass:getFrame()
return self._frame
end
function MemoryAnalyzerRowClass:setRowNumber(rowNumber)
if (rowNumber % 2 == 1) then
self._buttonFrame.BackgroundColor3 = StdRowColor3
self._labelFrame.BackgroundColor3 = StdRowColor3
self._valueFrame.BackgroundColor3 = StdRowColor3
else
self._buttonFrame.BackgroundColor3 = AltRowColor3
self._labelFrame.BackgroundColor3 = AltRowColor3
self._valueFrame.BackgroundColor3 = AltRowColor3
end
end
--////////////////////////////////////
--
-- BaseMemoryAnalyzerClass
-- The whole table.
--
--////////////////////////////////////
local BaseMemoryAnalyzerClass = {}
BaseMemoryAnalyzerClass.__index = BaseMemoryAnalyzerClass
BaseMemoryAnalyzerClass.Indent = string.rep(" ", 4)
function BaseMemoryAnalyzerClass.new(parentFrame)
local self = {}
setmetatable(self, BaseMemoryAnalyzerClass)
-- The gui widget containing the whole thing.
self._frame = Instance.new("Frame")
self._frame.Name = "MemoryAnalyzerClassFrame"
self._frame.ZIndex = parentFrame.ZIndex
self._frame.BackgroundTransparency = 1
-- a map from treeViewItem to the Row used to display the treeViewItem.
self._rowsByTreeViewItem = {}
-- things need to be laid out, either because I have new content or
-- the size of my parent container changed.
-- Starts out as 'false' because there's no rows -> nothing to lay out.
self._layoutDirty = false
self._heightChangedCallback = nil
self._heightInPix = 0
self._frame.Parent = parentFrame
return self
end
-- This needs to be overridden in derived classes.
function BaseMemoryAnalyzerClass:getMemoryUsageTree()
return nil
end
function BaseMemoryAnalyzerClass:setHeightChangedCallback(callback)
self._heightChangedCallback = callback
end
function BaseMemoryAnalyzerClass:__getOrMakeRowForTreeViewItem(treeViewItem)
if (self._rowsByTreeViewItem[treeViewItem] == nil) then
local row = MemoryAnalyzerRowClass.new(treeViewItem)
row:setExpansionToggleCallback(function()
self:__layoutRows()
end)
local frame = row:getFrame()
frame.Parent = self._frame
self._rowsByTreeViewItem[treeViewItem] = row
self._layoutDirty = true
end
return self._rowsByTreeViewItem[treeViewItem]
end
function BaseMemoryAnalyzerClass:__recursiveUpdateStatValue(treeViewItem)
local row = self:__getOrMakeRowForTreeViewItem(treeViewItem)
row:updateValue()
local children = treeViewItem:getChildren()
for i, child in ipairs(children) do
self:__recursiveUpdateStatValue(child)
end
end
-- Write latest stat values into each row.
function BaseMemoryAnalyzerClass:renderUpdates()
local treeViewItemRoot = self:getMemoryUsageTree()
if (treeViewItemRoot ~= nil) then
self:__recursiveUpdateStatValue(treeViewItemRoot)
end
if self._layoutDirty then
self:__layoutRows()
end
end
function BaseMemoryAnalyzerClass:__layoutRows()
self._layoutDirty = false
self._heightInPix = 0
local treeViewItemRoot = self:getMemoryUsageTree()
if (treeViewItemRoot ~= nil) then
self:__recursiveLayoutTreeViewItem(self:getMemoryUsageTree(), true)
end
self._frame.Size = UDim2.new(1, 0, 0, self._heightInPix)
self._frame.Position = UDim2.new(0, 0, 0, 0)
if (self._heightChangedCallback) then
self._heightChangedCallback(self._heightInPix)
end
end
function BaseMemoryAnalyzerClass:__recursiveLayoutTreeViewItem(treeViewItem, isVisible)
local row = self:__getOrMakeRowForTreeViewItem(treeViewItem)
local frame = row:getFrame()
-- caller has something to say about whether it's visible (ancestor may be collapsed).
-- I also may be invisible if I have never been non-zero.
isVisible = (isVisible and row:valueHasBeenNonZero())
if (isVisible) then
frame.Visible = true
frame.Size = UDim2.new(1, 0, 0, RowHeight)
frame.Position = UDim2.new(0, 0, 0, self._heightInPix)
row:setZIndex(self._frame.ZIndex)
row:setRowNumber(self._heightInPix/RowHeight)
self._heightInPix = self._heightInPix + RowHeight
else
frame.Visible = false
end
local children = treeViewItem:getChildren()
for i, child in ipairs(children) do
self:__recursiveLayoutTreeViewItem(child, row:isExpanded() and isVisible)
end
end
function BaseMemoryAnalyzerClass:getHeightInPix()
return self._heightInPix
end
return BaseMemoryAnalyzerClass
@@ -0,0 +1,128 @@
--[[
Filename: ClientMemoryAnalyzer.lua
Written by: dbanks
Description: Widget to display client memory usage.
--]]
--[[ Services ]]--
local StatsService = game:GetService("Stats")
local CoreGuiService = game:GetService('CoreGui')
--[[ Modules ]]--
local BaseMemoryAnalyzerClass = require(CoreGuiService.RobloxGui.Modules.Stats.BaseMemoryAnalyzer)
local CommonUtils = require(CoreGuiService.RobloxGui.Modules.Common.CommonUtil)
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
local TreeViewItem = require(CoreGuiService.RobloxGui.Modules.Stats.TreeViewItem)
--[[ Helper functions ]]--
local function __GetMemoryPerformanceStatsItem()
local performanceStats = StatsService and StatsService:FindFirstChild("PerformanceStats")
if performanceStats == nil then
return nil
end
local memoryStats = performanceStats:FindFirstChild(
StatsUtils.StatNames[StatsUtils.StatType_Memory])
return memoryStats
end
local function __FillInMemoryUsageTreeRecursive(treeViewItem, statsItem)
local statId = statsItem.Name
local statLabel = StatsUtils.GetMemoryAnalyzerStatName(statId)
local statValue = statsItem:GetValue()
treeViewItem:setLabelAndValue(statLabel, statValue)
local rawChildren = statsItem:GetChildren()
-- sort children by name.
local sortedChildren = CommonUtils.SortByName(rawChildren)
for i, childStatItem in ipairs(sortedChildren) do
local childStatId = childStatItem.Name
local childTreeItem = treeViewItem:getOrMakeChildById(childStatId)
__FillInMemoryUsageTreeRecursive(childTreeItem, childStatItem)
end
end
--[[ Classes ]]--
--////////////////////////////////////
--
-- ClientMemoryAnalyzerClass
-- The whole table customized for Client memory.
--
--////////////////////////////////////
local ClientMemoryAnalyzerClass = {}
-- Derive from BaseMemoryAnalyzerClass
setmetatable(ClientMemoryAnalyzerClass, BaseMemoryAnalyzerClass)
ClientMemoryAnalyzerClass.__index = ClientMemoryAnalyzerClass
function ClientMemoryAnalyzerClass.new(parentFrame)
local self = BaseMemoryAnalyzerClass.new(parentFrame)
setmetatable(self, ClientMemoryAnalyzerClass)
self._rootTreeViewItem = nil
-- am I currently listening for updates?
self._shouldListenForUpdates = false
-- management of loop that does the listening.
-- Note: it is not enough to just track _shouldListenForUpdates.
-- If we called
-- startListeningForUpdates
-- stopListeningForUpdates
-- startListeningForUpdates
-- in quick succession, then in the second startListeningForUpdates,
-- _shouldListenForUpdates is false, but we still have a loop spawned from
-- the earlier call to startListeningForUpdates.
self._spawnedLoopScheduled = false
return self
end
-- Start a thread that wakes up every n seconds
-- and updates contents of stats widget.
function ClientMemoryAnalyzerClass:startListeningForUpdates()
self._shouldListenForUpdates = true
-- If we have already scheduled a loop to start, we're done.
if (self._spawnedLoopScheduled) then
return
end
self._spawnedLoopScheduled = true
spawn(function()
while(self._shouldListenForUpdates) do
self:refreshMemoryUsageTree()
self:renderUpdates()
wait(1)
end
-- Note that scheduled loop is now dead.
self._spawnedLoopScheduled = false
end)
end
-- Stop the thread that does the updates.
function ClientMemoryAnalyzerClass:stopListeningForUpdates()
self._shouldListenForUpdates = false
end
-- Generate the memory usage tree.
function ClientMemoryAnalyzerClass:refreshMemoryUsageTree()
if (self._rootTreeViewItem == nil) then
self._rootTreeViewItem = TreeViewItem.new("root", nil)
end
local statsItem = __GetMemoryPerformanceStatsItem()
if statsItem == nil then
return nil
end
__FillInMemoryUsageTreeRecursive(self._rootTreeViewItem, statsItem)
end
function ClientMemoryAnalyzerClass:getMemoryUsageTree()
return self._rootTreeViewItem
end
return ClientMemoryAnalyzerClass
@@ -0,0 +1,93 @@
--[[
Filename: DecoratedValueLabel.lua
Written by: dbanks
Description: Icon, text label, numeric value.
Icon is set by creator/caller.
--]]
local CoreGuiService = game:GetService('CoreGui')
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
local RobloxTranslator = require(CoreGuiService.RobloxGui.Modules.RobloxTranslator)
function LocalizedGetKey(key)
local rtv = key
pcall(function()
rtv = RobloxTranslator:FormatByKey(key)
end)
return rtv
end
local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end)
local FFlagUseNotificationsLocalization = success and result
local DecoratedValueLabelClass = {}
DecoratedValueLabelClass.__index = DecoratedValueLabelClass
function DecoratedValueLabelClass.new(statType, valueName)
local self = {}
setmetatable(self, DecoratedValueLabelClass)
self._frame = Instance.new("Frame")
self._frame.Name = "PS_DecoratedValueLabel"
self._frame.BackgroundTransparency = 1.0
if FFlagUseNotificationsLocalization == true then
self._valueName = LocalizedGetKey(valueName)
else
self._valueName = valueName
end
self._statType = statType
self._decorationFrame = Instance.new("Frame")
self._decorationFrame.Name = "PS_Decoration"
self._decorationFrame.Parent = self._frame
self._decorationFrame.Position = UDim2.new(0, 0, 0.5, -StatsUtils.DecorationSize/2)
self._decorationFrame.Size = UDim2.new(0, StatsUtils.DecorationSize,
0, StatsUtils.DecorationSize)
self._decorationFrame.BackgroundTransparency = 1.0
self._label =Instance.new("TextLabel")
self._label.Name = "Label"
self._label.Parent = self._frame
self._label.Position = UDim2.new(0, StatsUtils.DecorationSize + StatsUtils.DecorationMargin,
0, 0)
self._label.Size = UDim2.new(1, -(StatsUtils.DecorationSize + StatsUtils.DecorationMargin),
1, 0)
self._label.FontSize = StatsUtils.PanelValueFontSize
self._label.TextXAlignment = Enum.TextXAlignment.Left
self._label.TextYAlignment = Enum.TextYAlignment.Center
StatsUtils.StyleTextWidget(self._label)
return self
end
function DecoratedValueLabelClass:SetZIndex(zIndex)
self._frame.ZIndex = zIndex
self._decorationFrame.ZIndex = zIndex
self._label.ZIndex = zIndex
end
function DecoratedValueLabelClass:PlaceInParent(parent, size, position)
self._frame.Parent = parent
self._frame.Size = size
self._frame.Position = position
end
function DecoratedValueLabelClass:GetDecorationFrame()
return self._decorationFrame
end
function DecoratedValueLabelClass:SetValue(value)
local formattedValue = StatsUtils.FormatTypedValue(value, self._statType)
self._label.Text = string.format("%s: %s", self._valueName, formattedValue)
end
return DecoratedValueLabelClass
@@ -0,0 +1,133 @@
--[[
Filename: ServerMemoryAnalyzer.lua
Written by: dbanks
Description: Widget to display server memory usage.
--]]
--[[ Services ]]--
local CoreGuiService = game:GetService('CoreGui')
--[[ Modules ]]--
local BaseMemoryAnalyzerClass = require(CoreGuiService.RobloxGui.Modules.Stats.BaseMemoryAnalyzer)
local TreeViewItem = require(CoreGuiService.RobloxGui.Modules.Stats.TreeViewItem)
local CommonUtils = require(CoreGuiService.RobloxGui.Modules.Common.CommonUtil)
--[[ Globals ]]--
local BYTES_PER_MB = 1048576.0;
-- labelToBytesUsedMap maps string to "num bytes used".
-- for each item in the map:
-- Make sure there's a child in the tree for that string.
-- Update that child to have latest value.
-- Add value to a sum total.
-- Return the sum total.
local function __ReadAndSumValues(treeViewItem, labelToBytesUsedMap)
local totalMB = 0;
for label, numBytes in pairs(labelToBytesUsedMap) do
-- Convert to MB.
local numMB = numBytes / BYTES_PER_MB
totalMB = totalMB + numMB
local childTreeViewItem = treeViewItem:getOrMakeChildById(label)
childTreeViewItem:setLabelAndValue(label, numMB)
end
return totalMB
end
--[[ Classes ]]--
--////////////////////////////////////
--
-- ServerMemoryAnalyzerClass
-- The whole table customized for Server memory.
--
--////////////////////////////////////
local ServerMemoryAnalyzerClass = {}
-- Derive from BaseMemoryAnalyzerClass
setmetatable(ServerMemoryAnalyzerClass, BaseMemoryAnalyzerClass)
ServerMemoryAnalyzerClass.__index = ServerMemoryAnalyzerClass
function ServerMemoryAnalyzerClass.new(parentFrame)
local self = BaseMemoryAnalyzerClass.new(parentFrame)
setmetatable(self, ServerMemoryAnalyzerClass)
self._cachedRootTreeViewItem = nil
self._coreTreeViewItem = nil
self._placeTreeViewItem = nil
self._untrackedTreeViewItem = nil
self._isVisible = false
return self
end
-- 'static' function.
-- 'stats' is a value table from server.
-- One top-level key is "ServerMemoryTree".
-- That contains a table that looks like this:
-- "totalServerMemory": <some value>
-- "developerTags": <table that looks like...>
-- <developer tag label>: <developer tag value>
-- (for all developer tags).
-- "internalCategories": <table that looks like...>
-- <category label>: <category value>
-- (for all categories associated with the "internal" developer tag.)
-- We want to 'filter' this so that we return just the "ServerMemoryTree" value.
function ServerMemoryAnalyzerClass:filterServerMemoryTreeStats(stats)
if (stats.ServerMemoryTree == nil) then
return {}
else
return stats.ServerMemoryTree
end
end
function ServerMemoryAnalyzerClass:updateWithTreeStats(stats)
local totalServerMemory = 0
if (self._cachedRootTreeViewItem == nil) then
self._cachedRootTreeViewItem = TreeViewItem.new("root", nil)
-- make sure the childen are in the order I want them in.
self._coreTreeViewItem = self._cachedRootTreeViewItem:getOrMakeChildById("CoreMemory")
self._placeTreeViewItem = self._cachedRootTreeViewItem:getOrMakeChildById("PlaceMemory")
self._untrackedTreeViewItem = self._cachedRootTreeViewItem:getOrMakeChildById("UntrackedMemory")
end
-- All values are in bytes.
-- Convert to MB ASAP.
for key, value in pairs(stats) do
if key == "totalServerMemory" then
totalServerMemory = value / BYTES_PER_MB
elseif key == "developerTags" then
local sum = __ReadAndSumValues(self._placeTreeViewItem, value)
self._placeTreeViewItem:setLabelAndValue("PlaceMemory", sum)
elseif key == "internalCategories" then
local sum = __ReadAndSumValues(self._coreTreeViewItem, value)
self._coreTreeViewItem:setLabelAndValue("CoreMemory", sum)
end
end
-- Update total.
self._cachedRootTreeViewItem:setLabelAndValue("Memory", totalServerMemory)
-- Update untracked.
local untrackedMemory = totalServerMemory -
(self._coreTreeViewItem:getValue() + self._placeTreeViewItem:getValue())
self._untrackedTreeViewItem:setLabelAndValue("UntrackedMemory", untrackedMemory)
self:renderUpdates();
end
function ServerMemoryAnalyzerClass:getMemoryUsageTree()
return self._cachedRootTreeViewItem
end
function ServerMemoryAnalyzerClass:setVisible(isVisible)
self._isVisible = isVisible
if (self._isVisible) then
self:renderUpdates()
end
end
return ServerMemoryAnalyzerClass
@@ -0,0 +1,156 @@
--[[
Filename: StatsAggregator.lua
Written by: dbanks
Description: Gather and store stats on regular heartbeat.
--]]
--[[ Services ]]--
local CoreGuiService = game:GetService('CoreGui')
--[[ Modules ]]--
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
--[[ Classes ]]--
local StatsAggregatorClass = {}
StatsAggregatorClass.__index = StatsAggregatorClass
function StatsAggregatorClass.new(statType, numSamples, pauseBetweenSamples)
local self = {}
setmetatable(self, StatsAggregatorClass)
self._statType = statType
self._numSamples = numSamples
self._pauseBetweenSamples = pauseBetweenSamples
self._statName = StatsUtils.StatNames[self._statType]
self._statMaxName = StatsUtils.StatMaxNames[self._statType]
-- init our circular buffer.
self._samples = {}
for i = 0, numSamples-1, 1 do
self._samples[i] = 0
end
self._oldestIndex = 0
self._listeners = {}
-- FIXME(dbanks)
-- Just want to be real clear this is a key, not an array index.
self._nextListenerId = 1001
return self
end
function StatsAggregatorClass:AddListener(callbackFunction)
local id = self._nextListenerId
self._nextListenerId = self._nextListenerId+1
self._listeners[id] = callbackFunction
return id
end
function StatsAggregatorClass:RemoveListener(listenerId)
self._listeners[listenerId] = nil
end
function StatsAggregatorClass:_notifyAllListeners()
for listenerId, listenerCallback in pairs(self._listeners) do
listenerCallback()
end
end
function StatsAggregatorClass:StartListening()
-- On a regular heartbeat, wake up and read the latest
-- value into circular buffer.
-- Don't bother if we're already listening.
if (self._listening == true) then
return
end
spawn(function()
self._listening = true
while(self._listening) do
local statValue = self:_getStatValue()
self:_storeStatValue(statValue)
self:_notifyAllListeners()
wait(self._pauseBetweenSamples)
end
end)
end
function StatsAggregatorClass:StopListening()
self._listening = false
end
function StatsAggregatorClass:GetValues()
-- Get the past N values, from oldest to newest.
local retval = {}
local actualIndex
for i = 0, self._numSamples-1, 1 do
actualIndex = (self._oldestIndex + i) % self._numSamples
retval[i+1] = self._samples[actualIndex]
end
return retval
end
function StatsAggregatorClass:GetAverage()
-- Get average of past N values.
local retval = 0.0
for i = 0, self._numSamples-1, 1 do
retval = retval + self._samples[i]
end
return retval / self._numSamples
end
function StatsAggregatorClass:GetLatestValue()
-- Get latest value.
local index = (self._oldestIndex + self._numSamples -1) % self._numSamples
return self._samples[index]
end
function StatsAggregatorClass:_storeStatValue(value)
-- Store this as the latest value in our circular buffer.
self._samples[self._oldestIndex] = value
self._oldestIndex = (self._oldestIndex + 1) % self._numSamples
end
function StatsAggregatorClass:_getStatValue()
-- Look up and return the statistic we care about.
local statsService = game:GetService("Stats")
if statsService == nil then
return 0
end
local performanceStats = statsService:FindFirstChild("PerformanceStats")
if performanceStats == nil then
return 0
end
local itemStats = performanceStats:FindFirstChild(self._statName)
if itemStats == nil then
return 0
end
return itemStats:GetValue()
end
function StatsAggregatorClass:GetTarget()
-- Look up and return the statistic we care about.
local statsService = game:GetService("Stats")
if statsService == nil then
return 0
end
local performanceStats = statsService:FindFirstChild("PerformanceStats")
if performanceStats == nil then
return 0
end
local itemStats = performanceStats:FindFirstChild(self._statMaxName)
if itemStats == nil then
return 0
end
return itemStats:GetValue()
end
return StatsAggregatorClass
@@ -0,0 +1,67 @@
--[[
Filename: StatsAggregatorManager.lua
Written by: dbanks
Description: Indexed array of stats aggregators, one for each stat.
--]]
--[[ Services ]]--
local CoreGuiService = game:GetService('CoreGui')
--[[ Modules ]]--
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
local StatsAggregatorClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAggregator)
--[[ Classes ]]--
local StatsAggregatorManagerClass = {}
StatsAggregatorManagerClass.__index = StatsAggregatorManagerClass
StatsAggregatorManagerClass.SecondsBetweenUpdate = 1.0
StatsAggregatorManagerClass.NumSamplesToKeep = 20
local statsAggregatorManagerSingleton = nil
function StatsAggregatorManagerClass.getSingleton()
if (statsAggregatorManagerSingleton == nil) then
statsAggregatorManagerSingleton = StatsAggregatorManagerClass.__new()
-- Start listening for updates in stats.
statsAggregatorManagerSingleton:StartListening()
end
return statsAggregatorManagerSingleton
end
function StatsAggregatorManagerClass.__new()
local self = {}
setmetatable(self, StatsAggregatorManagerClass)
self._statsAggregators = {}
for i, statType in ipairs(StatsUtils.AllStatTypes) do
local statsAggregator = StatsAggregatorClass.new(statType,
StatsAggregatorManagerClass.NumSamplesToKeep,
StatsAggregatorManagerClass.SecondsBetweenUpdate)
self._statsAggregators[statType] = statsAggregator
end
return self
end
function StatsAggregatorManagerClass:StartListening()
for i, statsAggregator in pairs(self._statsAggregators) do
statsAggregator:StartListening()
end
end
function StatsAggregatorManagerClass:StopListening()
for i, statsAggregator in pairs(self._statsAggregators) do
statsAggregator:StopListening()
end
end
function StatsAggregatorManagerClass:GetAggregator(statsType)
return self._statsAggregators[statsType]
end
return StatsAggregatorManagerClass
@@ -0,0 +1,240 @@
--[[
Filename: StatsAnnotatedGraph.lua
Written by: dbanks
Description: A graph plus extra annotations like axis markings,
target lines, etc.
--]]
--[[ Services ]]--
local CoreGuiService = game:GetService('CoreGui')
--[[ Globals ]]--
local Margin = 10
local LabelXWidth = 30
--[[ Modules ]]--
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
local BarGraphClass = require(CoreGuiService.RobloxGui.Modules.Stats.BarGraph)
--[[ Classes ]]--
local StatsAnnotatedGraphClass = {}
StatsAnnotatedGraphClass.__index = StatsAnnotatedGraphClass
function StatsAnnotatedGraphClass.new(statType, isMaximized)
local self = {}
setmetatable(self, StatsAnnotatedGraphClass)
self._statType = statType
self._statMaxName = StatsUtils.StatMaxNames[statType]
self._isMaximized = isMaximized
self._values = {}
-- Average value of all bars in the graph.
self._average = 0
-- Suggested max value for the stat being measured.
self._target = 0
-- Max value we display on the y-axis. Values higher than this are truncated.
self._axisMax = 0
self._frame = Instance.new("Frame")
self._frame.Name = "PS_AnnotatedGraph"
self._frame.BackgroundTransparency = 1.0
self._frame.ZIndex = StatsUtils.GraphZIndex
self._topLabel = Instance.new("TextLabel")
self._topLabel.Name = "PS_TopAxisLabel"
self._topLabel.Parent = self._frame
self._topLabel.TextXAlignment = Enum.TextXAlignment.Left
self._topLabel.TextYAlignment = Enum.TextYAlignment.Top
self._topLabel.FontSize = StatsUtils.PanelGraphFontSize
self._bottomLabel = Instance.new("TextLabel")
self._bottomLabel.Name = "PS_BottomAxisLabel"
self._bottomLabel.Parent = self._frame
self._bottomLabel.TextXAlignment = Enum.TextXAlignment.Left
self._bottomLabel.TextYAlignment = Enum.TextYAlignment.Bottom
self._bottomLabel.FontSize = StatsUtils.PanelGraphFontSize
local showExtras = isMaximized
self._graph = BarGraphClass.new(showExtras)
StatsUtils.StyleTextWidget(self._topLabel)
StatsUtils.StyleTextWidget(self._bottomLabel)
self:_layoutElements()
return self
end
function StatsAnnotatedGraphClass:SetZIndex(zIndex)
self._frame.ZIndex = zIndex
self._topLabel.ZIndex = zIndex
self._bottomLabel.ZIndex = zIndex
self._graph:SetZIndex(zIndex)
end
function StatsAnnotatedGraphClass:_layoutElements()
local labelWidth
if (self._isMaximized) then
labelWidth = LabelXWidth
self._topLabel.Visible = true
self._bottomLabel.Visible = true
else
labelWidth = 0
self._topLabel.Visible = false
self._bottomLabel.Visible = false
end
local GraphFramePosition = UDim2.new(0, Margin, 0, Margin)
local GraphFrameSize = UDim2.new(1, -(2 * Margin + labelWidth), 1, -2 * Margin)
local TopLabelFramePosition = UDim2.new(1, -(Margin + labelWidth), 0, Margin)
local TopLabelFrameSize = UDim2.new(0, labelWidth, 0.333, -2 * Margin)
local BottomLabelFramePosition = UDim2.new(1, -(Margin + labelWidth), 0.666, Margin)
local BottomLabelFrameSize = UDim2.new(0, labelWidth, 0.333, -2 * Margin)
self._topLabel.Size = TopLabelFrameSize
self._topLabel.Position = TopLabelFramePosition
self._bottomLabel.Size = BottomLabelFrameSize
self._bottomLabel.Position = BottomLabelFramePosition
self._graph:PlaceInParent(self._frame, GraphFrameSize, GraphFramePosition)
end
function StatsAnnotatedGraphClass:PlaceInParent(parent, size, position)
self._frame.Position = position
self._frame.Size = size
self._frame.Parent = parent
end
function StatsAnnotatedGraphClass:_getTarget()
-- Get the current target value for the graphed stat.
if self._performanceStats == nil then
return 0
end
local maxItemStats = self._performanceStats:FindFirstChild(self._statMaxName)
if maxItemStats == nil then
return 0
end
return maxItemStats:GetValue()
end
function StatsAnnotatedGraphClass:_render()
self._graph:SetAxisMax(self._axisMax)
self._graph:SetValues(self._values)
self._graph:SetAverage(self._average)
self._graph:SetTarget(self._target)
self._graph:Render()
self._topLabel.Text = string.format("%.2f", self._axisMax)
self._bottomLabel.Text = string.format("%.2f", 0.0)
end
function StatsAnnotatedGraphClass:_calculateAxisMax()
-- Calculate an optimal max axis label for this graph, given this 'target' value.
-- We want target to be roughly in the middle.
-- Say, roughly twice the target.
local max = self._target * 2
local orderOfMagnitude = self:_recursiveGetOrderOfMagnitude(1.0, max)
local div = math.floor(0.5 + max/orderOfMagnitude)
self._axisMax = div * orderOfMagnitude
end
function StatsAnnotatedGraphClass:SetStatsAggregator(aggregator)
if (self._aggregator) then
self._aggregator:RemoveListener(self._listenerId)
self._listenerId = nil
self._aggregator = nil
end
self._aggregator = aggregator
self:_refreshVisibility()
end
function StatsAnnotatedGraphClass:_stopListening()
if (self._aggregator == nil) then
return
end
if (self._listenerId == nil) then
return
end
self._aggregator:RemoveListener(self._listenerId)
self._listenerId = nil
end
function StatsAnnotatedGraphClass:_startListening()
if (self._aggregator == nil) then
return
end
if (self._listenerId ~= nil) then
return
end
self._listenerId = self._aggregator:AddListener(function()
self:_updateValuesAndRender()
end)
end
function StatsAnnotatedGraphClass:_shouldBeVisible()
if StatsUtils:PerformanceStatsShouldBeVisible() then
return self._frame.Visible
else
return false
end
end
function StatsAnnotatedGraphClass:SetVisible(visible)
self._frame.Visible = visible
self:_refreshVisibility()
end
function StatsAnnotatedGraphClass:_refreshVisibility()
if self:_shouldBeVisible() then
self:_startListening()
self:_updateValuesAndRender()
else
self:_stopListening()
end
end
function StatsAnnotatedGraphClass:OnPerformanceStatsShouldBeVisibleChanged()
self:_refreshVisibility()
end
function StatsAnnotatedGraphClass:_recursiveGetOrderOfMagnitude(estimate, target)
if (estimate > target) then
return self:_recursiveGetOrderOfMagnitude(estimate/10.0, target)
end
if (estimate * 10 >= target) then
return estimate
end
return self:_recursiveGetOrderOfMagnitude(estimate*10.0, target)
end
function StatsAnnotatedGraphClass:_updateValuesAndRender()
self._values = {}
self._average = 0
self._target = 0
if self._aggregator ~= nil then
self._values = self._aggregator:GetValues()
self._average = self._aggregator:GetAverage()
self._target = self._aggregator:GetTarget()
end
self:_calculateAxisMax()
self:_render()
end
return StatsAnnotatedGraphClass
@@ -0,0 +1,102 @@
--[[
Filename: StatsButton.lua
Written by: dbanks
Description: Button that displays latest deets of one or two
particular stats.
--]]
--[[ Services ]]--
local CoreGuiService = game:GetService('CoreGui')
--[[ Modules ]]--
local StyleWidgets = require(CoreGuiService.RobloxGui.Modules.StyleWidgets)
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
local StatsMiniTextPanelClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsMiniTextPanel)
local StatsAnnotatedGraphClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAnnotatedGraph)
--[[ Globals ]]--
local TextPanelXFraction = 0.5
local GraphXFraction = 1 - TextPanelXFraction
local TextPanelPosition = UDim2.new(0, 0, 0, 0)
local TextPanelSize = UDim2.new(TextPanelXFraction, 0, 1, -StyleWidgets.TabSelectionHeight)
local GraphPosition = UDim2.new(TextPanelXFraction, 0, 0, 0)
local GraphSize = UDim2.new(GraphXFraction, 0, 1, -StyleWidgets.TabSelectionHeight)
--[[ Classes ]]--
local StatsButtonClass = {}
StatsButtonClass.__index = StatsButtonClass
function StatsButtonClass.new(statType)
local self = {}
setmetatable(self, StatsButtonClass)
self._statType = statType
self._button = Instance.new("TextButton")
self._button.Name = "PS_Button"
self._button.Text = ""
StatsUtils.StyleButton(self._button)
self._textPanel = StatsMiniTextPanelClass.new(statType)
self._textPanel:PlaceInParent(self._button,
TextPanelSize,
TextPanelPosition)
self._graph = StatsAnnotatedGraphClass.new(statType, false)
self._graph:PlaceInParent(self._button,
GraphSize,
GraphPosition)
self._textPanel:SetZIndex(StatsUtils.TextZIndex)
self._graph:SetZIndex(StatsUtils.GraphZIndex)
self._tabSelection = StyleWidgets.MakeTabSelectionWidget(self._button)
self._isSelected = false
self:_updateTabSelectionState();
return self
end
function StatsButtonClass:OnPerformanceStatsShouldBeVisibleChanged()
if self._graph then
self._graph:OnPerformanceStatsShouldBeVisibleChanged()
end
if self._textPanel then
self._textPanel:OnPerformanceStatsShouldBeVisibleChanged()
end
end
function StatsButtonClass:SetToggleCallbackFunction(callbackFunction)
self._button.MouseButton1Click:connect(function()
callbackFunction(self._statType)
end)
end
function StatsButtonClass:SetSizeAndPosition(size, position)
self._button.Size = size;
self._button.Position = position;
end
function StatsButtonClass:SetIsSelected(isSelected)
self._isSelected = isSelected
self:_updateTabSelectionState();
end
function StatsButtonClass:_updateTabSelectionState()
self._tabSelection.Visible = self._isSelected
end
function StatsButtonClass:SetParent(parent)
self._button.Parent = parent
end
function StatsButtonClass:SetStatsAggregator(aggregator)
self._textPanel:SetStatsAggregator(aggregator)
self._graph:SetStatsAggregator(aggregator)
end
return StatsButtonClass
@@ -0,0 +1,175 @@
--[[
Filename: StatsMiniTextPanel.lua
Written by: dbanks
Description: Panel that shows title and current value for a given stat.
--]]
--[[ Globals ]]--
local TitleHeightYFraction = 0.4
local ValueHeightYFraction = 0.3
local TitleTopYFraction = (1 - TitleHeightYFraction -
ValueHeightYFraction)/2
local LeftMarginPix = 10
local TitlePosition = UDim2.new(0,
LeftMarginPix,
TitleTopYFraction,
0)
local TitleSize = UDim2.new(1,
-LeftMarginPix * 2,
TitleHeightYFraction,
0)
local ValuePosition = UDim2.new(0,
LeftMarginPix,
TitleTopYFraction + TitleHeightYFraction,
0)
local ValueSize = UDim2.new(1,
-LeftMarginPix * 2,
ValueHeightYFraction,
0)
--[[ Services ]]--
local CoreGuiService = game:GetService('CoreGui')
--[[ Modules ]]--
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
local StatsAggregatorClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAggregator)
--[[ Classes ]]--
local StatsMiniTextPanelClass = {}
StatsMiniTextPanelClass.__index = StatsMiniTextPanelClass
function StatsMiniTextPanelClass.new(statType)
local self = {}
setmetatable(self, StatsMiniTextPanelClass)
self._statType = statType
self._frame = Instance.new("Frame")
self._frame.Name = "StatsMiniTextPanelClass"
self._frame.BackgroundTransparency = 1.0
self._frame.ZIndex = StatsUtils.TextPanelZIndex
self._titleLabel = Instance.new("TextLabel")
self._titleLabel.Name = "TitleLabel"
self._valueLabel = Instance.new("TextLabel")
self._valueLabel.Name = "ValueLabel"
StatsUtils.StyleTextWidget(self._titleLabel)
StatsUtils.StyleTextWidget(self._valueLabel)
self._titleLabel.FontSize = StatsUtils.MiniPanelTitleFontSize
self._titleLabel.Text = self:_getTitle()
self._titleLabel.Parent = self._frame
self._titleLabel.Size = TitleSize
self._titleLabel.Position = TitlePosition
self._titleLabel.TextXAlignment = Enum.TextXAlignment.Left
self._valueLabel.FontSize = StatsUtils.MiniPanelValueFontSize
self._valueLabel.Text = "0"
self._valueLabel.Parent = self._frame
self._valueLabel.Size = ValueSize
self._valueLabel.Position = ValuePosition
self._valueLabel.TextXAlignment = Enum.TextXAlignment.Left
return self
end
function StatsMiniTextPanelClass:SetZIndex(zIndex)
self._frame.ZIndex = zIndex
self._titleLabel.ZIndex = zIndex
self.ZIndex = zIndex
end
function StatsMiniTextPanelClass:_getTitle()
return StatsUtils.TypeToShortName[self._statType]
end
function StatsMiniTextPanelClass:PlaceInParent(parent, size, position)
self._frame.Position = position
self._frame.Size = size
self._frame.Parent = parent
end
function StatsMiniTextPanelClass:SetStatsAggregator(aggregator)
if (self._aggregator) then
self._aggregator:RemoveListener(self._listenerId)
self._listenerId = nil
self._aggregator = nil
end
self._aggregator = aggregator
self:_refreshVisibility()
end
function StatsMiniTextPanelClass:SetVisible(visible)
self.frame.Visible = visible
self:_refreshVisibility()
end
function StatsMiniTextPanelClass:_shouldBeVisible()
if StatsUtils.PerformanceStatsShouldBeVisible() then
return self._frame.Visible
else
return false
end
end
function StatsMiniTextPanelClass:_refreshVisibility()
if self:_shouldBeVisible() then
self:_startListening()
self:_updateFromAggregator()
else
self:_stopListening()
end
end
function StatsMiniTextPanelClass:_stopListening()
if (self._aggregator == nil) then
return
end
if (self._listenerId == nil) then
return
end
self._aggregator:RemoveListener(self._listenerId)
self._listenerId = nil
end
function StatsMiniTextPanelClass:_startListening()
if (self._aggregator == nil) then
return
end
if (self._listenerId ~= nil) then
return
end
self._listenerId = self._aggregator:AddListener(function()
self:_updateFromAggregator()
end)
end
function StatsMiniTextPanelClass:OnPerformanceStatsShouldBeVisibleChanged()
self:_refreshVisibility()
end
function StatsMiniTextPanelClass:_updateFromAggregator()
local value
if self._aggregator ~= nil then
value = self._aggregator:GetLatestValue()
else
value = 0
end
self._valueLabel.Text = StatsUtils.FormatTypedValue(value, self._statType)
end
return StatsMiniTextPanelClass
@@ -0,0 +1,265 @@
--[[
Filename: StatsTextPanel.lua
Written by: dbanks
Description: Panel that shows a "Legend" for a graph, including:
- name of stat being displayed.
- current value of stat.
- target (suggested max) value of stat.
- average value of stat over the whole graph.
particular stat.
--]]
--[[ Services ]]--
local CoreGuiService = game:GetService('CoreGui')
--[[ Modules ]]--
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
local StatsAggregatorClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAggregator)
local DecoratedValueLabelClass = require(CoreGuiService.RobloxGui.Modules.Stats.DecoratedValueLabel)
--[[ Globals ]]--
-- Positions
local top = StatsUtils.TextPanelTopMarginPix
local TitlePosition = UDim2.new(0,
StatsUtils.TextPanelLeftMarginPix,
0,
top)
top = top + StatsUtils.TextPanelTitleHeightY
local CurrentValuePosition = UDim2.new(0,
StatsUtils.TextPanelLeftMarginPix,
0,
top)
top = top + StatsUtils.TextPanelLegendItemHeightY
local TargetValuePosition = UDim2.new(0,
StatsUtils.TextPanelLeftMarginPix,
0,
top)
top = top + StatsUtils.TextPanelLegendItemHeightY
local AverageValuePosition = UDim2.new(0,
StatsUtils.TextPanelLeftMarginPix,
0,
top)
-- Sizes
local TitleSize = UDim2.new(1,
-StatsUtils.TextPanelLeftMarginPix * 2,
0,
StatsUtils.TextPanelTitleHeightY)
local LegendItemValueSize = UDim2.new(1,
-StatsUtils.TextPanelLeftMarginPix * 2,
0,
StatsUtils.TextPanelLegendItemHeightY)
--[[ Classes ]]--
local StatsTextPanelClass = {}
StatsTextPanelClass.__index = StatsTextPanelClass
function StatsTextPanelClass.new(statType)
local self = {}
setmetatable(self, StatsTextPanelClass)
self._statType = statType
self._frame = Instance.new("Frame")
self._frame.BackgroundTransparency = 1.0
self._frame.ZIndex = StatsUtils.TextPanelZIndex
self._titleLabel = Instance.new("TextLabel")
StatsUtils.StyleTextWidget(self._titleLabel)
self._titleLabel.FontSize = StatsUtils.PanelTitleFontSize
self._titleLabel.Text = self:_getTitle()
self._titleLabel.Parent = self._frame
self._titleLabel.Size = TitleSize
self._titleLabel.Position = TitlePosition
self._titleLabel.TextXAlignment = Enum.TextXAlignment.Left
self._titleLabel.TextYAlignment = Enum.TextYAlignment.Top
-- Icon + text widgets to show the current value of this stat.
self:_addCurrentValueWidget()
-- Icon + text widgets to show the suggested max value of this stat.
self:_addTargetValueWidget()
-- Icon + text widgets to show the average value of this stat.
self:_addAverageValueWidget()
return self
end
function StatsTextPanelClass:_getTarget()
-- Get the current target value for the graphed stat.
if self._performanceStats == nil then
return 0
end
local maxItemStats = self._performanceStats:FindFirstChild(self._statMaxName)
if maxItemStats == nil then
return 0
end
return maxItemStats:GetValue()
end
function StatsTextPanelClass:_addCurrentValueWidget()
self._currentValueWidget = DecoratedValueLabelClass.new(self._statType,
"Current")
self._currentValueWidget:PlaceInParent(self._frame,
LegendItemValueSize,
CurrentValuePosition)
local decorationFrame = self._currentValueWidget:GetDecorationFrame()
local decoration = Instance.new("ImageLabel")
decoration.Position = UDim2.new(0.5, -StatsUtils.OvalKeySize/2,
0.5, -StatsUtils.OvalKeySize/2)
decoration.Size = UDim2.new(0, StatsUtils.OvalKeySize,
0, StatsUtils.OvalKeySize)
decoration.Parent = decorationFrame
decoration.BackgroundTransparency = 1
decoration.Image = 'rbxasset://textures/ui/PerformanceStats/OvalKey.png'
decoration.BorderSizePixel = 0
self._currentValueDecoration = decoration
end
function StatsTextPanelClass:_addTargetValueWidget()
self._targetValueWidget = DecoratedValueLabelClass.new(self._statType,
"Target")
self._targetValueWidget:PlaceInParent(self._frame,
LegendItemValueSize,
TargetValuePosition)
local decorationFrame = self._targetValueWidget:GetDecorationFrame()
local decoration = Instance.new("ImageLabel")
decoration.Position = UDim2.new(0.5, -StatsUtils.TargetKeyWidth/2,
0.5, -StatsUtils.TargetKeyHeight/2)
decoration.Size = UDim2.new(0, StatsUtils.TargetKeyWidth,
0, StatsUtils.TargetKeyHeight)
decoration.Parent = decorationFrame
decoration.BackgroundTransparency = 1
decoration.Image = 'rbxasset://textures/ui/PerformanceStats/TargetKey.png'
end
function StatsTextPanelClass:_addAverageValueWidget()
self._averageValueWidget = DecoratedValueLabelClass.new(self._statType,
"Average")
self._averageValueWidget:PlaceInParent(self._frame,
LegendItemValueSize,
AverageValuePosition)
local decorationFrame = self._averageValueWidget:GetDecorationFrame()
local decoration = Instance.new("Frame")
decoration.Position = UDim2.new(0, 0, 0.5, -StatsUtils.GraphAverageLineTotalThickness/2)
decoration.Size = UDim2.new(1, 0, 0, StatsUtils.GraphAverageLineInnerThickness)
decoration.Parent = decorationFrame
StatsUtils.StyleAverageLine(decoration)
end
function StatsTextPanelClass:_getTitle()
return StatsUtils.TypeToName[self._statType]
end
function StatsTextPanelClass:PlaceInParent(parent, size, position)
self._frame.Position = position
self._frame.Size = size
self._frame.Parent = parent
end
function StatsTextPanelClass:SetStatsAggregator(aggregator)
if (self._aggregator) then
self._aggregator:RemoveListener(self._listenerId)
self._listenerId = nil
self._aggregator = nil
end
self._aggregator = aggregator
self:_refreshVisibility()
end
function StatsTextPanelClass:SetVisible(visible)
self._frame.Visible = visible
self:_refreshVisibility()
end
function StatsTextPanelClass:_shouldBeVisible()
if StatsUtils.PerformanceStatsShouldBeVisible() then
return self._frame.Visible
else
return false
end
end
function StatsTextPanelClass:_refreshVisibility()
if self:_shouldBeVisible() then
self:_startListening()
self:_updateFromAggregator()
else
self:_stopListening()
end
end
function StatsTextPanelClass:_stopListening()
if (self._aggregator == nil) then
return
end
if (self._listenerId == nil) then
return
end
self._aggregator:RemoveListener(self._listenerId)
self._listenerId = nil
end
function StatsTextPanelClass:_startListening()
if (self._aggregator == nil) then
return
end
if (self._listenerId ~= nil) then
return
end
self._listenerId = self._aggregator:AddListener(function()
self:_updateFromAggregator()
end)
end
function StatsTextPanelClass:_updateFromAggregator()
local value = 0
local average = 0
local target = 0
if self._aggregator ~= nil then
value = self._aggregator:GetLatestValue()
average = self._aggregator:GetAverage()
target = self._aggregator:GetTarget()
end
self._currentValueWidget:SetValue(value)
self._targetValueWidget:SetValue(target)
self._averageValueWidget:SetValue(average)
self._currentValueDecoration.ImageColor3 = StatsUtils.GetColorForValue(value, target)
end
function StatsTextPanelClass:SetZIndex(zIndex)
-- Pass through to all children.
self._frame.ZIndex = zIndex
self._titleLabel.ZIndex = zIndex
self._currentValueWidget:SetZIndex(zIndex)
self._averageValueWidget:SetZIndex(zIndex)
end
return StatsTextPanelClass
@@ -0,0 +1,247 @@
--!nocheck
--[[
Filename: StatsUtils.lua
Written by: dbanks
Description: Common work in the performance stats world.
--]]
local CoreGuiService = game:GetService('CoreGui')
local PlayersService = game:GetService("Players")
local Settings = UserSettings()
local GameSettings = Settings.GameSettings
local StyleWidgets = require(CoreGuiService.RobloxGui.Modules.StyleWidgets)
local RobloxTranslator = require(CoreGuiService.RobloxGui.Modules.RobloxTranslator)
function LocalizedGetKey(key)
local rtv = key
pcall(function()
local LocalizationService = game:GetService("LocalizationService")
local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1]
rtv = RobloxTranslator:FormatByKey(key)
end)
return rtv
end
local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end)
local FFlagUseNotificationsLocalization = success and result
--[[ Classes ]]--
local StatsUtils = {}
-- Colors
StatsUtils.SelectedBackgroundColor = Color3.new(0.4, 0.4, 0.4)
StatsUtils.FontColor = Color3.new(1, 1, 1)
StatsUtils.GraphBarGreenColor = Color3.new(126/255.0, 211/255.0, 33/255.0)
StatsUtils.GraphBarYellowColor = Color3.new(209/255.0, 211/255.0, 33/255.0)
StatsUtils.GraphBarRedColor = Color3.new(211/255.0, 88/255.0, 33/255.0)
StatsUtils.GraphAverageLineColor = Color3.new(208/255.0, 1/255.0, 27/255.0)
StatsUtils.GraphAverageLineBorderColor = Color3.new(1, 1, 1)
StatsUtils.NormalColor = Color3.fromRGB(31, 31, 31)
StatsUtils.Transparency = 0.5
-- Font Sizes
StatsUtils.MiniPanelTitleFontSize = Enum.FontSize.Size12
StatsUtils.MiniPanelValueFontSize = Enum.FontSize.Size10
StatsUtils.PanelTitleFontSize = Enum.FontSize.Size24
StatsUtils.PanelValueFontSize = Enum.FontSize.Size14
StatsUtils.PanelGraphFontSize = Enum.FontSize.Size10
-- Layout
-- Layout: Buttons
StatsUtils.ButtonHeight = 36 + StyleWidgets.TabSelectionHeight
-- Layout: Viewer
StatsUtils.ViewerTopMargin = 10
StatsUtils.ViewerHeight = 144
StatsUtils.ViewerWidth = 306
StatsUtils.TextZIndex = 5
StatsUtils.GraphZIndex = 2
-- Layout: Graph
StatsUtils.GraphTargetLineInnerThickness = 2
StatsUtils.GraphAverageLineInnerThickness = 2
StatsUtils.GraphAverageLineBorderThickness = 1
StatsUtils.GraphAverageLineTotalThickness = (StatsUtils.GraphAverageLineInnerThickness +
2 * StatsUtils.GraphAverageLineBorderThickness)
-- Layout: Main Text Panel
StatsUtils.TextPanelTitleHeightY = 52
StatsUtils.TextPanelLegendItemHeightY = 20
StatsUtils.TextPanelLeftMarginPix = 10
StatsUtils.TextPanelTopMarginPix = 10
-- Layout: Graph Legend
StatsUtils.DecorationSize = 12
StatsUtils.OvalKeySize = 8
StatsUtils.TargetKeyWidth = 11
StatsUtils.TargetKeyHeight = 2
StatsUtils.DecorationMargin = 6
-- Enums
StatsUtils.StatType_Memory = "st_Memory"
StatsUtils.StatType_CPU = "st_CPU"
StatsUtils.StatType_GPU = "st_GPU"
StatsUtils.StatType_NetworkSent = "st_NetworkSent"
StatsUtils.StatType_NetworkReceived = "st_NetworkReceived"
StatsUtils.StatType_Ping = "st_Ping"
StatsUtils.AllStatTypes = {
StatsUtils.StatType_Memory,
StatsUtils.StatType_CPU,
StatsUtils.StatType_GPU,
StatsUtils.StatType_NetworkSent,
StatsUtils.StatType_NetworkReceived,
StatsUtils.StatType_Ping,
}
StatsUtils.StatNames = {
[StatsUtils.StatType_Memory] = "Memory",
[StatsUtils.StatType_CPU] = "CPU",
[StatsUtils.StatType_GPU] = "GPU",
[StatsUtils.StatType_NetworkSent] = "NetworkSent",
[StatsUtils.StatType_NetworkReceived] = "NetworkReceived",
[StatsUtils.StatType_Ping] = "Ping",
}
StatsUtils.StatMaxNames = {
[StatsUtils.StatType_Memory] = "MaxMemory",
[StatsUtils.StatType_CPU] = "MaxCPU",
[StatsUtils.StatType_GPU] = "MaxGPU",
[StatsUtils.StatType_NetworkSent] = "MaxNetworkSent",
[StatsUtils.StatType_NetworkReceived] = "MaxNetworkReceived",
[StatsUtils.StatType_Ping] = "MaxPing",
}
StatsUtils.NumButtonTypes = table.getn(StatsUtils.AllStatTypes)
local strSentNetwork = LocalizedGetKey("Sent") .. "\n" .. LocalizedGetKey("Network")
local strReceivedNetwork = LocalizedGetKey("Received") .. "\n" .. LocalizedGetKey("Network")
StatsUtils.TypeToName = {
[StatsUtils.StatType_Memory] = "Memory",
[StatsUtils.StatType_CPU] = "CPU",
[StatsUtils.StatType_GPU] = "GPU",
[StatsUtils.StatType_NetworkSent] = strSentNetwork,
[StatsUtils.StatType_NetworkReceived] = strReceivedNetwork,
[StatsUtils.StatType_Ping] = "Ping",
}
StatsUtils.TypeToShortName = {
[StatsUtils.StatType_Memory] = "Mem",
[StatsUtils.StatType_CPU] = "CPU",
[StatsUtils.StatType_GPU] = "GPU",
[StatsUtils.StatType_NetworkSent] = "Sent",
[StatsUtils.StatType_NetworkReceived] = "Recv",
[StatsUtils.StatType_Ping] = "Ping",
}
StatsUtils.MemoryAnalyzerTypeToName = {
["HumanoidTexture"] = "Humanoid Textures",
["HumanoidTextureOrphan"] = "Humanoid Textures (Unused)",
["OtherTexture"] = "Other Textures",
["OtherTextureOrphan"] = "Other Textures (Unused)",
["CoreScript"] = "Core Scripts",
["UserScript"] = "User Scripts",
["Sounds"] = "Sounds",
["CSG"] = "Solid Models",
["Meshes"] = "Meshes",
}
function StatsUtils.GetMemoryAnalyzerStatName(memoryAnalyzerStatType)
if (StatsUtils.MemoryAnalyzerTypeToName[memoryAnalyzerStatType] == nil) then
return memoryAnalyzerStatType
else
return StatsUtils.MemoryAnalyzerTypeToName[memoryAnalyzerStatType]
end
end
function StatsUtils.StyleFrame(frame)
frame.BackgroundColor3 = StatsUtils.NormalColor
frame.BackgroundTransparency = StatsUtils.Transparency
end
function StatsUtils.StyleButton(button)
button.BackgroundColor3 = StatsUtils.NormalColor
button.BackgroundTransparency = StatsUtils.Transparency
end
function StatsUtils.StyleTextWidget(textLabel)
textLabel.BackgroundTransparency = 1.0
textLabel.TextColor3 = StatsUtils.FontColor
textLabel.Font = Enum.Font.SourceSansBold
end
function StatsUtils.StyleButtonSelected(frame, isSelected)
StatsUtils.StyleButton(frame)
if (isSelected) then
frame.BackgroundColor3 = StatsUtils.SelectedBackgroundColor
end
end
local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end)
local FFlagUseNotificationsLocalization = success and result
local function LocalizedGetString(key, rtv)
pcall(function()
local LocalizationService = game:GetService("LocalizationService")
local CorescriptLocalization = LocalizationService:GetCorescriptLocalizations()[1]
rtv = CorescriptLocalization:GetString(LocalizationService.RobloxLocaleId, key)
end)
return rtv
end
function StatsUtils.FormatTypedValue(value, statType)
if FFlagUseNotificationsLocalization then
if statType == StatsUtils.StatType_CPU or statType == StatsUtils.StatType_GPU then
return string.gsub(LocalizedGetString("StatsUtil.ms",string.format("%.2f MB", value)),"{RBX_NUMBER}",string.format("%.2f",value))
elseif statType == StatsUtils.StatType_PlaceMemory then
return string.gsub(LocalizedGetString("StatsUtil.MB",string.format("%.2f ms", value)),"{RBX_NUMBER}",string.format("%.2f",value))
elseif statType == StatsUtils.StatType_NetworkSent or statType == StatsUtils.StatType_NetworkReceived then
return string.gsub(LocalizedGetString("StatsUtil.KBps",string.format("%.2f KB/s", value)),"{RBX_NUMBER}",string.format("%.2f",value))
end
end
if statType == StatsUtils.StatType_Memory then
return string.format("%.2f MB", value)
elseif statType == StatsUtils.StatType_CPU then
return string.format("%.2f ms", value)
elseif statType == StatsUtils.StatType_GPU then
return string.format("%.2f ms", value)
elseif statType == StatsUtils.StatType_NetworkSent then
return string.format("%.2f KB/s", value)
elseif statType == StatsUtils.StatType_NetworkReceived then
return string.format("%.2f KB/s", value)
elseif statType == StatsUtils.StatType_Ping then
return string.format("%.2f ms", value)
end
end
function StatsUtils.StyleAverageLine(frame)
frame.BackgroundColor3 = StatsUtils.GraphAverageLineColor
frame.BorderSizePixel = StatsUtils.GraphAverageLineBorderThickness
frame.BorderColor3 = StatsUtils.GraphAverageLineBorderColor
end
function StatsUtils.GetColorForValue(value, target)
if value < 0.666 * target then
return StatsUtils.GraphBarGreenColor
elseif value < 1.333 * target then
return StatsUtils.GraphBarYellowColor
else
return StatsUtils.GraphBarRedColor
end
end
function StatsUtils.PerformanceStatsShouldBeVisible()
local localPlayer = PlayersService.LocalPlayer
return (GameSettings.PerformanceStatsVisible and localPlayer ~= nil)
end
return StatsUtils
@@ -0,0 +1,139 @@
--[[
Filename: StatsButtonViewer.lua
Written by: dbanks
Description: Widget that displays one or more stats in closeup view:
text and graphics.
--]]
--[[ Services ]]--
local CoreGuiService = game:GetService('CoreGui')
--[[ Modules ]]--
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
local StatsTextPanelClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsTextPanel)
local StatsAnnotatedGraphClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAnnotatedGraph)
--[[ Globals ]]--
local TextPanelXFraction = 0.4
local GraphXFraction = 1 - TextPanelXFraction
local TextPanelPosition = UDim2.new(0, 0, 0, 0)
local TextPanelSize = UDim2.new(TextPanelXFraction, 0, 1, 0)
local GraphPosition = UDim2.new(TextPanelXFraction, 0, 0, 0)
local GraphSize = UDim2.new(GraphXFraction, 0, 1, 0)
--[[ Classes ]]--
local StatsViewerClass = {}
StatsViewerClass.__index = StatsViewerClass
function StatsViewerClass.new()
local self = {}
setmetatable(self, StatsViewerClass)
self._frameImageLabel = Instance.new("ImageLabel")
self._frameImageLabel.Name = "PS_Viewer"
self._frameImageLabel.Image = 'rbxasset://textures/ui/PerformanceStats/BackgroundRounded.png'
self._frameImageLabel.ScaleType = Enum.ScaleType.Slice
self._frameImageLabel.SliceCenter = Rect.new(10, 10, 22, 22)
self._frameImageLabel.BackgroundTransparency = 1
self._frameImageLabel.ImageColor3 = StatsUtils.NormalColor
self._frameImageLabel.ImageTransparency = StatsUtils.Transparency
self._textPanel = nil
self._statType = nil
self._graph = nil
return self
end
function StatsViewerClass:OnPerformanceStatsShouldBeVisibleChanged()
if self._graph then
self._graph:OnPerformanceStatsShouldBeVisibleChanged()
end
if self._textPanel then
self._textPanel:OnPerformanceStatsShouldBeVisibleChanged()
end
if self._textPanel then
self._textPanel:OnVisibilityChanged()
end
end
function StatsViewerClass:GetIsVisible()
return self._frameImageLabel.Visible
end
function StatsViewerClass:GetStatType()
return self._statType
end
function StatsViewerClass:SetSizeAndPosition(size, position)
self._frameImageLabel.Size = size
self._frameImageLabel.Position = position
end
function StatsViewerClass:SetParent(parent)
self._frameImageLabel.Parent = parent
end
function StatsViewerClass:SetVisible(visible)
self._frameImageLabel.Visible = visible
if (self._graph) then
self._graph:SetVisible(visible)
end
if (self._textPanel) then
self._textPanel:SetVisible(visible)
end
end
function StatsViewerClass:SetStatType(statType)
self._statType = statType
self._frameImageLabel:ClearAllChildren()
-- If there's already a text panel, make it clear it is no longer visible.
if (self._textPanel) then
self._textPanel:SetVisible(false)
self._textPanel = nil
end
self._textPanel = StatsTextPanelClass.new(statType)
self._textPanel:PlaceInParent(self._frameImageLabel,
TextPanelSize,
TextPanelPosition)
self._graph = StatsAnnotatedGraphClass.new(statType, true)
self._graph:PlaceInParent(self._frameImageLabel,
GraphSize,
GraphPosition)
self._textPanel:SetZIndex(StatsUtils.TextZIndex)
self._graph:SetZIndex(StatsUtils.GraphZIndex)
self._graph:SetVisible(self._frameImageLabel.Visible)
self._textPanel:SetVisible(self._frameImageLabel.Visible)
self:_applyStatsAggregator();
end
function StatsViewerClass:_applyStatsAggregator()
if (self._aggregator == nil) then
return
end
if (self._textPanel) then
self._textPanel:SetStatsAggregator(self._aggregator)
end
if (self._graph) then
self._graph:SetStatsAggregator(self._aggregator)
end
end
function StatsViewerClass:SetStatsAggregator(aggregator)
self._aggregator = aggregator
self:_applyStatsAggregator();
end
return StatsViewerClass
@@ -0,0 +1,90 @@
--[[
Filename: TreeViewItem.lua
Written by: dbanks
Description: Generic tree view data stucture.
--]]
--[[ Globals ]]--
--[[ Helper functions ]]--
--[[ Classes ]]--
local TreeViewItem = {}
TreeViewItem.__index = TreeViewItem
function TreeViewItem.new(id, parent)
local self = {}
setmetatable(self, TreeViewItem)
self._id = id
self._parent = parent
self._children = {}
return self
end
function TreeViewItem:getValue()
return self._value
end
function TreeViewItem:getLabel()
return self._label
end
function TreeViewItem:getChildren()
return self._children
end
function TreeViewItem:getId()
return self._id
end
function TreeViewItem:getStackDepth()
if (self._parent == nil) then
return 0
else
return 1 + self._parent:getStackDepth()
end
end
function TreeViewItem:setLabelAndValue(label, value)
self._label = label
self._value = value
end
function TreeViewItem:getOrMakeChildById(id)
for i, childTreeViewItem in ipairs(self._children) do
if (childTreeViewItem:getId() == id) then
return childTreeViewItem
end
end
local newChild = TreeViewItem.new(id, self)
table.insert(self._children, newChild)
return newChild
end
function TreeViewItem:removeChildren()
if not self._children then return end
for i, childTreeViewItem in ipairs(self._children) do
childTreeViewItem:removeChildren()
childTreeViewItem = nil
end
self._id = nil
self._parent = nil
self._children = nil
end
function TreeViewItem:removeChild(id)
for i, childTreeViewItem in ipairs(self._children) do
if (childTreeViewItem:getId() == id) then
childTreeViewItem:removeChildren()
return
end
end
end
return TreeViewItem