This commit is contained in:
lifestorm
2024-08-05 18:40:29 +03:00
parent 9f505a0646
commit c6d9b6f580
8044 changed files with 1853472 additions and 21 deletions

View File

@@ -0,0 +1,110 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
local get, update, onUpdate = FProfiler.UI.getModelValue, FProfiler.UI.updateModel, FProfiler.UI.onModelUpdate
--[[-------------------------------------------------------------------------
(Re)start clientside profiling
---------------------------------------------------------------------------]]
local function restartProfiling()
if get({"client", "shouldReset"}) then
FProfiler.Internal.reset()
update({"client", "recordTime"}, 0)
end
local focus = get({"client", "focusObj"})
update({"client", "sessionStart"}, CurTime())
update({"client", "sessionStartSysTime"}, SysTime())
FProfiler.Internal.start(focus)
end
--[[-------------------------------------------------------------------------
Stop profiling
---------------------------------------------------------------------------]]
local function stopProfiling()
FProfiler.Internal.stop()
local newTime = get({"client", "recordTime"}) + SysTime() - (get({"client", "sessionStartSysTime"}) or 0)
-- Get the aggregated data
local mostTime = FProfiler.Internal.getAggregatedResults(100)
update({"client", "bottlenecks"}, mostTime)
update({"client", "topLagSpikes"}, FProfiler.Internal.getMostExpensiveSingleCalls())
update({"client", "recordTime"}, newTime)
update({"client", "sessionStart"}, nil)
update({"client", "sessionStartSysTime"}, nil)
end
--[[-------------------------------------------------------------------------
Start/stop recording when the recording status is changed
---------------------------------------------------------------------------]]
onUpdate({"client", "status"}, function(new, old)
if new == old then return end
(new == "Started" and restartProfiling or stopProfiling)()
end)
--[[-------------------------------------------------------------------------
Update the current selected focus object when data is entered
---------------------------------------------------------------------------]]
onUpdate({"client", "focusStr"}, function(new)
update({"client", "focusObj"}, FProfiler.funcNameToObj(new))
end)
--[[-------------------------------------------------------------------------
Update info when a different line is selected
---------------------------------------------------------------------------]]
onUpdate({"client", "currentSelected"}, function(new)
if not new or not new.info or not new.info.linedefined or not new.info.lastlinedefined or not new.info.short_src then return end
update({"client", "sourceText"}, FProfiler.readSource(new.info.short_src, new.info.linedefined, new.info.lastlinedefined))
end)
--[[-------------------------------------------------------------------------
When a function is to be printed to console
---------------------------------------------------------------------------]]
onUpdate({"client", "toConsole"}, function(data)
if not data then return end
update({"client", "toConsole"}, nil)
show(data)
file.CreateDir("fprofiler")
file.Write("fprofiler/profiledata.txt", showStr(data))
MsgC(Color(200, 200, 200), "-----", Color(120, 120, 255), "NOTE", Color(200, 200, 200), "---------------\n")
MsgC(Color(200, 200, 200), "If the above function does not fit in console, you can find it in data/fprofiler/profiledata.txt\n\n")
end)
--[[-------------------------------------------------------------------------
API function: start profiling
---------------------------------------------------------------------------]]
function FProfiler.start(focus)
update({"client", "focusStr"}, tostring(focus))
update({"client", "focusObj"}, focus)
update({"client", "shouldReset"}, true)
update({"client", "status"}, "Started")
end
--[[-------------------------------------------------------------------------
API function: stop profiling
---------------------------------------------------------------------------]]
function FProfiler.stop()
update({"client", "status"}, "Stopped")
end
--[[-------------------------------------------------------------------------
API function: continue profiling
---------------------------------------------------------------------------]]
function FProfiler.continueProfiling()
update({"client", "shouldReset"}, false)
update({"client", "status"}, "Started")
end

475
lua/fprofiler/ui/frame.lua Normal file
View File

@@ -0,0 +1,475 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[-------------------------------------------------------------------------
The panel that contains the realm switcher
---------------------------------------------------------------------------]]
local REALMPANEL = {}
function REALMPANEL:Init()
self:DockPadding(0, 0, 0, 0)
self:DockMargin(0, 0, 5, 0)
self.realmLabel = vgui.Create("DLabel", self)
self.realmLabel:SetDark(true)
self.realmLabel:SetText("Realm:")
self.realmLabel:SizeToContents()
self.realmLabel:Dock(TOP)
self.realmbox = vgui.Create("DComboBox", self)
self.realmbox:AddChoice("Client")
self.realmbox:AddChoice("Server")
self.realmbox:Dock(TOP)
FProfiler.UI.onModelUpdate("realm", function(new)
self.realmbox.selected = new == "client" and 1 or 2
self.realmbox:SetText(new == "client" and "Client" or "Server")
end)
FProfiler.UI.onModelUpdate("serverAccess", function(hasAccess)
self.realmbox:SetDisabled(not hasAccess)
if not hasAccess and self.realmbox.selected == 2 then
FProfiler.UI.updateModel("realm", "client")
end
end)
self.realmbox.OnSelect = function(_, _, value) FProfiler.UI.updateModel("realm", string.lower(value)) end
end
function REALMPANEL:PerformLayout()
self.realmLabel:SizeToContents()
local top = ( self:GetTall() - self.realmLabel:GetTall() - self.realmbox:GetTall()) * 0.5
self:DockPadding(0, top, 0, 0)
end
derma.DefineControl("FProfileRealmPanel", "", REALMPANEL, "Panel")
--[[-------------------------------------------------------------------------
The little red or green indicator that indicates whether the focussing
function is correct
---------------------------------------------------------------------------]]
local FUNCINDICATOR = {}
function FUNCINDICATOR:Init()
self:SetTall(5)
self.color = Color(0, 0, 0, 0)
end
function FUNCINDICATOR:Paint()
draw.RoundedBox(0, 0, 0, self:GetWide(), self:GetTall(), self.color)
end
derma.DefineControl("FProfileFuncIndicator", "", FUNCINDICATOR, "DPanel")
--[[-------------------------------------------------------------------------
The panel that contains the focus text entry and the focus indicator
---------------------------------------------------------------------------]]
local FOCUSPANEL = {}
function FOCUSPANEL:Init()
self:DockPadding(0, 0, 0, 0)
self:DockMargin(0, 0, 5, 0)
self.focusLabel = vgui.Create("DLabel", self)
self.focusLabel:SetDark(true)
self.focusLabel:SetText("Profiling Focus:")
self.focusLabel:SizeToContents()
self.focusLabel:Dock(TOP)
self.funcIndicator = vgui.Create("FProfileFuncIndicator", self)
self.funcIndicator:Dock(BOTTOM)
self.focusBox = vgui.Create("DTextEntry", self)
self.focusBox:SetText("")
self.focusBox:SetWidth(150)
self.focusBox:Dock(BOTTOM)
self.focusBox:SetTooltip("Focus the profiling on a single function.\nEnter a global function name here (like player.GetAll)\nYou're not allowed to call functions in here (e.g. hook.GetTable() is not allowed)")
function self.focusBox:OnChange()
FProfiler.UI.updateCurrentRealm("focusStr", self:GetText())
end
FProfiler.UI.onCurrentRealmUpdate("focusObj", function(new)
self.funcIndicator.color = FProfiler.UI.getCurrentRealmValue("focusStr") == "" and Color(0, 0, 0, 0) or new and Color(80, 255, 80, 255) or Color(255, 80, 80, 255)
end)
FProfiler.UI.onCurrentRealmUpdate("focusStr", function(new, old)
if self.focusBox:GetText() == new then return end
self.focusBox:SetText(tostring(new))
end)
end
function FOCUSPANEL:PerformLayout()
self.focusBox:SetWide(200)
self.focusLabel:SizeToContents()
end
derma.DefineControl("FProfileFocusPanel", "", FOCUSPANEL, "Panel")
--[[-------------------------------------------------------------------------
The timer that keeps track of for how long the profiling has been going on
---------------------------------------------------------------------------]]
local TIMERPANEL = {}
function TIMERPANEL:Init()
self:DockPadding(0, 5, 0, 5)
self:DockMargin(5, 0, 5, 0)
self.timeLabel = vgui.Create("DLabel", self)
self.timeLabel:SetDark(true)
self.timeLabel:SetText("Total profiling time:")
self.timeLabel:SizeToContents()
self.timeLabel:Dock(TOP)
self.counter = vgui.Create("DLabel", self)
self.counter:SetDark(true)
self.counter:SetText("00:00:00")
self.counter:SizeToContents()
self.counter:Dock(RIGHT)
function self.counter:Think()
local recordTime, sessionStart = FProfiler.UI.getCurrentRealmValue("recordTime"), FProfiler.UI.getCurrentRealmValue("sessionStart")
local totalTime = recordTime + (sessionStart and (CurTime() - sessionStart) or 0)
self:SetText(string.FormattedTime(totalTime, "%02i:%02i:%02i"))
end
end
function TIMERPANEL:PerformLayout()
self.timeLabel:SizeToContents()
self.counter:SizeToContents()
end
derma.DefineControl("FProfileTimerPanel", "", TIMERPANEL, "Panel")
--[[-------------------------------------------------------------------------
The top bar
---------------------------------------------------------------------------]]
local MAGICBAR = {}
function MAGICBAR:Init()
self:DockPadding(5, 5, 5, 5)
self.realmpanel = vgui.Create("FProfileRealmPanel", self)
-- (Re)Start profiling
self.restartProfiling = vgui.Create("DButton", self)
self.restartProfiling:SetText(" (Re)Start\n Profiling")
self.restartProfiling:DockMargin(0, 0, 5, 0)
self.restartProfiling:Dock(LEFT)
self.restartProfiling.DoClick = function()
FProfiler.UI.updateCurrentRealm("shouldReset", true)
FProfiler.UI.updateCurrentRealm("status", "Started")
end
FProfiler.UI.onCurrentRealmUpdate("status", function(new)
self.restartProfiling:SetDisabled(new == "Started")
end)
-- Stop profiling
self.stopProfiling = vgui.Create("DButton", self)
self.stopProfiling:SetText(" Stop\n Profiling")
self.stopProfiling:DockMargin(0, 0, 5, 0)
self.stopProfiling:Dock(LEFT)
self.stopProfiling.DoClick = function()
FProfiler.UI.updateCurrentRealm("status", "Stopped")
end
FProfiler.UI.onCurrentRealmUpdate("status", function(new)
self.stopProfiling:SetDisabled(new == "Stopped")
end)
-- Continue profiling
self.continueProfiling = vgui.Create("DButton", self)
self.continueProfiling:SetText(" Continue\n Profiling")
self.continueProfiling:DockMargin(0, 0, 5, 0)
self.continueProfiling:Dock(LEFT)
self.continueProfiling.DoClick = function()
FProfiler.UI.updateCurrentRealm("shouldReset", false)
FProfiler.UI.updateCurrentRealm("status", "Started")
end
FProfiler.UI.onCurrentRealmUpdate("status", function(new)
self.continueProfiling:SetDisabled(new == "Started")
end)
self.realmpanel:Dock(LEFT)
self.focuspanel = vgui.Create("FProfileFocusPanel", self)
self.focuspanel:Dock(LEFT)
-- Timer
self.timerpanel = vgui.Create("FProfileTimerPanel", self)
self.timerpanel:Dock(RIGHT)
end
function MAGICBAR:PerformLayout()
self.realmpanel:SizeToChildren(true, false)
self.focuspanel:SizeToChildren(true, false)
self.timerpanel:SizeToChildren(true, false)
end
derma.DefineControl("FProfileMagicBar", "", MAGICBAR, "DPanel")
--[[-------------------------------------------------------------------------
A custom sort by column function to deal with sorting by numeric value
--------------------------------------------------------------------------]]
local function SortByColumn(self, ColumnID, Desc)
table.Copy(self.Sorted, self.Lines)
table.sort(self.Sorted, function(a, b)
if Desc then
a, b = b, a
end
local aval = a:GetSortValue(ColumnID) or a:GetColumnText(ColumnID)
local bval = b:GetSortValue(ColumnID) or b:GetColumnText(ColumnID)
local anum = tonumber(aval)
local bnum = tonumber(bval)
if anum and bnum then
return anum < bnum
end
return tostring(aval) < tostring(bval)
end)
self:SetDirty(true)
self:InvalidateLayout()
end
--[[-------------------------------------------------------------------------
The Bottlenecks tab's contents
---------------------------------------------------------------------------]]
local BOTTLENECKTAB = {}
BOTTLENECKTAB.SortByColumn = SortByColumn
function BOTTLENECKTAB:Init()
self:SetMultiSelect(false)
self:AddColumn("Name")
self:AddColumn("Path")
self:AddColumn("Lines")
self:AddColumn("Amount of times called")
self:AddColumn("Total time in ms (inclusive)")
self:AddColumn("Average time in ms (inclusive)")
FProfiler.UI.onCurrentRealmUpdate("bottlenecks", function(new)
self:Clear()
for _, row in ipairs(new) do
local names = {}
local path = row.info.short_src
local lines = path ~= "[C]" and row.info.linedefined .. " - " .. row.info.lastlinedefined or "N/A"
local amountCalled = row.total_called
local totalTime = row.total_time * 100
local avgTime = row.average_time * 100
for _, fname in ipairs(row.names or {}) do
if fname.namewhat == "" and fname.name == "" then continue end
table.insert(names, fname.namewhat .. " " .. fname.name)
end
if #names == 0 then names[1] = "Unknown" end
local line = self:AddLine(table.concat(names, "/"), path, lines, amountCalled, totalTime, avgTime)
line.data = row
end
end)
FProfiler.UI.onCurrentRealmUpdate("currentSelected", function(new, old)
if new == old then return end
for _, line in pairs(self.Lines) do
line:SetSelected(line.data.func == new.func)
end
end)
end
function BOTTLENECKTAB:OnRowSelected(id, line)
FProfiler.UI.updateCurrentRealm("currentSelected", line.data)
end
derma.DefineControl("FProfileBottleNecks", "", BOTTLENECKTAB, "DListView")
--[[-------------------------------------------------------------------------
The Top n lag spikes tab's contents
---------------------------------------------------------------------------]]
local TOPTENTAB = {}
TOPTENTAB.SortByColumn = SortByColumn
function TOPTENTAB:Init()
self:SetMultiSelect(false)
self:AddColumn("Name")
self:AddColumn("Path")
self:AddColumn("Lines")
self:AddColumn("Runtime in ms")
FProfiler.UI.onCurrentRealmUpdate("topLagSpikes", function(new)
self:Clear()
for _, row in ipairs(new) do
if not row.func then break end
local name = row.info.name and row.info.name ~= "" and (row.info.namewhat .. " " .. row.info.name) or "Unknown"
local path = row.info.short_src
local lines = path ~= "[C]" and row.info.linedefined .. " - " .. row.info.lastlinedefined or "N/A"
local runtime = row.runtime * 100
local line = self:AddLine(name, path, lines, runtime)
line.data = row
end
end)
FProfiler.UI.onCurrentRealmUpdate("currentSelected", function(new, old)
if new == old then return end
for _, line in pairs(self.Lines) do
line:SetSelected(line.data.func == new.func)
end
end)
end
function TOPTENTAB:OnRowSelected(id, line)
FProfiler.UI.updateCurrentRealm("currentSelected", line.data)
end
derma.DefineControl("FProfileTopTen", "", TOPTENTAB, "DListView")
--[[-------------------------------------------------------------------------
The Tab panel of the bottlenecks and top n lag spikes
---------------------------------------------------------------------------]]
local RESULTSHEET = {}
function RESULTSHEET:Init()
self:DockMargin(0, 10, 0, 0)
self:SetPadding(0)
self.bottlenecksTab = vgui.Create("FProfileBottleNecks")
self:AddSheet("Bottlenecks", self.bottlenecksTab)
self.toptenTab = vgui.Create("FProfileTopTen")
self:AddSheet("Top 50 most expensive function calls", self.toptenTab)
end
derma.DefineControl("FProfileResultSheet", "", RESULTSHEET, "DPropertySheet")
--[[-------------------------------------------------------------------------
The function details panel
---------------------------------------------------------------------------]]
local FUNCDETAILS = {}
function FUNCDETAILS:Init()
self.titleLabel = vgui.Create("DLabel", self)
self.titleLabel:SetDark(true)
self.titleLabel:SetFont("DermaLarge")
self.titleLabel:SetText("Function Details")
self.titleLabel:SizeToContents()
-- self.titleLabel:Dock(TOP)
self.focus = vgui.Create("DButton", self)
self.focus:SetText("Focus")
self.focus:SetTall(50)
self.focus:SetFont("DermaDefaultBold")
self.focus:Dock(BOTTOM)
function self.focus:DoClick()
local sel = FProfiler.UI.getCurrentRealmValue("currentSelected")
if not sel then return end
FProfiler.UI.updateCurrentRealm("focusStr", sel.func)
end
self.source = vgui.Create("DTextEntry", self)
self.source:SetKeyboardInputEnabled(false)
self.source:DockMargin(0, 40, 0, 0)
self.source:SetMultiline(true)
self.source:Dock(FILL)
FProfiler.UI.onCurrentRealmUpdate("sourceText", function(new)
self.source:SetText(string.Replace(new, "\t", " "))
end)
self.toConsole = vgui.Create("DButton", self)
self.toConsole:SetText("Print Details to Console")
self.toConsole:SetTall(50)
self.toConsole:SetFont("DermaDefaultBold")
self.toConsole:Dock(BOTTOM)
function self.toConsole:DoClick()
FProfiler.UI.updateCurrentRealm("toConsole", FProfiler.UI.getCurrentRealmValue("currentSelected"))
end
end
function FUNCDETAILS:PerformLayout()
self.titleLabel:CenterHorizontal()
end
derma.DefineControl("FProfileFuncDetails", "", FUNCDETAILS, "DPanel")
--[[-------------------------------------------------------------------------
The actual frame
---------------------------------------------------------------------------]]
local FRAME = {}
local frameInstance
function FRAME:Init()
self:SetTitle("FProfiler profiling tool")
self:SetSize(ScrW() * 0.8, ScrH() * 0.8)
self:Center()
self:SetVisible(true)
self:MakePopup()
self:SetDeleteOnClose(false)
self.magicbar = vgui.Create("FProfileMagicBar", self)
self.magicbar:SetTall(math.max(self:GetTall() * 0.07, 48))
self.magicbar:Dock(TOP)
self.resultsheet = vgui.Create("FProfileResultSheet", self)
self.resultsheet:SetWide(self:GetWide() * 0.8)
self.resultsheet:Dock(LEFT)
self.details = vgui.Create("FProfileFuncDetails", self)
self.details:SetWide(self:GetWide() * 0.2 - 12)
self.details:DockMargin(5, 31, 0, 0)
self.details:Dock(RIGHT)
end
function FRAME:OnClose()
FProfiler.UI.updateModel("frameVisible", false)
end
derma.DefineControl("FProfileFrame", "", FRAME, "DFrame")
--[[-------------------------------------------------------------------------
The command to start the profiler
---------------------------------------------------------------------------]]
concommand.Add("FProfiler",
function()
frameInstance = frameInstance or vgui.Create("FProfileFrame")
frameInstance:SetVisible(true)
FProfiler.UI.updateModel("frameVisible", true)
end,
nil, "Starts FProfiler")

188
lua/fprofiler/ui/model.lua Normal file
View File

@@ -0,0 +1,188 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[-------------------------------------------------------------------------
The model describes the data that the drives the UI
Loosely based on the Elm architecture
---------------------------------------------------------------------------]]
local model =
{
realm = "client", -- "client" or "server"
serverAccess = false, -- Whether the player has access to profile the server
frameVisible = false, -- Whether the frame is visible
client = {
status = "Stopped", -- Started or Stopped
shouldReset = true, -- Whether profiling should start anew
recordTime = 0, -- Total time spent on the last full profiling session
sessionStart = nil, -- When the last profiling session was started
sessionStartSysTime = nil, -- When the last profiling session was started, measured in SysTime
bottlenecks = {}, -- The list of bottleneck functions
topLagSpikes = {}, -- Top of lagging functions
currentSelected = nil, -- Currently selected function
focusObj = nil, -- The current function being focussed upon in profiling
focusStr = "", -- The current function name being entered
toConsole = nil, -- Any functions that should be printed to console
sourceText = "", -- The text of the source function (if available)
},
server = {
status = "Stopped", -- Started or Stopped
shouldReset = true, -- Whether profiling should start anew
bottlenecks = {}, -- The list of bottleneck functions
recordTime = 0, -- Total time spent on the last full profiling session
sessionStart = nil, -- When the last profiling session was started
topLagSpikes = {}, -- Top of lagging functions
currentSelected = nil, -- Currently selected function
focusObj = nil, -- The current function being focussed upon in profiling
focusStr = "", -- The current function name
toConsole = nil, -- Any functions that should be printed to console
sourceText = "", -- The text of the source function (if available)
fromServer = false, -- Whether a change of the model came from the server.
},
}
local updaters = {}
--[[-------------------------------------------------------------------------
Update the model.
Automatically calls the registered update hook functions
e.g. updating the realm would be:
FProfiler.UI.updateModel("realm", "server")
---------------------------------------------------------------------------]]
function FProfiler.UI.updateModel(path, value)
path = istable(path) and path or {path}
local updTbl = updaters
local mdlTbl = model
local key = path[#path]
for i = 1, #path - 1 do
mdlTbl = mdlTbl[path[i]]
updTbl = updTbl and updTbl[path[i]]
end
local oldValue = mdlTbl[key]
mdlTbl[key] = value
for _, updFunc in ipairs(updTbl and updTbl[key] or {}) do
updFunc(value, oldValue)
end
end
--[[-------------------------------------------------------------------------
Update the model of the current realm
---------------------------------------------------------------------------]]
function FProfiler.UI.updateCurrentRealm(path, value)
path = istable(path) and path or {path}
table.insert(path, 1, model.realm)
FProfiler.UI.updateModel(path, value)
end
--[[-------------------------------------------------------------------------
Retrieve a value of the model
---------------------------------------------------------------------------]]
function FProfiler.UI.getModelValue(path)
path = istable(path) and path or {path}
local mdlTbl = model
local key = path[#path]
for i = 1, #path - 1 do
mdlTbl = mdlTbl[path[i]]
end
return mdlTbl[key]
end
--[[-------------------------------------------------------------------------
Retrieve a value of the model regardless of realm
---------------------------------------------------------------------------]]
function FProfiler.UI.getCurrentRealmValue(path)
path = istable(path) and path or {path}
table.insert(path, 1, model.realm)
return FProfiler.UI.getModelValue(path)
end
--[[-------------------------------------------------------------------------
Registers a hook that gets triggered when a certain part of the model is updated
e.g. FProfiler.UI.onModelUpdate("realm", print) prints when the realm is changed
---------------------------------------------------------------------------]]
function FProfiler.UI.onModelUpdate(path, func)
path = istable(path) and path or {path}
local updTbl = updaters
local mdlTbl = model
local key = path[#path]
for i = 1, #path - 1 do
mdlTbl = mdlTbl[path[i]]
updTbl[path[i]] = updTbl[path[i]] or {}
updTbl = updTbl[path[i]]
end
updTbl[key] = updTbl[key] or {}
table.insert(updTbl[key], func)
-- Call update with the initial value
if mdlTbl[key] ~= nil then
func(mdlTbl[key], mdlTbl[key])
end
end
--[[-------------------------------------------------------------------------
Registers a hook to both realms
---------------------------------------------------------------------------]]
function FProfiler.UI.onCurrentRealmUpdate(path, func)
path = istable(path) and path or {path}
table.insert(path, 1, "client")
FProfiler.UI.onModelUpdate(path, function(...)
if FProfiler.UI.getModelValue("realm") == "server" then return end
func(...)
end)
path[1] = "server"
FProfiler.UI.onModelUpdate(path, function(...)
if FProfiler.UI.getModelValue("realm") == "client" then return end
func(...)
end)
end
--[[-------------------------------------------------------------------------
When the realm is changed, all update functions of the new realm are to be called
---------------------------------------------------------------------------]]
FProfiler.UI.onModelUpdate("realm", function(new, old)
if not updaters[new] then return end
for k, funcTbl in pairs(updaters[new]) do
for _, func in ipairs(funcTbl) do
func(model[new][k], model[new][k])
end
end
end)

305
lua/fprofiler/ui/server.lua Normal file
View File

@@ -0,0 +1,305 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[-------------------------------------------------------------------------
The server is involved in the ui in the sense that it interacts with its model
---------------------------------------------------------------------------]]
-- Net messages
util.AddNetworkString("FProfile_startProfiling")
util.AddNetworkString("FProfile_stopProfiling")
util.AddNetworkString("FProfile_focusObj")
util.AddNetworkString("FProfile_getSource")
util.AddNetworkString("FProfile_printFunction")
util.AddNetworkString("FProfile_fullModelUpdate")
util.AddNetworkString("FProfile_focusUpdate")
util.AddNetworkString("FProfile_unsubscribe")
--[[-------------------------------------------------------------------------
Simplified version of the model
Contains only what the server needs to know
---------------------------------------------------------------------------]]
local model =
{
focusObj = nil, -- the function currently in focus
sessionStart = nil, -- When the last profiling session was started. Used for the live timer.
sessionStartSysTime = nil, -- Same as sessionStart, but measured in SysTime. Used to update recordTime
recordTime = 0, -- Total time spent on the last full profiling session
bottlenecks = {}, -- The list of bottleneck functions
topLagSpikes = {}, -- Top of lagging functions
subscribers = RecipientFilter(), -- The players that get updates of the profiler model
}
--[[-------------------------------------------------------------------------
Helper function: receive a net message
---------------------------------------------------------------------------]]
local function receive(msg, f)
net.Receive(msg, function(len, ply)
-- Check access.
CAMI.PlayerHasAccess(ply, "FProfiler", function(b, _)
if not b then return end
f(len, ply)
end)
end)
end
--[[-------------------------------------------------------------------------
Helper function:
Write generic row data to a net message
---------------------------------------------------------------------------]]
local function writeRowData(row)
net.WriteString(tostring(row.func))
net.WriteString(row.info.short_src)
net.WriteUInt(row.info.linedefined, 16)
net.WriteUInt(row.info.lastlinedefined, 16)
end
--[[-------------------------------------------------------------------------
Helper function:
Send the bottlenecks to the client
Only sends the things displayed
---------------------------------------------------------------------------]]
local function writeBottleNecks()
net.WriteUInt(#model.bottlenecks, 16)
for i, row in ipairs(model.bottlenecks) do
writeRowData(row)
net.WriteUInt(#row.names, 8)
for j, name in ipairs(row.names) do
net.WriteString(name.name)
net.WriteString(name.namewhat)
end
net.WriteUInt(row.total_called, 32)
net.WriteDouble(row.total_time)
net.WriteDouble(row.average_time)
end
end
--[[-------------------------------------------------------------------------
Helper function:
Sends the top n functions
---------------------------------------------------------------------------]]
local function writeTopN()
local count = #model.topLagSpikes
-- All top N f
for i = count, 0, -1 do
if model.topLagSpikes and model.topLagSpikes[i] and model.topLagSpikes[i].info then break end -- Entry exists
count = i
end
net.WriteUInt(count, 8)
for i = 1, count do
local row = model.topLagSpikes[i]
if not row.info then break end
writeRowData(row)
net.WriteString(row.info.name or "")
net.WriteString(row.info.namewhat or "")
net.WriteDouble(row.runtime)
end
end
-- Start profiling
local function startProfiling()
model.sessionStart = CurTime()
model.sessionStartSysTime = SysTime()
FProfiler.Internal.start(model.focusObj)
net.Start("FProfile_startProfiling")
net.WriteDouble(model.recordTime)
net.WriteDouble(model.sessionStart)
net.Send(model.subscribers:GetPlayers())
end
-- Stop profiling
local function stopProfiling()
FProfiler.Internal.stop()
model.recordTime = model.recordTime + SysTime() - (model.sessionStartSysTime or 0)
model.sessionStart = nil
model.sessionStartSysTime = nil
model.bottlenecks = FProfiler.Internal.getAggregatedResults(100)
model.topLagSpikes = FProfiler.Internal.getMostExpensiveSingleCalls()
net.Start("FProfile_stopProfiling")
net.WriteDouble(model.recordTime)
writeBottleNecks()
writeTopN()
net.Send(model.subscribers:GetPlayers())
end
--[[-------------------------------------------------------------------------
Receive an update of the function to focus on
---------------------------------------------------------------------------]]
receive("FProfile_focusObj", function(_, ply)
local funcStr = net.ReadString()
model.focusObj = FProfiler.funcNameToObj(funcStr)
net.Start("FProfile_focusObj")
net.WriteBool(model.focusObj and true or false)
net.Send(ply)
-- Send a focus update to all other players
net.Start("FProfile_focusUpdate")
net.WriteString(funcStr)
net.WriteBool(model.focusObj and true or false)
model.subscribers:RemovePlayer(ply)
net.Send(model.subscribers:GetPlayers())
model.subscribers:AddPlayer(ply)
end)
--[[-------------------------------------------------------------------------
Receive a "start profiling" signal
---------------------------------------------------------------------------]]
receive("FProfile_startProfiling", function(_, ply)
local shouldReset = net.ReadBool()
if shouldReset then
FProfiler.Internal.reset()
model.recordTime = 0
end
startProfiling()
end)
--[[-------------------------------------------------------------------------
Receive a stop profiling signal
---------------------------------------------------------------------------]]
receive("FProfile_stopProfiling", function(_, ply)
stopProfiling()
end)
--[[-------------------------------------------------------------------------
Send the source of a function to a client
---------------------------------------------------------------------------]]
receive("FProfile_getSource", function(_, ply)
local func = FProfiler.funcNameToObj(net.ReadString())
if not func then return end
local info = debug.getinfo(func)
if not info then return end
net.Start("FProfile_getSource")
net.WriteString(FProfiler.readSource(info.short_src, info.linedefined, info.lastlinedefined) or "")
net.Send(ply)
end)
--[[-------------------------------------------------------------------------
Print the details of a function
---------------------------------------------------------------------------]]
receive("FProfile_printFunction", function(_, ply)
local source = net.ReadBool() -- true is from bottlenecks, false is from Top-N
local dataSource = source and model.bottlenecks or model.topLagSpikes
local func = net.ReadString()
local data
for _, row in ipairs(dataSource or {}) do
if tostring(row.func) == func then data = row break end
end
if not data then return end
-- Show the data
show(data)
local plaintext = showStr(data)
-- Write to file if necessary
file.CreateDir("fprofiler")
file.Write("fprofiler/profiledata.txt", plaintext)
MsgC(Color(200, 200, 200), "-----", Color(120, 120, 255), "NOTE", Color(200, 200, 200), "---------------\n")
MsgC(Color(200, 200, 200), "If the above function does not fit in console, you can find it in data/fprofiler/profiledata.txt\n\n")
-- Listen server hosts already see the server console
if ply:IsListenServerHost() then return end
-- Send a plaintext version to the client
local binary = util.Compress(plaintext)
net.Start("FProfile_printFunction")
net.WriteData(binary, #binary)
net.Send(ply)
end)
--[[-------------------------------------------------------------------------
Request of a full model update
Particularly useful when someone else has done (or is performing) a profiling session
and the current player wants to see the results
---------------------------------------------------------------------------]]
receive("FProfile_fullModelUpdate", function(_, ply)
-- This player is now subscribed to the updates
model.subscribers:AddPlayer(ply)
net.Start("FProfile_fullModelUpdate")
net.WriteBool(model.focusObj ~= nil)
if model.focusObj ~= nil then net.WriteString(tostring(model.focusObj)) end
-- Bool also indicates whether it's currently profiling
net.WriteBool(model.sessionStart ~= nil)
if model.sessionStart ~= nil then net.WriteDouble(model.sessionStart) end
net.WriteDouble(model.recordTime)
writeBottleNecks()
writeTopN()
net.Send(ply)
end)
--[[-------------------------------------------------------------------------
Unsubscribe from the updates of the profiler
---------------------------------------------------------------------------]]
receive("FProfile_unsubscribe", function(_, ply)
model.subscribers:RemovePlayer(ply)
end)
--[[-------------------------------------------------------------------------
API function: start profiling
---------------------------------------------------------------------------]]
function FProfiler.start(focus)
FProfiler.Internal.reset()
model.recordTime = 0
model.focusObj = focus
startProfiling()
end
--[[-------------------------------------------------------------------------
API function: stop profiling
---------------------------------------------------------------------------]]
function FProfiler.stop()
stopProfiling()
end
--[[-------------------------------------------------------------------------
API function: continue profiling
---------------------------------------------------------------------------]]
function FProfiler.continueProfiling()
startProfiling()
end

View File

@@ -0,0 +1,238 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
local get, update, onUpdate = FProfiler.UI.getModelValue, FProfiler.UI.updateModel, FProfiler.UI.onModelUpdate
--[[-------------------------------------------------------------------------
Update the current selected focus object when data is entered
---------------------------------------------------------------------------]]
onUpdate({"server", "focusStr"}, function(new)
if not new or get({"server", "fromServer"}) then return end
net.Start("FProfile_focusObj")
net.WriteString(new)
net.SendToServer()
end)
net.Receive("FProfile_focusObj", function()
update({"server", "focusObj"}, net.ReadBool() and get({"server", "focusStr"}) or nil)
end)
-- A focus update occurs when someone else changes the focus
net.Receive("FProfile_focusUpdate", function()
update({"server", "fromServer"}, true)
local focusStr = net.ReadString()
update({"server", "focusStr"}, focusStr)
update({"server", "focusObj"}, net.ReadBool() and focusStr or nil)
update({"server", "fromServer"}, false)
end)
--[[-------------------------------------------------------------------------
(Re)start profiling
---------------------------------------------------------------------------]]
local function restartProfiling()
local shouldReset = get({"server", "shouldReset"})
net.Start("FProfile_startProfiling")
net.WriteBool(shouldReset)
net.SendToServer()
end
net.Receive("FProfile_startProfiling", function()
update({"server", "fromServer"}, true)
update({"server", "status"}, "Started")
update({"server", "recordTime"}, net.ReadDouble())
update({"server", "sessionStart"}, net.ReadDouble())
update({"server", "fromServer"}, false)
end)
--[[-------------------------------------------------------------------------
Stop profiling
---------------------------------------------------------------------------]]
local function stopProfiling()
net.Start("FProfile_stopProfiling")
net.SendToServer()
end
-- Read a row from a net message
local function readDataRow(countSize, readSpecific)
local res = {}
local count = net.ReadUInt(countSize)
for i = 1, count do
local row = {}
row.info = {}
row.func = net.ReadString()
row.info.short_src = net.ReadString()
row.info.linedefined = net.ReadUInt(16)
row.info.lastlinedefined = net.ReadUInt(16)
readSpecific(row)
table.insert(res, row)
end
return res
end
-- Read a bottleneck row
local function readBottleneckRow(row)
local nameCount = net.ReadUInt(8)
row.names = {}
for i = 1, nameCount do
table.insert(row.names, {
name = net.ReadString(),
namewhat = net.ReadString()
})
end
row.total_called = net.ReadUInt(32)
row.total_time = net.ReadDouble()
row.average_time = net.ReadDouble()
end
-- Read the top n row
local function readTopNRow(row)
row.info.name = net.ReadString()
row.info.namewhat = net.ReadString()
row.runtime = net.ReadDouble()
end
net.Receive("FProfile_stopProfiling", function()
update({"server", "fromServer"}, true)
update({"server", "status"}, "Stopped")
update({"server", "sessionStart"}, nil)
update({"server", "recordTime"}, net.ReadDouble())
update({"server", "bottlenecks"}, readDataRow(16, readBottleneckRow))
update({"server", "topLagSpikes"}, readDataRow(8, readTopNRow))
update({"server", "fromServer"}, false)
end)
--[[-------------------------------------------------------------------------
Start/stop recording when the recording status is changed
---------------------------------------------------------------------------]]
onUpdate({"server", "status"}, function(new, old)
if new == old or get({"server", "fromServer"}) then return end
(new == "Started" and restartProfiling or stopProfiling)()
end)
--[[-------------------------------------------------------------------------
Update info when a different line is selected
---------------------------------------------------------------------------]]
onUpdate({"server", "currentSelected"}, function(new)
if not new or not new.info or not new.info.linedefined or not new.info.lastlinedefined or not new.info.short_src then return end
net.Start("FProfile_getSource")
net.WriteString(tostring(new.func))
net.SendToServer()
end)
net.Receive("FProfile_getSource", function()
update({"server", "sourceText"}, net.ReadString())
end)
--[[-------------------------------------------------------------------------
When a function is to be printed to console
---------------------------------------------------------------------------]]
onUpdate({"server", "toConsole"}, function(data)
if not data then return end
update({"server", "toConsole"}, nil)
net.Start("FProfile_printFunction")
net.WriteBool(data.total_called and true or false) -- true for bottleneck function, false for top-n function
net.WriteString(tostring(data.func))
net.SendToServer()
end)
net.Receive("FProfile_printFunction", function(len)
local data = net.ReadData(len)
local decompressed = util.Decompress(data)
-- Print the text line by line, otherwise big parts of big data will not be printed
local split = string.Explode("\n", decompressed, false)
for _, line in ipairs(split) do
MsgN(line)
end
-- Write the thing to a file
file.CreateDir("fprofiler")
file.Write("fprofiler/profiledata.txt", showStr(data))
MsgC(Color(200, 200, 200), "-----", Color(120, 120, 255), "NOTE", Color(200, 200, 200), "---------------\n")
MsgC(Color(200, 200, 200), "In the server's console you can find a colour coded version of the above output.\nIf the above function does not fit in console, you can find it in data/fprofiler/profiledata.txt\n\n")
end)
--[[-------------------------------------------------------------------------
Check access when the frame opens
Also request a full serverside model update
---------------------------------------------------------------------------]]
onUpdate("frameVisible", function(isOpen)
-- Don't network if the server doesn't have FProfiler installed
if util.NetworkStringToID("FProfile_fullModelUpdate") == 0 then
update("serverAccess", false)
return
end
-- Update access
CAMI.PlayerHasAccess(LocalPlayer(), "FProfiler", function(b, _)
update("serverAccess", b)
end)
if not isOpen then
net.Start("FProfile_unsubscribe")
net.SendToServer()
return
end
net.Start("FProfile_fullModelUpdate")
net.SendToServer()
end)
net.Receive("FProfile_fullModelUpdate", function()
update({"server", "fromServer"}, true)
local focusExists = net.ReadBool()
if focusExists then
local focus = net.ReadString()
update({"server", "focusObj"}, focus)
update({"server", "focusStr"}, focus)
end
local startingTimeExists = net.ReadBool()
if startingTimeExists then
update({"server", "status"}, "Started")
update({"server", "sessionStart"}, net.ReadDouble())
else
update({"server", "status"}, "Stopped")
end
update({"server", "recordTime"}, net.ReadDouble())
update({"server", "bottlenecks"}, readDataRow(16, readBottleneckRow))
update({"server", "topLagSpikes"}, readDataRow(8, readTopNRow))
update({"server", "fromServer"}, false)
end)