Skip to content

Instantly share code, notes, and snippets.

@samisalreadytaken
Last active October 21, 2025 19:53
Show Gist options
  • Select an option

  • Save samisalreadytaken/6069a082871d8076ceee5ea4d5de1740 to your computer and use it in GitHub Desktop.

Select an option

Save samisalreadytaken/6069a082871d8076ceee5ea4d5de1740 to your computer and use it in GitHub Desktop.
//-----------------------------------------------------------------------
// github.com/samisalreadytaken
//-----------------------------------------------------------------------
//
// sqdbg profiler visualiser
//
// cvars: sqdbg_profvis_server, sqdbg_profvis_server_perframe, sqdbg_profvis_server_callgraph
// sqdbg_profvis_client, sqdbg_profvis_client_perframe, sqdbg_profvis_client_callgraph
//
// Manual:
// local out = sqdbg_prof_gets(0);
// g_ProfVis <- CProfVis();
// g_ProfVis.ParseOutput( out );
//
local g_bPerFrame = false;
local g_bProfOutType = 0;
local g_flThinkRate = 0.1;
if ( SERVER_DLL )
{
local NetMsg = NetMsg, split = split;
local function SendProfDataToClient( player )
{
sqdbg_prof_pause();
local out = sqdbg_prof_gets( g_bProfOutType );
if ( out )
{
if ( g_bPerFrame )
sqdbg_prof_reset();
local lines = split( out, "\n" );
local linecount = lines.len();
NetMsg.Start( "sqdbg_profvis_data_init" );
NetMsg.WriteShort( linecount - 1 );
NetMsg.Send( player, true );
for ( local i = 1; i < linecount; ++i )
{
NetMsg.Start( "sqdbg_profvis_data" );
NetMsg.WriteShort( i - 1 );
NetMsg.WriteString( lines[i] );
NetMsg.Send( player, true );
}
}
sqdbg_prof_resume();
return g_flThinkRate;
}
Convars.RegisterConvar( "sqdbg_profvis_server_perframe", "0", "", FCVAR_GAMEDLL );
Convars.SetChangeCallback( "sqdbg_profvis_server_perframe", function( cvar, szOld, flOld, szNew, flNew )
{
if ( flNew == 2.0 )
{
g_flThinkRate = 0.0;
}
else
{
g_flThinkRate = 0.1;
}
g_bPerFrame = !!flNew;
} );
Convars.RegisterConvar( "sqdbg_profvis_server_callgraph", "1", "", FCVAR_CLIENTDLL );
Convars.SetChangeCallback( "sqdbg_profvis_server_callgraph", function( cvar, szOld, flOld, szNew, flNew )
{
if ( flNew )
{
g_bProfOutType = 0;
}
else
{
g_bProfOutType = 1;
}
} );
Convars.RegisterConvar( "sqdbg_profvis_server", "0", "", FCVAR_GAMEDLL );
Convars.SetChangeCallback( "sqdbg_profvis_server", function( cvar, szOld, flOld, szNew, flNew )
{
local ent = Entities.GetLocalPlayer();
if ( !ent )
return;
if ( flNew )
{
ent.SetContextThink( "sqdbg_profvis_data", SendProfDataToClient, 0.1 );
}
else
{
ent.SetContextThink( "sqdbg_profvis_data", null, 0.0 );
NetMsg.Start( "sqdbg_profvis_data_init" );
NetMsg.WriteShort( 0 );
NetMsg.Send( player, true );
}
} );
}
if ( CLIENT_DLL )
{
local MOUSE_ENABLE_KEY = ButtonCode.KEY_F2;
local sqdbg_prof_pause = dummy, sqdbg_prof_resume = dummy;
local NetMsg = NetMsg, split = split, strip = strip, lstrip = lstrip, format = format;
local surface = surface, input = input, vgui = vgui;
local g_ServerLines = [];
if ( "g_ServerProfVis" in getroottable() && ::g_ServerProfVis )
::g_ServerProfVis.Destroy();
if ( "g_ClientProfVis" in getroottable() && ::g_ClientProfVis )
::g_ClientProfVis.Destroy();
::g_ServerProfVis <- null;
::g_ClientProfVis <- null;
if ( "g_ProfVisNodePool" in getroottable() )
{
::g_ProfVisNodePool.clear();
}
else
{
::g_ProfVisNodePool <- [];
}
local g_ClientProfVis = ::g_ClientProfVis,
g_ServerProfVis = ::g_ServerProfVis,
g_ProfVisNodePool = ::g_ProfVisNodePool;
local kAddrLen = 2 + _intsize_ * 2;
const kMarginX = 16;
const kMarginY = 24;
{
local CONST = getconsttable();
delete CONST.kMarginX;
delete CONST.kMarginY;
}
class ::CProfVis
{
static node_t = class
{
percentage = 0.0;
time = null;
calls = null;
funcname = null;
longname = null;
details = null;
basenode = null;
sub_end = null;
}
m_Panel = null;
m_ResetTree = null;
m_Nodes = null;
m_Width = 0;
m_Font = 0;
m_CharWidth = 0;
m_BarHeight = 0;
m_iMouseOver = null;
m_iMouseOverTmp = null;
m_SelectedTree = null;
m_iBaseNode = null;
m_flPercentageMultiplier = 1.0;
m_MouseX = 0;
m_MouseY = 0;
m_bCursorVisible = false;
constructor()
{
sqdbg_prof_pause();
m_Nodes = [];
m_SelectedTree = [];
InitFont();
m_Panel = vgui.CreatePanel( "Frame", vgui.GetRootPanel(), "" );
m_Panel.SetVisible( true );
m_Panel.SetMoveable( true )
m_Panel.SetSizeable( true )
m_Panel.SetTitleBarVisible( false )
m_Panel.SetCloseButtonVisible( false )
m_Panel.SetDeleteSelfOnClose( false )
m_Panel.SetCallback( "PerformLayout", PerformLayout.bindenv(this) );
m_Panel.SetCallback( "Paint", Draw.bindenv(this) );
m_Panel.SetCallback( "OnMousePressed", OnMousePressed.bindenv(this) );
m_Panel.MakePopup();
m_Panel.SetMouseInputEnabled( false );
m_Panel.SetKeyBoardInputEnabled( false );
m_Panel.SetPos( XRES(32), YRES(32) );
m_Panel.SetSize( XRES(640 - 32 * 2), kMarginY * 2 + m_BarHeight * 11 );
sqdbg_prof_resume();
}
function Destroy()
{
sqdbg_prof_pause();
m_Panel.Destroy();
m_Panel =
m_ResetTree =
m_SelectedTree =
m_iBaseNode =
m_iMouseOver = null;
ClearNodes( m_Nodes );
sqdbg_prof_resume();
}
function CreateTreeResetButton()
{
if ( m_ResetTree )
{
m_ResetTree.SetVisible( true );
}
else
{
m_ResetTree = vgui.CreatePanel( "Button", m_Panel, "" );
m_ResetTree.MakeReadyForUse();
PerformLayout_ResetButton();
m_ResetTree.SetVisible( true );
m_ResetTree.SetPaintBorderEnabled( true );
m_ResetTree.SetCallback( "DoClick", ResetTreeSelection.bindenv(this) );
}
}
function PerformLayout_ResetButton()
{
if ( m_ResetTree )
{
m_ResetTree.SetDefaultColor( 0, 0, 0, 0, 255, 200, 50, 127 );
m_ResetTree.SetDepressedColor( 0, 0, 0, 0, 255, 255, 255, 127 );
m_ResetTree.SetArmedColor( 0, 0, 0, 0, 255, 255, 255, 127 );
m_ResetTree.SetPos( kMarginX, kMarginY / 2 );
m_ResetTree.SetSize( m_BarHeight, m_BarHeight );
}
}
function SetTreeSelection( iNode )
{
local node = m_Nodes[ iNode ];
m_iBaseNode = iNode;
m_flPercentageMultiplier = 1.0 / node.percentage;
CreateTreeResetButton();
m_SelectedTree.clear();
do
{
local aidx = node.details.find( "0x" );
local addr = node.details.slice( aidx, aidx + kAddrLen );
m_SelectedTree.append( addr );
}
while ( node = node.basenode );
}
function ResetTreeSelection()
{
m_iBaseNode = null;
m_flPercentageMultiplier = 1.0;
m_ResetTree.SetVisible( false );
m_SelectedTree.clear();
}
function InitFont()
{
m_Font = surface.GetFont( "DefaultFixedOutline", false, "Tracker" );
m_CharWidth = surface.GetCharacterWidth( m_Font, 'A' );
m_BarHeight = surface.GetFontTall( m_Font ) + 2;
}
function PerformLayout()
{
sqdbg_prof_pause();
InitFont();
m_Panel.SetBgColor( 0, 0, 0, 31 );
m_Width = m_Panel.GetWide() - kMarginX * 2;
PerformLayout_ResetButton();
return sqdbg_prof_resume();
}
function NewNode()
{
if ( 0 in g_ProfVisNodePool )
{
local n = g_ProfVisNodePool.pop();
n.time =
n.funcname =
n.longname =
n.details =
n.basenode =
n.sub_end = null;
return n;
}
return node_t();
}
function ClearNodes( mem )
{
g_ProfVisNodePool.extend( mem );
return mem.clear();
}
function FindNode( mem, idx, end, addr )
{
for ( ; idx < end; ++idx )
{
local n = mem[idx];
if ( n.details.find( addr ) != null )
return idx;
}
}
function ParseOutput( lines )
{
sqdbg_prof_pause();
ClearNodes( m_Nodes );
if ( 0 in lines )
{
switch ( typeof lines )
{
case "string":
{
local i = lines[3] == '%' ? lines.find( "\n" ) + 1 : 0;
while ( i in lines )
i = ParseLines( m_Nodes, null, lines, i );
break;
}
case "array":
{
local i = ( lines[0][3] == '%' ).tointeger();
while ( i in lines )
i = ParseLineSplit( m_Nodes, null, lines, i );
break;
}
default: throw "invalid";
}
if ( m_iBaseNode != null )
{
local iSel = m_SelectedTree.len() - 1;
local iEnd = m_Nodes.len();
local iNode = -1;
local node;
do
{
local addr = m_SelectedTree[ iSel ];
iNode = FindNode( m_Nodes, iNode + 1, iEnd, addr );
if ( iNode != null )
{
node = m_Nodes[ iNode ];
iEnd = node.sub_end;
}
else
{
break;
}
}
while ( iSel-- && iEnd );
if ( iNode != null )
{
m_iBaseNode = iNode;
m_flPercentageMultiplier = 1.0 / node.percentage;
}
else
{
ResetTreeSelection();
}
}
m_iMouseOver = null;
}
return sqdbg_prof_resume();
}
function ParseLineSplit( nodes, baseNode, lines, i )
{
for (;;)
{
local node = NewNode();
nodes.append( node );
local line = lines[i];
if ( line.find( " N/A" ) != 0 )
node.percentage = line.slice( 0, 6 ).tofloat() * 0.01;
local time = node.time = strip( line.slice( 19, 28 ) );
if ( line.find( " N/A", 29 ) != 29 )
node.calls = lstrip( line.slice( 29, 39 ) );
local fidx = 41;
while ( fidx in line && line[ fidx ] == '|' )
fidx += 3;
--fidx;
local extra = ( line[ fidx ] >= '1' && line[ fidx ] <= '9' ).tointeger();
++fidx;
local funcname = node.funcname = line.slice( fidx - extra, line.find( ",", fidx - extra + 1 ) );
node.details = line.slice( fidx - extra, line.len() );
node.longname = format( "%s (%s)", funcname, time );
node.basenode = baseNode;
// next line
++i;
if ( i in lines )
{
line = lines[i];
// Has subcalls
if ( fidx in line && line[ fidx ] == '|' )
{
i = ParseLineSplit( nodes, node, lines, i );
node.sub_end = nodes.len();
if ( i in lines )
{
line = lines[i];
}
else
{
break;
}
}
fidx -= 3;
// Last call in tree
if ( !( fidx in line && line[ fidx ] == '|' ) )
break;
}
else
{
break;
}
}
return i;
}
function ParseLines( nodes, baseNode, lines, iBase )
{
for (;;)
{
local node = NewNode();
nodes.append( node );
local end = lines.find( "\n", iBase );
if ( end == null )
end = lines.len();
if ( lines.find( " N/A", iBase ) != iBase )
node.percentage = lines.slice( iBase, iBase + 6 ).tofloat() * 0.01;
iBase += 19;
local time = node.time = strip( lines.slice( iBase, iBase + 9 ) );
iBase += 10;
if ( lines.find( " N/A", iBase ) != iBase )
node.calls = lstrip( lines.slice( iBase, iBase + 10 ) );
iBase += 12;
local fidx = 41;
while ( iBase in lines && lines[ iBase ] == '|' )
{
iBase += 3;
fidx += 3;
}
--iBase;
local extra = ( lines[ iBase ] >= '1' && lines[ iBase ] <= '9' ).tointeger();
++iBase;
local funcname = node.funcname = lines.slice( iBase - extra, lines.find( ",", iBase - extra + 1 ) );
node.details = lines.slice( iBase - extra, end );
node.longname = format( "%s (%s)", funcname, time );
node.basenode = baseNode;
// next line
iBase = end + 1;
if ( iBase in lines )
{
// Has subcalls
if ( iBase + fidx in lines && lines[ iBase + fidx ] == '|' )
{
iBase = ParseLines( nodes, node, lines, iBase );
node.sub_end = nodes.len();
if ( !( iBase in lines ) )
break;
}
fidx -= 3;
// Last call in tree
if ( !( iBase + fidx in lines && lines[ iBase + fidx ] == '|' ) )
break;
}
else
{
break;
}
}
return iBase;
}
// returns int32 [ index (16), xpos (16) ]
function DrawNode( i, x, y )
{
local node = m_Nodes[i];
local barWidth = ( m_Width * node.percentage * m_flPercentageMultiplier + 0.5 ).tointeger();
if ( barWidth < 2 )
return i << 16;
if ( m_bCursorVisible &&
m_MouseX >= x && m_MouseX < x + barWidth &&
m_MouseY >= y && m_MouseY < y + m_BarHeight - 1 )
{
m_iMouseOverTmp = i;
surface.SetColor( 255, 255, 255, 255 );
surface.DrawOutlinedRect( x, y, barWidth, m_BarHeight, 2 );
}
else
{
surface.SetColor( 195, 195, 195, 255 );
surface.DrawOutlinedRect( x, y, barWidth, m_BarHeight, 1 );
}
barWidth -= 2;
surface.SetColor( 195, 100, 100, 63 );
surface.DrawFilledRect( x + 1, y + 1, barWidth, m_BarHeight - 2 );
surface.SetTextPos( x + 2, y + 2 );
local text = node.longname;
local textWidth = m_CharWidth * text.len();
if ( textWidth > barWidth )
{
text = node.funcname;
textWidth = m_CharWidth * text.len();
}
if ( textWidth <= barWidth )
{
surface.DrawText( text, 0 );
}
else if ( 1 in text && m_CharWidth * 5 <= barWidth )
{
surface.DrawUnicodeChar( text[0], 0 );
surface.DrawUnicodeChar( text[1], 0 );
surface.DrawText( "...", 0 );
}
local end = node.sub_end;
if ( end ) for ( ++i;; )
{
local ret = DrawNode( i, x, y + m_BarHeight - 1 );
x += ret & 0x0000ffff;
i = ( ret & 0xffff0000 ) >>> 16;
if ( ++i >= end )
{
--i;
break;
}
}
return ( barWidth + 1 ) | ( i << 16 );
}
function Draw()
{
sqdbg_prof_pause();
if ( m_bCursorVisible )
{
if ( !input.IsButtonDown( MOUSE_ENABLE_KEY ) )
{
m_Panel.SetMouseInputEnabled( m_bCursorVisible = false );
}
}
else
{
if ( input.IsButtonDown( MOUSE_ENABLE_KEY ) )
{
m_Panel.SetMouseInputEnabled( m_bCursorVisible = true );
m_Panel.MoveToFront();
input.SetCursorPos(
m_Panel.GetXPos() + m_Panel.GetWide() / 2,
m_Panel.GetYPos() + m_Panel.GetTall() / 2 );
}
}
local x = kMarginX, y = kMarginY * 1.5;
m_MouseX = input.GetAnalogValue( AnalogCode.MOUSE_X ) - m_Panel.GetXPos();
m_MouseY = input.GetAnalogValue( AnalogCode.MOUSE_Y ) - m_Panel.GetYPos();
surface.SetTextFont( m_Font );
surface.SetTextColor( 255, 255, 255, 255 );
if ( m_iBaseNode != null )
{
DrawNode( m_iBaseNode, x, y );
}
else
{
local i = 0;
local count = m_Nodes.len();
for ( ; i < count; ++i )
{
local ret = DrawNode( i, x, y );
x += ret & 0x0000ffff;
i = ( ret & 0xffff0000 ) >>> 16;
}
}
if ( m_iMouseOverTmp != null )
{
local node = m_Nodes[ m_iMouseOverTmp ];
local h = surface.GetFontTall( m_Font );
local w = node.details.len() * m_CharWidth;
local mx = m_MouseX;
local my = m_MouseY;
local pw = m_Panel.GetWide() - w;
if ( mx > pw )
mx = pw;
surface.SetColor( 100, 100, 100, 195 );
surface.DrawFilledRect( mx, my, w, h * 3 + 3 );
surface.SetTextPos( mx + 2, my + 2 );
surface.DrawText( node.details, 0 );
surface.SetTextPos( mx + 2, my + 2 + h + 1 );
surface.DrawText( node.time, 0 );
surface.SetTextPos( mx + 2, my + 2 + h + h + 2 );
surface.DrawText( node.calls, 0 );
m_iMouseOver = m_iMouseOverTmp;
m_iMouseOverTmp = null;
}
else
{
m_iMouseOver = null;
}
return sqdbg_prof_resume();
}
function OnMousePressed( code )
{
if ( code == ButtonCode.MOUSE_LEFT )
{
if ( m_iMouseOver != null )
{
return SetTreeSelection( m_iMouseOver );
}
}
}
}
local function RecvProfVisDataLen()
{
sqdbg_prof_pause();
local size = NetMsg.ReadShort();
g_ServerLines.clear();
g_ServerLines.resize( size );
if ( size )
{
if ( !g_ServerProfVis )
g_ServerProfVis = CProfVis();
}
else if ( g_ServerProfVis )
{
g_ServerProfVis.Destroy();
}
return sqdbg_prof_resume();
}
local function RecvProfVisData()
{
sqdbg_prof_pause();
local index = NetMsg.ReadShort();
local line = NetMsg.ReadString();
if ( 0 in g_ServerLines )
{
g_ServerLines[ index ] = line;
if ( index == g_ServerLines.len() - 1 )
g_ServerProfVis.ParseOutput( g_ServerLines );
}
return sqdbg_prof_resume();
}
NetMsg.Receive( "sqdbg_profvis_data_init", RecvProfVisDataLen );
NetMsg.Receive( "sqdbg_profvis_data", RecvProfVisData );
local function ThinkProfVis(_)
{
sqdbg_prof_pause();
local out = sqdbg_prof_gets( g_bProfOutType );
if ( out )
{
if ( g_bPerFrame )
sqdbg_prof_reset();
g_ClientProfVis.ParseOutput( out );
}
sqdbg_prof_resume();
return g_flThinkRate;
}
Convars.RegisterConvar( "sqdbg_profvis_client_perframe", "0", "", FCVAR_CLIENTDLL );
Convars.SetChangeCallback( "sqdbg_profvis_client_perframe", function( cvar, szOld, flOld, szNew, flNew )
{
if ( flNew == 2.0 )
{
g_flThinkRate = 0.0;
}
else
{
g_flThinkRate = 0.1;
}
g_bPerFrame = !!flNew;
} );
Convars.RegisterConvar( "sqdbg_profvis_client_callgraph", "1", "", FCVAR_CLIENTDLL );
Convars.SetChangeCallback( "sqdbg_profvis_client_callgraph", function( cvar, szOld, flOld, szNew, flNew )
{
if ( flNew )
{
g_bProfOutType = 0;
}
else
{
g_bProfOutType = 1;
}
} );
Convars.RegisterConvar( "sqdbg_profvis_client", "0", "", FCVAR_CLIENTDLL );
Convars.SetChangeCallback( "sqdbg_profvis_client", function( cvar, szOld, flOld, szNew, flNew )
{
local ent = Entities.First();
if ( !ent )
return;
if ( flNew )
{
ent.SetContextThink( "sqdbg_profvis_data", ThinkProfVis, 0.1 );
if ( !g_ClientProfVis )
g_ClientProfVis = CProfVis();
}
else
{
ent.SetContextThink( "sqdbg_profvis_data", null, 0.0 );
if ( g_ClientProfVis )
{
g_ClientProfVis.Destroy();
g_ClientProfVis = null;
}
}
} );
Convars.RegisterConvar( "sqdbg_profvis_exclude_self", "1", "", FCVAR_CLIENTDLL );
Convars.SetChangeCallback( "sqdbg_profvis_exclude_self", function( cvar, szOld, flOld, szNew, flNew )
{
if ( flNew )
{
if ( !( "sqdbg_prof_pause" in getroottable() ) )
return Convars.SetInt( cvar, 0 );
sqdbg_prof_pause = ::sqdbg_prof_pause;
sqdbg_prof_resume = ::sqdbg_prof_resume;
return;
}
sqdbg_prof_pause = ::dummy;
sqdbg_prof_resume = ::dummy;
} );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment