This commit is contained in:
lifestorm
2024-08-05 18:40:29 +03:00
parent c4d91bf369
commit 324f19217d
8040 changed files with 1853423 additions and 21 deletions

View 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

View 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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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