mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-17 21:53:46 +03:00
Upload
This commit is contained in:
110
lua/fprofiler/ui/clientcontrol.lua
Normal file
110
lua/fprofiler/ui/clientcontrol.lua
Normal 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
475
lua/fprofiler/ui/frame.lua
Normal 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
188
lua/fprofiler/ui/model.lua
Normal 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
305
lua/fprofiler/ui/server.lua
Normal 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
|
||||
238
lua/fprofiler/ui/servercontrol.lua
Normal file
238
lua/fprofiler/ui/servercontrol.lua
Normal 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)
|
||||
|
||||
Reference in New Issue
Block a user