// memlog.cpp : mines memory data from vxconsole logs (see game/client/c_memorylog.cpp) #include "utlbuffer.h" #include #include #include #include #include #include #include #include #include #define _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS #include #include #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 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') CUtlVectorbadMaps; // 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 CMapHash; // For quickly determining is a string is in gMapNames CMapHash gMapHash; typedef hash_map 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 *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 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 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 &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> 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 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 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 &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 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 &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 &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 &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 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 &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] \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:] 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' 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:] pass log entries with map names containing this substring\n" ); printf( "[-player:] 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:] pass logs with the language containing this substring\n" ); printf( "[-platform:] 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 &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 : "" ); 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 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 : "" ); 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; }