mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-16 21:33:46 +03:00
Upload
This commit is contained in:
48
addons/fprofiler/lua/autorun/fprofiler.lua
Normal file
48
addons/fprofiler/lua/autorun/fprofiler.lua
Normal file
@@ -0,0 +1,48 @@
|
||||
--[[
|
||||
| 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/
|
||||
--]]
|
||||
|
||||
FProfiler = FProfiler or {}
|
||||
FProfiler.Internal = {}
|
||||
FProfiler.UI = {}
|
||||
|
||||
AddCSLuaFile()
|
||||
AddCSLuaFile("fprofiler/cami.lua")
|
||||
AddCSLuaFile("fprofiler/gather.lua")
|
||||
AddCSLuaFile("fprofiler/report.lua")
|
||||
AddCSLuaFile("fprofiler/util.lua")
|
||||
AddCSLuaFile("fprofiler/prettyprint.lua")
|
||||
|
||||
AddCSLuaFile("fprofiler/ui/model.lua")
|
||||
AddCSLuaFile("fprofiler/ui/frame.lua")
|
||||
AddCSLuaFile("fprofiler/ui/clientcontrol.lua")
|
||||
AddCSLuaFile("fprofiler/ui/servercontrol.lua")
|
||||
|
||||
include("fprofiler/cami.lua")
|
||||
|
||||
CAMI.RegisterPrivilege{
|
||||
Name = "FProfiler",
|
||||
MinAccess = "superadmin"
|
||||
}
|
||||
|
||||
|
||||
include("fprofiler/prettyprint.lua")
|
||||
include("fprofiler/util.lua")
|
||||
include("fprofiler/gather.lua")
|
||||
include("fprofiler/report.lua")
|
||||
|
||||
|
||||
if CLIENT then
|
||||
include("fprofiler/ui/model.lua")
|
||||
include("fprofiler/ui/frame.lua")
|
||||
include("fprofiler/ui/clientcontrol.lua")
|
||||
include("fprofiler/ui/servercontrol.lua")
|
||||
else
|
||||
include("fprofiler/ui/server.lua")
|
||||
end
|
||||
534
addons/fprofiler/lua/fprofiler/cami.lua
Normal file
534
addons/fprofiler/lua/fprofiler/cami.lua
Normal file
@@ -0,0 +1,534 @@
|
||||
--[[
|
||||
| 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/
|
||||
--]]
|
||||
|
||||
--[[
|
||||
CAMI - Common Admin Mod Interface.
|
||||
Makes admin mods intercompatible and provides an abstract privilege interface
|
||||
for third party addons.
|
||||
|
||||
IMPORTANT: This is a draft script. It is very much WIP.
|
||||
|
||||
Follows the specification on this page:
|
||||
https://docs.google.com/document/d/1QIRVcAgZfAYf1aBl_dNV_ewR6P25wze2KmUVzlbFgMI
|
||||
|
||||
|
||||
Structures:
|
||||
CAMI_USERGROUP, defines the charactaristics of a usergroup:
|
||||
{
|
||||
Name
|
||||
string
|
||||
The name of the usergroup
|
||||
Inherits
|
||||
string
|
||||
The name of the usergroup this usergroup inherits from
|
||||
}
|
||||
|
||||
CAMI_PRIVILEGE, defines the charactaristics of a privilege:
|
||||
{
|
||||
Name
|
||||
string
|
||||
The name of the privilege
|
||||
MinAccess
|
||||
string
|
||||
One of the following three: user/admin/superadmin
|
||||
HasAccess
|
||||
function(
|
||||
privilege :: CAMI_PRIVILEGE,
|
||||
actor :: Player,
|
||||
target :: Player
|
||||
) :: bool
|
||||
optional
|
||||
Function that decides whether a player can execute this privilege,
|
||||
optionally on another player (target).
|
||||
}
|
||||
]]
|
||||
|
||||
-- Version number in YearMonthDay format.
|
||||
local version = 20150902.1
|
||||
|
||||
if CAMI and CAMI.Version >= version then return end
|
||||
|
||||
CAMI = CAMI or {}
|
||||
CAMI.Version = version
|
||||
|
||||
--[[
|
||||
usergroups
|
||||
Contains the registered CAMI_USERGROUP usergroup structures.
|
||||
Indexed by usergroup name.
|
||||
]]
|
||||
local usergroups = CAMI.GetUsergroups and CAMI.GetUsergroups() or {
|
||||
user = {
|
||||
Name = "user",
|
||||
Inherits = "user"
|
||||
},
|
||||
admin = {
|
||||
Name = "admin",
|
||||
Inherits = "user"
|
||||
},
|
||||
superadmin = {
|
||||
Name = "superadmin",
|
||||
Inherits = "admin"
|
||||
}
|
||||
}
|
||||
|
||||
--[[
|
||||
privileges
|
||||
Contains the registered CAMI_PRIVILEGE privilege structures.
|
||||
Indexed by privilege name.
|
||||
]]
|
||||
local privileges = CAMI.GetPrivileges and CAMI.GetPrivileges() or {}
|
||||
|
||||
--[[
|
||||
CAMI.RegisterUsergroup
|
||||
Registers a usergroup with CAMI.
|
||||
|
||||
Parameters:
|
||||
usergroup
|
||||
CAMI_USERGROUP
|
||||
(see CAMI_USERGROUP structure)
|
||||
source
|
||||
any
|
||||
Identifier for your own admin mod. Can be anything.
|
||||
Use this to make sure CAMI.RegisterUsergroup function and the
|
||||
CAMI.OnUsergroupRegistered hook don't cause an infinite loop
|
||||
|
||||
|
||||
|
||||
Return value:
|
||||
CAMI_USERGROUP
|
||||
The usergroup given as argument.
|
||||
]]
|
||||
function CAMI.RegisterUsergroup(usergroup, source)
|
||||
usergroups[usergroup.Name] = usergroup
|
||||
|
||||
hook.Call("CAMI.OnUsergroupRegistered", nil, usergroup, source)
|
||||
return usergroup
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.UnregisterUsergroup
|
||||
Unregisters a usergroup from CAMI. This will call a hook that will notify
|
||||
all other admin mods of the removal.
|
||||
|
||||
Call only when the usergroup is to be permanently removed.
|
||||
|
||||
Parameters:
|
||||
usergroupName
|
||||
string
|
||||
The name of the usergroup.
|
||||
source
|
||||
any
|
||||
Identifier for your own admin mod. Can be anything.
|
||||
Use this to make sure CAMI.UnregisterUsergroup function and the
|
||||
CAMI.OnUsergroupUnregistered hook don't cause an infinite loop
|
||||
|
||||
Return value:
|
||||
bool
|
||||
Whether the unregistering succeeded.
|
||||
]]
|
||||
function CAMI.UnregisterUsergroup(usergroupName, source)
|
||||
if not usergroups[usergroupName] then return false end
|
||||
|
||||
local usergroup = usergroups[usergroupName]
|
||||
usergroups[usergroupName] = nil
|
||||
|
||||
hook.Call("CAMI.OnUsergroupUnregistered", nil, usergroup, source)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.GetUsergroups
|
||||
Retrieves all registered usergroups.
|
||||
|
||||
Return value:
|
||||
Table of CAMI_USERGROUP, indexed by their names.
|
||||
]]
|
||||
function CAMI.GetUsergroups()
|
||||
return usergroups
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.GetUsergroup
|
||||
Receives information about a usergroup.
|
||||
|
||||
Return value:
|
||||
CAMI_USERGROUP
|
||||
Returns nil when the usergroup does not exist.
|
||||
]]
|
||||
function CAMI.GetUsergroup(usergroupName)
|
||||
return usergroups[usergroupName]
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.UsergroupInherits
|
||||
Returns true when usergroupName1 inherits usergroupName2.
|
||||
Note that usergroupName1 does not need to be a direct child.
|
||||
Every usergroup trivially inherits itself.
|
||||
|
||||
Parameters:
|
||||
usergroupName1
|
||||
string
|
||||
The name of the usergroup that is queried.
|
||||
usergroupName2
|
||||
string
|
||||
The name of the usergroup of which is queried whether usergroupName1
|
||||
inherits from.
|
||||
|
||||
Return value:
|
||||
bool
|
||||
Whether usergroupName1 inherits usergroupName2.
|
||||
]]
|
||||
function CAMI.UsergroupInherits(usergroupName1, usergroupName2)
|
||||
repeat
|
||||
if usergroupName1 == usergroupName2 then return true end
|
||||
|
||||
usergroupName1 = usergroups[usergroupName1] and
|
||||
usergroups[usergroupName1].Inherits or
|
||||
usergroupName1
|
||||
until not usergroups[usergroupName1] or
|
||||
usergroups[usergroupName1].Inherits == usergroupName1
|
||||
|
||||
-- One can only be sure the usergroup inherits from user if the
|
||||
-- usergroup isn't registered.
|
||||
return usergroupName1 == usergroupName2 or usergroupName2 == "user"
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.InheritanceRoot
|
||||
All usergroups must eventually inherit either user, admin or superadmin.
|
||||
Regardless of what inheritance mechism an admin may or may not have, this
|
||||
always applies.
|
||||
|
||||
This method always returns either user, admin or superadmin, based on what
|
||||
usergroups eventually inherit.
|
||||
|
||||
Parameters:
|
||||
usergroupName
|
||||
string
|
||||
The name of the usergroup of which the root of inheritance is
|
||||
requested
|
||||
|
||||
Return value:
|
||||
string
|
||||
The name of the root usergroup (either user, admin or superadmin)
|
||||
]]
|
||||
function CAMI.InheritanceRoot(usergroupName)
|
||||
if not usergroups[usergroupName] then return end
|
||||
|
||||
local inherits = usergroups[usergroupName].Inherits
|
||||
while inherits ~= usergroups[usergroupName].Inherits do
|
||||
usergroupName = usergroups[usergroupName].Inherits
|
||||
end
|
||||
|
||||
return usergroupName
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.RegisterPrivilege
|
||||
Registers a privilege with CAMI.
|
||||
Note: do NOT register all your admin mod's privileges with this function!
|
||||
This function is for third party addons to register privileges
|
||||
with admin mods, not for admin mods sharing the privileges amongst one
|
||||
another.
|
||||
|
||||
Parameters:
|
||||
privilege
|
||||
CAMI_PRIVILEGE
|
||||
See CAMI_PRIVILEGE structure.
|
||||
|
||||
Return value:
|
||||
CAMI_PRIVILEGE
|
||||
The privilege given as argument.
|
||||
]]
|
||||
function CAMI.RegisterPrivilege(privilege)
|
||||
privileges[privilege.Name] = privilege
|
||||
|
||||
hook.Call("CAMI.OnPrivilegeRegistered", nil, privilege)
|
||||
|
||||
return privilege
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.UnregisterPrivilege
|
||||
Unregisters a privilege from CAMI. This will call a hook that will notify
|
||||
all other admin mods of the removal.
|
||||
|
||||
Call only when the privilege is to be permanently removed.
|
||||
|
||||
Parameters:
|
||||
privilegeName
|
||||
string
|
||||
The name of the privilege.
|
||||
|
||||
Return value:
|
||||
bool
|
||||
Whether the unregistering succeeded.
|
||||
]]
|
||||
function CAMI.UnregisterPrivilege(privilegeName)
|
||||
if not privileges[privilegeName] then return false end
|
||||
|
||||
local privilege = privileges[privilegeName]
|
||||
privileges[privilegeName] = nil
|
||||
|
||||
hook.Call("CAMI.OnPrivilegeUnregistered", nil, privilege)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.GetPrivileges
|
||||
Retrieves all registered privileges.
|
||||
|
||||
Return value:
|
||||
Table of CAMI_PRIVILEGE, indexed by their names.
|
||||
]]
|
||||
function CAMI.GetPrivileges()
|
||||
return privileges
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.GetPrivilege
|
||||
Receives information about a privilege.
|
||||
|
||||
Return value:
|
||||
CAMI_PRIVILEGE when the privilege exists.
|
||||
nil when the privilege does not exist.
|
||||
]]
|
||||
function CAMI.GetPrivilege(privilegeName)
|
||||
return privileges[privilegeName]
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.PlayerHasAccess
|
||||
Queries whether a certain player has the right to perform a certain action.
|
||||
Note: this function does NOT return an immediate result!
|
||||
The result is in the callback!
|
||||
|
||||
Parameters:
|
||||
actorPly
|
||||
Player
|
||||
The player of which is requested whether they have the privilege.
|
||||
privilegeName
|
||||
string
|
||||
The name of the privilege.
|
||||
callback
|
||||
function(bool, string)
|
||||
This function will be called with the answer. The bool signifies the
|
||||
yes or no answer as to whether the player is allowed. The string
|
||||
will optionally give a reason.
|
||||
targetPly
|
||||
Optional.
|
||||
The player on which the privilege is executed.
|
||||
extraInfoTbl
|
||||
Optional.
|
||||
Table containing extra information.
|
||||
Officially supported members:
|
||||
Fallback
|
||||
string
|
||||
Either of user/admin/superadmin. When no admin mod replies,
|
||||
the decision is based on the admin status of the user.
|
||||
Defaults to admin if not given.
|
||||
IgnoreImmunity
|
||||
bool
|
||||
Ignore any immunity mechanisms an admin mod might have.
|
||||
CommandArguments
|
||||
table
|
||||
Extra arguments that were given to the privilege command.
|
||||
|
||||
Return value:
|
||||
None, the answer is given in the callback function in order to allow
|
||||
for the admin mod to perform e.g. a database lookup.
|
||||
]]
|
||||
-- Default access handler
|
||||
local defaultAccessHandler = {["CAMI.PlayerHasAccess"] =
|
||||
function(_, actorPly, privilegeName, callback, _, extraInfoTbl)
|
||||
-- The server always has access in the fallback
|
||||
if not IsValid(actorPly) then return callback(true, "Fallback.") end
|
||||
|
||||
local priv = privileges[privilegeName]
|
||||
|
||||
local fallback = extraInfoTbl and (
|
||||
not extraInfoTbl.Fallback and actorPly:IsAdmin() or
|
||||
extraInfoTbl.Fallback == "user" and true or
|
||||
extraInfoTbl.Fallback == "admin" and actorPly:IsAdmin() or
|
||||
extraInfoTbl.Fallback == "superadmin" and actorPly:IsSuperAdmin())
|
||||
|
||||
|
||||
if not priv then return callback(fallback, "Fallback.") end
|
||||
|
||||
callback(
|
||||
priv.MinAccess == "user" or
|
||||
priv.MinAccess == "admin" and actorPly:IsAdmin() or
|
||||
priv.MinAccess == "superadmin" and actorPly:IsSuperAdmin()
|
||||
, "Fallback.")
|
||||
end,
|
||||
["CAMI.SteamIDHasAccess"] =
|
||||
function(_, _, _, callback)
|
||||
callback(false, "No information available.")
|
||||
end
|
||||
}
|
||||
function CAMI.PlayerHasAccess(actorPly, privilegeName, callback, targetPly,
|
||||
extraInfoTbl)
|
||||
hook.Call("CAMI.PlayerHasAccess", defaultAccessHandler, actorPly,
|
||||
privilegeName, callback, targetPly, extraInfoTbl)
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.GetPlayersWithAccess
|
||||
Finds the list of currently joined players who have the right to perform a
|
||||
certain action.
|
||||
NOTE: this function will NOT return an immediate result!
|
||||
The result is in the callback!
|
||||
|
||||
Parameters:
|
||||
privilegeName
|
||||
string
|
||||
The name of the privilege.
|
||||
callback
|
||||
function(players)
|
||||
This function will be called with the list of players with access.
|
||||
targetPly
|
||||
Optional.
|
||||
The player on which the privilege is executed.
|
||||
extraInfoTbl
|
||||
Optional.
|
||||
Table containing extra information.
|
||||
Officially supported members:
|
||||
Fallback
|
||||
string
|
||||
Either of user/admin/superadmin. When no admin mod replies,
|
||||
the decision is based on the admin status of the user.
|
||||
Defaults to admin if not given.
|
||||
IgnoreImmunity
|
||||
bool
|
||||
Ignore any immunity mechanisms an admin mod might have.
|
||||
CommandArguments
|
||||
table
|
||||
Extra arguments that were given to the privilege command.
|
||||
]]
|
||||
function CAMI.GetPlayersWithAccess(privilegeName, callback, targetPly,
|
||||
extraInfoTbl)
|
||||
local allowedPlys = {}
|
||||
local allPlys = player.GetAll()
|
||||
local countdown = #allPlys
|
||||
|
||||
local function onResult(ply, hasAccess, _)
|
||||
countdown = countdown - 1
|
||||
|
||||
if hasAccess then table.insert(allowedPlys, ply) end
|
||||
if countdown == 0 then callback(allowedPlys) end
|
||||
end
|
||||
|
||||
for _, ply in pairs(allPlys) do
|
||||
CAMI.PlayerHasAccess(ply, privilegeName,
|
||||
function(...) onResult(ply, ...) end,
|
||||
targetPly, extraInfoTbl)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.SteamIDHasAccess
|
||||
Queries whether a player with a steam ID has the right to perform a certain
|
||||
action.
|
||||
Note: the player does not need to be in the server for this to
|
||||
work.
|
||||
|
||||
Note: this function does NOT return an immediate result!
|
||||
The result is in the callback!
|
||||
|
||||
Parameters:
|
||||
actorSteam
|
||||
Player
|
||||
The SteamID of the player of which is requested whether they have
|
||||
the privilege.
|
||||
privilegeName
|
||||
string
|
||||
The name of the privilege.
|
||||
callback
|
||||
function(bool, string)
|
||||
This function will be called with the answer. The bool signifies the
|
||||
yes or no answer as to whether the player is allowed. The string
|
||||
will optionally give a reason.
|
||||
targetSteam
|
||||
Optional.
|
||||
The SteamID of the player on which the privilege is executed.
|
||||
extraInfoTbl
|
||||
Optional.
|
||||
Table containing extra information.
|
||||
Officially supported members:
|
||||
IgnoreImmunity
|
||||
bool
|
||||
Ignore any immunity mechanisms an admin mod might have.
|
||||
CommandArguments
|
||||
table
|
||||
Extra arguments that were given to the privilege command.
|
||||
|
||||
Return value:
|
||||
None, the answer is given in the callback function in order to allow
|
||||
for the admin mod to perform e.g. a database lookup.
|
||||
]]
|
||||
function CAMI.SteamIDHasAccess(actorSteam, privilegeName, callback,
|
||||
targetSteam, extraInfoTbl)
|
||||
hook.Call("CAMI.SteamIDHasAccess", defaultAccessHandler, actorSteam,
|
||||
privilegeName, callback, targetSteam, extraInfoTbl)
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.SignalUserGroupChanged
|
||||
Signify that your admin mod has changed the usergroup of a player. This
|
||||
function communicates to other admin mods what it thinks the usergroup
|
||||
of a player should be.
|
||||
|
||||
Listen to the hook to receive the usergroup changes of other admin mods.
|
||||
|
||||
Parameters:
|
||||
ply
|
||||
Player
|
||||
The player for which the usergroup is changed
|
||||
old
|
||||
string
|
||||
The previous usergroup of the player.
|
||||
new
|
||||
string
|
||||
The new usergroup of the player.
|
||||
source
|
||||
any
|
||||
Identifier for your own admin mod. Can be anything.
|
||||
]]
|
||||
function CAMI.SignalUserGroupChanged(ply, old, new, source)
|
||||
hook.Call("CAMI.PlayerUsergroupChanged", nil, ply, old, new, source)
|
||||
end
|
||||
|
||||
--[[
|
||||
CAMI.SignalSteamIDUserGroupChanged
|
||||
Signify that your admin mod has changed the usergroup of a disconnected
|
||||
player. This communicates to other admin mods what it thinks the usergroup
|
||||
of a player should be.
|
||||
|
||||
Listen to the hook to receive the usergroup changes of other admin mods.
|
||||
|
||||
Parameters:
|
||||
ply
|
||||
string
|
||||
The steam ID of the player for which the usergroup is changed
|
||||
old
|
||||
string
|
||||
The previous usergroup of the player.
|
||||
new
|
||||
string
|
||||
The new usergroup of the player.
|
||||
source
|
||||
any
|
||||
Identifier for your own admin mod. Can be anything.
|
||||
]]
|
||||
function CAMI.SignalSteamIDUserGroupChanged(steamId, old, new, source)
|
||||
hook.Call("CAMI.SteamIDUsergroupChanged", nil, steamId, old, new, source)
|
||||
end
|
||||
267
addons/fprofiler/lua/fprofiler/gather.lua
Normal file
267
addons/fprofiler/lua/fprofiler/gather.lua
Normal file
@@ -0,0 +1,267 @@
|
||||
--[[
|
||||
| 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 timeMeasurementFunc = SysTime
|
||||
|
||||
-- Helper function
|
||||
-- Get all local variables
|
||||
local NIL = {}
|
||||
setmetatable(NIL, {__tostring = function() return "nil" end})
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
Call counts:
|
||||
|
||||
registers how often function have been called
|
||||
---------------------------------------------------------------------------]]
|
||||
local callcounts = {}
|
||||
|
||||
|
||||
-- Gets the call counts
|
||||
FProfiler.Internal.getCallCounts = function() return callcounts end
|
||||
|
||||
|
||||
-- Resets the call counts
|
||||
function FProfiler.Internal.resetCallCounts()
|
||||
callcounts = {}
|
||||
end
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
Inclusive function times
|
||||
|
||||
Keeps track of how long functions take in total
|
||||
i.e. average time between the start and return of a function * times called
|
||||
|
||||
This includes the time that any function called by this function takes
|
||||
(that's what the "inclusive" refers to).
|
||||
Note: recursive calls are not counted double
|
||||
---------------------------------------------------------------------------]]
|
||||
|
||||
local inclusiveTimes = {}
|
||||
|
||||
-- Gets the inclusive times
|
||||
FProfiler.Internal.getInclusiveTimes = function() return inclusiveTimes end
|
||||
|
||||
-- Resets the inclusive times
|
||||
function FProfiler.Internal.resetInclusiveTimes()
|
||||
inclusiveTimes = {}
|
||||
end
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
Top n most expensive single function calls
|
||||
Keeps track of the functions that took the longest time to run
|
||||
Note: functions can appear in this list at most once
|
||||
---------------------------------------------------------------------------]]
|
||||
local mostExpensiveSingleCalls = {}
|
||||
|
||||
-- Gets most expensive single calls
|
||||
FProfiler.Internal.getMostExpensiveSingleCalls = function() return mostExpensiveSingleCalls end
|
||||
|
||||
-- Dictionary to make sure the same function doesn't appear multiple times
|
||||
-- in the top n
|
||||
local mostExpensiveSingleDict = {}
|
||||
|
||||
function FProfiler.Internal.resetMostExpensiveSingleCalls()
|
||||
for i = 1, 50 do mostExpensiveSingleCalls[i] = {runtime = 0} end
|
||||
mostExpensiveSingleDict = {}
|
||||
end
|
||||
|
||||
-- Initial empty list
|
||||
FProfiler.Internal.resetMostExpensiveSingleCalls()
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
Function information
|
||||
Using debug.getinfo on a function object won't give you any function names
|
||||
that's because functions can have multiple names.
|
||||
Luckily, when the functions are called, debug.getinfo(level) gives the
|
||||
function name and scope
|
||||
---------------------------------------------------------------------------]]
|
||||
local functionNames = {}
|
||||
|
||||
FProfiler.Internal.getFunctionNames = function() return functionNames end
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
Recursion depth
|
||||
|
||||
Used internally to make sure recursive functions' times aren't counted
|
||||
multiple times
|
||||
---------------------------------------------------------------------------]]
|
||||
local recursiveCount = {}
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
Function start times
|
||||
|
||||
Used internally to keep track of when functions were called
|
||||
---------------------------------------------------------------------------]]
|
||||
local startTimes = {}
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
Lua code event handlers
|
||||
---------------------------------------------------------------------------]]
|
||||
|
||||
-- The recursion depth of the function that is in focus.
|
||||
-- Only applies when profiling a specific function (i.e. laying focus upon)
|
||||
local focusDepth = 0
|
||||
|
||||
-- Called when a function in the code is called
|
||||
local function registerFunctionCall(funcInfo)
|
||||
local func = funcInfo.func
|
||||
|
||||
-- Update call counts
|
||||
callcounts[func] = (callcounts[func] or 0) + 1
|
||||
|
||||
-- Increase recursion depth for this function
|
||||
recursiveCount[func] = (recursiveCount[func] or 0) + 1
|
||||
|
||||
-- Store function info
|
||||
local funcname = funcInfo.name or ""
|
||||
functionNames[func] = functionNames[func] or {}
|
||||
functionNames[func][funcname] = functionNames[func][funcname] or
|
||||
{ namewhat = funcInfo.namewhat,
|
||||
nparams = funcInfo.nparams
|
||||
}
|
||||
|
||||
local time = timeMeasurementFunc()
|
||||
|
||||
-- Update inclusive function times,
|
||||
-- only when we're on the first recursive call
|
||||
if recursiveCount[func] == 1 then
|
||||
startTimes[func] = time
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Called when a function returns
|
||||
local function registerReturn(funcInfo)
|
||||
local time = timeMeasurementFunc()
|
||||
local func = funcInfo.func
|
||||
local runtime = time - startTimes[func]
|
||||
|
||||
-- Update inclusive function time
|
||||
-- Only update on the topmost call, to prevent recursive
|
||||
-- calls for being counted multiple times.
|
||||
if recursiveCount[func] == 1 then
|
||||
inclusiveTimes[func] = (inclusiveTimes[func] or 0) + runtime
|
||||
end
|
||||
|
||||
-- Maintain recursion depth
|
||||
recursiveCount[func] = recursiveCount[func] - 1
|
||||
|
||||
-- Update top n list
|
||||
-- This path will be taken most often: the function isn't special
|
||||
-- Also only counts the top recursion
|
||||
if runtime <= mostExpensiveSingleCalls[50].runtime or recursiveCount[func] > 1 then return end
|
||||
|
||||
-- If the function already appears in the top 10, replace it or discard the result
|
||||
if mostExpensiveSingleDict[func] then
|
||||
local i = mostExpensiveSingleDict[func]
|
||||
|
||||
-- Discard this info
|
||||
if runtime < mostExpensiveSingleCalls[i].runtime then return end
|
||||
|
||||
-- Update the entry
|
||||
mostExpensiveSingleCalls[i].runtime = runtime
|
||||
mostExpensiveSingleCalls[i].info = funcInfo
|
||||
mostExpensiveSingleCalls[i].func = func
|
||||
|
||||
-- Move the updated entry up the top 10 list if applicable
|
||||
while i > 1 and runtime > mostExpensiveSingleCalls[i - 1].runtime do
|
||||
mostExpensiveSingleDict[mostExpensiveSingleCalls[i - 1].func] = i
|
||||
mostExpensiveSingleCalls[i - 1], mostExpensiveSingleCalls[i] = mostExpensiveSingleCalls[i], mostExpensiveSingleCalls[i - 1]
|
||||
i = i - 1
|
||||
end
|
||||
|
||||
mostExpensiveSingleDict[func] = i
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- Knowing that the function belongs in the top n, find its position
|
||||
local i = 50
|
||||
while i >= 1 and runtime > mostExpensiveSingleCalls[i].runtime do
|
||||
-- Update the dictionary
|
||||
-- All functions faster than the current one move down the list
|
||||
if not mostExpensiveSingleCalls[i].func then i = i - 1 continue end
|
||||
mostExpensiveSingleDict[mostExpensiveSingleCalls[i].func] = i + 1
|
||||
|
||||
i = i - 1
|
||||
end
|
||||
|
||||
-- Insert the expensive call in the top n
|
||||
mostExpensiveSingleDict[func] = i + 1
|
||||
table.insert(mostExpensiveSingleCalls, i + 1,
|
||||
{
|
||||
func = func,
|
||||
runtime = runtime,
|
||||
info = funcInfo,
|
||||
})
|
||||
|
||||
|
||||
-- What was previously the 50th most expensive function
|
||||
-- is now kicked out of the top 10
|
||||
if mostExpensiveSingleCalls[51].func then
|
||||
mostExpensiveSingleDict[mostExpensiveSingleCalls[51].func] = nil
|
||||
end
|
||||
mostExpensiveSingleCalls[51] = nil
|
||||
end
|
||||
|
||||
|
||||
-- Called on any Lua event
|
||||
local function onLuaEvent(event, focus)
|
||||
local info = debug.getinfo(3)
|
||||
local func = info.func
|
||||
|
||||
if event == "call" or event == "tail call" then
|
||||
-- Only track the focussed function and the functions
|
||||
-- called by the focussed function
|
||||
if focus == func then focusDepth = focusDepth + 1 end
|
||||
if focus and focusDepth == 0 then return end
|
||||
|
||||
registerFunctionCall(info)
|
||||
else
|
||||
-- Functions that return right after the call to FProfiler.Internal.start()
|
||||
-- are not to be counted
|
||||
if not recursiveCount[func] or recursiveCount[func] == 0 then return end
|
||||
|
||||
if focus == func then focusDepth = focusDepth - 1 end
|
||||
if focus and focusDepth == 0 then return end
|
||||
|
||||
registerReturn(info)
|
||||
end
|
||||
end
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
Profiling control
|
||||
---------------------------------------------------------------------------]]
|
||||
|
||||
-- Start profiling
|
||||
-- focus: only measure data of everything that happens within a certain function
|
||||
function FProfiler.Internal.start(focus)
|
||||
-- Empty start times, so unfinished functions aren't
|
||||
-- registered as returns on a second profiling session
|
||||
-- local time = SysTime()
|
||||
-- for k,v in pairs(startTimes) do startTimes[k] = time end
|
||||
table.Empty(startTimes)
|
||||
table.Empty(recursiveCount)
|
||||
|
||||
debug.sethook(function(event) onLuaEvent(event, focus) end, "cr")
|
||||
end
|
||||
|
||||
|
||||
-- Stop profiling
|
||||
function FProfiler.Internal.stop()
|
||||
debug.sethook()
|
||||
end
|
||||
|
||||
-- Reset all profiling data
|
||||
function FProfiler.Internal.reset()
|
||||
FProfiler.Internal.resetCallCounts()
|
||||
FProfiler.Internal.resetInclusiveTimes()
|
||||
FProfiler.Internal.resetMostExpensiveSingleCalls()
|
||||
end
|
||||
594
addons/fprofiler/lua/fprofiler/prettyprint.lua
Normal file
594
addons/fprofiler/lua/fprofiler/prettyprint.lua
Normal file
@@ -0,0 +1,594 @@
|
||||
--[[
|
||||
| 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/
|
||||
--]]
|
||||
|
||||
-- Based on MDave's thing
|
||||
-- https://gist.github.com/mentlerd/d56ad9e6361f4b86af84
|
||||
if SERVER then AddCSLuaFile() end
|
||||
|
||||
local type_weight = {
|
||||
[TYPE_FUNCTION] = 1,
|
||||
[TYPE_TABLE] = 2,
|
||||
}
|
||||
|
||||
local type_colors = {
|
||||
[TYPE_BOOL] = Color(175, 130, 255),
|
||||
[TYPE_NUMBER] = Color(175, 130, 255),
|
||||
[TYPE_STRING] = Color(230, 220, 115),
|
||||
[TYPE_FUNCTION] = Color(100, 220, 240)
|
||||
}
|
||||
|
||||
local color_neutral = Color(220, 220, 220)
|
||||
local color_name = Color(260, 150, 30)
|
||||
|
||||
local color_reference = Color(150, 230, 50)
|
||||
local color_comment = Color( 30, 210, 30)
|
||||
|
||||
-- 'nil' value
|
||||
local NIL = {}
|
||||
|
||||
-- Localise for faster access
|
||||
local pcall = pcall
|
||||
|
||||
local string_len = string.len
|
||||
local string_sub = string.sub
|
||||
local string_find = string.find
|
||||
|
||||
local table_concat = table.concat
|
||||
local table_insert = table.insert
|
||||
local table_sort = table.sort
|
||||
|
||||
|
||||
-- Stream interface
|
||||
local gMsgF -- Print fragment
|
||||
local gMsgN -- Print newline
|
||||
local gMsgC -- Set print color
|
||||
|
||||
local PrintLocals, gBegin, gFinish, PrintTableGrep
|
||||
|
||||
do
|
||||
local grep_color = Color(235, 70, 70)
|
||||
|
||||
-- Grep parameters (static between gBegin/gEnd)
|
||||
local grep
|
||||
local grep_raw
|
||||
|
||||
local grep_proximity
|
||||
|
||||
|
||||
-- Current line parameters
|
||||
local buffer
|
||||
local colors
|
||||
local markers
|
||||
|
||||
local baseColor
|
||||
local currColor
|
||||
|
||||
local length
|
||||
|
||||
-- History
|
||||
local history
|
||||
local remain
|
||||
|
||||
|
||||
-- Actual printing
|
||||
local function gCheckMatch( buffer )
|
||||
local raw = table_concat(buffer)
|
||||
|
||||
return raw, string_find(raw, grep, 0, grep_raw)
|
||||
end
|
||||
|
||||
local function gFlushEx( raw, markers, colors, baseColor )
|
||||
|
||||
-- Print entire buffer
|
||||
local len = string_len(raw)
|
||||
|
||||
-- Keep track of the current line properties
|
||||
local index = 1
|
||||
local marker = 1
|
||||
|
||||
local currColor = baseColor
|
||||
|
||||
-- Method to print to a preset area
|
||||
local function printToIndex( limit, color )
|
||||
local mark = markers and markers[marker]
|
||||
|
||||
-- Print all marker areas until we would overshoot
|
||||
while mark and mark < limit do
|
||||
|
||||
-- Catch up to the marker
|
||||
MsgC(color or currColor or color_neutral, string_sub(raw, index, mark))
|
||||
index = mark +1
|
||||
|
||||
-- Set new color
|
||||
currColor = colors[marker]
|
||||
|
||||
-- Select next marker
|
||||
marker = marker +1
|
||||
mark = markers[marker]
|
||||
|
||||
end
|
||||
|
||||
-- Print the remaining between the last marker and the limit
|
||||
MsgC(color or currColor or color_neutral, string_sub(raw, index, limit))
|
||||
index = limit +1
|
||||
end
|
||||
|
||||
-- Grep!
|
||||
local match, last = 1
|
||||
local from, to = string_find(raw, grep, 0, grep_raw)
|
||||
|
||||
while from do
|
||||
printToIndex(from -1)
|
||||
printToIndex(to, grep_color)
|
||||
|
||||
last = to +1
|
||||
from, to = string_find(raw, grep, last, grep_raw)
|
||||
end
|
||||
|
||||
printToIndex(len)
|
||||
MsgN()
|
||||
end
|
||||
|
||||
|
||||
local function gCommit()
|
||||
if grep_proximity then
|
||||
-- Check if the line has at least one match
|
||||
local raw, match = gCheckMatch(buffer)
|
||||
|
||||
if match then
|
||||
|
||||
-- Divide matches
|
||||
if history[grep_proximity] then
|
||||
MsgN("...")
|
||||
end
|
||||
|
||||
-- Flush history
|
||||
if grep_proximity ~= 0 then
|
||||
local len = #history
|
||||
|
||||
for index = len -1, 1, -1 do
|
||||
local entry = history[index]
|
||||
history[index] = nil
|
||||
|
||||
gFlushEx( entry[1], entry[2], entry[3], entry[4] )
|
||||
end
|
||||
|
||||
history[len] = nil
|
||||
end
|
||||
|
||||
-- Flush line, allow next X lines to get printed
|
||||
gFlushEx( raw, markers, colors, baseColor )
|
||||
remain = grep_proximity -1
|
||||
|
||||
history[grep_proximity +1] = nil
|
||||
elseif remain > 0 then
|
||||
-- Flush immediately
|
||||
gFlushEx( raw, markers, colors, baseColor )
|
||||
remain = remain -1
|
||||
else
|
||||
-- Store in history
|
||||
table_insert(history, 1, {raw, markers, colors, baseColor})
|
||||
history[grep_proximity +1] = nil
|
||||
end
|
||||
else
|
||||
-- Flush anyway
|
||||
gFlushEx( table_concat(buffer), markers, colors, baseColor )
|
||||
end
|
||||
|
||||
-- Reset state
|
||||
length = 0
|
||||
buffer = {}
|
||||
|
||||
markers = nil
|
||||
colors = nil
|
||||
|
||||
baseColor = nil
|
||||
currColor = nil
|
||||
end
|
||||
|
||||
-- State machine
|
||||
function gBegin( new, prox )
|
||||
grep = isstring(new) and new
|
||||
|
||||
if grep then
|
||||
grep_raw = not pcall(string_find, ' ', grep)
|
||||
grep_proximity = isnumber(prox) and prox
|
||||
|
||||
-- Reset everything
|
||||
buffer = {}
|
||||
history = {}
|
||||
end
|
||||
|
||||
length = 0
|
||||
remain = 0
|
||||
|
||||
baseColor = nil
|
||||
currColor = nil
|
||||
end
|
||||
|
||||
function gFinish()
|
||||
if grep_proximity and history and history[1] then
|
||||
MsgN("...")
|
||||
end
|
||||
|
||||
-- Free memory
|
||||
buffer = nil
|
||||
markers = nil
|
||||
colors = nil
|
||||
|
||||
history = nil
|
||||
end
|
||||
|
||||
|
||||
function gMsgC( color )
|
||||
if grep then
|
||||
|
||||
-- Try to save some memory by not immediately allocating colors
|
||||
if length == 0 then
|
||||
baseColor = color
|
||||
return
|
||||
end
|
||||
|
||||
-- Record color change
|
||||
if color ~= currColor then
|
||||
if not markers then
|
||||
markers = {}
|
||||
colors = {}
|
||||
end
|
||||
|
||||
-- Record color change
|
||||
table_insert(markers, length)
|
||||
table_insert(colors, color)
|
||||
end
|
||||
end
|
||||
|
||||
currColor = color
|
||||
end
|
||||
|
||||
function gMsgF( str )
|
||||
if grep then
|
||||
|
||||
-- Split multiline fragments to separate ones
|
||||
local fragColor = currColor or baseColor
|
||||
|
||||
local last = 1
|
||||
local from, to = string_find(str, '\n')
|
||||
|
||||
while from do
|
||||
local frag = string_sub(str, last, from -1)
|
||||
local len = from - last
|
||||
|
||||
-- Merge fragment to the line
|
||||
length = length + len
|
||||
table_insert(buffer, frag)
|
||||
|
||||
-- Print finished line
|
||||
gCommit()
|
||||
|
||||
-- Assign base color as previous fragColor
|
||||
baseColor = fragColor
|
||||
|
||||
-- Look for more
|
||||
last = to +1
|
||||
from, to = string_find(str, '\n', last)
|
||||
end
|
||||
|
||||
-- Push last fragment
|
||||
local frag = string_sub(str, last)
|
||||
local len = string_len(str) - last +1
|
||||
|
||||
length = length + len
|
||||
table_insert(buffer, frag)
|
||||
else
|
||||
-- Push immediately
|
||||
MsgC(currColor or baseColor or color_neutral, str)
|
||||
end
|
||||
end
|
||||
|
||||
function gMsgN()
|
||||
-- Print everything in the buffer
|
||||
if grep then
|
||||
gCommit()
|
||||
else
|
||||
MsgN()
|
||||
end
|
||||
|
||||
baseColor = nil
|
||||
currColor = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function InternalPrintValue( value )
|
||||
|
||||
-- 'nil' values can also be printed
|
||||
if value == NIL then
|
||||
gMsgC(color_comment)
|
||||
gMsgF("nil")
|
||||
return
|
||||
end
|
||||
|
||||
local color = type_colors[ TypeID(value) ]
|
||||
|
||||
-- For strings, place quotes
|
||||
if isstring(value) then
|
||||
if string_len(value) <= 1 then
|
||||
value = string.format([['%s']], value)
|
||||
else
|
||||
value = string.format([["%s"]], value)
|
||||
end
|
||||
|
||||
gMsgC(color)
|
||||
gMsgF(value)
|
||||
return
|
||||
end
|
||||
|
||||
-- Workaround for userdata not using MetaName
|
||||
if string_sub(tostring(value), 0, 8) == "userdata" then
|
||||
local meta = getmetatable(value)
|
||||
|
||||
if meta and meta.MetaName then
|
||||
value = string.format("%s: %p", meta.MetaName, value)
|
||||
end
|
||||
end
|
||||
|
||||
-- General print
|
||||
gMsgC(color)
|
||||
gMsgF(tostring(value))
|
||||
|
||||
-- For functions append source info
|
||||
if isfunction(value) then
|
||||
local info = debug.getinfo(value, 'S')
|
||||
local aux
|
||||
|
||||
if info.what == 'C' then
|
||||
aux = "\t-- [C]: -1"
|
||||
else
|
||||
if info.linedefined ~= info.lastlinedefined then
|
||||
aux = string.format("\t-- [%s]: %i-%i", info.short_src, info.linedefined, info.lastlinedefined)
|
||||
else
|
||||
aux = string.format("\t-- [%s]: %i", info.short_src, info.linedefined)
|
||||
end
|
||||
end
|
||||
|
||||
gMsgC(color_comment)
|
||||
gMsgF(aux)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Associated to object keys
|
||||
local objID
|
||||
|
||||
local function isprimitive( value )
|
||||
local id = TypeID(value)
|
||||
|
||||
return id <= TYPE_FUNCTION and id ~= TYPE_TABLE
|
||||
end
|
||||
|
||||
local function InternalPrintTable( table, path, prefix, names, todo )
|
||||
|
||||
-- Collect keys and some info about them
|
||||
local keyList = {}
|
||||
local keyStr = {}
|
||||
|
||||
local keyCount = 0
|
||||
|
||||
for key, value in pairs( table ) do
|
||||
-- Add to key list for later sorting
|
||||
table_insert(keyList, key)
|
||||
|
||||
-- Describe key as string
|
||||
if isprimitive(key) then
|
||||
keyStr[key] = tostring(key)
|
||||
else
|
||||
-- Lookup already known name
|
||||
local name = names[key]
|
||||
|
||||
-- Assign a new unique identifier
|
||||
if not name then
|
||||
objID = objID +1
|
||||
name = string.format("%s: obj #%i", tostring(key), objID)
|
||||
|
||||
names[key] = name
|
||||
todo[key] = true
|
||||
end
|
||||
|
||||
-- Substitute object with name
|
||||
keyStr[key] = name
|
||||
end
|
||||
|
||||
keyCount = keyCount +1
|
||||
end
|
||||
|
||||
|
||||
-- Exit early for empty tables
|
||||
if keyCount == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
-- Determine max key length
|
||||
local keyLen = 4
|
||||
|
||||
for key, str in pairs(keyStr) do
|
||||
keyLen = math.max(keyLen, string.len(str))
|
||||
end
|
||||
|
||||
-- Sort table keys
|
||||
if keyCount > 1 then
|
||||
table_sort( keyList, function( A, B )
|
||||
|
||||
-- Sort numbers numerically correct
|
||||
if isnumber(A) and isnumber(B) then
|
||||
return A < B
|
||||
end
|
||||
|
||||
-- Weight types
|
||||
local wA = type_weight[ TypeID( table[A] ) ] or 0
|
||||
local wB = type_weight[ TypeID( table[B] ) ] or 0
|
||||
|
||||
if wA ~= wB then
|
||||
return wA < wB
|
||||
end
|
||||
|
||||
-- Order by string representation
|
||||
return keyStr[A] < keyStr[B]
|
||||
|
||||
end )
|
||||
end
|
||||
|
||||
-- Determine the next level ident
|
||||
local new_prefix = string.format( "%s║%s", prefix, string.rep(' ', keyLen) )
|
||||
|
||||
-- Mark object as done
|
||||
todo[table] = nil
|
||||
|
||||
-- Start describing table
|
||||
for index, key in ipairs(keyList) do
|
||||
local value = table[key]
|
||||
|
||||
-- Assign names to already described keys/values
|
||||
local kName = names[key]
|
||||
local vName = names[value]
|
||||
|
||||
-- Decide to either fully describe, or print the value
|
||||
local describe = not isprimitive(value) and ( not vName or todo[value] )
|
||||
|
||||
-- Ident
|
||||
gMsgF(prefix)
|
||||
|
||||
-- Fancy table guides
|
||||
local moreLines = (index ~= keyCount) or describe
|
||||
|
||||
if index == 1 then
|
||||
gMsgF(moreLines and '╦ ' or '═ ')
|
||||
else
|
||||
gMsgF(moreLines and '╠ ' or '╚ ')
|
||||
end
|
||||
|
||||
-- Print key
|
||||
local sKey = kName or keyStr[key]
|
||||
|
||||
gMsgC(kName and color_reference or color_name)
|
||||
gMsgF(sKey)
|
||||
|
||||
-- Describe non primitives
|
||||
describe = istable(value) and ( not names[value] or todo[value] ) and value ~= NIL
|
||||
|
||||
-- Print key postfix
|
||||
local padding = keyLen - string.len(sKey)
|
||||
local postfix = string.format(describe and ":%s" or "%s = ", string.rep(' ', padding))
|
||||
|
||||
gMsgC(color_neutral)
|
||||
gMsgF(postfix)
|
||||
|
||||
-- Print the value
|
||||
if describe then
|
||||
gMsgN()
|
||||
|
||||
-- Expand access path
|
||||
local new_path = sKey
|
||||
|
||||
if isnumber(key) or kName then
|
||||
new_path = string.format("%s[%s]", path or '', key)
|
||||
elseif path then
|
||||
new_path = string.format("%s.%s", path, new_path)
|
||||
end
|
||||
|
||||
-- Name the object to mark it done
|
||||
names[value] = names[value] or new_path
|
||||
|
||||
-- Describe
|
||||
InternalPrintTable(value, new_path, new_prefix, names, todo)
|
||||
else
|
||||
-- Print the value (or the reference name)
|
||||
if vName and not todo[value] then
|
||||
gMsgC(color_reference)
|
||||
gMsgF(string.format("ref: %s",vName))
|
||||
else
|
||||
InternalPrintValue(value)
|
||||
end
|
||||
|
||||
gMsgN()
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function PrintTableGrep( table, grep, proximity )
|
||||
local base = {
|
||||
[_G] = "_G",
|
||||
[table] = "root"
|
||||
}
|
||||
|
||||
gBegin(grep, proximity)
|
||||
objID = 0
|
||||
InternalPrintTable(table, nil, "", base, {})
|
||||
gFinish()
|
||||
end
|
||||
|
||||
function PrintLocals( level )
|
||||
local level = level or 2
|
||||
local hash = {}
|
||||
|
||||
for index = 1, 255 do
|
||||
local name, value = debug.getlocal(2, index)
|
||||
|
||||
if not name then
|
||||
break
|
||||
end
|
||||
|
||||
if value == nil then
|
||||
value = NIL
|
||||
end
|
||||
|
||||
hash[name] = value
|
||||
end
|
||||
|
||||
PrintTableGrep( hash )
|
||||
end
|
||||
|
||||
function show(...)
|
||||
local n = select('#', ...)
|
||||
local tbl = {...}
|
||||
|
||||
for i = 1, n do
|
||||
if istable(tbl[i]) then MsgN(tostring(tbl[i])) PrintTableGrep(tbl[i])
|
||||
else InternalPrintValue(tbl[i]) MsgN() end
|
||||
end
|
||||
end
|
||||
|
||||
-- Hacky way of creating a pretty string from the above code
|
||||
-- because I don't feel like refactoring the entire thing
|
||||
local strResult
|
||||
local toStringMsgF = function(txt)
|
||||
table.insert(strResult, txt)
|
||||
end
|
||||
|
||||
local toStringMsgN = function()
|
||||
table.insert(strResult, "\n")
|
||||
end
|
||||
|
||||
local toStringMsgC = function(_, txt)
|
||||
table.insert(strResult, txt)
|
||||
end
|
||||
|
||||
function showStr(...)
|
||||
local oldF, oldN, oldMsgC, oldMsgN = gMsgF, gMsgN, MsgC, MsgN
|
||||
gMsgF, gMsgN, MsgC, MsgN = toStringMsgF, toStringMsgN, toStringMsgC, toStringMsgN
|
||||
|
||||
strResult = {}
|
||||
show(...)
|
||||
|
||||
gMsgF, gMsgN, MsgC, MsgN = oldF, oldN, oldMsgC, oldMsgN
|
||||
|
||||
return table.concat(strResult, "")
|
||||
end
|
||||
114
addons/fprofiler/lua/fprofiler/report.lua
Normal file
114
addons/fprofiler/lua/fprofiler/report.lua
Normal file
@@ -0,0 +1,114 @@
|
||||
--[[
|
||||
| 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 function getData()
|
||||
local callCounts = FProfiler.Internal.getCallCounts()
|
||||
local inclusiveTimes = FProfiler.Internal.getInclusiveTimes()
|
||||
local funcNames = FProfiler.Internal.getFunctionNames()
|
||||
|
||||
local data = {}
|
||||
for func, called in pairs(callCounts) do
|
||||
local row = {}
|
||||
row.func = func
|
||||
row.info = debug.getinfo(func, "nfS")
|
||||
row.total_called = called
|
||||
row.total_time = inclusiveTimes[func] or 0
|
||||
row.average_time = row.total_time / row.total_called
|
||||
|
||||
row.name, row.namewhat = nil, nil
|
||||
|
||||
row.names = {}
|
||||
for name, namedata in pairs(funcNames[func]) do
|
||||
table.insert(row.names, {name = name, namewhat = namedata.namewhat, nparams = namedata.nparams})
|
||||
end
|
||||
|
||||
table.insert(data, row)
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local function cull(data, count)
|
||||
if not count then return data end
|
||||
|
||||
for i = count + 1, #data do
|
||||
data[i] = nil
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
The functions that are called most often
|
||||
Their implementations are O(n lg n),
|
||||
which is probably suboptimal but not worth my time optimising.
|
||||
---------------------------------------------------------------------------]]
|
||||
function FProfiler.Internal.mostOftenCalled(count)
|
||||
local sorted = getData()
|
||||
|
||||
table.SortByMember(sorted, "total_called")
|
||||
|
||||
return cull(sorted, count)
|
||||
end
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
The functions that take the longest time in total
|
||||
---------------------------------------------------------------------------]]
|
||||
function FProfiler.Internal.mostTimeInclusive(count)
|
||||
local sorted = getData()
|
||||
|
||||
table.SortByMember(sorted, "total_time")
|
||||
|
||||
return cull(sorted, count)
|
||||
end
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
The functions that take the longest average time
|
||||
---------------------------------------------------------------------------]]
|
||||
function FProfiler.Internal.mostTimeInclusiveAverage(count)
|
||||
local sorted = getData()
|
||||
|
||||
table.SortByMember(sorted, "average_time")
|
||||
|
||||
return cull(sorted, count)
|
||||
end
|
||||
|
||||
--[[-------------------------------------------------------------------------
|
||||
Get the top <count> of most often called, time inclusive and average
|
||||
NOTE: This will almost definitely return more than <count> results.
|
||||
Up to three times <count> is possible.
|
||||
---------------------------------------------------------------------------]]
|
||||
function FProfiler.Internal.getAggregatedResults(count)
|
||||
count = count or 100
|
||||
|
||||
local dict = {}
|
||||
local mostTime = FProfiler.Internal.mostTimeInclusive(count)
|
||||
for i = 1, #mostTime do dict[mostTime[i].func] = true end
|
||||
|
||||
local mostAvg = FProfiler.Internal.mostTimeInclusiveAverage(count)
|
||||
|
||||
for i = 1, #mostAvg do
|
||||
if dict[mostAvg[i].func] then continue end
|
||||
dict[mostAvg[i].func] = true
|
||||
table.insert(mostTime, mostAvg[i])
|
||||
end
|
||||
|
||||
local mostCalled = FProfiler.Internal.mostOftenCalled(count)
|
||||
|
||||
for i = 1, #mostCalled do
|
||||
if dict[mostCalled[i].func] then continue end
|
||||
dict[mostCalled[i].func] = true
|
||||
table.insert(mostTime, mostCalled[i])
|
||||
end
|
||||
|
||||
table.SortByMember(mostTime, "total_time")
|
||||
|
||||
return mostTime
|
||||
end
|
||||
110
addons/fprofiler/lua/fprofiler/ui/clientcontrol.lua
Normal file
110
addons/fprofiler/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
addons/fprofiler/lua/fprofiler/ui/frame.lua
Normal file
475
addons/fprofiler/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
addons/fprofiler/lua/fprofiler/ui/model.lua
Normal file
188
addons/fprofiler/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
addons/fprofiler/lua/fprofiler/ui/server.lua
Normal file
305
addons/fprofiler/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
addons/fprofiler/lua/fprofiler/ui/servercontrol.lua
Normal file
238
addons/fprofiler/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)
|
||||
|
||||
52
addons/fprofiler/lua/fprofiler/util.lua
Normal file
52
addons/fprofiler/lua/fprofiler/util.lua
Normal file
@@ -0,0 +1,52 @@
|
||||
--[[
|
||||
| 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/
|
||||
--]]
|
||||
|
||||
|
||||
-- Try to find the function represented by a string
|
||||
function FProfiler.funcNameToObj(str)
|
||||
if isfunction(str) then return str end
|
||||
|
||||
local times = FProfiler.Internal.getCallCounts()
|
||||
for func, _ in pairs(times) do
|
||||
if tostring(func) == str then return func end
|
||||
end
|
||||
|
||||
local tbl = _G
|
||||
local exploded = string.Explode(".", str, false)
|
||||
if not exploded or not exploded[1] then return end
|
||||
|
||||
for i = 1, #exploded - 1 do
|
||||
tbl = (tbl or {})[exploded[i]]
|
||||
if not istable(tbl) then return end
|
||||
end
|
||||
|
||||
local func = (tbl or {})[exploded[#exploded]]
|
||||
|
||||
if not isfunction(func) then return end
|
||||
|
||||
return func
|
||||
end
|
||||
|
||||
-- Read a file
|
||||
function FProfiler.readSource(fname, startLine, endLine)
|
||||
if not file.Exists(fname, "GAME") then return "" end
|
||||
if startLine < 0 or endLine < 0 or endLine < startLine then return "" end
|
||||
|
||||
local f = file.Open(fname, "r", "GAME")
|
||||
|
||||
for i = 1, startLine - 1 do f:ReadLine() end
|
||||
|
||||
local res = {}
|
||||
for i = startLine, endLine do
|
||||
table.insert(res, f:ReadLine() or "")
|
||||
end
|
||||
|
||||
return table.concat(res, "\n")
|
||||
end
|
||||
Reference in New Issue
Block a user