GMADc\FProfiler{ "description": "Description", "type": "tool", "tags": [] }Author Namelua/autorun/fprofiler.lua�Ú�ÍDlua/fprofiler/cami.luaê=ÃE²Dlua/fprofiler/gather.luad%ÈϽlua/fprofiler/prettyprint.luaë7;­lua/fprofiler/report.luaë ’ x™lua/fprofiler/ui/clientcontrol.lua¹ÕVÆlua/fprofiler/ui/frame.luaÇ8–ÜÎrlua/fprofiler/ui/model.luaŒ"ïò lua/fprofiler/ui/server.luaT'O G lua/fprofiler/ui/servercontrol.luaÿÖ,x lua/fprofiler/util.luaQ´êFProfiler = {} 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 --[[ 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 local timeMeasurementFunc = SysTime -- Helper function, created by some ancient Lua dev -- Retrieves the local variables and their values of a function local function getupvalues(f) local t, i, k, v = {}, 1, debug.getupvalue(f, 1) while k do t[k] = v i = i + 1 k,v = debug.getupvalue(f, i) end return t end -- Helper function -- Get all local variables local NIL = {} setmetatable(NIL, {__tostring = function() return "nil" end}) local function getlocals(level) local i = 1 local name, value local vars = {} while true do name, value = debug.getlocal(level, i) if not name then break end value = value == nil and NIL or value vars[name] = value i = i + 1 end return vars 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].upvalues = getupvalues(func) mostExpensiveSingleCalls[i].locals = getlocals(5) 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, upvalues = getupvalues(func), locals = getlocals(5) }) -- 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 -- 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 = !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 !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 !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 = !isprimitive(value) and ( !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 ( !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 !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 !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 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 of most often called, time inclusive and average NOTE: This will almost definitely return more than results. Up to three times 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 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 --[[------------------------------------------------------------------------- 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") --[[------------------------------------------------------------------------- The Bottlenecks tab's contents ---------------------------------------------------------------------------]] local BOTTLENECKTAB = {} 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 = {} 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") --[[------------------------------------------------------------------------- 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) --[[------------------------------------------------------------------------- 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 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) -- 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 ÊÖùõ