csgo-2018-source/devtools/memlog/memlog.cpp
2021-07-24 21:11:47 -07:00

2518 lines
79 KiB
C++

// memlog.cpp : mines memory data from vxconsole logs (see game/client/c_memorylog.cpp)
#include "utlbuffer.h"
#include <conio.h>
#include <math.h>
#include <iostream>
#include <tchar.h>
#include <assert.h>
#include <fstream>
#include <sstream>
#include <vector>
#include <map>
#define _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS
#include <hash_map>
#include <algorithm>
#include "windows.h"
using namespace std;
using namespace stdext;
// We write this to output files on conversion, and test it on read to see if files need re-converting:
static const int MEMLOG_VERSION = 4;
enum DVDHosted_t
{
// Is the game running FULLY off the DVD? (i.e. it reads no data from the HDD - no texture streaming)
// If so, it will use more memory for audio/anim/texture data.
DVDHOSTED_UNKNOWN = 0,
DVDHOSTED_YES,
DVDHOSTED_NO
};
enum Platform_t
{
PLATFORM_UNKNOWN = 0,
PLATFORM_360 = 1,
PLATFORM_PS3 = 2,
PLATFORM_PC = 3
};
const char *gPlatformNames[] = { "unknown", "360", "PS3", "PC" };
enum TrackStat_t
{
TRACKSTAT_UNKNOWN = 0,
TRACKSTAT_MINFREE_CPU = 1,
TRACKSTAT_MINFREE_GPU = 2
};
// TODO: these will eventually vary by platform (certainly, these numbers are not useful on PC)
#define CPU_MEM_SIZE 512
#define GPU_MEM_SIZE 256
static const int MAX_LANG_LEN = 64;
struct CHeaderInfo
{
CHeaderInfo() : memlogVersion( 0 ), dvdHosted( DVDHOSTED_UNKNOWN ), buildNumber(-1), platform( PLATFORM_UNKNOWN ) { language[0] = 0; }
int memlogVersion;
DVDHosted_t dvdHosted;
int buildNumber;
Platform_t platform;
char language[MAX_LANG_LEN];
};
// Bitfield type used to filter logs w.r.t subsets of maps/players
class FilterBitfield
{
public:
FilterBitfield( void ) { for ( int i = 0; i < NUM_INT64S; i++ ) bits[i] = 0; }
void SetAll( void ) { for ( int i = 0; i < NUM_INT64S; i++ ) bits[i] = -1; }
void Set( int bit ) { bits[bit/64] |= ((__int64)1) << (bit&63); }
bool IsSet( int bit ) { return !!( bits[bit/64] & ((__int64)1) << (bit&63) ); }
bool Intersects( const FilterBitfield &other ) { __int64 result = 0; for ( int i = 0; i < NUM_INT64S; i++ ) result |= ( bits[i] & other.bits[i] ); return !!result; }
bool operator ! ( void ) const { __int64 result = 0; for ( int i = 0; i < NUM_INT64S; i++ ) result |= bits[i]; return !result; }
static const int NUM_INT64S = 4;
__int64 bits[NUM_INT64S];
};
class CStringList
{
public:
static const int MAX_STRINGLIST_ENTRIES = 64*FilterBitfield::NUM_INT64S;
~CStringList( void )
{
for ( int i = 0; i < m_Strings.Count(); i++ ) delete m_Strings[ i ];
}
int AddString( const char *string )
{
int existingIndex = StringToInt( string );
if ( existingIndex != -1 )
return existingIndex;
if ( m_Strings.Count() == MAX_STRINGLIST_ENTRIES )
{
Assert( 0 );
printf( "----ERROR: more than %d strings added to stringlist (e.g. '%s'), have to increase the size of FilterBitfield.bits!\n\n", MAX_STRINGLIST_ENTRIES, m_Strings[0] );
return -1;
}
return m_Strings.AddToTail( strdup( string ) );
}
int StringToInt( const char *string )
{
// TODO: make this search faster (BUT we still need to be able to index into m_Strings and FilterBitfields... storing CUtlSymbolTable indices in m_String could work)
for ( int i = 0; i < m_Strings.Count(); i++ )
{
if ( !stricmp( string, m_Strings[ i ] ) )
return i;
}
return -1;
}
FilterBitfield SubstringToBitfield( const char *subString )
{
FilterBitfield bitField;
if ( !subString || !subString[0] )
{
bitField.SetAll();
}
else
{
for ( int i = 0; i < m_Strings.Count(); i++ )
{
if ( V_stristr( m_Strings[ i ], subString ) )
bitField.Set( i );
}
}
return bitField;
}
private:
CUtlVector< const char *>m_Strings;
};
struct MapMin_t // See CLogFile::badMaps
{
int map; // Map number
float minMem; // Minimum memory observed in this map
};
struct CItem
{
int time;
float freeMem;
float gpuFree;
int globalMapIndex;
int logMapIndex;
int numBots;
int numPlayers;
int numLocalPlayers;
int numSpecators;
bool isServer;
FilterBitfield playerBitfield; // Bits represent items in CLogFile::playerList
};
struct CLogFile
{
public:
CLogFile( void ) {}
CUtlVector<CItem> entries;
string memoryLog; // Path of the source memorylog file
string consoleLog; // Path of the source vxconsole file
ULONGLONG modifyTime; // Last modification time of the source vxconsole file
bool isActive; // Last time we checked, the vxconsole file was still being written
float minFreeMem; // Minimum free memory in any entry in this log
float minGPUFreeMem; // Minimum free GPU memory in any entry in this log
bool isServer; // True if the local box is the Server for any entry in this log
bool isSplitscreen; // True if the local box is running splitscreen (>= 2 local players) for any entry in this log
int maxPlayers; // Maximum players in any entry in this log
CStringList mapList; // The maps present in this log
CStringList playerList; // The players present in this log
float memorylogTick; // Seconds between entries in this log
CHeaderInfo headerInfo; // Various global data about the log (stuff that can't be computed from 'entries')
CUtlVector<MapMin_t>badMaps; // A temp list that accumulates maps in this log which pass the current filters
private:
CLogFile( const CLogFile &other ) {}
};
struct CLogStats
{
CLogStats( void )
{
for ( int i = 0; i < CStringList::MAX_STRINGLIST_ENTRIES; i++ )
{
mapMin[ i ] = CPU_MEM_SIZE;
mapGPUMin[ i ] = GPU_MEM_SIZE;
mapAverage[ i ] = 0.0f;
mapSeconds[ i ] = 0;
mapSamples[ i ] = 0;
}
}
float mapMin[ CStringList::MAX_STRINGLIST_ENTRIES ];
float mapGPUMin[ CStringList::MAX_STRINGLIST_ENTRIES ];
float mapAverage[ CStringList::MAX_STRINGLIST_ENTRIES ];
float mapSeconds[ CStringList::MAX_STRINGLIST_ENTRIES ];
unsigned int mapSamples[ CStringList::MAX_STRINGLIST_ENTRIES ];
};
static const char *gKnownMaps[] = { "none",
/*"credits", */
/* L4D1
"l4d_hospital01_apartment",
"l4d_hospital02_subway",
"l4d_hospital03_sewers",
"l4d_hospital04_interior",
"l4d_hospital05_rooftop",
"l4d_airport01_greenhouse",
"l4d_airport02_offices",
"l4d_airport03_garage",
"l4d_airport04_terminal",
"l4d_airport05_runway",
"l4d_farm01_hilltop",
"l4d_farm02_traintunnel",
"l4d_farm03_bridge",
"l4d_farm04_barn",
"l4d_farm05_cornfield",
"l4d_smalltown01_caves",
"l4d_smalltown02_drainage",
"l4d_smalltown03_ranchhouse",
"l4d_smalltown04_mainstreet",
"l4d_smalltown05_houseboat",
"l4d_vs_hospital01_apartment",
"l4d_vs_hospital02_subway",
"l4d_vs_hospital03_sewers",
"l4d_vs_hospital04_interior",
"l4d_vs_hospital05_rooftop",
"l4d_vs_airport01_greenhouse",
"l4d_vs_airport02_offices",
"l4d_vs_airport03_garage",
"l4d_vs_airport04_terminal",
"l4d_vs_airport05_runway",
"l4d_vs_farm01_hilltop",
"l4d_vs_farm02_traintunnel",
"l4d_vs_farm03_bridge",
"l4d_vs_farm04_barn",
"l4d_vs_farm05_cornfield",
"l4d_vs_smalltown01_caves",
"l4d_vs_smalltown02_drainage",
"l4d_vs_smalltown03_ranchhouse",
"l4d_vs_smalltown04_mainstreet",
"l4d_vs_smalltown05_houseboat",
"backgroundstreet", */
/* L4D2
"c1m1_hotel",
"c1m2_streets",
"c1m3_mall",
"c1m4_atrium",
"c2m1_highway",
"c2m2_fairgrounds",
"c2m3_coaster",
"c2m4_barns",
"c2m5_concert",
"c3m1_plankcountry",
"c3m2_swamp",
"c3m3_shantytown",
"c3m4_plantation",
"c4m1_milltown_a",
"c4m2_sugarmill_a",
"c4m3_sugarmill_b",
"c4m4_milltown_b",
"c4m5_milltown_escape",
"c5m1_waterfront",
"c5m2_park",
"c5m3_cemetery",
"c5m4_quarter",
"c5m5_bridge", */
/* // Portal 2 SP
"sp_a1_intro1",
"sp_a1_intro2",
"sp_a1_intro3",
"sp_a1_intro4",
"sp_a1_intro5",
"sp_a1_intro6",
"sp_a1_intro7",
"sp_a1_wakeup",
"sp_a2_intro",
"sp_a2_laser_intro",
"sp_a2_laser_stairs",
"sp_a2_dual_lasers",
"sp_a2_laser_over_goo",
"sp_a2_catapult_intro",
"sp_a2_trust_fling",
"sp_a2_pit_flings",
"sp_a2_fizzler_intro",
"sp_a2_sphere_peek",
"sp_a2_ricochet",
"sp_a2_bridge_intro",
"sp_a2_bridge_the_gap",
"sp_a2_turret_intro",
"sp_a2_laser_relays",
"sp_a2_turret_blocker",
"sp_a2_laser_vs_turret",
"sp_a2_pull_the_rug",
"sp_a2_column_blocker",
"sp_a2_laser_chaining",
"sp_a2_turret_tower",
"sp_a2_triple_laser",
"sp_a2_bts1",
"sp_a2_bts2",
"sp_a2_bts3",
"sp_a2_bts4",
"sp_a2_bts5",
"sp_a2_bts6",
"sp_a2_core",
"sp_a3_00",
"sp_a3_01",
"sp_a3_03",
"sp_a3_jump_intro",
"sp_a3_bomb_flings",
"sp_a3_crazy_box",
"sp_a3_transition01",
"sp_a3_speed_ramp",
"sp_a3_speed_flings",
"sp_a3_portal_intro",
"sp_a3_end",
"sp_a4_intro",
"sp_a4_tb_intro",
"sp_a4_tb_trust_drop",
"sp_a4_tb_wall_button",
"sp_a4_tb_polarity",
"sp_a4_tb_catch",
"sp_a4_stop_the_box",
"sp_a4_laser_catapult",
"sp_a4_laser_platform",
"sp_a4_speed_tb_catch",
"sp_a4_jump_polarity",
"sp_a4_finale1",
"sp_a4_finale2",
"sp_a4_finale3",
"sp_a4_finale4",
// Portal 2 Co-op
"mp_coop_start",
"mp_coop_lobby_2",
"mp_coop_doors",
"mp_coop_race_2",
"mp_coop_laser_2",
"mp_coop_rat_maze",
"mp_coop_laser_crusher",
"mp_coop_teambts",
"mp_coop_fling_3",
"mp_coop_infinifling_train",
"mp_coop_come_along",
"mp_coop_fling_1",
"mp_coop_catapult_1",
"mp_coop_multifling_1",
"mp_coop_fling_crushers",
"mp_coop_fan",
"mp_coop_wall_intro",
"mp_coop_wall_2",
"mp_coop_catapult_wall_intro",
"mp_coop_wall_block",
"mp_coop_catapult_2",
"mp_coop_turret_walls",
"mp_coop_turret_ball",
"mp_coop_wall_5",
"mp_coop_tbeam_redirect",
"mp_coop_tbeam_drill",
"mp_coop_tbeam_catch_grind_1",
"mp_coop_tbeam_laser_1",
"mp_coop_tbeam_polarity",
"mp_coop_tbeam_polarity2",
"mp_coop_tbeam_polarity3",
"mp_coop_tbeam_maze",
"mp_coop_tbeam_end",
"mp_coop_paint_come_along",
"mp_coop_paint_redirect",
"mp_coop_paint_bridge",
"mp_coop_paint_walljumps",
"mp_coop_paint_speed_fling",
"mp_coop_paint_red_racer",
"mp_coop_paint_speed_catch",
"mp_coop_paint_longjump_intro",
"mp_coop_separation_1",
"mp_coop_rocket_block",
"mp_coop_race_3",
"mp_coop_laser_1",
"mp_coop_wall_1",
"mp_coop_2guns_longjump_intro",
"mp_coop_paint_crazy_box",
"mp_coop_wall_6",
"mp_coop_button_tower",
"mp_coop_tbeam_fling_float_1",
// Portal 2 demo
"demo_intro",
"demo_paint",
"demo_underground", */
// CSGO
"cs_italy",
"cs_office",
"de_aztec",
"de_dust2",
"de_dust",
"de_inferno",
"de_nuke",
"de_lake",
"de_safehouse",
"de_shorttrain"
"de_sugarcane",
"de_stmarc",
"de_bank",
"ar_shoots",
"ar_baggage",
"de_train",
"training1",
};
const int gNumKnownMaps = ARRAYSIZE( gKnownMaps );
static const char *gIgnoreMaps[] = {"devtest",
/*"test_box",
"test_box2",
"nav_test",
"transition_test01",
"transition_test02",
"c2m4_concert",
"c5m2_cemetery",
"c5m3_quarter",
"c5m4_bridge"*/ };
const int gNumIgnoreMaps = ARRAYSIZE( gIgnoreMaps );
CUtlVector< const char * > gMapNames; // List of encountered map names
typedef hash_map<string, int> CMapHash; // For quickly determining is a string is in gMapNames
CMapHash gMapHash;
typedef hash_map<string, CLogFile*> CLogFiles;
const int FILTER_SIZE = 64;
struct Config
{
char sourcePath[ _MAX_PATH ]; // Where we're getting logs from
char prevCommandLine[ _MAX_PATH ]; // Previously entered command
bool recurse; // Recurse the tree under 'sourcePath'
bool update; // Re-convert vxconsole logs, if they're newer than their corresponding memorylogs
bool updateActive; // Re-convert vxconsole logs,
bool forceUpdate; // Re-convert vxconsole logs, even if they've been converted before
// Console mode:
bool consoleMode; // Accept commands from the user 'till they quit
bool load; // Load new logs from 'sourcePath'
bool unload; // Unload all logs from 'sourcePath'
bool unloadAll; // Unload all logs
bool quitting; // We're done, exit the app
bool help; // User wants to see the help text
// Memory tracking (CSV files to plot memory stats over time)
char trackFile[MAX_PATH]; // Tracking file to update from the current filter set
char trackColumn[32]; // New column to add to the tracking file
TrackStat_t trackStat; // Stat to track (min free CPU memory, min free GPU memory...)
// Data analysis filters (per-log-entry)
float dangerLimit; // Spew log entries in which memory drops below this limit (in MB)
int minPlayers; // Spew log entries with at least this many concurrent players
int maxPlayers; // Spew log entries with at most this many concurrent players
bool isSplitscreen; // Spew log entries in which the machine has more than one local player
bool isSinglescreen; // Spew log entries in which the machine has AT MOST one local player
char mapFilter[FILTER_SIZE]; // Spew log entries with map names which contain this substring
char playerFilter[FILTER_SIZE]; // Spew log entries with player names which contain this substring
// Data analysis filters (per-log-file)
int dangerTime; // Spew logs in which memory drops below the danger limit within this many seconds
int duration; // Spew logs in which the timer reaches this many seconds
int minAge; // Spew logs updated at least this many seconds ago
int maxAge; // Spew logs updated at most this many seconds ago
bool isServer; // Spew logs in which the machine is a listen server at least once
bool isClient; // Spew logs in which the machine is NEVER a listen server
bool isActive; // Spew logs currently being written
DVDHosted_t dvdHosted; // Spew logs matching this DVD hosted state (UNKNOWN means accept all)
char languageFilter[FILTER_SIZE]; // Spew logs with a language which contain this substring
Platform_t platform; // Spew logs generated from the specified platform (UNKNOWN means accept all)
};
Config gConfig;
// 10 million 100-nanosecond intervals per second in FILETIME
const ULONGLONG ONE_SECOND = 10000000LL;
int AddNewMapName( const char *name )
{
char *nameCopy = strdup( name ); // Leak
strlwr( nameCopy );
if ( gMapHash.find( string( nameCopy ) ) != gMapHash.end() )
{
printf( "ERROR: duplicate map name in gMapNames!!!! (%s)\n", nameCopy );
printf( "Aborting... press any key to exit\n" );
DebuggerBreakIfDebugging();
getchar();
exit( 1 );
}
int index = gMapNames.AddToTail( nameCopy );
gMapHash[ string( nameCopy ) ] = index;
return index;
}
bool ParseLineForDVDHostedInfo( string &line, CHeaderInfo &headerInfo )
{
// Game spews text like this on startup:
//
// Xbox Launched From DVD.
// Install Status:
// Version: 746360 (english) (Xbox|PS3|PC)
// DVD Hosted: Enabled <---- this is the important line
// Progress: 0/1200 MB
// Active Image: 746360
//
// We use that to determine if we are running SOLELY off the DVD (no HDD access, no texture streaming),
// in which case textures/audio/anims take more memory, so the minimum free memory watermark is lower.
// UPDATE: these memory effects apply to L4D/L4D2 but not PORTAL2 (it doesn't do texture streaming)
if ( headerInfo.dvdHosted == DVDHOSTED_UNKNOWN )
{
const char *cursor = line.c_str();
if ( strstr( cursor, "Device\\Harddisk" ) )
{
// Running from the HDD (this test works for old images without the "DVD Hosted:" spew)
headerInfo.dvdHosted = DVDHOSTED_NO;
return true;
}
else if ( strstr( cursor, "DVD Hosted:" ) )
{
headerInfo.dvdHosted = strstr( cursor, "Enabled" ) ? DVDHOSTED_YES : DVDHOSTED_NO;
return true;
}
}
return false;
}
bool ParseLineForBuildNumberLanguageAndPlatformInfo( string &line, CHeaderInfo &headerInfo )
{
// Game spews text like this on startup:
//
// Xbox Launched From DVD.
// Install Status:
// Version: 746360 (english) (Xbox|PS3|PC) <---- this is the important line
// DVD Hosted: Enabled
// Progress: 0/1200 MB
// Active Image: 746360
//
// We use that to determine current build number and language.
if ( headerInfo.buildNumber == -1 )
{
const char *cursor = line.c_str();
cursor = strstr( cursor, "Version:" );
if ( cursor )
{
static char language[1024], platform[1024];
if ( 3 == sscanf( cursor, "Version: %d (%s (%s", &headerInfo.buildNumber, &(language[0]), &(platform[0]) ) )
{
strncpy( &(headerInfo.language[0]), &(language[0]), MAX_LANG_LEN );
char *closingBrace = strstr( headerInfo.language, ")" );
if ( closingBrace ) *closingBrace = 0;
strlwr( &( headerInfo.language[0] ) );
closingBrace = strstr( platform, ")" );
if ( closingBrace ) *closingBrace = 0;
if ( !stricmp( platform, "Xbox" ) )
headerInfo.platform = PLATFORM_360;
else if ( !stricmp( platform, "PS3" ) )
headerInfo.platform = PLATFORM_PS3;
else if ( !stricmp( platform, "PC" ) )
headerInfo.platform = PLATFORM_PC;
return true;
}
headerInfo.buildNumber = -1;
headerInfo.language[0] = 0;
headerInfo.platform = PLATFORM_UNKNOWN;
}
}
return false;
}
bool ParseLineForHeaderInfo( string &line, CHeaderInfo &headerInfo )
{
if ( ParseLineForDVDHostedInfo( line, headerInfo ) )
return true;
if ( ParseLineForBuildNumberLanguageAndPlatformInfo( line, headerInfo ) )
return true;
return false;
}
int ConvertItem( string &line, CItem &result, const CItem &prevItem, CHeaderInfo *headerInfo, const char *fileName, int lineNum, vector<int> *truncated, CStringList *mapList, CStringList *playerList, bool skipIgnored )
{
// Accumulate header lines as we go through the log
if ( headerInfo && ParseLineForHeaderInfo( line, *headerInfo ) )
return -1;
// Special-case 'out of memory' lines:
const char *start = strstr( line.c_str(), "*** OUT OF MEMORY!" );
if ( start )
{
// Replace this with a fake "zero memory free" line
static char fakeLine[1024];
_snprintf( fakeLine, sizeof( fakeLine ), "[MEMORYLOG] Time:%6d | Free:%6.2f | GPU:%6.2f | %s | Map: %-32s | Bots:%2d | Players: 0",
prevItem.time, 0.0f, 0.0f, prevItem.isServer ? "Server" : "Client",
( ( prevItem.globalMapIndex < 0 ) ? "none" : gMapNames[ prevItem.globalMapIndex ] ), prevItem.numBots );
printf( "---- OUT OF MEMORY on line %d, in %s\n\n", lineNum, fileName );
line = fakeLine;
}
start = strstr( line.c_str(), "[MEMORYLOG] " );
if ( !start )
return -1;
// Get rid of line endings returned by GetLine()
while ( ( line[ line.size() - 1 ] == '\r' ) || ( line[ line.size() - 1 ] == '\n' ) )
line.resize( line.size() - 1 );
int time;
float freeMem;
float GPUFree;
char hostType[33];
char mapName[33];
int numBots;
int numPlayers;
CMapHash::iterator it;
int numRead = sscanf( start,
"[MEMORYLOG] Time:%d | Free: %f | GPU: %f | %s | Map: %s | Bots: %d | Players: %d",
&time,
&freeMem,
&GPUFree,
hostType,
mapName,
&numBots,
&numPlayers );
if ( numRead != 7 )
goto error;
bool isServer;
if ( !stricmp( hostType, "Server" ) )
isServer = true;
else if ( !stricmp( hostType, "Client" ) )
isServer = false;
else
goto error;
int globalMapIndex = -1;
strlwr( mapName );
it = gMapHash.find( mapName );
if ( it != gMapHash.end() )
{
globalMapIndex = it->second;
if ( skipIgnored && ( globalMapIndex < gNumIgnoreMaps ) )
return -1;
}
else
{
// Add the new name to gMapNames and gMapHash
globalMapIndex = AddNewMapName( mapName );
// This message notifies us when new maps appear on the horizon (e.g. 'credits'!)
printf( "----WARNING: unrecognised map name '%s' on line %d, in %s\n\n", mapName, lineNum, fileName );
}
// Also add the map name to a per-logfile list
int logMapIndex = mapList ? mapList->AddString( mapName ) : -1;
// Iterate over players
int numLocalPlayers = 0, numSpectators = 0, numNamedBots = 0;
const char *playerStart = start;
for ( int i = 0; i < numPlayers; i++ )
{
playerStart = strstr( playerStart, ", " );
if ( !playerStart )
goto playerError;
playerStart += 2;
const char *playerEnd = playerStart;
while ( playerEnd[0] && ( playerEnd[0] != '|' ) && ( playerEnd[0] != ',' ) ) playerEnd++;
if ( playerList )
{
// Build a vector of player name references
string playerName( playerStart, ( playerEnd - playerStart ) );
strlwr( (char*)playerName.c_str() );
int playerIndex = playerList->AddString( playerName.c_str() );
result.playerBitfield.Set( playerIndex );
}
// Track how many local/spectator players there are
while ( playerEnd[0] == '|' )
{
playerEnd++;
if ( !strncmp( playerEnd, "LOCAL", 5 ) )
{
numLocalPlayers++;
playerEnd += 5;
}
else if ( !strncmp( playerEnd, "SPEC", 4 ) )
{
numSpectators++;
playerEnd += 4;
}
else if ( !strncmp( playerEnd, "BOT", 3 ) )
{
numNamedBots++;
playerEnd += 3;
}
else
goto playerError;
if ( playerEnd[0] && ( playerEnd[0] != ',' ) && ( playerEnd[0] != '|' ) && ( playerEnd[0] != ' ' ) )
goto playerError;
}
}
Assert( !numNamedBots || ( numNamedBots == numBots ) );
if ( numNamedBots && ( numNamedBots != numBots ) )
goto playerError;
if ( false )
{
playerError:
// Don't quit on errors in the player list, keep the data we managed to get (usually truncated lines in versus games)
if ( strlen( line.c_str() ) == 264 )
{
// TODO: Somewhere, lines are getting capped at 264 chars (don't think it's in VXConsole, it's max command len is 4096)
if ( truncated ) truncated->push_back( lineNum );
}
else
{
printf( "----ERROR: unrecognised [MEMORYLOG] syntax on line %d, in %s\n", lineNum, fileName );
printf( " \"%s\"\n\n", line.c_str() );
}
}
result.freeMem = freeMem;
result.gpuFree = GPUFree;
result.time = time;
result.globalMapIndex = globalMapIndex;
result.logMapIndex = logMapIndex;
result.numBots = numBots;
result.numPlayers = numPlayers;
result.numLocalPlayers = numLocalPlayers;
result.numSpecators = numSpectators;
result.isServer = isServer;
return ( start - line.c_str() );
error:
printf( "----ERROR: unrecognised [MEMORYLOG] syntax on line %d, in %s\n", lineNum, fileName );
printf( " \"%s\"\n\n", line.c_str() );
return -1;
}
bool ReadFile( const string &fileName, CUtlBuffer &buffer )
{
FILE *fp = fopen( fileName.c_str(), "rb" );
if (!fp)
return false;
fseek( fp, 0, SEEK_END );
int nFileLength = ftell( fp );
fseek( fp, 0, SEEK_SET );
buffer.EnsureCapacity( nFileLength );
int nBytesRead = fread( buffer.Base(), 1, nFileLength, fp );
fclose( fp );
buffer.SeekPut( CUtlBuffer::SEEK_HEAD, nBytesRead );
return true;
}
bool WriteFile( const string &fileName, CUtlBuffer &buffer, CUtlBuffer *header )
{
FILE *fp = fopen( fileName.c_str(), "wb" );
if ( !fp )
return false;
if ( header )
fwrite( header->Base(), 1, header->TellPut(), fp );
fwrite( buffer.Base(), 1, buffer.TellPut(), fp );
fclose( fp );
return true;
}
char *SpewHeaderSummary( char *buffer, int bufferSize, const CHeaderInfo &headerInfo )
{
const char *dvdHosted = ( headerInfo.dvdHosted == DVDHOSTED_UNKNOWN ) ? " - " : ( ( headerInfo.dvdHosted == DVDHOSTED_YES ) ? "DVD" : "HDD" );
bool languageKnown = !!strcmp( headerInfo.language, "unknown" );
_snprintf( buffer, bufferSize, "%7d %4s %10s ", headerInfo.buildNumber, dvdHosted, languageKnown ? headerInfo.language : " -- " );
return buffer;
}
void WriteHeaderInfoToBuffer( CUtlBuffer &buffer, CHeaderInfo &headerInfo )
{
buffer.Clear();
buffer.Printf( "[HDR]:version=%d\n", MEMLOG_VERSION );
buffer.Printf( "[HDR]:dvdhosted=%s\n", ( headerInfo.dvdHosted == DVDHOSTED_UNKNOWN ) ? "unknown" : ( ( headerInfo.dvdHosted == DVDHOSTED_YES ) ? "yes" : "no" ) );
buffer.Printf( "[HDR]:build=%d\n", headerInfo.buildNumber );
buffer.Printf( "[HDR]:language=%s\n", ( headerInfo.language && headerInfo.language[0] ) ? headerInfo.language : "unknown" );
buffer.Printf( "[HDR]:platform=%s\n", gPlatformNames[ headerInfo.platform ] );
}
void InitItem( CItem &item )
{
item.time = 0;
item.freeMem = CPU_MEM_SIZE;
item.gpuFree = GPU_MEM_SIZE;
item.globalMapIndex = gMapHash[ string( "backgroundstreet" ) ];
item.logMapIndex = -1;
item.numBots = 0;
item.numPlayers = 0;
item.numLocalPlayers = 0;
item.numSpecators = 0;
item.isServer = false;
}
bool ConvertVXConsoleLog( string &consoleLog, string &memoryLog )
{
CUtlBuffer iBuffer( 0, 0, CUtlBuffer::TEXT_BUFFER|CUtlBuffer::CONTAINS_CRLF );
CUtlBuffer oBuffer( 0, 0, CUtlBuffer::TEXT_BUFFER|CUtlBuffer::CONTAINS_CRLF );
CUtlBuffer ohBuffer( 0, 0, CUtlBuffer::TEXT_BUFFER|CUtlBuffer::CONTAINS_CRLF );
if ( !ReadFile( consoleLog, iBuffer ) )
{
printf( "----ERROR: Failed to read file %s\n\n", consoleLog.c_str() );
return false;
}
bool isEmpty = true;
int lineNum = 1;
vector<int> truncated;
CHeaderInfo headerInfo;
CItem prevItem;
InitItem( prevItem );
while ( iBuffer.IsValid() )
{
static char lineBuf[ 10000 ];
iBuffer.GetLine( lineBuf, sizeof( lineBuf ) );
// CUtlBuffer.GetLine() returns TWO lines for lines ending in '\r\n' :o/
if ( lineBuf[0] == '\n' ) continue;
if ( !lineBuf[0] && ( iBuffer.TellGet() != iBuffer.TellPut() ) )
{
// CUtlBuffer.GetLine() can fail if there are non-ASCII characters in the log. If that happens
// (which it does - vxconsole bug?), we'll get a zero-length line, so unstick the buffer:
iBuffer.GetChar();
continue;
}
CItem item;
string line = lineBuf;
int start = ConvertItem( line, item, prevItem, &headerInfo, consoleLog.c_str(), lineNum, &truncated, NULL, NULL, false );
if ( start >= 0 )
{
// If the line is valid, copy it to the output file
oBuffer.Put( ( line.c_str() + start ), ( line.size() - start ) );
oBuffer.Put( "\n", 1 );
isEmpty = false;
prevItem = item;
}
lineNum++;
}
// Write the headerInfo to a buffer
WriteHeaderInfoToBuffer( ohBuffer, headerInfo );
if ( truncated.size() )
{
printf( "----WARNING: [MEMORYLOG] text truncated in %s\n", consoleLog.c_str() );
printf( "---- Truncated lines: %d", truncated[0] );
for ( unsigned int i = 1; i < truncated.size(); i++ ) printf( ", %d", truncated[i] );
printf( "\n" );
}
// Write the output file, with zero or more items in it
if ( !WriteFile( memoryLog, oBuffer, &ohBuffer ) )
{
printf( "----ERROR: Failed to write file %s\n\n", memoryLog.c_str() );
return false;
}
return !isEmpty;
}
CLogFile &InitLogFile( CLogFiles &results, const string &memoryLog, const string &consoleLog, const ULONGLONG &modifyTime, bool isActive )
{
CLogFile *result = new CLogFile();
pair<CLogFiles::iterator,bool> insertion = results.insert( make_pair( memoryLog, result ) );
if ( !insertion.second || ( insertion.first == results.end() ) )
{
printf( "----ERROR: hash_map insertion failed...?\n\n" );
insertion.first = results.find( memoryLog );
if ( insertion.first == results.end() )
printf( "----ERROR: hash_map totally b0rked, gonna crash...\n\n" );
}
result->memoryLog = memoryLog;
result->consoleLog = consoleLog;
result->modifyTime = modifyTime;
result->isActive = isActive;
result->minFreeMem = CPU_MEM_SIZE;
result->minGPUFreeMem = GPU_MEM_SIZE;
result->isServer = false;
result->isSplitscreen = false;
result->maxPlayers = 0;
return *result;
}
bool StringToPlatformName( const char *name, Platform_t &platform )
{
for ( int i = 0; i < ARRAYSIZE( gPlatformNames ); i++ )
{
if ( !strnicmp( name, gPlatformNames[ i ], strlen( gPlatformNames[ i ] ) ) )
{
platform = (Platform_t)i;
return true;
}
}
return false;
}
bool ReadHeaderLine( const char *lineBuf, CHeaderInfo &headerInfo )
{
static const char HEADER_PREFIX[] = "[HDR]:";
static const int HEADER_PREFIX_LEN = sizeof( HEADER_PREFIX ) - 1;
static const char KEY_VERSION[] = "version=";
static const int KEY_VERSION_LEN = sizeof( KEY_VERSION ) - 1;
static const char KEY_DVD_HOSTED[] = "dvdhosted=";
static const int KEY_DVD_HOSTED_LEN = sizeof( KEY_DVD_HOSTED ) - 1;
static const char KEY_BUILDNUM[] = "build=";
static const int KEY_BUILDNUM_LEN = sizeof( KEY_BUILDNUM ) - 1;
static const char KEY_LANGUAGE[] = "language=";
static const int KEY_LANGUAGE_LEN = sizeof( KEY_LANGUAGE ) - 1;
static const char KEY_PLATFORM[] = "platform=";
static const int KEY_PLATFORM_LEN = sizeof( KEY_PLATFORM ) - 1;
const char *cursor = lineBuf;
if ( strncmp( cursor, HEADER_PREFIX, HEADER_PREFIX_LEN ) )
return false;
cursor += HEADER_PREFIX_LEN;
if ( !strncmp( cursor, KEY_VERSION, KEY_VERSION_LEN ) )
{
cursor += KEY_VERSION_LEN;
headerInfo.memlogVersion = atoi( cursor );
return true;
}
else if ( !strncmp( cursor, KEY_DVD_HOSTED, KEY_DVD_HOSTED_LEN ) )
{
cursor += KEY_DVD_HOSTED_LEN;
if ( !strncmp( cursor, "yes", 3 ) )
{
headerInfo.dvdHosted = DVDHOSTED_YES;
}
else if ( !strncmp( cursor, "no", 2 ) )
{
headerInfo.dvdHosted = DVDHOSTED_NO;
}
return true;
}
else if ( !strncmp( cursor, KEY_BUILDNUM, KEY_BUILDNUM_LEN ) )
{
cursor += KEY_BUILDNUM_LEN;
headerInfo.buildNumber = atoi( cursor );
return true;
}
else if ( !strncmp( cursor, KEY_LANGUAGE, KEY_LANGUAGE_LEN ) )
{
cursor += KEY_LANGUAGE_LEN;
strncpy( headerInfo.language, cursor, MAX_LANG_LEN );
char *eol = strstr( headerInfo.language, "\n" );
if ( eol ) *eol = 0;
return true;
}
else if ( !strncmp( cursor, KEY_PLATFORM, KEY_PLATFORM_LEN ) )
{
cursor += KEY_PLATFORM_LEN;
if ( StringToPlatformName( cursor, headerInfo.platform ) )
return true;
}
printf( "----ERROR: Unrecognised header line: (%s)\n\n", lineBuf );
return true;
}
int ReadMemoryLog( string &memoryLog, string &consoleLog, CLogFiles & results, const ULONGLONG &modifyTime, bool isActive, bool checkVersion )
{
CUtlBuffer iBuffer( 0, 0, CUtlBuffer::TEXT_BUFFER|CUtlBuffer::CONTAINS_CRLF );
if ( !ReadFile( memoryLog.c_str(), iBuffer ) )
{
printf( "----ERROR: Failed to open input file %s\n\n", memoryLog.c_str() );
return 0;
}
CLogFile &result = InitLogFile( results, memoryLog, consoleLog, modifyTime, isActive );
bool bOrderError = false;
bool bReallyEmpty = true;
bool bDoneHeader = false;
int lineNum = 1;
int lastTime = 0;
CItem prevItem;
InitItem( prevItem );
while ( iBuffer.IsValid() )
{
// create+add an item
static char lineBuf[ 10000 ];
iBuffer.GetLine( lineBuf, sizeof( lineBuf ) );
// CUtlBuffer.GetLine() returns TWO lines for lines ending in '\r\n' :o/
if ( lineBuf[0] == '\n' ) continue;
if ( !bDoneHeader )
{
// Read one or more header lines at the top of the memorylog file
if ( ReadHeaderLine( lineBuf, result.headerInfo ) )
{
lineNum++;
continue;
}
bDoneHeader = true;
// Check file version
if ( ( result.headerInfo.memlogVersion < MEMLOG_VERSION ) && checkVersion )
{
printf( "----Log is from an old version, re-converting...\n" );
CLogFile *oldLog = results.find( memoryLog )->second;
if ( oldLog )
{
delete oldLog;
results.erase( memoryLog );
}
return -1;
}
else if ( result.headerInfo.memlogVersion > MEMLOG_VERSION )
{
printf( "----ERROR: memory log file is newer than this version of memlog.exe!\n\n" );
}
}
CItem item;
string line = lineBuf;
if ( ConvertItem( line, item, prevItem, NULL, memoryLog.c_str(), lineNum, NULL, &result.mapList, &result.playerList, true ) >= 0 )
{
bReallyEmpty = false;
if ( item.globalMapIndex >= 0 )
{
result.entries.AddToTail( item );
prevItem = item;
}
bOrderError = bOrderError || ( item.time < lastTime );
lastTime = item.time;
}
lineNum++;
}
// VXConsole may have concatenated logs from multiple runs - DO NOT WANT!
if ( bOrderError )
printf( "----ERROR: '[MEMORYLOG]' timestamps are out-of-order! Log file may contain multiple runs of the game...\n\n" );
// It's ok to load logs with [MEMORYLOG] lines only for invalid maps, but
// logs with no [MEMORYLOG] lines should be zero size and not loaded at all.
// UPDATE: the header info might still be useful, so we load 'em anyway...
if ( bReallyEmpty )
printf( "----Empty memorylog file.\n" );
// Compute a few aggregate stats in order to speed up log filtering:
int noneMap = gMapHash[ string( "none" ) ];
int menuMap = gMapHash[ string( "backgroundstreet" ) ];
int creditsMap = gMapHash[ string( "credits" ) ];
float memorylogTick = 0;
for ( int i = 0; i < result.entries.Count(); i++ )
{
CItem &item = result.entries[ i ];
result.minFreeMem = min( result.minFreeMem, item.freeMem );
result.minGPUFreeMem = min( result.minGPUFreeMem, item.gpuFree );
result.maxPlayers = max( result.maxPlayers, item.numPlayers );
if ( item.numLocalPlayers > 1 )
result.isSplitscreen = true;
if ( item.isServer && ( item.globalMapIndex != noneMap ) && ( item.globalMapIndex != menuMap ) && ( item.globalMapIndex != creditsMap ) )
result.isServer = true;
if ( i > 0 ) // Average the intervals between memorylog entries
memorylogTick += ( item.time - result.entries[ i - 1 ].time );
}
if ( result.entries.Count() > 1 )
memorylogTick /= ( result.entries.Count() - 1 );
result.memorylogTick = memorylogTick;
return 1;
}
ULONGLONG WriteTime( WIN32_FIND_DATA &fileData )
{
return ( ((ULONGLONG)fileData.ftLastWriteTime.dwHighDateTime ) << 32 ) | (ULONGLONG)fileData.ftLastWriteTime.dwLowDateTime;
}
/*ULONGLONG CreateTime( WIN32_FIND_DATA &fileData )
{
return ( ((ULONGLONG)fileData.ftCreationTime.dwHighDateTime ) << 32 ) | (ULONGLONG)fileData.ftCreationTime.dwLowDateTime;
}*/
ULONGLONG SystemTime( void )
{
SYSTEMTIME LocalSysTime;
FILETIME LocalFileTime;
FILETIME UTCFileTime;
BOOL success = TRUE;
GetLocalTime( &LocalSysTime );
success = SystemTimeToFileTime( &LocalSysTime, &LocalFileTime );
success = ( LocalFileTimeToFileTime( &LocalFileTime, &UTCFileTime ) || success );
if ( !success )
{
static int errCount = 0;
if ( !errCount++ )
{
printf( "----ERROR: error generating system time... all logs will be considered 'active'\n\n" );
DebuggerBreak();
}
return 0;
}
return ( ((ULONGLONG)UTCFileTime.dwHighDateTime ) << 32 ) | (ULONGLONG)UTCFileTime.dwLowDateTime;
}
bool IsActive( ULONGLONG lastWriteTime )
{
return ( SystemTime() < ( lastWriteTime + 60*ONE_SECOND ) );
}
void Tick( void )
{
// Let the user know we're still working (recursing through fileserver is sloooow)
SYSTEMTIME systemTime;
GetLocalTime( &systemTime );
static int lastTime = -10;
if ( ( ( systemTime.wSecond % 10 ) == 0 ) && ( systemTime.wSecond != lastTime ) )
{
printf( "%02d:%02d:%02d--------------------------------------------------------------------------------------------\n", systemTime.wHour, systemTime.wMinute, systemTime.wSecond );
lastTime = systemTime.wSecond;
}
}
void _ProcessLogFiles( const char *path, CLogFiles &results,
int &numLoaded, int &numUnloaded, int &numConverted, int &numUpdated, int &numActive )
{
WIN32_FIND_DATA fileData;
HANDLE handle;
Tick();
if ( gConfig.load )
{
// Convert 'vxconsole_*.log' files
static char pathBuf[ _MAX_PATH ]; // No recursion in this loop
_snprintf( pathBuf, sizeof( pathBuf ), "%svxconsole_*.log", path );
handle = FindFirstFile( pathBuf, &fileData );
if ( handle != INVALID_HANDLE_VALUE )
{
do
{
_snprintf( pathBuf, sizeof( pathBuf ) , "%s%s", path, fileData.cFileName );
string consoleLog = pathBuf;
// Does this 'vxconsole_*.log' have a corresponding 'memorylog_*.log'?
static char prefix[] = "vxconsole_";
char *suffix = fileData.cFileName + sizeof( prefix ) - 1;
_snprintf( pathBuf, sizeof( pathBuf ) , "%smemorylog_%s", path, suffix );
string memoryLog = pathBuf;
WIN32_FIND_DATA fileData2;
HANDLE handle2 = FindFirstFile( memoryLog.c_str(), &fileData2 );
// Convert all logs if asked to do a forced update
bool bNeedsConverting = gConfig.forceUpdate;
bool isActive = ( handle2 != INVALID_HANDLE_VALUE ) && IsActive( WriteTime( fileData ) );
if ( !bNeedsConverting )
{
if ( isActive )
{
// Only convert currently-active logs if that is specifically requested
bNeedsConverting = gConfig.updateActive;
if ( !gConfig.updateActive )
printf( "--Skipping update of ACTIVE log %s\n", consoleLog.c_str() );
numActive++;
}
else if ( handle2 == INVALID_HANDLE_VALUE )
{
// Convert logs we haven't converted before
bNeedsConverting = true;
}
else if ( gConfig.update )
{
// We have been asked to reconvert logs that have been updated
bNeedsConverting = WriteTime( fileData ) > WriteTime( fileData2 );
}
}
if ( bNeedsConverting )
{
// Create/update the memory log
bool bUpdating = ( handle2 != INVALID_HANDLE_VALUE );
printf( "--%s %s\n", ( bUpdating ? "Updating" : "Converting" ), consoleLog.c_str() );
if ( isActive )
printf( "----Log is ACTIVE\n" );
ConvertVXConsoleLog( consoleLog, memoryLog );
numConverted += bUpdating ? 0 : 1;
numUpdated += bUpdating ? 1 : 0;
}
FindClose( handle2 );
Tick();
}
while( FindNextFile( handle, &fileData ) );
FindClose( handle );
}
}
// Note that update/updateActive/forceUpdate imply 'load' (it's confusing otherwise)
if ( gConfig.load || gConfig.unload )
{
// Read in 'memorylog_*.log' files
static char pathBuf[ _MAX_PATH ]; // No recursion in this loop
_snprintf( pathBuf, sizeof( pathBuf ), "%smemorylog_*.log", path );
handle = FindFirstFile( pathBuf, &fileData );
if ( handle != INVALID_HANDLE_VALUE )
{
do
{
// Compute full memorylog_*/vxconsole_* paths
string memoryLog = path;
memoryLog += fileData.cFileName;
strlwr( (char *)memoryLog.c_str() );
// Is this memory log already in memory?
CLogFiles::iterator it = results.find( memoryLog );
bool bLoaded = ( it != results.end() );
// Re-load active logs if requested
if ( bLoaded && it->second->isActive && gConfig.updateActive )
{
CLogFile *oldLog = it->second;
if ( oldLog ) delete oldLog;
results.erase( it );
bLoaded = false;
}
if ( gConfig.load && !bLoaded )
{
// Get the timestamp for the corresponding vxconsole log (if there is one)
char *suffix = fileData.cFileName + strlen( "memorylog_" );
_snprintf( pathBuf, sizeof( pathBuf ) , "%svxconsole_%s", path, suffix );
string consoleLog = pathBuf;
strlwr( (char *)consoleLog.c_str() );
WIN32_FIND_DATA fileData2;
HANDLE handle2 = FindFirstFile( consoleLog.c_str(), &fileData2 );
ULONGLONG timeStamp = ( handle2 != INVALID_HANDLE_VALUE ) ? WriteTime( fileData2 ) : WriteTime( fileData );
bool isActive = ( handle2 != INVALID_HANDLE_VALUE ) ? IsActive( WriteTime( fileData2 ) ) : false;
if ( handle2 == INVALID_HANDLE_VALUE ) consoleLog = "";
FindClose( handle2 );
// Load each memorylog file, and add it to the results
printf( "--Loading %s\n", memoryLog.c_str() );
if ( isActive )
printf( "----Log is ACTIVE, %s\n", ( gConfig.updateActive | gConfig.forceUpdate ) ? "loaded up-to-date memorylog data" : "loading old memorylog data" );
bool checkVersion = true, ignoreVersion = false;
int retVal = ReadMemoryLog( memoryLog, consoleLog, results, timeStamp, isActive, checkVersion );
if ( retVal == -1 )
{
// Memorylog is out of date, re-convert it
numUpdated += ConvertVXConsoleLog( consoleLog, memoryLog ) ? 1 : 0;
printf( "----Loading re-converted log\n", memoryLog.c_str() );
retVal = ReadMemoryLog( memoryLog, consoleLog, results, timeStamp, isActive, ignoreVersion );
}
if ( retVal > 0 )
numLoaded++;
}
else if ( gConfig.unload && bLoaded )
{
// Unload this memorylog file
printf( "--Unloading %s\n", memoryLog.c_str() );
results.erase( memoryLog );
numUnloaded++;
}
Tick();
}
while( FindNextFile( handle, &fileData ) );
FindClose( handle );
}
}
if ( gConfig.recurse )
{
// Recurse to subdirectories
char *searchPath = new char[ _MAX_PATH ]; // Avoid blowing the stack due to recursion
_snprintf( searchPath, _MAX_PATH, "%s*.*", path );
handle = FindFirstFile( searchPath, &fileData );
if ( handle != INVALID_HANDLE_VALUE )
{
do
{
if ( ( fileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) &&
( fileData.cFileName[ 0 ] != '.' ) )
{
char *subDir = new char[ _MAX_PATH ]; // Avoid blowing the stack due to recursion
_snprintf( subDir, _MAX_PATH, "%s%s\\", path, fileData.cFileName );
_ProcessLogFiles( subDir, results, numLoaded, numUnloaded, numConverted, numUpdated, numActive );
delete[] subDir;
}
}
while( FindNextFile( handle, &fileData ) );
}
FindClose( handle );
delete[] searchPath;
}
}
void ProcessLogFiles( const char *path, CLogFiles &results )
{
if ( gConfig.unloadAll )
{
CLogFiles::iterator it;
for ( it = results.begin(); it != results.end(); it++ )
{
printf( "--Unloading %s\n", it->first.c_str() );
delete it->second;
}
printf( "\n" );
printf( "%d logs unloaded\n", results.size() );
printf( "\n" );
results.clear();
return;
}
if ( !path || !path[0] )
return;
if ( gConfig.load || gConfig.unload )
{
int numLoaded = 0, numUnloaded = 0, numConverted = 0, numUpdated = 0, numEmpty = 0, numActive = 0;
_ProcessLogFiles( gConfig.sourcePath, results, numLoaded, numUnloaded, numConverted, numUpdated, numActive );
for ( CLogFiles::iterator it = results.begin(); it != results.end(); it++ )
{
numEmpty += it->second->entries.Count() ? 0 : 1;
}
printf( "\n" );
/* TODO:
* numLoaded is confusing... not sure what it means
* I updated a bunch of logs from fileserver and saw this:
* 2 logs loaded
* 26 new logs converted
* 12 logs re-converted
* 7 active logs (converted)
* (1435 empty log files)
* in light of that, I have no idea what "logs loaded" means...
* hmm, it seems to occur for empty logs!
* then I ran "r u a" a second time (without copying over any more files) and it found another empty log... and listed logs loaded as 1
* then I did it again and it loaded no logs...
* weird
*/
if ( numLoaded ) printf( "%d logs loaded\n", numLoaded );
if ( numUnloaded ) printf( "%d logs unloaded\n", numUnloaded );
if ( numConverted ) printf( "%d new logs converted\n", numConverted );
if ( numUpdated ) printf( "%d logs re-converted\n", numUpdated );
if ( numActive ) printf( "%d active logs %s\n", numActive, gConfig.updateActive ? "(converted)" : "(not converted)" );
if ( numEmpty ) printf( "(%d empty log files)\n", numEmpty );
printf( "\n" );
}
}
int trim( string &s )
{
// Remove whitespace from the head+tail of the string
size_t oldlen = s.length(), head = 0, tail = 0;
while( ( head < oldlen ) && V_isspace( s[head] ) ) head++;
while( ( tail < oldlen ) && V_isspace( s[oldlen-1-tail] ) ) tail++;
size_t numRemoved = head + tail;
if ( !numRemoved )
return 0;
s = s.substr( head, MAX(0,oldlen-numRemoved) ); // The 'MAX' fixes the case where the string is ALL whitespace :)
return numRemoved;
}
struct TrackSort_t
{
int line;
float minFree;
};
int TrackSortFunc( const void *a, const void *b )
{
TrackSort_t *A = (TrackSort_t *)a, *B = (TrackSort_t *)b;
return ( A->minFree < B->minFree ) ? -1 : +1;
}
void NormalizeCSVRowLengths( vector< vector< string > > &lines )
{
unsigned int nMaxLineLength = 0;
for ( unsigned int i = 0; i < lines.size(); i++ )
{
nMaxLineLength = max( nMaxLineLength, lines[i].size() );
}
for ( unsigned int i = 0; i < lines.size(); i++ )
{
string empty = i ? "" : "unknown";
vector<string> &line = lines[i];
while ( line.size() < nMaxLineLength )
{
line.push_back( empty );
}
}
}
bool UpdateTrackFile( const char *trackFile, const char *trackColumn,
unsigned int *mapSamples, float *mapMin, float *mapGPUMin )
{
// Make a backup before we start
CUtlBuffer fileBuffer;
string fileName = trackFile;
if ( ReadFile( fileName, fileBuffer ) )
{
int nCharsToExtension = V_stristr( trackFile, ".csv" ) - trackFile;
Assert( nCharsToExtension > 0 );
string backupName = fileName.substr( 0, nCharsToExtension ) + "_bak.csv";
if ( !WriteFile( backupName, fileBuffer, NULL ) )
{
printf( "ERROR: could not write backup tracking .CSV file (%s), aborting!\n\n", backupName.c_str() );
return false;
}
}
// Now read the .CSV into a vector of vectors of strings
const string MAP_NAME_HEADER = "Map Name";
unsigned int nMaxRowLen = 0;
vector<vector<string>> lines;
CMapHash mapsInCSV;
ifstream file;
file.open( trackFile );
if ( file.is_open() )
{
while ( !file.eof() )
{
string lineString;
getline( file, lineString );
if ( !lineString.size() )
break;
// Each line is a vector of strings:
string item;
vector<string> line;
stringstream lineStream( lineString );
while ( !lineStream.eof() )
{
getline( lineStream, item, ',' );
trim( item ); // avoid duplicates differing only by whitespace
line.push_back( item );
}
nMaxRowLen = max( nMaxRowLen, line.size() );
lines.push_back( line );
}
file.close();
}
// Init an empty .CSV file:
if ( !nMaxRowLen || !lines.size() )
{
vector<string> headerLine;
headerLine.push_back( MAP_NAME_HEADER );
lines.clear();
lines.push_back( headerLine );
}
// Validate the .CSV file
for ( unsigned int i = 0; i < lines.size(); i++ )
{
string &firstItem = lines[ i ][ 0 ];
if ( ( ( i == 0 ) && ( firstItem != MAP_NAME_HEADER ) ) || !firstItem.size() )
{
printf( "ERROR: first item on line %d in tracking .CSV file (%s) is invalid, aborting!\n\n", i+1, trackFile );
return false;
}
// Make a note of all the maps found in the file
if ( i > 0 )
{
if ( mapsInCSV.find( firstItem ) != mapsInCSV.end() )
{
printf( "ERROR: map '%s' occurs more than once in tracking .CSV file (%s), aborting!\n\n", firstItem.c_str(), trackFile );
return false;
}
mapsInCSV[ firstItem ] = i;
}
}
// Normalize row lengths
NormalizeCSVRowLengths( lines );
// Add a new column
vector<string> &headerLine = lines[0];
if ( trackColumn[0] )
{
headerLine.push_back( string( trackColumn ) );
}
else
{
ostringstream columnName;
columnName << "column_";
columnName << headerLine.size();
headerLine.push_back( columnName.str() );
}
// Add data to the .CSV file
bool trackGPUMem = ( gConfig.trackStat == TRACKSTAT_MINFREE_GPU );
unsigned int numMaps = gMapNames.Count();
for ( unsigned int i = gNumIgnoreMaps; i < numMaps; i++ )
{
string empty = "", mapName = gMapNames[ i ];
if ( mapsInCSV.find( mapName ) == mapsInCSV.end() )
{
// This map is not present in the .CSV file, so add a new row
vector<string> newLine;
newLine.push_back( mapName );
while ( newLine.size() < nMaxRowLen )
{
newLine.push_back( empty );
}
lines.push_back( newLine );
mapsInCSV[ mapName ] = lines.size() - 1;
}
// Add the new data sample to the end of this line
int nMapLine = mapsInCSV[ mapName ];
vector<string> &line = lines[ nMapLine ];
if ( mapSamples[ i ] )
{
// Add the worst case sample for this map
ostringstream minFree;
minFree << ( trackGPUMem ? mapGPUMin[ i ] : mapMin[ i ] );
line.push_back( minFree.str() );
}
else
{
// Add an empty entry for this map
// (it makes diffing/sorting/combining/comparing these CSVs easier if they all contain the same set of maps)
line.push_back( empty );
}
}
// Normalize line lengths again, in case some maps weren't updated:
NormalizeCSVRowLengths( lines );
// Sort the .CSV file so the maps with the 'worst' most recent data point are at the top
// (maps missing recent data points are sorted to the top, 'older' maps being biased higher)
vector< TrackSort_t > sortedLines;
for ( unsigned int i = 1; i < lines.size(); i++ )
{
int memSize = trackGPUMem ? GPU_MEM_SIZE : CPU_MEM_SIZE;
TrackSort_t trackSort = { i, memSize };
vector<string> &line = lines[ i ];
for ( unsigned int j = line.size()-1; j > 0; j-- )
{
if ( 1 == sscanf( line[j].c_str(), "%f", &trackSort.minFree ) )
{
trackSort.minFree -= memSize*(line.size()-(j+1)); // The older the data, the higher up the list it goes
sortedLines.push_back( trackSort );
break;
}
if ( j == 1 )
{
//printf( "ERROR: no valid datapoints for map '%s' in tracking .CSV file (%s)\n\n", line[0].c_str(), trackFile );
sortedLines.push_back( trackSort );
}
}
}
qsort( &sortedLines[0], sortedLines.size(), sizeof(TrackSort_t), TrackSortFunc );
// Write out the updated file
ofstream output;
output.open( trackFile );
if ( !output.is_open() )
{
printf( "ERROR: could write to tracking .CSV file (%s), aborting!\n\n", trackFile );
return false;
}
for ( unsigned int i = 0; i < lines.size(); i++ )
{
int nLine = ( i == 0 ) ? 0 : sortedLines[i-1].line;
vector<string> &line = lines[ nLine ];
for ( unsigned int j = 0; j < line.size(); j++ )
{
if ( j > 0 ) output.write( ",", 1 );
output.write( line[j].c_str(), line[j].size() );
}
if ( i < (lines.size()-1) ) output.write( "\n", 1 );
}
output.close();
return true;
}
bool FilterLogFile( CLogFile &logFile, CLogStats &filteredLogStats )
{
// TODO: these filters are very inconsistent, rework it to allow more arbitrary filter specification
// like "include:numplayers:1-4" or "exclude:numplayers:5-8"
// Filter based on aggregate log properties
if ( gConfig.dangerLimit && ( logFile.minFreeMem > gConfig.dangerLimit ) && ( logFile.minGPUFreeMem > gConfig.dangerLimit ) )
return false; // Log contains no entries with memory below the danger limit
if ( gConfig.isServer && !logFile.isServer )
return false; // Log doesn't contain any entries where the local machine is the Server
if ( gConfig.isClient && logFile.isServer )
return false; // Log contains entries where the local machine is the Server
if ( gConfig.isSplitscreen && !logFile.isSplitscreen )
return false; // Log doesn't contain any entries where the local machine is running Splitscreen
if ( gConfig.minPlayers && ( logFile.maxPlayers < gConfig.minPlayers ) )
return false; // Log contains no entries with the requisite number of players
ULONGLONG systemTime = SystemTime();
if ( gConfig.minAge && ( logFile.modifyTime > ( systemTime - gConfig.minAge*ONE_SECOND ) ) )
return false; // Too recent
if ( gConfig.maxAge && ( logFile.modifyTime < ( systemTime - gConfig.maxAge*ONE_SECOND ) ) )
return false; // Too old
if ( gConfig.isActive && !logFile.isActive )
return false; // Not active (not currently being written)
if ( ( gConfig.dvdHosted == DVDHOSTED_YES ) && ( logFile.headerInfo.dvdHosted != DVDHOSTED_YES ) )
return false; // Using the HDD
if ( ( gConfig.dvdHosted == DVDHOSTED_NO ) && ( logFile.headerInfo.dvdHosted != DVDHOSTED_NO ) )
return false; // Only using the DVD
if ( gConfig.languageFilter[0] )
{
if ( !strstr( logFile.headerInfo.language, gConfig.languageFilter ) )
return false; // Did not play in the specified language
}
if ( gConfig.platform != PLATFORM_UNKNOWN )
{
if ( logFile.headerInfo.platform != gConfig.platform )
return false; // Log file not for the specified platform
}
FilterBitfield mapFilter = logFile.mapList.SubstringToBitfield( gConfig.mapFilter );
if ( !mapFilter )
return false; // Log doesn't contain the specified map(s)
FilterBitfield playerFilter = logFile.playerList.SubstringToBitfield( gConfig.playerFilter );
if ( !playerFilter )
return false; // Log doesn't contain the specified player(s)
// The log's aggregate properties passed the filters, so now filter individual log entries
int dangerTime = -1;
int numPassingEntries = 0;
int duration = 0;
for ( int i = 0; i < logFile.entries.Count(); i++ )
{
CItem &item = logFile.entries[ i ];
if ( gConfig.dangerLimit && ( item.freeMem > gConfig.dangerLimit ) )
continue; // Filter out entries above the danger limit
if ( dangerTime == -1 )
dangerTime = item.time;
if ( gConfig.dangerTime && ( dangerTime > gConfig.dangerTime ) )
return false; // Log fails if no entries pass the danger limit soon enough
// NOTE: we don't filter individual entries by isServer, since we want to see the effects of playing maps AFTER being a server
if ( gConfig.isSplitscreen && ( item.numLocalPlayers <= 1 ) )
continue; // Filter out single-screen entries
if ( gConfig.isSinglescreen && ( item.numLocalPlayers >= 2 ) )
continue; // Filter out splitscreen entries
if ( gConfig.mapFilter[0] && !( mapFilter.IsSet( item.logMapIndex ) ) )
continue; // Filter out entries not on the specified map(s)
if ( gConfig.minPlayers && ( item.numPlayers < gConfig.minPlayers ) )
continue; // Filter out entries with too few players
if ( gConfig.maxPlayers && ( item.numPlayers > gConfig.maxPlayers ) )
continue; // Filter out entries with too many players
if ( gConfig.playerFilter[0] && !( playerFilter.Intersects( item.playerBitfield ) ) )
continue; // Filter out entries not containing the specified player(s)
// Ok, this entry passes all filters, so update the filtered aggregate stats
filteredLogStats.mapMin[ item.logMapIndex ] = min( item.freeMem, filteredLogStats.mapMin[ item.logMapIndex ] );
filteredLogStats.mapGPUMin[ item.logMapIndex ] = min( item.gpuFree, filteredLogStats.mapGPUMin[ item.logMapIndex ] );
filteredLogStats.mapAverage[ item.logMapIndex ] += item.freeMem;
filteredLogStats.mapSeconds[ item.logMapIndex ] += logFile.memorylogTick;
filteredLogStats.mapSamples[ item.logMapIndex ] ++;
duration = max( duration, item.time ); // Only update this for entries passing the filters
numPassingEntries++;
}
if ( numPassingEntries == 0 )
return false; // Log contains no lines passing the filters
if ( gConfig.duration && ( duration < gConfig.duration ) )
return false; // App run time too short (at filter-passing entries)
for ( int i = 0; i < CStringList::MAX_STRINGLIST_ENTRIES; i++ )
{
// Compute final per-map averages (for passing entries)
if ( filteredLogStats.mapSamples[ i ] > 0 )
filteredLogStats.mapAverage[ i ] /= filteredLogStats.mapSamples[ i ];
}
// We have a winner!
return true;
}
bool HasFilter( void )
{
// Are we filtering the logs in any meaningful way?
return ( gConfig.dangerLimit || gConfig.dangerTime || gConfig.duration || gConfig.minAge || gConfig.maxAge || gConfig.minPlayers || gConfig.maxPlayers ||
gConfig.isServer || gConfig.isClient || gConfig.isSplitscreen || gConfig.isSinglescreen || gConfig.isActive || ( gConfig.dvdHosted != DVDHOSTED_UNKNOWN ) ||
gConfig.mapFilter[0] || gConfig.playerFilter[0] || gConfig.languageFilter[0] || ( gConfig.platform != PLATFORM_UNKNOWN ) );
}
void PrintStats( CLogFiles &results )
{
int numMaps = gMapNames.Count();
if ( ( results.size() == 0 ) || ( numMaps == 0 ) )
{
printf( "Aggregate stats\n" );
printf( "---------------\n" );
printf( "No log files loaded\n" );
return;
}
if ( gConfig.isActive )
{
// Update which of our loaded logs are still active
for ( CLogFiles::iterator logIt = results.begin(); logIt != results.end(); logIt++ )
{
CLogFile &logFile = *logIt->second;
if ( logFile.isActive )
{
// If it was active when we loaded it, is it still?
WIN32_FIND_DATA fileData;
HANDLE handle = FindFirstFile( logFile.consoleLog.c_str(), &fileData );
if ( ( handle == INVALID_HANDLE_VALUE ) || !IsActive( WriteTime( fileData ) ) )
{
logFile.isActive = false;
}
}
}
}
typedef multimap<ULONGLONG, const CLogFile *> CFilteredLogs;
CFilteredLogs filteredLogs;
float *mapMin = new float[ numMaps ];
float *mapGPUMin = new float[ numMaps ];
float *mapAverage = new float[ numMaps ];
float *mapSeconds = new float[ numMaps ];
unsigned int *mapSamples = new unsigned int[ numMaps ];
unsigned int *mapLogs = new unsigned int[ numMaps ];
for ( int i = 0; i < numMaps; i++ )
{
mapMin[i] = CPU_MEM_SIZE;
mapGPUMin[i] = GPU_MEM_SIZE;
mapAverage[i] = 0.0f;
mapSeconds[i] = 0;
mapSamples[i] = 0;
mapLogs[i] = 0;
}
for ( CLogFiles::iterator logIt = results.begin(); logIt != results.end(); logIt++ )
{
CLogFile &logFile = *logIt->second;
CLogStats filteredLogStats;
// Run the log file through our filters
logFile.badMaps.Purge();
if ( FilterLogFile( logFile, filteredLogStats ) )
{
// Score! Use the data from this log to update per-map aggregate stats:
for ( int i = 0; i < numMaps; i++ )
{
int index = logFile.mapList.StringToInt( gMapNames[ i ] ); // Index into this logfile's map list
if ( ( index >= 0 ) && ( filteredLogStats.mapSamples[ index ] > 0 ) )
{
mapMin[ i ] = min( mapMin[ i ], filteredLogStats.mapMin[ index ] );
mapGPUMin[ i ] = min( mapGPUMin[ i ], filteredLogStats.mapGPUMin[ index ] );
mapAverage[ i ] += filteredLogStats.mapAverage[ index ]*filteredLogStats.mapSamples[ index ];
mapSeconds[ i ] += filteredLogStats.mapSeconds[ index ];
mapSamples[ i ] += filteredLogStats.mapSamples[ index ];
mapLogs[ i ]++;
// Build a list of the 'bad' maps in this log file, for spewage below:
MapMin_t mapMin = { i, filteredLogStats.mapMin[ index ] };
logFile.badMaps.AddToTail( mapMin );
}
}
// Add this to the list of logs to spew:
filteredLogs.insert( make_pair( logFile.modifyTime, &logFile ) );
}
}
if ( gConfig.trackFile[0] )
{
UpdateTrackFile( gConfig.trackFile, gConfig.trackColumn, mapSamples, mapMin, mapGPUMin );
}
// Spew the names of logs passing our filters (multimap-sorted by log last-modified time):
// TODO: will this spew scroll faster if we print multiple lines at a time?
if ( HasFilter() )
{
printf( "\n\nLog files which pass filters ( command-line \"%s\" )\n", gConfig.prevCommandLine );
printf( "----------------------------\n" );
CFilteredLogs::iterator it;
for ( it = filteredLogs.begin(); it != filteredLogs.end(); it++ )
{
static char headerString[1024];
SpewHeaderSummary( headerString, ARRAYSIZE( headerString ), it->second->headerInfo );
printf( " [%s] %s\n", headerString, it->second->consoleLog.c_str()[0] ? it->second->consoleLog.c_str() : it->second->memoryLog.c_str() );
// Spew a little summary of all offending maps in this log file (makes associating map issues w/ logs much faster)
printf( " " );
const CUtlVector<MapMin_t> &badMaps = it->second->badMaps;
for ( int i = 0; i < badMaps.Count(); i++ ) printf( " %4.1fMB:%-28s|", badMaps[i].minMem, gMapNames[ badMaps[i].map ] );
printf( "\n" );
}
printf( "\n" );
}
// NOTE: empty log files will always fail filters, unless you set "danger:512" (TODO: not sure this workaround works any more...)
printf( "Aggregate stats ( command-line \"%s\" )\n", gConfig.prevCommandLine );
printf( "--------------- (%d logs pass filters, of %d loaded)\n", filteredLogs.size(), results.size() );
int totalSeconds = 0;
if ( filteredLogs.size() )
{
for ( int i = 0; i < numMaps; i++ )
{
if ( mapSamples[ i ] )
{
mapAverage[ i ] /= mapSamples[ i ]; // Compute the final per-map average
totalSeconds += mapSeconds[ i ];
printf( "Map %-32s %4d logs, %6d samples, min %6.2fMB, average %6.2fMB\n",
gMapNames[ i ], mapLogs[ i ], mapSamples[ i ], mapMin[ i ], mapAverage[ i ] );
}
}
}
int days = (int)( totalSeconds / 86400 );
totalSeconds -= days*86400;
int hours = (int)( totalSeconds / 3600 );
totalSeconds -= hours*3600;
int mins = (int)( totalSeconds / 60 );
printf( " Total play time represented by these samples: %dd:%2dh:%2dm\n", days, hours, mins );
delete mapMin;
delete mapGPUMin;
delete mapAverage;
delete mapSamples;
delete mapSeconds;
delete mapLogs;
}
void InitMapHash( void )
{
for ( int i = 0; i < gNumIgnoreMaps; i++ ) AddNewMapName( gIgnoreMaps[ i ] );
for ( int i = 0; i < gNumKnownMaps; i++ ) AddNewMapName( gKnownMaps[ i ] );
}
const char *CleanPath( const char *path )
{
strncpy( gConfig.sourcePath, path, sizeof( gConfig.sourcePath ) );
strlwr( gConfig.sourcePath );
int pathLen = (int)strlen( gConfig.sourcePath );
if ( pathLen && ( gConfig.sourcePath[ pathLen - 1 ] != '\\' ) && ( gConfig.sourcePath[ pathLen - 1 ] != '/' ) )
{
strncat( gConfig.sourcePath, "\\", sizeof( gConfig.sourcePath ) );
}
return gConfig.sourcePath;
}
void Usage()
{
printf( "\n" );
printf( "memlog mines vxconsole logs for memory data\n" );
printf( "\n" );
printf( " USAGE: memlog [options] <folder>\n" );
printf( "\n" );
printf( "Input is a folder. memlog will convert all vxconsole logs in that folder into\n" );
printf( "memlog files (prefix 'memorylog_'). It will then output aggregate memory data for\n" );
printf( "those files.\n" );
printf( "\n" );
printf( "options:\n" );
printf( "[-c|console] run in console mode (see below)\n" );
printf( "[-r|recurse] recurse the input folder tree\n" );
printf( "[-u|update] reconvert vxconsole logs that have been updated\n" );
printf( "[-a|updateactive] reconvert vxconsole logs that are still being updated\n" );
printf( "[-f|force] reconvert all vxconsole logs\n" );
printf( "\n" );
printf( "tracking:\n" );
printf( "[-track:<file>] update a CSV file which tracks memory stats over time\n" );
printf( " the filename must end with one of these suffices:\n" );
printf( " '_MinFreeCPU' - track minimium free CPU memory\n" );
printf( " '_MinFreeGPU' - track minimium free GPU memory\n" );
printf( "[-trackcol:<name>] 'name' is the new column to add (rows are maps)\n" );
printf( "\n" );
printf( "analysis: (spews data passing these filters - all default to off)\n" );
printf( " ----the following options are applied per log entry----\n" );
printf( "[-danger:N] pass log entries in which memory is below N kilobytes\n" );
printf( "[-minplayers:N] pass log entries with at least this many concurrent players\n" );
printf( "[-maxplayers:N] pass log entries with at most this many concurrent players\n" );
printf( "[-issinglescreen] pass log entries in which the local box has 1 player\n" );
printf( "[-issplitscreen] pass log entries in which the local box has 2 players\n" );
printf( "[-map:<name>] pass log entries with map names containing this substring\n" );
printf( "[-player:<name>] pass log entries with player names containing this substring\n" );
printf( " ----the following options are applied per log file----\n" );
printf( "[-dangertime:N] pass logs dropping below 'danger' within this many minutes\n" );
printf( "[-duration:X] pass logs in which the timer reaches this many minutes\n" );
printf( "[-minage:N] pass logs updated at least this many hours ago\n" );
printf( "[-maxage:N] pass logs updated at most this many hours ago\n" );
printf( "[-isserver] pass logs in which the local box is a listen server at least once\n" );
printf( "[-isclient] pass logs in which the local box is NEVER a listen server\n" );
printf( "[-isactive] pass logs that are still being updated\n" );
printf( "[-isdvdonly] pass logs in which the local box is fully DVD hosted\n" );
printf( "[-isusinghdd] pass logs in which the local box is using the HDD\n" );
printf( "[-language:<name>] pass logs with the language containing this substring\n" );
printf( "[-platform:<name>] pass logs generated on this platform ('360', 'PS3', 'PC')\n" );
printf( "\n" );
printf( "\n" );
printf( "Console mode:\n" );
printf( "\n" );
printf( " USAGE: [options] [folder]\n" );
printf( "\n" );
printf( "In console mode, memlog keeps running until told to quit, allowing the user to\n" );
printf( "perform any number of log mining operations without having to re-load all the logs\n" );
printf( "each time. Commands in console mode are the same as the above command-line\n" );
printf( "options (without the leading '-'). Enter a set of commands, in any order (followed\n" );
printf( "by a folder path if appropriate), and hit enter. If you don't specify a path, the\n" );
printf( "most recently entered path will be used.\n" );
printf( "\n" );
printf( "console-mode-specific commands:\n" );
printf( "[load] load more memory logs, from the specified folder\n" );
printf( "[unload] unload memory logs in the specified folder\n" );
printf( "[unloadall] unload all memory logs\n" );
printf( "\n" );
}
void InitConfig( bool bCommandLine = false )
{
if ( bCommandLine )
{
// These persist after the first set of commands:
gConfig.sourcePath[0] = 0;
gConfig.consoleMode = false;
}
gConfig.recurse = false;
gConfig.update = false;
gConfig.updateActive = false;
gConfig.forceUpdate = false;
gConfig.load = false;
gConfig.unload = false;
gConfig.unloadAll = false;
gConfig.quitting = false;
gConfig.help = false;
// 'track' updates a memory tracking file, but only do it when asked
gConfig.trackFile[0] = 0;
gConfig.trackColumn[0] = 0;
gConfig.trackStat = TRACKSTAT_UNKNOWN;
// Default all filters off (no analysis)
gConfig.dangerLimit = 0;
gConfig.dangerTime = 0;
gConfig.duration = 0;
gConfig.minAge = 0;
gConfig.maxAge = 0;
gConfig.minPlayers = 0;
gConfig.maxPlayers = 0;
gConfig.isServer = false;
gConfig.isClient = false;
gConfig.isSplitscreen = false;
gConfig.isSinglescreen = false;
gConfig.isActive = false;
gConfig.dvdHosted = DVDHOSTED_UNKNOWN;
gConfig.mapFilter[0] = 0;
gConfig.playerFilter[0] = 0;
gConfig.languageFilter[0] = 0;
gConfig.platform = PLATFORM_UNKNOWN;
}
bool ParseOption( const char *option )
{
// Make everything lower-case for simplicity
strlwr( (char *)option );
if ( option[0] == '-' )
option++;
// Console mode
{
if ( !strcmp( option, "c" ) || !strcmp( option, "console" ) )
{
gConfig.consoleMode = true;
return true;
}
if ( !strcmp( option, "quit" ) || !strcmp( option, "exit" ) )
{
gConfig.quitting = true;
return true;
}
if ( !strcmp( option, "help" ) || !strcmp( option, "h" ) || !strcmp( option, "usage" ) || !strcmp( option, "?" ) )
{
gConfig.help = true;
return true;
}
}
// Data analysis (filters)
{
int dangerLimit;
if ( sscanf( option, "danger:%d", &dangerLimit ) == 1 )
{
if ( dangerLimit > 0 )
{
gConfig.dangerLimit = dangerLimit / 1024.0f; // KB -> MB
return true;
}
printf( "!'danger' must be > 0!\n" );
return false;
}
int dangerTime;
if ( sscanf( option, "dangertime:%d", &dangerTime ) == 1 )
{
if ( dangerTime > 0 )
{
gConfig.dangerTime = dangerTime*60; // Minutes
return true;
}
printf( "!'dangertime' must be > 0!\n" );
return false;
}
int duration;
if ( sscanf( option, "duration:%d", &duration ) == 1 )
{
if ( duration > 0 )
{
gConfig.duration = duration*60; // Minutes
return true;
}
printf( "!'duration' must be > 0!\n" );
return false;
}
int minAge;
if ( sscanf( option, "minage:%d", &minAge ) == 1 )
{
if ( minAge >= 0 )
{
gConfig.minAge = minAge*3600; // Hours
return true;
}
printf( "!'minage' must be >= 0!\n" );
return false;
}
int maxAge;
if ( sscanf( option, "maxage:%d", &maxAge ) == 1 )
{
if ( maxAge >= 0 )
{
gConfig.maxAge = maxAge*3600; // Hours
return true;
}
printf( "!'maxage' must be >= 0!\n" );
return false;
}
int minPlayers;
if ( sscanf( option, "minplayers:%d", &minPlayers ) == 1 )
{
if ( minPlayers >= 1 )
{
gConfig.minPlayers = minPlayers;
return true;
}
printf( "!'minplayers' must be >= 1!\n" );
return false;
}
int maxPlayers;
if ( sscanf( option, "maxplayers:%d", &maxPlayers ) == 1 )
{
if ( maxPlayers >= 1 )
{
gConfig.maxPlayers = maxPlayers;
return true;
}
printf( "!'maxplayers' must be >= 1!\n" );
return false;
}
if ( !strcmp( option, "isclient" ) )
{
if ( gConfig.isServer )
{
printf( "!'isclient' and 'isserver' are mutually exclusive!\n" );
return false;
}
gConfig.isClient = true;
return true;
}
if ( !strcmp( option, "isserver" ) )
{
if ( gConfig.isClient )
{
printf( "!'isclient' and 'isserver' are mutually exclusive!\n" );
return false;
}
gConfig.isServer = true;
return true;
}
if ( !strcmp( option, "issplitscreen" ) )
{
if ( gConfig.isSinglescreen )
{
printf( "!'issinglescreen' and 'issplitscreen' are mutually exclusive!\n" );
return false;
}
gConfig.isSplitscreen = true;
return true;
}
if ( !strcmp( option, "issinglescreen" ) )
{
if ( gConfig.isSplitscreen )
{
printf( "!'issinglescreen' and 'issplitscreen' are mutually exclusive!\n" );
return false;
}
gConfig.isSinglescreen = true;
return true;
}
if ( !strcmp( option, "isactive" ) )
{
gConfig.isActive = true;
return true;
}
if ( !strcmp( option, "isdvdonly" ) )
{
if ( gConfig.dvdHosted == DVDHOSTED_NO )
{
printf( "!'isdvdonly' and 'isusinghdd' are mutually exclusive!\n" );
return false;
}
gConfig.dvdHosted = DVDHOSTED_YES;
return true;
}
if ( !strcmp( option, "isusinghdd" ) )
{
if ( gConfig.dvdHosted == DVDHOSTED_YES )
{
printf( "!'isdvdonly' and 'isusinghdd' are mutually exclusive!\n" );
return false;
}
gConfig.dvdHosted = DVDHOSTED_NO;
return true;
}
char mapFilter[ FILTER_SIZE ] = "";
if ( sscanf( option, "map:%32s", mapFilter ) == 1 )
{
if ( mapFilter[0] )
{
strncpy( gConfig.mapFilter, mapFilter, sizeof( gConfig.mapFilter ) );
return true;
}
return false;
}
char playerFilter[ FILTER_SIZE ] = "";
if ( sscanf( option, "player:%32s", playerFilter ) == 1 )
{
if ( playerFilter[0] )
{
strncpy( gConfig.playerFilter, playerFilter, sizeof( gConfig.playerFilter ) );
return true;
}
return false;
}
char languageFilter[ FILTER_SIZE ] = "";
if ( sscanf( option, "language:%32s", languageFilter ) == 1 )
{
if ( languageFilter[0] )
{
strncpy( gConfig.languageFilter, languageFilter, sizeof( gConfig.languageFilter ) );
return true;
}
return false;
}
char platformFilter[ 16 ] = "";
if ( sscanf( option, "platform:%16s", platformFilter ) == 1 )
{
return StringToPlatformName( platformFilter, gConfig.platform );
}
}
// File processing
{
if ( !strcmp( option, "r" ) || !stricmp( option, "recurse" ) )
{
gConfig.recurse = true;
return true;
}
if ( !strcmp( option, "u" ) || !stricmp( option, "update" ) )
{
if ( gConfig.unload || gConfig.unloadAll )
{
printf( "!'update' is mututally exclusive with 'unload'!\n" );
return false;
}
gConfig.update = true;
gConfig.load = true; // Less confusing if update implies load
return true;
}
if ( !strcmp( option, "a" ) || !stricmp( option, "updateactive" ) )
{
if ( gConfig.unload || gConfig.unloadAll )
{
printf( "!'updateactive' is mututally exclusive with 'unload'!\n" );
return false;
}
gConfig.updateActive = true;
gConfig.update = true; // Less confusing if updateActive implies update
gConfig.load = true; // Less confusing if update implies load
return true;
}
if ( !strcmp( option, "f" ) || !stricmp( option, "force" ) )
{
// TODO: these error checks should really be a post-step - this depends on ordering (i.e. we don't get a message if force appears *before* unload)
if ( gConfig.unload || gConfig.unloadAll )
{
printf( "!'force' is mututally exclusive with 'unload'!\n" );
return false;
}
gConfig.load = true; // Less confusing if update implies load
gConfig.forceUpdate = true;
return true;
}
if ( !strcmp( option, "load" ) )
{
if ( gConfig.unload || gConfig.unloadAll )
{
printf( "!'load' is mututally exclusive with 'unload'!\n" );
return false;
}
gConfig.load = true;
return true;
}
if ( !strcmp( option, "unload" ) )
{
if ( gConfig.load )
{
printf( "!'load' is mututally exclusive with 'unload'!\n" );
return false;
}
gConfig.unload = true;
return true;
}
if ( !strcmp( option, "unloadall" ) )
{
if ( gConfig.load )
{
printf( "!'load' is mututally exclusive with 'unloadall'!\n" );
return false;
}
gConfig.unloadAll = true;
return true;
}
COMPILE_TIME_ASSERT( sizeof( gConfig.trackFile ) == MAX_PATH );
if ( 1 == sscanf( option, "track:%260s", gConfig.trackFile ) )
{
int nStrLen = strlen( gConfig.trackFile );
const char *ext = V_stristr( gConfig.trackFile, ".csv" );
if ( !ext || ( ( ext - &gConfig.trackFile[0] ) != ( nStrLen - 4 ) ) || ( nStrLen <= 4 ) )
{
printf( "!'track' must specify a .csv file!\n" );
return false;
}
// Filename suffix determines the stat tracked in the file
if ( V_stristr( gConfig.trackFile, "_MinFreeCPU" ) )
gConfig.trackStat = TRACKSTAT_MINFREE_CPU;
else if ( V_stristr( gConfig.trackFile, "_MinFreeGPU" ) )
gConfig.trackStat = TRACKSTAT_MINFREE_GPU;
else
{
printf( "!'track' .csv file must end with a valid stat type suffix!\n" );
return false;
}
return true;
}
COMPILE_TIME_ASSERT( sizeof( gConfig.trackColumn ) == 32 );
if ( 1 == sscanf( option, "trackcol:%32s", gConfig.trackColumn ) )
{
return true;
}
}
return false;
}
bool ParseCommandLine( int argc, _TCHAR* argv[], Config &config )
{
bool bCommandLine = true;
InitConfig( bCommandLine );
// Cache off the command-line:
gConfig.prevCommandLine[0] = 0;
for ( int i = 1; i < argc; i++ )
{
strcat( gConfig.prevCommandLine, argv[i] );
strcat( gConfig.prevCommandLine, " " );
}
int numOptions = 1;
while ( argv[numOptions] && argv[numOptions][0] == '-' )
{
if ( !ParseOption( argv[numOptions] ) )
{
printf( "ERROR: invalid command-line option '%s'!\n\n\n", argv[numOptions] );
Usage();
return false;
}
numOptions++;
}
if ( numOptions == argc )
{
if ( !gConfig.consoleMode )
{
printf( "ERROR: no folder path specified!\n\n\n" );
Usage();
return false;
}
CleanPath( "" );
}
else
{
gConfig.load = true;
CleanPath( argv[numOptions] );
}
return true;
}
void ExtractTokens( char *buffer, vector<string> &tokens )
{
char *context = NULL;
char *token = strtok_s( buffer, " ", &context );
while( token )
{
tokens.push_back( string( token ) );
token = strtok_s( NULL, " ", &context );
}
}
bool ParseConsoleCommand( void )
{
if ( !gConfig.consoleMode )
return false;
while( true )
{
// Loop until the user inputs a command without errors
bool bError = false;
// NOTE: this remembers the last folder path used
InitConfig();
printf( "\n\n\n\n" );
printf( "> current folder path: '%s'\n", gConfig.sourcePath[0] ? gConfig.sourcePath : "<none>" );
printf( "> Enter a command to process:\n" );
printf( ">\n" );
printf( "-> " );
char buffer[4*_MAX_PATH] = "";
// TODO: (?while debugging?) this interferes with command prompt cut'n'paste (QuickEdit gets around it):
cin.getline( buffer, sizeof( buffer ) );
strcpy( gConfig.prevCommandLine, buffer );
vector<string> commands;
ExtractTokens( buffer, commands );
if ( commands.size() < 1 )
bError = true;
for ( unsigned int i = 0; i < commands.size(); i++ )
{
string & command = commands[ i ];
if ( !ParseOption( command.c_str() ) )
{
// If parsing files, the last item should be the folder path
if ( ( i == ( commands.size() - 1 ) ) &&
( gConfig.load || gConfig.unload ) )
{
CleanPath( command.c_str() );
}
else
{
printf( "\nInvalid command '%s'\n\n", command.c_str() );
bError = true;
}
}
}
if ( ( gConfig.load || gConfig.unload ) && !gConfig.sourcePath[0] )
{
printf( "\nYou must specify a path in order to use '%s'\n\n", gConfig.load ? "load" : "unload" );
bError = true;
}
if ( gConfig.quitting )
return false;
if ( gConfig.help )
{
Usage();
}
else if ( !bError )
{
printf( ">\n" );
printf( "> current folder path: '%s'\n", gConfig.sourcePath[0] ? gConfig.sourcePath : "<none>" );
printf( "> Processing command...\n" );
printf( "\n" );
return true;
}
}
}
// NOTE: this app doesn't bother with little things like freeing memory - enjoy!
int _tmain(int argc, _TCHAR* argv[])
{
InitMapHash();
// Grab command-line options
if ( !ParseCommandLine( argc, argv, gConfig ) )
return 1;
CLogFiles results;
do
{
// Process log files
// TODO: aggregate error messages and spew them at the end, so it's easier to notice+read them (list of: " 'log' plus all errors for 'log' ", for every 'log' with errors)
ProcessLogFiles( gConfig.sourcePath, results );
// Print stats gathered from the log files
PrintStats( results );
}
// Continue processing commands (console mode) if requested
while( ParseConsoleCommand() );
// TODO: add a new option to filter on the 'memory dip' during a map - i.e. the biggest reduction in free memory during a map (always ignore the first ?2? entries for a given map - find the biggest dips to see if 2 is right... ideally, we want to synch up with charlie's numbers for this to be useful)
// TODO: quantize [MEMORYLOG] timestamps (set 'next time' rather than 'prev time' -> :00, :20, :40) so spew aligns for all clients in a game (well, it wouldn't really be synchronized, depending on how long they sat at their respective menus...)
// TODO: spew aggregate memlog data
// - worst-cases
// - av/min/max per map, per machine, globally
// probably want to ignore early high results, concentrate on longer-term numbers (after X times or minutes on a map)
// maybe spew results for "at least x minutes", for several different values of x (15, 30, 60, 90, 120...)
// - correlate mem with: play time, map loads, numplayers, listen Vs dedicated server,
// player join/quits, team death/restarts, campaign starts/ends, exit to menu...
// o load entries for a log file
// o create aggregates for a log file for various criteria
// o combine aggregates across all files
// TODO: would also like to parse SBH spew
// store values in the header (want maxima, per-alloc-size & per-heap)....
// - detect start of an SBH dump and write a func to process it (take note of Toms recent change to the spew - therell be two types of spew out there)
// think about detecting the main menu by looking for adjacent 'none' lines...
// - post-process the log and convert all reasonable 'none's into 'menu's
// - convert runs with a full minute of 'none' at either end
// - menu items must also be "client" and have zero players
if ( IsDebuggerPresent() )
{
printf( "\n\nPress any key to exit...\n" );
_getche();
}
return 0;
}