// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "TestPALLog.h" #include "Parent.h" #include "Misc/Guid.h" #include "Stats/StatsMisc.h" #include "HAL/RunnableThread.h" #include "HAL/PlatformMemory.h" #include "HAL/PlatformApplicationMisc.h" #include "GenericPlatform/GenericApplication.h" #include "TestDirectoryWatcher.h" #include "RequiredProgramMainCPPInclude.h" #include "HAL/MallocPoisonProxy.h" #include "HAL/ThreadSafeCounter64.h" #include "Misc/Fork.h" DEFINE_LOG_CATEGORY(LogTestPAL); IMPLEMENT_APPLICATION(TestPAL, "TestPAL"); #define ARG_PROC_TEST "proc" #define ARG_PROC_TEST_CHILD "proc-child" #define ARG_CASE_SENSITIVITY_TEST "case" #define ARG_MESSAGEBOX_TEST "messagebox" #define ARG_DIRECTORY_WATCHER_TEST "dirwatcher" #define ARG_THREAD_SINGLETON_TEST "threadsingleton" #define ARG_SYSINFO_TEST "sysinfo" #define ARG_CRASH_TEST "crash" #define ARG_ENSURE_TEST "ensure" #define ARG_STRINGPRECISION_TEST "stringprecision" #define ARG_DSO_TEST "dso" #define ARG_GET_ALLOCATION_SIZE_TEST "getallocationsize" #define ARG_MALLOC_THREADING_TEST "mallocthreadtest" #define ARG_MALLOC_REPLAY "mallocreplay" #define ARG_THREAD_PRIO_TEST "threadpriotest" #define ARG_INLINE_CALLSTACK_TEST "inline" #define ARG_STRINGS_ALLOCATION_TEST "stringsallocation" #define ARG_CREATEGUID_TEST "createguid" #define ARG_THREADSTACK_TEST "threadstack" #define ARG_EXEC_PROCESS_TEST "exec-process" #define ARG_FORK_TEST "fork" #define ARG_CMDLINE_PARSE_TEST "cmdline" namespace TestPAL { FString CommandLine; }; /** * FProcHandle test (child instance) */ int32 ProcRunAsChild(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); // set a random delay pretending to do some useful work up to a minute. srand(FPlatformProcess::GetCurrentProcessId()); double RandomWorkTime = FMath::FRandRange(0.0f, 6.0f); UE_LOG(LogTestPAL, Display, TEXT("Running proc test as child (pid %d), will be doing work for %f seconds."), FPlatformProcess::GetCurrentProcessId(), RandomWorkTime); double StartTime = FPlatformTime::Seconds(); // Use all the CPU! for (;;) { double CurrentTime = FPlatformTime::Seconds(); if (CurrentTime - StartTime >= RandomWorkTime) { break; } } UE_LOG(LogTestPAL, Display, TEXT("Child (pid %d) finished work."), FPlatformProcess::GetCurrentProcessId()); FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * FProcHandle test (parent instance) */ int32 ProcRunAsParent(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Running proc test as parent.")); // Run child instance continuously int NumChildrenToSpawn = 255, MaxAtOnce = 5; FParent Parent(NumChildrenToSpawn, MaxAtOnce); Parent.Run(); UE_LOG(LogTestPAL, Display, TEXT("Parent quit.")); FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * Tests a single file. */ void TestCaseInsensitiveFile(const FString & Filename, const FString & WrongFilename) { IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); IFileHandle * CreationHandle = PlatformFile.OpenWrite(*Filename); checkf(CreationHandle, TEXT("Could not create a test file for '%s'"), *Filename); delete CreationHandle; IFileHandle* CheckGoodHandle = PlatformFile.OpenRead(*Filename); checkf(CheckGoodHandle, TEXT("Could not open a test file for '%s' (zero probe)"), *Filename); delete CheckGoodHandle; IFileHandle* CheckWrongCaseRelHandle = PlatformFile.OpenRead(*WrongFilename); checkf(CheckWrongCaseRelHandle, TEXT("Could not open a test file for '%s'"), *WrongFilename); delete CheckWrongCaseRelHandle; PlatformFile.DeleteFile(*Filename); } /** * Case-(in)sensitivity test/ */ int32 CaseTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Running case sensitivity test.")); TestCaseInsensitiveFile(TEXT("Test.Test"), TEXT("teSt.teSt")); FString File(TEXT("Test^%!CaseInsens")); FString AbsFile = FPaths::ConvertRelativePathToFull(File); FString AbsFileUpper = AbsFile.ToUpper(); TestCaseInsensitiveFile(AbsFile, AbsFileUpper); FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * Message box test/ */ int32 MessageBoxTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Running message box test.")); FString Display(TEXT("I am a big big string in a big big game, it's not a big big thing if you print me. But I do do feel that I do do will be displayed wrong, displayed wrong... or not.")); FString Caption(TEXT("I am a big big caption in a big big game, it's not a big big thing if you print me. But I do do feel that I do do will be displayed wrong, displayed wrong... or not.")); EAppReturnType::Type Result = FPlatformMisc::MessageBoxExt(EAppMsgType::YesNo, *Display, *Caption); UE_LOG(LogTestPAL, Display, TEXT("MessageBoxExt result: %d."), static_cast(Result)); FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * ************ Thread singleton test ***************** */ /** * Per-thread singleton */ struct FPerThreadTestSingleton : public TThreadSingleton { FPerThreadTestSingleton() { UE_LOG(LogTestPAL, Log, TEXT("FPerThreadTestSingleton (this=%p) created for thread %d"), this, FPlatformTLS::GetCurrentThreadId()); } void DoSomething() { UE_LOG(LogTestPAL, Log, TEXT("Thread %d is about to quit"), FPlatformTLS::GetCurrentThreadId()); } virtual ~FPerThreadTestSingleton() { UE_LOG(LogTestPAL, Log, TEXT("FPerThreadTestSingleton (%p) destroyed for thread %d"), this, FPlatformTLS::GetCurrentThreadId()); } }; //DECLARE_THREAD_SINGLETON( FPerThreadTestSingleton ); /** * Thread runnable */ struct FSingletonTestingThread : public FRunnable { virtual uint32 Run() { FPerThreadTestSingleton& Dummy = FPerThreadTestSingleton::Get(); FPlatformProcess::Sleep(3.0f); Dummy.DoSomething(); return 0; } }; /** * Thread singleton test */ int32 ThreadSingletonTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Running thread singleton test.")); const int kNumTestThreads = 10; FSingletonTestingThread * RunnableArray[kNumTestThreads] = { nullptr }; FRunnableThread * ThreadArray[kNumTestThreads] = { nullptr }; // start all threads for (int Idx = 0; Idx < kNumTestThreads; ++Idx) { RunnableArray[Idx] = new FSingletonTestingThread(); ThreadArray[Idx] = FRunnableThread::Create(RunnableArray[Idx], *FString::Printf(TEXT("TestThread%d"), Idx)); } GLog->FlushThreadedLogs(); GLog->Flush(); // join all threads for (int Idx = 0; Idx < kNumTestThreads; ++Idx) { ThreadArray[Idx]->WaitForCompletion(); delete ThreadArray[Idx]; ThreadArray[Idx] = nullptr; delete RunnableArray[Idx]; RunnableArray[Idx] = nullptr; } FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } #if PLATFORM_LINUX static float ToMB(uint64 Value) { return Value * (1.0 / (1024.0 * 1024.0)); } #endif // PLATFORM_LINUX /** * Sysinfo test */ int32 SysInfoTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Running system info test.")); bool bIsRunningOnBattery = FPlatformMisc::IsRunningOnBattery(); UE_LOG(LogTestPAL, Display, TEXT(" FPlatformMisc::IsRunningOnBattery() = %s"), bIsRunningOnBattery ? TEXT("true") : TEXT("false")); GenericApplication * PlatformApplication = FPlatformApplicationMisc::CreateApplication(); checkf(PlatformApplication, TEXT("Could not create platform application!")); bool bIsMouseAttached = PlatformApplication->IsMouseAttached(); UE_LOG(LogTestPAL, Display, TEXT(" FPlatformMisc::IsMouseAttached() = %s"), bIsMouseAttached ? TEXT("true") : TEXT("false")); FString OSInstanceGuid = FPlatformMisc::GetOperatingSystemId(); UE_LOG(LogTestPAL, Display, TEXT(" FPlatformMisc::GetOperatingSystemId() = '%s'"), *OSInstanceGuid); FString UserDir = FPlatformProcess::UserDir(); UE_LOG(LogTestPAL, Display, TEXT(" FPlatformProcess::UserDir() = '%s'"), *UserDir); FString UserHomeDir = FPlatformProcess::UserHomeDir(); UE_LOG(LogTestPAL, Display, TEXT(" FPlatformProcess::UserHomeDir() = '%s'"), *UserHomeDir); FString UserTempDir = FPlatformProcess::UserTempDir(); UE_LOG(LogTestPAL, Display, TEXT(" FPlatformProcess::UserTempDir() = '%s'"), *UserTempDir); FString ApplicationSettingsDir = FPlatformProcess::ApplicationSettingsDir(); UE_LOG(LogTestPAL, Display, TEXT(" FPlatformProcess::ApplicationSettingsDir() = '%s'"), *ApplicationSettingsDir); FString ApplicationCurrentWorkingDir = FPlatformProcess::GetCurrentWorkingDirectory(); UE_LOG(LogTestPAL, Display, TEXT(" FPlatformProcess::GetCurrentWorkingDirectory() = '%s'"), *ApplicationCurrentWorkingDir); FPlatformMemory::DumpStats(*GLog); #if PLATFORM_LINUX FExtendedPlatformMemoryStats StatsEx = FUnixPlatformMemory::GetExtendedStats(); UE_LOG(LogTestPAL, Display, TEXT("Shared_Clean:%.2f MB, Shared_Dirty:%.2f MB"), ToMB(StatsEx.Shared_Clean), ToMB(StatsEx.Shared_Dirty)); UE_LOG(LogTestPAL, Display, TEXT("Private_Clean:%.2fMB Private_Dirty:%.2fMB"), ToMB(StatsEx.Private_Clean), ToMB(StatsEx.Private_Dirty)); #endif // PLATFORM_LINUX FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * Crash test */ int32 CrashTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Running crash test (this should not exit).")); if (FParse::Param(CommandLine, TEXT("logfatal"))) { UE_LOG(LogTestPAL, Fatal, TEXT(" LogFatal!")); } else if (FParse::Param(CommandLine, TEXT("check"))) { checkf(false, TEXT(" checkf!")); } else if (FParse::Param(CommandLine, TEXT("ensure"))) { ensureMsgf(false, TEXT(" ensureMsgf!")); } else if (FParse::Param(CommandLine, TEXT("unaligned"))) { FQuat Quat[2]; uint8* BytePtr = (uint8*) &Quat; FQuat* QuatBadPtr = (FQuat*)(BytePtr + 3); *QuatBadPtr = FQuat::Identity; // This never going to be called, but kept here so entire block is not optimized out UE_LOG(LogTestPAL, Warning, TEXT("Quat.X = %f"), Quat[0].X); } else { *(int *)0x10 = 0x11; } FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * Ensure test */ int32 EnsureTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Running ensure test.")); // try ensures first (each ensure fires just once) { UE_LOG(LogTestPAL, Display, TEXT("Trying 5 ensureAlways 5 times.")); UE_LOG(LogTestPAL, Display, TEXT("-------------------------------------------------------------------------")); for (int IdxEnsure = 0; IdxEnsure < 5; ++IdxEnsure) { { UE_LOG(LogTestPAL, Display, TEXT("*************** FIRST ensureAlways #%d time ***************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled FIRST ensureAlways() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensureAlways(false); } { UE_LOG(LogTestPAL, Display, TEXT("*************** SECOND ensureAlways #%d time ***************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled SECOND ensureAlways() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensureAlways(false); } { UE_LOG(LogTestPAL, Display, TEXT("*************** THIRD ensureAlways #%d time ***************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled THIRD ensureAlways() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensureAlways(false); } { UE_LOG(LogTestPAL, Display, TEXT("*************** FOURTH ensureAlways #%d time ***************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled FOURTH ensureAlways() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensureAlways(false); } { UE_LOG(LogTestPAL, Display, TEXT("*************** FIFTH ensureAlways #%d time ***************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled FIFTH ensureAlways() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensureAlways(false); } } UE_LOG(LogTestPAL, Display, TEXT("Trying 10 ensures 5 times.")); UE_LOG(LogTestPAL, Display, TEXT("-------------------------------------------------------------------------")); for (int IdxEnsure = 0; IdxEnsure < 5; ++IdxEnsure) { { UE_LOG(LogTestPAL, Display, TEXT("****************** FIRST ensure #%d time ******************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled FIRST ensure() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensure(false); } { UE_LOG(LogTestPAL, Display, TEXT("****************** SECOND ensure #%d time ******************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled SECOND ensure() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensure(false); } { UE_LOG(LogTestPAL, Display, TEXT("****************** THIRD ensure #%d time ******************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled THIRD ensure() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensure(false); } { UE_LOG(LogTestPAL, Display, TEXT("****************** FOURTH ensure #%d time ******************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled FOURTH ensure() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensure(false); } { UE_LOG(LogTestPAL, Display, TEXT("****************** FIFTH ensure #%d time ******************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled FIFTH ensure() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensure(false); } { UE_LOG(LogTestPAL, Display, TEXT("****************** SIXTH ensure #%d time ******************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled SIXTH ensure() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensure(false); } { UE_LOG(LogTestPAL, Display, TEXT("****************** SEVENTH ensure #%d time ******************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled SEVENTH ensure() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensure(false); } { UE_LOG(LogTestPAL, Display, TEXT("****************** EIGTH ensure #%d time ******************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled EIGHTH ensure() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensure(false); } { UE_LOG(LogTestPAL, Display, TEXT("****************** NINETH ensure #%d time ******************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled NINETH ensure() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensure(false); } { UE_LOG(LogTestPAL, Display, TEXT("****************** TENTH ensure #%d time ******************"), IdxEnsure); FScopeLogTime EnsureLogTime(*FString::Printf(TEXT("Handled TENTH ensure() #%d time"), IdxEnsure), nullptr, FScopeLogTime::ScopeLog_Seconds); ensure(false); } } } FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * String Precision test */ int32 StringPrecisionTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Running string precision test.")); const FString TestString(TEXT("TestString")); int32 Indent = 15; UE_LOG(LogTestPAL, Display, TEXT("%*s"), Indent, *TestString); UE_LOG(LogTestPAL, Display, TEXT("Begining of the line %*s"), Indent, *TestString); UE_LOG(LogTestPAL, Display, TEXT("%*s end of the line"), Indent, *TestString); int Width = 2; for (uint32 Idx = 0; Idx < 10; ++Idx) { UE_LOG(LogTestPAL, Display, TEXT("DynSize: %d SignedDynFormat%0*d"), Width, Width, Idx); UE_LOG(LogTestPAL, Display, TEXT("DynSize: %d Size_tDynFormat%0*zd"), Width, Width, static_cast(Idx)); ++Width; } FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * Test Push/PopDll */ int32 DynamicLibraryTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Attempting to load Steam library")); FString RootSteamPath; FString LibraryName; if (PLATFORM_LINUX) { RootSteamPath = FPaths::EngineDir() / FString(TEXT("Binaries/ThirdParty/Steamworks/*")); IFileManager& PlatformFileManager = IFileManager::Get(); TArray FoundDirectories; PlatformFileManager.FindFiles(FoundDirectories, *RootSteamPath, false, true); // Just use the first directory we find, this test does not have to be very sophisticated. if (FoundDirectories.Num() > 0) { // This only gives us directories, so remove the wildcard from our initial search RootSteamPath.RemoveFromEnd(TEXT("*")); // And append the directory name we found. RootSteamPath += FoundDirectories[0]; } else { UE_LOG(LogTestPAL, Fatal, TEXT("Could not find any steam versions.")); } RootSteamPath += TEXT("/x86_64-unknown-linux-gnu/"); LibraryName = TEXT("libsteam_api.so"); } else { UE_LOG(LogTestPAL, Fatal, TEXT("This test is not implemented for this platform.")); } FPlatformProcess::PushDllDirectory(*RootSteamPath); void* SteamDLLHandle = FPlatformProcess::GetDllHandle(*LibraryName); FPlatformProcess::PopDllDirectory(*RootSteamPath); if (SteamDLLHandle == nullptr) { // try bundled one UE_LOG(LogTestPAL, Error, TEXT("Could not load via Push/PopDll, loading directly.")); SteamDLLHandle = FPlatformProcess::GetDllHandle(*(RootSteamPath + LibraryName)); if (SteamDLLHandle == nullptr) { UE_LOG(LogTestPAL, Fatal, TEXT("Could not load Steam library!")); } } if (SteamDLLHandle) { UE_LOG(LogTestPAL, Log, TEXT("Loaded Steam library at %p"), SteamDLLHandle); FPlatformProcess::FreeDllHandle(SteamDLLHandle); SteamDLLHandle = nullptr; } FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * FMalloc::GetAllocationSize() test */ int32 GetAllocationSizeTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Running GMalloc->GetAllocationSize() test.")); struct Allocation { void * Memory; SIZE_T RequestedSize; SIZE_T Alignment; SIZE_T ActualSize; }; TArray Allocs; SIZE_T TotalMemoryRequested = 0, TotalMemoryAllocated = 0; // force proxy malloc FMalloc* OldGMalloc = UE::Private::GMalloc; UE::Private::GMalloc = new FMallocPoisonProxy(UE::Private::GMalloc); // allocate all the memory and initialize with 0 for (uint32 Size = 16; Size < 4096; Size += 16) { for (SIZE_T AlignmentPower = 4; AlignmentPower <= 7; ++AlignmentPower) { SIZE_T Alignment = ((SIZE_T)1 << AlignmentPower); Allocation New; New.RequestedSize = Size; New.Alignment = Alignment; New.Memory = UE::Private::GMalloc->Malloc(New.RequestedSize, New.Alignment); if (!UE::Private::GMalloc->GetAllocationSize(New.Memory, New.ActualSize)) { UE_LOG(LogTestPAL, Fatal, TEXT("Could not get allocation size for %p"), New.Memory); } FMemory::Memzero(New.Memory, New.RequestedSize); TotalMemoryRequested += New.RequestedSize; TotalMemoryAllocated += New.ActualSize; Allocs.Add(New); } } UE_LOG(LogTestPAL, Log, TEXT("Allocated %llu memory (%llu requested) in %d chunks"), TotalMemoryAllocated, TotalMemoryRequested, Allocs.Num()); if (FParse::Param(CommandLine, TEXT("realloc"))) { for (Allocation & Alloc : Allocs) { // resize Alloc.RequestedSize += 16; Alloc.Memory = UE::Private::GMalloc->Realloc(Alloc.Memory, Alloc.RequestedSize, Alloc.Alignment); FMemory::Memzero(Alloc.Memory, Alloc.RequestedSize); } } else { for (Allocation & Alloc : Allocs) { // only fill the difference, if any if (Alloc.ActualSize > Alloc.RequestedSize) { SIZE_T Difference = Alloc.ActualSize - Alloc.RequestedSize; FMemory::Memset((void *)((SIZE_T)Alloc.Memory + Alloc.RequestedSize), 0xAA, Difference); } } } // check if any alloc got stomped for (Allocation & Alloc : Allocs) { for (SIZE_T Idx = 0; Idx < Alloc.RequestedSize; ++Idx) { if (((const uint8 *)Alloc.Memory)[Idx] != 0) { UE_LOG(LogTestPAL, Fatal, TEXT("Allocation at %p (offset %llu) got stomped with 0x%x"), Alloc.Memory, Idx, ((const uint8 *)Alloc.Memory)[Idx] ); } } } UE_LOG(LogTestPAL, Log, TEXT("No memory stomping detected")); for (Allocation & Alloc : Allocs) { UE::Private::GMalloc->Free(Alloc.Memory); } UE::Private::GMalloc = OldGMalloc; Allocs.Empty(); FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * Strings allocation test */ int32 StringsAllocationTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Running StringsAllocationTest test.")); const int32 NumOfStrings = 1000000; const TCHAR* SampleText = TEXT("Lorem ipsum dolor sit amet"); FString** Strings = new FString*[NumOfStrings]; UE_LOG(LogTestPAL, Display, TEXT("Allocating %u strings '%s'"), NumOfStrings, SampleText); for(int32 i = 0; i < NumOfStrings; ++i) { Strings[i] = new FString(SampleText); } if (GWarn) { UE::Private::GMalloc->DumpAllocatorStats(*GWarn); } for(int32 i = 0; i < NumOfStrings; ++i) { delete Strings[i]; } delete [] Strings; // UE::Private::GMalloc = OldGMalloc; FEngineLoop::AppExit(); return 0; } /** * Thread runnable * bUseGenericMisc Whether to use FGenericPlatformMisc CreateGuid */ template struct FCreateGuidThread : public FRunnable { /** Number of CreateGuid calls to make. */ int32 NumCalls; FCreateGuidThread(int32 InNumCalls) : FRunnable() , NumCalls(InNumCalls) { } virtual uint32 Run() { FGuid Result; for (int32 IdxRun = 0; IdxRun < NumCalls; ++IdxRun) { if (bUseGenericMisc) { FGenericPlatformMisc::CreateGuid(Result); } else { FPlatformMisc::CreateGuid(Result); } } return 0; } }; /** * CreateGuid test */ int32 CreateGuidTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Running CreateGuid test.")); TArray RunnableArray; TArray ThreadArray; UE_LOG(LogTestPAL, Display, TEXT("Accepted options:")); UE_LOG(LogTestPAL, Display, TEXT(" -numthreads=N")); UE_LOG(LogTestPAL, Display, TEXT(" -numcalls=N (how many times each thread will call CreateGuid)")); UE_LOG(LogTestPAL, Display, TEXT(" -norandomguids (disable SYS_getrandom syscall)")); int32 NumTestThreads = 4, NumCalls = 1000000; if (FParse::Value(CommandLine, TEXT("numthreads="), NumTestThreads)) { NumTestThreads = FMath::Max(1, NumTestThreads); } if (FParse::Value(CommandLine, TEXT("numcalls="), NumCalls)) { NumCalls = FMath::Max(1, NumCalls); } // start all threads for (int Idx = 0; Idx < 2; ++Idx ) { bool bUseGenericMisc = (Idx == 0); double WallTimeDuration = FPlatformTime::Seconds(); for (int IdxThread = 0; IdxThread < NumTestThreads; ++IdxThread) { RunnableArray.Add(bUseGenericMisc ? static_cast(new FCreateGuidThread(NumCalls)) : static_cast(new FCreateGuidThread(NumCalls)) ); ThreadArray.Add( FRunnableThread::Create(RunnableArray[IdxThread], *FString::Printf(TEXT("GuidTest%d"), IdxThread)) ); } GLog->FlushThreadedLogs(); GLog->Flush(); // join all threads for (int IdxThread = 0; IdxThread < NumTestThreads; ++IdxThread) { ThreadArray[IdxThread]->WaitForCompletion(); delete ThreadArray[IdxThread]; ThreadArray[IdxThread] = nullptr; delete RunnableArray[IdxThread]; RunnableArray[IdxThread] = nullptr; } WallTimeDuration = FPlatformTime::Seconds() - WallTimeDuration; UE_LOG(LogTestPAL, Display, TEXT("--- Results for %s ---"), bUseGenericMisc ? TEXT("FGenericPlatformMisc::CreateGuid") : TEXT("FPlatformMisc::CreateGuid")); UE_LOG(LogTestPAL, Display, TEXT("Total wall time: %f seconds, Threads: %d, Calls: %d"), WallTimeDuration, NumTestThreads, NumCalls); ThreadArray.Empty(); RunnableArray.Empty(); } // Spew out a small sample of GUIDs to visually check we haven't horribly broken something UE_LOG(LogTestPAL, Display, TEXT("--- CreateGuid Samples ---")); for (int Idx = 0; Idx < 20; ++Idx) { FGuid GuidPlatform; FGuid GuidGeneric; FPlatformMisc::CreateGuid(GuidPlatform); FGenericPlatformMisc::CreateGuid(GuidGeneric); UE_LOG(LogTestPAL, Display, TEXT("%3d GuidGeneric:%s GuidPlatform:%s"), Idx, *GuidGeneric.ToString(EGuidFormats::DigitsWithHyphensInBraces), *GuidPlatform.ToString(EGuidFormats::DigitsWithHyphensInBraces)); } // UE::Private::GMalloc = OldGMalloc; FEngineLoop::AppExit(); return 0; } /** An ugly way to pass a parameters to FRunnable; shouldn't matter for this test code. */ int32 GMallocTestNumRuns = 500; int32 GMallocTestMemoryPerThreadKB = 64*1024; // 64 MB bool GMallocTestTouchTMemory = false; FThreadSafeCounter64 GTotalAllocsDone; FThreadSafeCounter64 GTotalMemoryAllocated; /** * Thread runnable * bUseSystemMalloc Whether to use system malloc for speed comparison. */ template struct FMemoryAllocatingThread : public FRunnable { virtual uint32 Run() { void* Ptrs[8192]; for (int32 IdxRun = 0; IdxRun < GMallocTestNumRuns; ++IdxRun) { FMemory::Memzero(Ptrs); int32 NumAllocs = 0; uint64 MemAllocatedThisRun = 0; while(MemAllocatedThisRun < GMallocTestMemoryPerThreadKB * 1024 && NumAllocs < UE_ARRAY_COUNT(Ptrs)) { // increase the probability of the smallest chunks, but allow fairly large (up to 16 MB) double Uniform = FMath::FRand(); double Asymptote = (Uniform >= DBL_EPSILON) ? (1 / Uniform) : 1.0; double SkewedTowardsSmall = FMath::Min(1.0, Asymptote / 4096.0); // 4096 is an arbitrary constant const int32 ChunkSize = 1 + static_cast(16384.0 * 1024.0 * SkewedTowardsSmall); //printf("Uniform: %f, Asymptote: %f, SkewedTowardsSmall: %f, ChunkSize: %d bytes\n", Uniform, Asymptote, SkewedTowardsSmall, ChunkSize); //fflush(stdout); void* Ptr = nullptr; if (bUseSystemMalloc) { Ptr = malloc(ChunkSize); } else { Ptr = FMemory::Malloc(ChunkSize); } if (LIKELY(Ptr)) { Ptrs[NumAllocs++] = Ptr; MemAllocatedThisRun += ChunkSize; // touch the memory if not measuring the speed if (UNLIKELY(GMallocTestTouchTMemory)) { FMemory::Memset(Ptr, 0xff, ChunkSize); } } else { break; } } //UE_LOG(LogTestPAL, Display, TEXT("NumAllocs = %d, MemAllocatedThisRun=%llu"), NumAllocs, MemAllocatedThisRun); GTotalAllocsDone.Add(NumAllocs); GTotalMemoryAllocated.Add(MemAllocatedThisRun); // allocate in somewhat atypical order to make it harder for malloc // (reinterpret Ptrs table as a 2D array of kAllocFieldWidth x NumAllocH allocs) const int32 kAllocFieldWidth = 4; int32 NumAllocsH = (NumAllocs / kAllocFieldWidth) + 1; for (int32 IdxAllocX = kAllocFieldWidth - 1; IdxAllocX >= 0; --IdxAllocX) { for (int32 IdxAllocY = 0; IdxAllocY < NumAllocsH; ++IdxAllocY) { int32 IdxAlloc = IdxAllocY * kAllocFieldWidth + IdxAllocX; if (LIKELY(IdxAlloc < NumAllocs)) { void* Ptr = Ptrs[IdxAlloc]; if (LIKELY(Ptr)) { if (bUseSystemMalloc) { free(Ptr); } else { FMemory::Free(Ptr); } } } } } } return 0; } }; /** * Malloc threading test */ int32 MallocThreadingTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); TArray RunnableArray; TArray ThreadArray; UE_LOG(LogTestPAL, Display, TEXT("Accepted options:")); UE_LOG(LogTestPAL, Display, TEXT(" -systemmalloc to use system malloc")); UE_LOG(LogTestPAL, Display, TEXT(" -numthreads=N")); UE_LOG(LogTestPAL, Display, TEXT(" -memperthread=N (how much memory each thread will allocate in KB)")); UE_LOG(LogTestPAL, Display, TEXT(" -numruns=N (how many times the thread will allocate and free that memory)")); UE_LOG(LogTestPAL, Display, TEXT(" -touchmem (touch the memory to make sure it's backed by physical RAM - this can dominate the execution time)")); bool bUseSystemMalloc = FParse::Param(CommandLine, TEXT("systemmalloc")); int32 NumTestThreads = 4, InNumTestThreads = 0, InNumRuns = 0, InMemPerThreadKB = 0; if (FParse::Value(CommandLine, TEXT("numthreads="), InNumTestThreads)) { NumTestThreads = FMath::Max(1, InNumTestThreads); } if (FParse::Value(CommandLine, TEXT("numruns="), InNumRuns)) { GMallocTestNumRuns = FMath::Max(1, InNumRuns); } if (FParse::Value(CommandLine, TEXT("memperthread="), InMemPerThreadKB)) { GMallocTestMemoryPerThreadKB = FMath::Max(0, InMemPerThreadKB); } if (FParse::Param(CommandLine, TEXT("touchmem"))) { GMallocTestTouchTMemory = true; } UE_LOG(LogTestPAL, Display, TEXT("Running malloc threading test using %s malloc and %d threads, each allocating %sup to %d KB (%d MB) memory %d times."), bUseSystemMalloc ? TEXT("libc") : UE::Private::GMalloc->GetDescriptiveName(), NumTestThreads, GMallocTestTouchTMemory ? TEXT("and touching ") : TEXT(""), GMallocTestMemoryPerThreadKB, GMallocTestMemoryPerThreadKB / 1024, GMallocTestNumRuns ); // start all threads double WallTimeDuration = 0.0; { FSimpleScopeSecondsCounter Duration(WallTimeDuration); for (int Idx = 0; Idx < NumTestThreads; ++Idx) { RunnableArray.Add( bUseSystemMalloc ? static_cast(new FMemoryAllocatingThread()) : static_cast(new FMemoryAllocatingThread()) ); ThreadArray.Add( FRunnableThread::Create(RunnableArray[Idx], *FString::Printf(TEXT("MallocTest%d"), Idx)) ); } GLog->FlushThreadedLogs(); GLog->Flush(); // join all threads for (int Idx = 0; Idx < NumTestThreads; ++Idx) { ThreadArray[Idx]->WaitForCompletion(); delete ThreadArray[Idx]; ThreadArray[Idx] = nullptr; delete RunnableArray[Idx]; RunnableArray[Idx] = nullptr; } } UE_LOG(LogTestPAL, Display, TEXT("--- Results for %s malloc ---"), bUseSystemMalloc ? TEXT("libc") : UE::Private::GMalloc->GetDescriptiveName()); UE_LOG(LogTestPAL, Display, TEXT("Total wall time: %f seconds"), WallTimeDuration); UE_LOG(LogTestPAL, Display, TEXT("Total allocations done: %llu"), GTotalAllocsDone.GetValue()); UE_LOG(LogTestPAL, Display, TEXT("Total memory allocated: %llu bytes (%llu KB, %llu MB)"), GTotalMemoryAllocated.GetValue(), GTotalMemoryAllocated.GetValue() / 1024, GTotalMemoryAllocated.GetValue() / (1024 * 1024)); double AllocsPerSecond = (WallTimeDuration > 0.0) ? GTotalAllocsDone.GetValue() / WallTimeDuration : 0.0; double BytesPerSecond = (WallTimeDuration > 0.0) ? GTotalMemoryAllocated.GetValue() / WallTimeDuration : 0.0; UE_LOG(LogTestPAL, Display, TEXT("Speed in allocs: %.1f Kallocs/sec (%.1f allocs/sec)%s"), AllocsPerSecond / 1000.0, AllocsPerSecond, GMallocTestTouchTMemory ? TEXT("- less relevant since memory was touched") : TEXT("")); UE_LOG(LogTestPAL, Display, TEXT("Speed in bytes: %.1f MB/sec (%.1f KB/sec, %.1f bytes/sec)%s"), BytesPerSecond / (1024.0 * 1024.0), BytesPerSecond / 1024.0, BytesPerSecond, GMallocTestTouchTMemory ? TEXT("- less relevant since memory is touched") : TEXT("")); FPlatformMemoryStats Stats = FPlatformMemory::GetStats(); UE_LOG(LogTestPAL, Display, TEXT("Peak used resident RAM: %llu MB (%llu KB, %llu bytes)"), Stats.PeakUsedPhysical / (1024 * 1024), Stats.PeakUsedPhysical / 1024, Stats.PeakUsedPhysical); UE_LOG(LogTestPAL, Display, TEXT("Peak used virtual RAM: %llu MB (%llu KB, %llu bytes)"), Stats.PeakUsedVirtual / (1024 * 1024), Stats.PeakUsedVirtual / 1024, Stats.PeakUsedVirtual); FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * Replays a malloc save file, streaming it from the disk. Waits for Ctrl-C until exiting. * * @param ReplayFileName file name to read. Must be reachable from cwd (current working directory) or absolute * @param OperationToStopAfter number of operation to stop on - no further reads will be done. Useful to compare peak usage in the middle of file. * @param bSuppressErrors Whether to print errors */ void ReplayMallocFile(const FString& ReplayFileName, uint64 OperationToStopAfter, bool bSuppressErrors) { #if PLATFORM_WINDOWS FILE* ReplayFile = nullptr; if (fopen_s(&ReplayFile, TCHAR_TO_UTF8(*ReplayFileName), "rb") != 0 || ReplayFile == nullptr) #else FILE* ReplayFile = fopen(TCHAR_TO_UTF8(*ReplayFileName), "rb"); if (ReplayFile == nullptr) #endif // PLATFORM_WINDOWS { return; } // For file format, see FMallocReplayProxy // skip the first line since it contains column headers { char DummyBuffer[4096]; fgets(DummyBuffer, sizeof(DummyBuffer), ReplayFile); } double WallTimeDuration = 0.0; uint64 OperationNumber = 0; TMap FilePointerToRamPointers; for(;;) { FSimpleScopeSecondsCounter Duration(WallTimeDuration); char OpBuffer[128] = {0}; uint64 PtrOut, PtrIn, Size, Ordinal; uint32 Alignment; #if PLATFORM_WINDOWS if (fscanf_s(ReplayFile, "%s %llu %llu %llu %u\t# %llu\n", OpBuffer, static_cast(sizeof(OpBuffer)), &PtrOut, &PtrIn, &Size, &Alignment, &Ordinal) != 6) #else if (fscanf(ReplayFile, "%s %llu %llu %llu %u\t# %llu\n", OpBuffer, &PtrOut, &PtrIn, &Size, &Alignment, &Ordinal) != 6) #endif // PLATFORM_WINDOWS { UE_LOG(LogTestPAL, Display, TEXT("Hit end of the replay file on %llu-th operation."), OperationNumber); break; } if (!FCStringAnsi::Strcmp(OpBuffer, "Malloc")) { if (FilePointerToRamPointers.Find(PtrOut) == nullptr) { void* Result = FMemory::Malloc(Size, Alignment); FilePointerToRamPointers.Add(PtrOut, Result); } else if (!bSuppressErrors) { UE_LOG(LogTestPAL, Error, TEXT("Replay file contains operation # %llu that returned pointer %llu, which was already allocated at that moment. Skipping."), Ordinal, PtrOut); } } else if (!FCStringAnsi::Strcmp(OpBuffer, "Realloc")) { void* PtrToRealloc = nullptr; if (PtrIn != 0) { void** RamPointer = FilePointerToRamPointers.Find(PtrIn); if (RamPointer != nullptr) { PtrToRealloc = *RamPointer; } else if (!bSuppressErrors) { UE_LOG(LogTestPAL, Error, TEXT("Replay file contains operation # %llu to reallocate pointer %llu, which was not allocated at that moment. Substituting with nullptr."), Ordinal, PtrIn); } } void* Result = FMemory::Realloc(PtrToRealloc, Size, Alignment); FilePointerToRamPointers.Remove(PtrIn); FilePointerToRamPointers.Add(PtrOut, Result); } else if (!FCStringAnsi::Strcmp(OpBuffer, "Free")) { void* PtrToFree = nullptr; if (PtrIn != 0) { void** RamPointer = FilePointerToRamPointers.Find(PtrIn); if (RamPointer != nullptr) { PtrToFree = *RamPointer; } else if (!bSuppressErrors) { UE_LOG(LogTestPAL, Error, TEXT("Replay file contains operation # %llu to free pointer %llu, which was not allocated at that moment. Substituting with nullptr."), Ordinal, PtrIn); } } FMemory::Free(PtrToFree); FilePointerToRamPointers.Remove(PtrIn); } else if (!bSuppressErrors) { UE_LOG(LogTestPAL, Error, TEXT("Replay file contains unknown operation '%s', skipping."), ANSI_TO_TCHAR(OpBuffer)); } ++OperationNumber; if (OperationNumber >= OperationToStopAfter) { UE_LOG(LogTestPAL, Display, TEXT("Stopping after %llu-th operation."), OperationNumber); break; } } UE_LOG(LogTestPAL, Display, TEXT("Replayed %llu operations in %f seconds, waiting for Ctrl-C to proceed further. You can now examine heap/process state."), OperationNumber, WallTimeDuration); while(!IsEngineExitRequested()) { FPlatformProcess::Sleep(1.0); } } /** * Malloc replaying test */ int32 MallocReplayTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); FString ReplayFileName; if (FParse::Value(CommandLine, TEXT("replayfile="), ReplayFileName)) { uint64 OperationToStopAfter = (uint64)(-1); if (!FParse::Value(CommandLine, TEXT("stopafter="), OperationToStopAfter)) { UE_LOG(LogTestPAL, Display, TEXT("You can pass -stopafter=N to stop after Nth operation.")); } bool bSuppressErrors = FParse::Param(CommandLine, TEXT("suppresserrors")); ReplayMallocFile(ReplayFileName, OperationToStopAfter, bSuppressErrors); } else { UE_LOG(LogTestPAL, Error, TEXT("No file to replay. Pass -replayfile=PathToFile.txt")); } FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } /** * Translates priorities into strings. * For priorities values, see GenericPlatformAffinity.h. */ const TCHAR* ThreadPrioToString(EThreadPriority Prio) { switch(Prio) { #define RET_PRIO(PrioName) case PrioName: return TEXT(#PrioName); RET_PRIO(TPri_Normal) RET_PRIO(TPri_AboveNormal) RET_PRIO(TPri_BelowNormal) RET_PRIO(TPri_Highest) RET_PRIO(TPri_Lowest) RET_PRIO(TPri_SlightlyBelowNormal) RET_PRIO(TPri_TimeCritical) #undef RET_PRIO default: checkf(false, TEXT("Unknown priority!")); break; } return nullptr; } /** * Thread (will be run with different prios) */ struct FThreadPrioTester : public FRunnable { /** Priority assigned to this thread (for printing purposes). */ EThreadPriority Prio; /** Counter incremented by this thread. */ uint64 Counter; /** Time to run (all threads should have the same value. */ double SecondsToRun; /** How much time the thread was actually running. Should be close to SecondsToRun, but may differ. */ double SecondsActuallyRan; FThreadPrioTester(EThreadPriority InPrio, double InSecondsToRun) : FRunnable() , Prio(InPrio) , Counter(0) , SecondsToRun(InSecondsToRun) { } virtual uint32 Run() override { double StartTime = FPlatformTime::Seconds(); for(;;) { // don't check too often to avoid threads bottlenecking on access to clock if (Counter % 65536 == 0) { // account for an unlikely case that we will never get to run in the allotted time and check first double RanForSoFar = FPlatformTime::Seconds() - StartTime; if (RanForSoFar >= SecondsToRun) { SecondsActuallyRan = RanForSoFar; break; } } ++Counter; } return 0; } }; /** * Thread priorities test */ int32 ThreadPriorityTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); UE_LOG(LogTestPAL, Display, TEXT("Accepted options:")); UE_LOG(LogTestPAL, Display, TEXT(" -secondstorun=N (for how long to run)")); UE_LOG(LogTestPAL, Display, TEXT(" -numthreadgroups=N (how many groups of threads to run - so we can saturate the CPU)")); float SecondsToRun = 16.0f, InSecondsToRun = 0.0f; int32 NumThreadGroups = 8, InNumThreadGroups = 0; if (FParse::Value(CommandLine, TEXT("secondstorun="), InSecondsToRun)) { SecondsToRun = FMath::Max(1.0f, InSecondsToRun); } if (FParse::Value(CommandLine, TEXT("numthreadgroups="), InNumThreadGroups)) { NumThreadGroups = FMath::Max(1, InNumThreadGroups); } UE_LOG(LogTestPAL, Display, TEXT("Running thread priority test (%d thread groups) for %.1f seconds."), NumThreadGroups, SecondsToRun ); /* Note that enum - as of now at least - is not ordered in any way and looks like this: TPri_Normal, TPri_AboveNormal, TPri_BelowNormal, TPri_Highest, TPri_Lowest, TPri_SlightlyBelowNormal, TPri_TimeCritical */ TArray RunnableArray; TArray ThreadArray; for (int32 IdxGroup = 0; IdxGroup < NumThreadGroups; ++IdxGroup) { RunnableArray.Add(new FThreadPrioTester(TPri_Normal, SecondsToRun)); RunnableArray.Add(new FThreadPrioTester(TPri_AboveNormal, SecondsToRun)); RunnableArray.Add(new FThreadPrioTester(TPri_BelowNormal, SecondsToRun)); RunnableArray.Add(new FThreadPrioTester(TPri_Highest, SecondsToRun)); RunnableArray.Add(new FThreadPrioTester(TPri_Lowest, SecondsToRun)); RunnableArray.Add(new FThreadPrioTester(TPri_SlightlyBelowNormal, SecondsToRun)); RunnableArray.Add(new FThreadPrioTester(TPri_TimeCritical, SecondsToRun)); } UE_LOG(LogTestPAL, Display, TEXT("Creating %d threads"), RunnableArray.Num()); for (int32 Idx = 0, NumThreads = RunnableArray.Num(); Idx < NumThreads; ++Idx) { ThreadArray.Add( FRunnableThread::Create(RunnableArray[Idx], *FString::Printf(TEXT("(%d)%s"), Idx / 7, ThreadPrioToString(RunnableArray[Idx]->Prio)), 0, RunnableArray[Idx]->Prio) ); } // join all threads for (int32 Idx = 0, NumThreads = RunnableArray.Num(); Idx < NumThreads; ++Idx) { ThreadArray[Idx]->WaitForCompletion(); delete ThreadArray[Idx]; ThreadArray[Idx] = nullptr; } GLog->FlushThreadedLogs(); GLog->Flush(); UE_LOG(LogTestPAL, Display, TEXT("--- Results ---")); // tally up all threads (note that as of now there are seven prios) uint64 AllThreadsCounters[7] = {0}; double AllThreadsTimes[7] = {0}; uint64 TotalCount = 0; // describe all threads for (int32 Idx = 0, NumThreads = RunnableArray.Num(); Idx < NumThreads; ++Idx) { uint32 PrioIndex = static_cast(RunnableArray[Idx]->Prio); if (PrioIndex >= UE_ARRAY_COUNT(AllThreadsCounters)) { UE_LOG(LogTestPAL, Fatal, TEXT("EThreadPriority enum changed and has values larger than 7 now. Revisit this code.")); } else { AllThreadsCounters[PrioIndex] += RunnableArray[Idx]->Counter; AllThreadsTimes[PrioIndex] += RunnableArray[Idx]->SecondsActuallyRan; TotalCount += RunnableArray[Idx]->Counter; } delete RunnableArray[Idx]; RunnableArray[Idx] = nullptr; } // compare to TPri_Normal priority uint64 BaseCount = FMath::Max(1ULL, AllThreadsCounters[static_cast(TPri_Normal)]); double BaseCountDbl = static_cast(BaseCount); uint32 OrderToPrint[7] = { static_cast(TPri_Lowest), static_cast(TPri_BelowNormal), static_cast(TPri_SlightlyBelowNormal), static_cast(TPri_Normal), static_cast(TPri_AboveNormal), static_cast(TPri_Highest), static_cast(TPri_TimeCritical) }; for (uint32 IdxPrio = 0; IdxPrio < UE_ARRAY_COUNT(OrderToPrint); ++IdxPrio) { uint32 Prio = OrderToPrint[IdxPrio]; checkf(Prio < UE_ARRAY_COUNT(AllThreadsCounters), TEXT("Number of thread priority enums changed, revisit this code.")); UE_LOG(LogTestPAL, Display, TEXT("Threads with prio %24s incremented counters %14llu times (%.1f x speed of TPri_Normal) during %1.f sec total"), ThreadPrioToString(static_cast(Prio)), AllThreadsCounters[Prio], static_cast(AllThreadsCounters[Prio]) / BaseCountDbl, AllThreadsTimes[Prio]); } FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } // inlined/non-inlined functions for testing namespace { void FORCENOINLINE LexicalBlock() { { { ensure(false); } } } void FORCENOINLINE LabelSwitch() { switch (1) { case 1: ensure(false); break; default: break; } } PRAGMA_DISABLE_UNREACHABLE_CODE_WARNINGS void FORCENOINLINE LabelGoto() { goto end; ensure(false); // skips all of these. Just want to create a bunch of inline statements in this single non inlined one ensure(false); ensure(false); ensure(false); ensure(false); ensure(false); end: ensure(false); } PRAGMA_RESTORE_UNREACHABLE_CODE_WARNINGS void FORCEINLINE inline_three_ensures() { ensure(false); } void FORCEINLINE inline_two_calls_inline_three() { inline_three_ensures(); } void FORCEINLINE inline_one_calls_inline_two() { inline_two_calls_inline_three(); } void FORCENOINLINE MultipleInlineDeep() { inline_one_calls_inline_two(); } void FORCEINLINE inline_crash() { *(int*)0x1 = 0x0; } void FORCENOINLINE no_inline_to_inline_crash() { inline_crash(); } } int32 InlineCallstacksTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); /* Three unique cases that we can only test two: * * LexicalBlocks (ie. scope/if/for/while blocks * Labels (ie. goto/switch statements) * Try/Catch (cannot use when execptions are disabled, but we *should* handle this case) * * As well as testing, multiple deep inlining. ie. Calling multiple inline functions and still * seeing the call site * * Just make sure that the functions used + their callsite into ensure(false) are correctly reported in * the callstack */ UE_LOG(LogTestPAL, Warning, TEXT("\n*** Lexical Block ***")); LexicalBlock(); UE_LOG(LogTestPAL, Warning, TEXT("\n*** Label Switch ***")); LabelSwitch(); UE_LOG(LogTestPAL, Warning, TEXT("\n*** Label Goto ***")); LabelGoto(); UE_LOG(LogTestPAL, Warning, TEXT("\n*** Multiple Inlined ***")); MultipleInlineDeep(); // Should always be the last case, as this crashes UE_LOG(LogTestPAL, Warning, TEXT("\n*** Array delegate to crash***")); TArray> a; a.Push([] { no_inline_to_inline_crash(); }); a[0](); return 0; } struct FThreadStackTest : public FRunnable { FThreadStackTest(uint64 InThreadId = ~0) : ThreadId(InThreadId) { } uint64 ThreadId; virtual uint32 Run() { // If we dont have a valid ThreadId lets use the threads id as default if (ThreadId == ~0) { ThreadId = FPlatformTLS::GetCurrentThreadId(); } // Hopefully this wont be to much extra stack space for a testing thread const SIZE_T StackTraceSize = 65536; ANSICHAR StackTrace[StackTraceSize] = {0}; FPlatformStackWalk::ThreadStackWalkAndDump(StackTrace, StackTraceSize, 0, ThreadId); UE_LOG(LogTestPAL, Warning, TEXT("***** ThreadStackWalkAndDump for ThreadId(%lu) ******\n%s"), ThreadId, ANSI_TO_TCHAR(StackTrace)); const SIZE_T BackTraceSize = 100; uint64 BackTrace[BackTraceSize]; int32 BackTraceCount = FPlatformStackWalk::CaptureThreadStackBackTrace(ThreadId, BackTrace, BackTraceSize); UE_LOG(LogTestPAL, Warning, TEXT("***** CaptureThreadStackBackTrace for ThreadId(%lu) ******"), ThreadId); for (int i = 0; i < BackTraceCount; i++) { UE_LOG(LogTestPAL, Warning, TEXT("0x%llx"), BackTrace[i]); } UE_LOG(LogTestPAL, Warning, TEXT("\n\n")); UE_LOG(LogTestPAL, Warning, TEXT("***** ProgramCounterToHumanReadableString for BackTrace for ThreadId(%lu) ******"), ThreadId); for (int i = 0; i < BackTraceCount; i++) { ANSICHAR TempString[1024] = {0}; FPlatformStackWalk::ProgramCounterToHumanReadableString(i, BackTrace[i], TempString, UE_ARRAY_COUNT(TempString)); UE_LOG(LogTestPAL, Warning, TEXT("%s"), ANSI_TO_TCHAR(TempString)); } return 0; } }; int32 ThreadTraceTest(const TCHAR* CommandLine) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); // Dump the callstack/backtrace of the thread that is running { FThreadStackTest* ThreadStackTest = new FThreadStackTest; FRunnableThread* Runnable = FRunnableThread::Create(ThreadStackTest, ANSI_TO_TCHAR("ThreadStackTest"), 0); Runnable->WaitForCompletion(); delete ThreadStackTest; } // Dump the GGameThreadId callstack/backtrace from the thread { FThreadStackTest* ThreadStackTest = new FThreadStackTest(GGameThreadId); FRunnableThread* Runnable = FRunnableThread::Create(ThreadStackTest, ANSI_TO_TCHAR("ThreadStackTest"), 0); Runnable->WaitForCompletion(); delete ThreadStackTest; } FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } int32 ForkTest(const TCHAR* CommandLine) { // Only supported on Linux if (PLATFORM_LINUX) { FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); if (FPlatformProcess::SupportsMultithreading()) { UE_LOG(LogTestPAL, Error, TEXT("WaitAndFork is only supported with '-nothreading' command line")); } else { UE_LOG(LogTestPAL, Warning, TEXT("About to fork. Press Ctrl+C to close parent process")); FPlatformProcess::EWaitAndForkResult Result = FPlatformProcess::WaitAndFork(); if (Result == FPlatformProcess::EWaitAndForkResult::Child) { UE_LOG(LogTestPAL, Display, TEXT("Child process: %d IsChildProcess: %d"), FPlatformProcess::GetCurrentProcessId(), FForkProcessHelper::IsForkedChildProcess()); } else { // parent, WaitAndFork holds the parent process to spawn more children until the we are closing UE_LOG(LogTestPAL, Display, TEXT("Parent process: %d IsChildProcess: %d"), FPlatformProcess::GetCurrentProcessId(), FForkProcessHelper::IsForkedChildProcess()); } } FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); } return 0; } /** * Launch the external tool as a process and wait for it to complete * * Set executable and parameters via something like: * * ./Engine/Binaries/Linux/TestPAL exec-process exe=/bin/ls params=/tmp * */ int32 ExecProcessTest(const TCHAR *CommandLine) { FString Exe; FString Params; FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); if (!FParse::Value(CommandLine, TEXT("exe="), Exe)) { Exe = TEXT("nonexistent-exe"); } if (!FParse::Value(CommandLine, TEXT("params="), Params)) { Params = TEXT(""); } UE_LOG(LogTestPAL, Display, TEXT(" Exe:'%s' Parameters:'%s'"), *Exe, *Params); int32 ReturnCode; FString OutStdOut; FString OutStdErr; bool Ret = FPlatformProcess::ExecProcess(*Exe, *Params, &ReturnCode, &OutStdOut, &OutStdErr); UE_LOG(LogTestPAL, Display, TEXT("ExecProcess returns %d"), Ret); UE_LOG(LogTestPAL, Display, TEXT(" ReturnCode:%d"), ReturnCode); UE_LOG(LogTestPAL, Display, TEXT(" StdOut:%s"), *OutStdOut); UE_LOG(LogTestPAL, Display, TEXT(" StdErr:%s"), *OutStdErr); FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 0; } int32 CmdlineParseTest(const TCHAR *CommandLine) { int32 NotOk = 0; // zero when everything has passed so far FPlatformMisc::SetCrashHandler(NULL); FPlatformMisc::SetGracefulTerminationHandler(); GEngineLoop.PreInit(CommandLine); void* ReadPipe = nullptr; void* WritePipe = nullptr; verify(FPlatformProcess::CreatePipe(ReadPipe, WritePipe)); uint32 ProcessID; // key is the command line to test, value is the expected parse result when run through 'echo' TArray> tests = { { TEXT("foo"), TEXT("foo") }, { TEXT("\"foo bar\""), TEXT("foo bar") }, { TEXT("\"\"foo bar\"\""), TEXT("\"foo bar\"") }, { TEXT("\"foo"), TEXT("") }, { TEXT("\"\"foo"), TEXT("") }, { TEXT("-logpath=\"path with space\""), TEXT("-logpath=path with space") }, { TEXT("-logpath=\"\"double quoted path with space\"\""), TEXT("-logpath=\"double quoted path with space\"") }, { TEXT("bar \"\"foo blarg"), TEXT("bar") }, }; for(int i=0;iGetAllocationSize()"), UTF8_TO_TCHAR(ARG_GET_ALLOCATION_SIZE_TEST)); UE_LOG(LogTestPAL, Warning, TEXT(" %s: test malloc for thread-safety and performance."), UTF8_TO_TCHAR(ARG_MALLOC_THREADING_TEST)); UE_LOG(LogTestPAL, Warning, TEXT(" %s: test by replaying a saved malloc history saved by -mallocsavereplay. Possible options: -replayfile=File, -stopafter=N (operation), -suppresserrors"), UTF8_TO_TCHAR(ARG_MALLOC_REPLAY)); UE_LOG(LogTestPAL, Warning, TEXT(" %s: test thread priorities."), UTF8_TO_TCHAR(ARG_THREAD_PRIO_TEST)); UE_LOG(LogTestPAL, Warning, TEXT(" %s: test inline callstacks through ensures and a final crash."), UTF8_TO_TCHAR(ARG_INLINE_CALLSTACK_TEST)); UE_LOG(LogTestPAL, Warning, TEXT(" %s: test string allocations."), UTF8_TO_TCHAR(ARG_STRINGS_ALLOCATION_TEST)); UE_LOG(LogTestPAL, Warning, TEXT(" %s: test CreateGuid."), UTF8_TO_TCHAR(ARG_CREATEGUID_TEST)); UE_LOG(LogTestPAL, Warning, TEXT(" %s: test ThreadWalkStackAndDump and CaptureThreadBackTrace."), UTF8_TO_TCHAR(ARG_THREADSTACK_TEST)); UE_LOG(LogTestPAL, Warning, TEXT(" %s: test ExecProcess. Possible options: [-exe=executable] [-params=parameters]"), UTF8_TO_TCHAR(ARG_EXEC_PROCESS_TEST)); if (PLATFORM_LINUX) { UE_LOG(LogTestPAL, Warning, TEXT(" %s: test WaitAndFork"), UTF8_TO_TCHAR(ARG_FORK_TEST)); UE_LOG(LogTestPAL, Warning, TEXT(" %s: test CmdlineParse"), UTF8_TO_TCHAR(ARG_CMDLINE_PARSE_TEST)); } UE_LOG(LogTestPAL, Warning, TEXT("")); UE_LOG(LogTestPAL, Warning, TEXT("Pass one of those to run an appropriate test.")); FEngineLoop::AppPreExit(); FEngineLoop::AppExit(); return 1; } int32 main(int32 ArgC, char* ArgV[]) { TestPAL::CommandLine = TEXT(""); for (int32 Option = 1; Option < ArgC; Option++) { TestPAL::CommandLine += TEXT(" "); FString Argument(ANSI_TO_TCHAR(ArgV[Option])); if (Argument.Contains(TEXT(" "))) { if (Argument.Contains(TEXT("="))) { FString ArgName; FString ArgValue; Argument.Split( TEXT("="), &ArgName, &ArgValue ); Argument = FString::Printf( TEXT("%s=\"%s\""), *ArgName, *ArgValue ); } else { Argument = FString::Printf(TEXT("\"%s\""), *Argument); } } TestPAL::CommandLine += Argument; } return MultiplexedMain(ArgC, ArgV); }