├── README.md └── lua └── autorun ├── server └── sv_net_dumper.lua ├── sh_hookorder.lua ├── sh_hookperf.lua ├── sh_indexcounter.lua ├── sh_net_logger.lua └── sh_netperf.lua /README.md: -------------------------------------------------------------------------------- 1 | # :computer: gmod_perfutils 2 | Various scripts I've made over the years to find performance bottle necks in garrysmod. 3 | 4 | ## Installation 5 | Either clone/download the repository and put it in your `addons/` folder or run the individual files, the files are designed to be able to be ran by themselves and have superadmin checks. 6 | 7 | ## Usage 8 | | File | Commands | Description | 9 | | --- | --- | --- | 10 | | `sh_hookperf.lua` | `red_sv_hookperf` `red_cl_hookperf` | This script will log the time taken to run each hook over the span of the amount of time give, defaults to 10 seconds. This can be useful to find laggy hooks without having to use something as heavy as FProfiler as that can cause significiant lag while active, thus being hard to use on servers. | 11 | | `sh_hookorder.lua` | `red_sv_hookorder` `red_cl_hookorder` | This script will print out the order in which hooks are called. This can be useful to determine if a hook is returning early and preventing other hooks from being called. Also shows the time taken to run each hook. 12 | `sh_netperf.lua` | `red_sv_netperf_start` `red_cl_netperf_start` `red_sv_netperf_stop` `red_cl_netperf_stop` | This script will detour all net receivers log the amount of bytes received and the time taken to process the message. This can be useful to reduce networking load on both server and client. 13 | | `sh_indexcounter.lua` | `red_sv_indexcounter` `red_cl_indexcounter` | This script will count the amount of __index metamethod calls on entities. The more the worse, entity indexing is significiantly slower than using the entity table directly (ent:GetTable()). 14 | | `sh_net_logger.lua` | `red_(sv/cl)_netlogger_start` `red_(sv/cl)_netlogger_stop` `red_(sv/cl)_netlogger_ignore` | Starts the netlogger, all incomming net.* functions will be printed to console in order as they're being read. Spammy net messages can be ignored using `red_(sv/cl)_netlogger_ignore netmsgname` | 15 | | `sv_net_dumper.lua` | `red_sv_netdump` | This script will find all `net.Receive` function origins and dump their files to the data folder. This can be useful for locating badly performing hooks and exploits. | 16 | 17 | ## Extra tools 18 | Tools i often use and can recommend for performance profiling. 19 | 20 | | Tool | Description | 21 | | ------------- | ------------- | 22 | | [FProfiler](https://github.com/FPtje/FProfiler) | A tool that can be used to profile lua code. It can be used to find performance bottlenecks in your code after you've determined them being an issue with +showbudget or +showvprof. | 23 | | concmd `+showbudget` | Shows the budget panel in the top right corner of the screen. This shows how long each frame is taking to render, super useful as the first step in finding performance issues. This isn't limited to lua either which is a big bonus. | 24 | | concmd `+showvprof` | Shows the vprof panel, has detailed information about costs. | 25 | | convar `phys_speeds 1` | Shows the amount of time it took per tick to execute see [here](https://github.com/ValveSoftware/source-sdk-2013/blob/0d8dceea4310fde5706b3ce1c70609d72a38efdf/mp/src/game/server/physics.cpp#L1697) for the source. | 26 | -------------------------------------------------------------------------------- /lua/autorun/server/sv_net_dumper.lua: -------------------------------------------------------------------------------- 1 | concommand.Add( "red_sv_netdump", function( ply ) 2 | if IsValid( ply ) and not ply:IsSuperAdmin() then 3 | return ply:ChatPrint("No permission.") 4 | end 5 | 6 | if file.Exists( "netdump", "DATA" ) then 7 | for _, fil in ipairs( file.Find( "netdump/*", "DATA" ) ) do 8 | file.Delete( "netdump/" .. fil ) 9 | end 10 | file.Delete( "netdump" ) 11 | end 12 | file.CreateDir( "netdump" ) 13 | 14 | for name, func in pairs( net.Receivers ) do 15 | local info = debug.getinfo( func, "S" ) 16 | local source = info.short_src 17 | local lineDefined = info.linedefined - 5 18 | local lineEnd = info.lastlinedefined + 5 19 | 20 | local found = false 21 | local path = "GAME" 22 | 23 | if file.Exists( source, "GAME" ) then 24 | found = true 25 | path = "GAME" 26 | elseif file.Exists( source, "LUA" ) then 27 | found = true 28 | path = "LUA" 29 | else 30 | print( "[NETDUMP] Unknown source:", name, " - ", source ) 31 | end 32 | 33 | if found then 34 | file.AsyncRead( source, path, function( _, _, status, data ) 35 | if status ~= FSASYNC_OK then 36 | print( "[NETDUMP] Failed to read file:", name, " - ", source ) 37 | return 38 | end 39 | 40 | local lines = string.Explode( "\n", data ) 41 | local dump = "--" .. name .. " " .. source .. ":" .. lineDefined .. "-" .. lineEnd .. "\n" 42 | 43 | for lineNum, line in ipairs( lines ) do 44 | if lineNum < lineDefined then continue end 45 | if lineNum > lineEnd then break end 46 | dump = dump .. line .. "\n" 47 | end 48 | 49 | file.Write( "netdump/" .. name .. ".txt", dump ) 50 | print( "[NETDUMP] Dumped:", name, " - ", source ) 51 | end ) 52 | end 53 | end 54 | end ) 55 | -------------------------------------------------------------------------------- /lua/autorun/sh_hookorder.lua: -------------------------------------------------------------------------------- 1 | local blue = Color( 58, 150, 221 ) 2 | local darkGray = Color( 100, 100, 100 ) 3 | local softWhite = Color( 205, 205, 205 ) 4 | local lineNumGreen = Color( 19, 161, 14 ) 5 | local softRed = Color( 231, 72, 86 ) 6 | local pink = Color( 235, 52, 137 ) 7 | 8 | local hookRanNum = 0 9 | 10 | local function wrapHooks( hookTable, hookName, hookCount ) 11 | hookRanNum = 0 12 | for hookID, originalFunc in pairs( hookTable ) do 13 | local originInfo = debug.getinfo( originalFunc, "S" ) 14 | local hookFuncOrigin = originInfo.short_src 15 | local hookFuncLastDefined = originInfo.lastlinedefined 16 | 17 | local wrappedHook = function( ... ) 18 | hookRanNum = hookRanNum + 1 19 | 20 | if hookRanNum == 1 then 21 | print( ... ) 22 | end 23 | 24 | local startTime = SysTime() 25 | local a, b, c, d, e, f = originalFunc( ... ) 26 | local timeTook = SysTime() - startTime 27 | local fancyTime = math.Round( timeTook * 1000, 6 ) 28 | 29 | MsgC( lineNumGreen, hookRanNum, darkGray, ": ", blue, hookID, softWhite, " ", softRed, fancyTime, "ms ", softWhite, hookFuncOrigin, ":", hookFuncLastDefined, "\n" ) 30 | 31 | if a ~= nil then 32 | MsgC( pink, "Hook returned a value, printing it to console.\n" ) 33 | for k, v in pairs( { a, b, c, d, e, f } ) do 34 | if v == "" then v = "!Empty string!" end 35 | print( k, v ) 36 | end 37 | end 38 | 39 | if hookCount == hookRanNum then 40 | MsgC( pink, "Done!\n" ) 41 | end 42 | 43 | hook.Add( hookName, hookID, originalFunc ) 44 | return a, b, c, d, e, f 45 | end 46 | hook.Add( hookName, hookID, wrappedHook ) 47 | end 48 | end 49 | 50 | concommand.Add( SERVER and "red_sv_hookorder" or "red_cl_hookorder", function( ply, _, _, str ) 51 | if IsValid( ply ) and not ply:IsSuperAdmin() then 52 | return ply:ChatPrint("No permission.") 53 | end 54 | 55 | local hookTable = hook.GetTable() 56 | local hooks = hookTable[str] 57 | 58 | if not hooks then 59 | return MsgC( pink, "No hooks with given hook name.\n" ) 60 | end 61 | 62 | local hookCount = table.Count( hooks ) 63 | 64 | MsgC( pink, "Wrapping " .. hookCount .. " hooks...\n" ) 65 | wrapHooks( hooks, str, hookCount ) 66 | MsgC( pink, "Waiting for hook: ", blue, str, pink, " to run...\n" ) 67 | end ) 68 | -------------------------------------------------------------------------------- /lua/autorun/sh_hookperf.lua: -------------------------------------------------------------------------------- 1 | local hookC = Color( 58, 150, 221 ) 2 | local darkGray = Color( 100, 100, 100 ) 3 | local softWhite = Color( 205, 205, 205 ) 4 | local hookIDC = Color( 19, 161, 14 ) 5 | local timeC = Color( 231, 72, 86 ) 6 | local countC = Color( 52, 64, 235 ) 7 | 8 | local colors = { hookIDC, hookC, timeC, countC, softWhite, darkGray } 9 | local function printer( ... ) 10 | local order = {} 11 | for i, txt in pairs( { ... } ) do 12 | table.insert( order, colors[i] ) 13 | table.insert( order, tostring( txt ) .. " " ) 14 | end 15 | 16 | table.insert( order, "\n" ) 17 | 18 | MsgC( unpack( order ) ) 19 | end 20 | 21 | local cmd = SERVER and "red_sv_hookperf" or "red_cl_hookperf" 22 | concommand.Add( cmd, function( ply, _, args ) 23 | if SERVER and IsValid( ply ) and not ply:IsSuperAdmin() then 24 | return ply:ChatPrint("No permission.") 25 | end 26 | 27 | local time = tonumber( args[1] ) or 10 28 | 29 | if HOOK_PERF_RUNNING then 30 | MsgC( softWhite, "Hook performance profiler is already running.\n" ) 31 | return 32 | end 33 | 34 | local lagTbl = {} 35 | HOOK_PERF_ORIGINALS = HOOK_PERF_ORIGINALS or {} 36 | HOOK_PERF_RUNNING = true 37 | 38 | for hookName, hookTable in pairs( hook.GetTable() ) do 39 | for hookEvent, hookFunc in pairs( hookTable ) do 40 | HOOK_PERF_ORIGINALS[hookName] = HOOK_PERF_ORIGINALS[hookName] or {} 41 | HOOK_PERF_ORIGINALS[hookName][hookEvent] = HOOK_PERF_ORIGINALS[hookName][hookEvent] or hookFunc 42 | 43 | local originalFunc = HOOK_PERF_ORIGINALS[hookName][hookEvent] 44 | local originInfo = debug.getinfo( originalFunc, "S" ) 45 | local hookFuncOrigin = originInfo.short_src 46 | local hookFuncLastDefined = originInfo.lastlinedefined 47 | 48 | local function wrapper( ... ) 49 | local info = lagTbl[hookEvent] 50 | if not info then 51 | info = { count = 0, time = 0, hook = hookName, origin = hookFuncOrigin, lastDefined = hookFuncLastDefined, isGM = false } 52 | lagTbl[hookEvent] = info 53 | end 54 | info.count = info.count + 1 55 | 56 | local sysTime = SysTime() 57 | local a, b, c, d, e, f = originalFunc( ... ) 58 | lagTbl[hookEvent].time = lagTbl[hookEvent].time + SysTime() - sysTime 59 | 60 | return a, b, c, d, e, f 61 | end 62 | 63 | hook.Add( hookName, hookEvent, wrapper ) 64 | end 65 | end 66 | 67 | local GM = GAMEMODE or GM 68 | GM_ORIGINALS = GM_ORIGINALS or {} 69 | for methodName, func in pairs( GM ) do 70 | if isfunction( func ) then 71 | GM_ORIGINALS[methodName] = GM_ORIGINALS[methodName] or func 72 | local original = GM_ORIGINALS[methodName] 73 | local originInfo = debug.getinfo( original, "S" ) 74 | 75 | local function detour( ... ) 76 | local startTime = SysTime() 77 | local a, b, c, d, e, f = original( ... ) 78 | 79 | local info = lagTbl[methodName] 80 | if not info then 81 | info = { count = 0, time = 0, hook = "GM:" .. methodName, origin = originInfo.short_src, lastDefined = originInfo.lastlinedefined, isGM = true } 82 | lagTbl[methodName] = info 83 | end 84 | 85 | lagTbl[methodName].time = lagTbl[methodName].time + SysTime() - startTime 86 | return a, b, c, d, e, f 87 | end 88 | 89 | GM[methodName] = detour 90 | end 91 | end 92 | 93 | timer.Simple( time, function() 94 | -- restore 95 | for hookName, hookTable in pairs( hook.GetTable() ) do 96 | for hookEvent in pairs( hookTable ) do 97 | hook.Remove( hookName, hookEvent ) 98 | hook.Add( hookName, hookEvent, HOOK_PERF_ORIGINALS[hookName][hookEvent] ) 99 | end 100 | end 101 | 102 | for methodName, func in pairs( GM_ORIGINALS ) do 103 | GM[methodName] = func 104 | end 105 | 106 | -- sort 107 | local sorted = {} 108 | for k, v in pairs( lagTbl ) do 109 | table.insert( sorted, { k, v } ) 110 | end 111 | 112 | table.sort( sorted, function( a, b ) 113 | return a[2].time > b[2].time 114 | end ) 115 | 116 | MsgC( softWhite, "Laggy hooks:\n" ) 117 | printer( "Name", "Hook", "Time", "Count", "Origin", "Line defined" ) 118 | for i = 1, 100 do 119 | local v = sorted[i] 120 | if not v then break end 121 | 122 | printer( ( v[2].isGM and "GM:" or "" ) .. v[1], v[2].hook, v[2].time, v[2].count, v[2].origin, v[2].lastDefined ) 123 | end 124 | 125 | HOOK_PERF_ORIGINALS = nil 126 | HOOK_PERF_RUNNING = nil 127 | end ) 128 | end ) 129 | -------------------------------------------------------------------------------- /lua/autorun/sh_indexcounter.lua: -------------------------------------------------------------------------------- 1 | local entMeta = FindMetaTable( "Entity" ) 2 | local plyMeta = FindMetaTable( "Player" ) 3 | local wepMeta = FindMetaTable( "Weapon" ) 4 | 5 | ENT_INDEX = ENT_INDEX or entMeta.__index 6 | PLAYER_INDEX = PLAYER_INDEX or plyMeta.__index 7 | WEAPON_INDEX = WEAPON_INDEX or wepMeta.__index 8 | 9 | local toWrap = { 10 | [entMeta] = ENT_INDEX, 11 | [plyMeta] = PLAYER_INDEX, 12 | [wepMeta] = WEAPON_INDEX 13 | } 14 | 15 | local cmd = SERVER and "red_sv_indexcounter" or "red_cl_indexcounter" 16 | concommand.Add( cmd, function( ply, _, args ) 17 | if SERVER and IsValid( ply ) then return end 18 | local time = tonumber( args[1] ) or 10 19 | 20 | local origins = {} 21 | 22 | for meta, original in pairs( toWrap ) do 23 | meta.__index = function( tbl, key ) 24 | local info = debug.getinfo( 2 ) 25 | local name = info.short_src .. ":" .. info.linedefined 26 | origins[name] = origins[name] and origins[name] + 1 or 1 27 | 28 | return original( tbl, key ) 29 | end 30 | end 31 | 32 | timer.Simple( time, function() 33 | entMeta.__index = ENT_INDEX 34 | plyMeta.__index = PLAYER_INDEX 35 | wepMeta.__index = WEAPON_INDEX 36 | 37 | local indexed = {} 38 | for origin, count in pairs( origins ) do 39 | table.insert( indexed, { origin = origin, count = count } ) 40 | end 41 | 42 | table.sort( indexed, function( a, b ) return a.count > b.count end ) 43 | 44 | local max = 100 45 | local max_count = indexed[1].count 46 | 47 | for _, data in ipairs( indexed ) do 48 | max = max - 1 49 | if max < 0 then break end 50 | 51 | MsgC( Color( 255, 255 - ( data.count / max_count * 255 ), 0 ), data.count, color_white, " | ", data.origin, "\n" ) 52 | end 53 | 54 | if CLIENT then 55 | chat.AddText( Color( 255, 0, 0 ), "Entity index profiling complete. See console for results." ) 56 | end 57 | print( "Entity index profiling complete. See console for results." ) 58 | end ) 59 | end ) 60 | -------------------------------------------------------------------------------- /lua/autorun/sh_net_logger.lua: -------------------------------------------------------------------------------- 1 | local running = false 2 | 3 | local netReaders = { 4 | "ReadAngle", 5 | "ReadBit", 6 | "ReadBool", 7 | "ReadColor", 8 | "ReadData", 9 | "ReadDouble", 10 | "ReadEntity", 11 | "ReadFloat", 12 | "ReadInt", 13 | "ReadMatrix", 14 | "ReadNormal", 15 | "ReadPlayer", 16 | "ReadString", 17 | "ReadType", 18 | "ReadUInt", 19 | "ReadUInt64", 20 | "ReadVector" 21 | } 22 | 23 | local ignoreNets = { wire_overlay_data = true } 24 | net._ReadTable = net._ReadTable or net.ReadTable 25 | 26 | local white = Color( 255, 255, 255 ) 27 | local startColor = Color( 0, 255, 0 ) 28 | local endColor = Color( 255, 0, 0 ) 29 | local blueColor = Color( 166, 190, 255 ) 30 | local realmColor = CLIENT and Color( 255, 200, 0 ) or Color( 0, 200, 255 ) 31 | 32 | local function wrapNetRead() 33 | function net.ReadTable() 34 | print( "NET: START net.ReadTable" ) 35 | local tbl = net._ReadTable() 36 | print( "NET: END net.ReadTable" ) 37 | return tbl 38 | end 39 | 40 | for _, reader in ipairs( netReaders ) do 41 | local oldFunc = net[reader] 42 | net["_" .. reader] = net["_" .. reader] or oldFunc 43 | net[reader] = function( ... ) 44 | local args = { ... } 45 | local res = oldFunc( ... ) 46 | if #args ~= 0 then 47 | local argStr = "" 48 | for i, arg in ipairs( args ) do 49 | argStr = argStr .. tostring( arg ) 50 | if i ~= #args then 51 | argStr = argStr .. ", " 52 | end 53 | end 54 | 55 | MsgC( " net." .. reader, "(", argStr, ")", white, ": ", blueColor, tostring( res ), "\n" ) 56 | else 57 | MsgC( " net." .. reader, white, ": ", blueColor, tostring( res ), "\n" ) 58 | end 59 | return res 60 | end 61 | end 62 | end 63 | 64 | local function unwrapNetRead() 65 | for _, reader in ipairs( netReaders ) do 66 | net[reader] = net["_" .. reader] 67 | net["_" .. reader] = nil 68 | end 69 | 70 | net.ReadTable = net._ReadTable 71 | end 72 | 73 | local function applyWrap() 74 | netperf_net_Incoming = netperf_net_Incoming or net.Incoming 75 | local net_Incoming = netperf_net_Incoming 76 | 77 | function net.Incoming( len, client ) 78 | local headerNum = net.ReadHeader() 79 | local strName = util.NetworkIDToString( headerNum ) 80 | 81 | net_ReadHeader = net.ReadHeader 82 | net.ReadHeader = function() 83 | return headerNum 84 | end 85 | 86 | if not ignoreNets[strName] then 87 | MsgC( startColor, "NET START: ", realmColor, strName, blueColor, " (" .. len .. " bytes)", "\n" ) 88 | wrapNetRead() 89 | end 90 | 91 | local sysTime = SysTime() 92 | ProtectedCall( net_Incoming, len, client ) 93 | local took = SysTime() - sysTime 94 | 95 | if not ignoreNets[strName] then 96 | MsgC( endColor, "NET END: ", realmColor, strName, blueColor, " (" .. string.format( "%.4f", took * 1000 ) .. "ms)", "\n\n" ) 97 | unwrapNetRead() 98 | end 99 | 100 | net.ReadHeader = net_ReadHeader 101 | end 102 | end 103 | 104 | concommand.Add( SERVER and "red_sv_netlogger_start" or "red_cl_netlogger_start", function( ply ) 105 | if SERVER and IsValid( ply ) and not ply:IsSuperAdmin() then 106 | return ply:ChatPrint("No permission.") 107 | end 108 | 109 | applyWrap() 110 | running = true 111 | print( "Netlogger started" ) 112 | end ) 113 | 114 | concommand.Add( SERVER and "red_sv_netlogger_stop" or "red_cl_netlogger_stop", function( ply ) 115 | if SERVER and IsValid( ply ) and not ply:IsSuperAdmin() then 116 | return ply:ChatPrint("No permission.") 117 | end 118 | 119 | if not running then 120 | print( "Netlogger isn't running." ) 121 | return 122 | end 123 | 124 | running = false 125 | net.Incoming = netperf_net_Incoming 126 | end ) 127 | 128 | concommand.Add( SERVER and "red_sv_netlogger_ignore" or "red_cl_netlogger_ignore", function( ply, _, args ) 129 | if SERVER and IsValid( ply ) and not ply:IsSuperAdmin() then 130 | return ply:ChatPrint("No permission.") 131 | end 132 | 133 | if not running then 134 | print( "Netlogger isn't running." ) 135 | return 136 | end 137 | 138 | local name = args[1] 139 | if not name then 140 | print( "No name specified." ) 141 | return 142 | end 143 | 144 | ignoreNets[name] = true 145 | print( "Ignoring net message: " .. name ) 146 | end ) -------------------------------------------------------------------------------- /lua/autorun/sh_netperf.lua: -------------------------------------------------------------------------------- 1 | local perfTable = {} 2 | local bitTable = {} 3 | local totalStartTime = 0 4 | local running = false 5 | 6 | local function applyWrap() 7 | netperf_net_Incoming = netperf_net_Incoming or net.Incoming 8 | local net_Incoming = netperf_net_Incoming 9 | 10 | function net.Incoming( len, client ) 11 | local headerNum = net.ReadHeader() 12 | local strName = util.NetworkIDToString( headerNum ) 13 | 14 | net_ReadHeader = net.ReadHeader 15 | net.ReadHeader = function() 16 | return headerNum 17 | end 18 | 19 | bitTable[strName] = ( bitTable[strName] or 0 ) + len 20 | local start = SysTime() 21 | 22 | net_Incoming( len, client ) 23 | 24 | local endtime = SysTime() 25 | perfTable[strName] = ( perfTable[strName] or 0 ) + ( endtime - start ) 26 | 27 | net.ReadHeader = net_ReadHeader 28 | end 29 | end 30 | 31 | local nw2Table = {} 32 | hook.Add( "EntityNetworkedVarChanged", "NetPerf", function( ent, name, oldval, newval ) 33 | if not running then return end 34 | 35 | nw2Table[name] = ( nw2Table[name] or 0 ) + 1 36 | end ) 37 | 38 | concommand.Add( SERVER and "red_sv_netperf_start" or "red_cl_netperf_start", function( ply ) 39 | if SERVER and IsValid( ply ) and not ply:IsSuperAdmin() then 40 | return ply:ChatPrint("No permission.") 41 | end 42 | 43 | totalStartTime = SysTime() 44 | table.Empty( perfTable ) 45 | table.Empty( bitTable ) 46 | table.Empty( nw2Table ) 47 | 48 | applyWrap() 49 | running = true 50 | print( "Netperf started" ) 51 | end ) 52 | 53 | concommand.Add( SERVER and "red_sv_netperf_stop" or "red_cl_netperf_stop", function( ply ) 54 | if SERVER and IsValid( ply ) and not ply:IsSuperAdmin() then 55 | return ply:ChatPrint("No permission.") 56 | end 57 | 58 | if not running then 59 | print( "Netperf isn't running." ) 60 | return 61 | end 62 | 63 | running = false 64 | print( "Netperf netresults:\n" ) 65 | print( "Netmessages Sorted by time:\n" ) 66 | 67 | local perfstr = "" 68 | for k, v in SortedPairsByValue( perfTable, true ) do 69 | perfstr = perfstr .. k .. " - " .. v .. "s - " .. bitTable[k] .. " bits\n" 70 | end 71 | print( perfstr ) 72 | 73 | print( "Netmessages Sorted by bits:\n" ) 74 | local bitstr = "" 75 | for k, v in SortedPairsByValue( bitTable, true ) do 76 | bitstr = bitstr .. k .. " - " .. v .. " bits - " .. perfTable[k] .. "s\n" 77 | end 78 | print( bitstr ) 79 | 80 | print( "NW2 Sorted by count:\n" ) 81 | for k, v in SortedPairsByValue( nw2Table, true ) do 82 | print( "NW2: " .. k .. " - " .. v .. " times" ) 83 | end 84 | 85 | print( "Time ran: " .. ( SysTime() - totalStartTime ) .. "s" ) 86 | 87 | net.Incoming = netperf_net_Incoming 88 | end ) 89 | --------------------------------------------------------------------------------