bool UParticleSystemAuditCommandlet::DumpSimpleSet(TSet<FString>& InSet, const TCHAR* InShortFilename, const TCHAR* InObjectClassName)
{
	if (InSet.Num() > 0)
	{
		check(InShortFilename != NULL);
		check(InObjectClassName != NULL);

		FArchive* OutputStream = GetOutputFile(InShortFilename);
		if (OutputStream != NULL)
		{
			UE_LOG(LogParticleSystemAuditCommandlet, Log, TEXT("Dumping '%s' results..."), InShortFilename);
			OutputStream->Logf(TEXT("%s,..."), InObjectClassName);
			for (TSet<FString>::TIterator DumpIt(InSet); DumpIt; ++DumpIt)
			{
				FString ObjName = *DumpIt;
				OutputStream->Logf(TEXT("%s"), *ObjName);
			}

			OutputStream->Close();
			delete OutputStream;
		}
		else
		{
			return false;
		}
	}
	return true;
}
Ejemplo n.º 2
0
/**
 * Dumps the frame times information to the special stats log file.
 */
void UEngine::DumpFrameTimesToStatsLog( float TotalTime, float DeltaTime, int32 NumFrames, const FString& InMapName )
{
#if ALLOW_DEBUG_FILES
	// Create folder for FPS chart data.
	const FString OutputDir = CreateOutputDirectory();

	// Create archive for log data.
	const FString ChartType = GetFPSChartType();
	const FString ChartName = OutputDir / CreateFileNameForChart( ChartType, InMapName, TEXT( ".csv" ) );
	FArchive* OutputFile = IFileManager::Get().CreateDebugFileWriter( *ChartName );

	if( OutputFile )
	{
		OutputFile->Logf(TEXT("Percentile,Frame (ms), GT (ms), RT (ms), GPU (ms)"));
		TArray<float> FrameTimesCopy = GFrameTimes;
		TArray<float> GameThreadFrameTimesCopy = GGameThreadFrameTimes;
		TArray<float> RenderThreadFrameTimesCopy = GRenderThreadFrameTimes;
		TArray<float> GPUFrameTimesCopy = GGPUFrameTimes;
		// using selection a few times should still be faster than full sort once,
		// since it's linear vs non-linear (O(n) vs O(n log n) for quickselect vs quicksort)
		for (int32 Percentile = 25; Percentile <= 75; Percentile += 25)
		{
			OutputFile->Logf(TEXT("%d,%.2f,%.2f,%.2f,%.2f"), Percentile,
				GetPercentileValue(FrameTimesCopy, Percentile) * 1000,
				GetPercentileValue(GameThreadFrameTimesCopy, Percentile) * 1000,
				GetPercentileValue(RenderThreadFrameTimesCopy, Percentile) * 1000,
				GetPercentileValue(GPUFrameTimesCopy, Percentile) * 1000
				);
		}

		OutputFile->Logf(TEXT("Time (sec),Frame (ms), GT (ms), RT (ms), GPU (ms)"));
		float ElapsedTime = 0;
		for( int32 i=0; i<GFrameTimes.Num(); i++ )
		{
			OutputFile->Logf(TEXT("%.2f,%.2f,%.2f,%.2f,%.2f"),ElapsedTime,GFrameTimes[i]*1000,GGameThreadFrameTimes[i]*1000,GRenderThreadFrameTimes[i]*1000,GGPUFrameTimes[i]*1000);
			ElapsedTime += GFrameTimes[i];
		}
		delete OutputFile;
	}
#endif
}
int32 UFixupRedirectsCommandlet::Main( const FString& Params )
{
	// Retrieve list of all packages in .ini paths.
	TArray<FString> PackageList;
	FEditorFileUtils::FindAllPackageFiles(PackageList);
	if( !PackageList.Num() )
	{
		return 0;
	}

	// process the commandline
	FString Token;
	const TCHAR* CommandLine	= *Params;
	bool bIsQuietMode			= false;
	bool bIsTestOnly			= false;
	bool bShouldRestoreProgress= false;
	bool bIsSCCDisabled		= false;
	bool bUpdateEnginePackages = false;
	bool bDeleteRedirects		= true;
	bool bDeleteOnlyReferencedRedirects = false;
	bool bAutoSubmit			= false;
	bool bSandboxed			= IFileManager::Get().IsSandboxEnabled();

	while (FParse::Token(CommandLine, Token, 1))
	{
		if (Token == TEXT("-nowarn"))
		{
			bIsQuietMode = true;
		}
		else if (Token == TEXT("-testonly"))
		{
			bIsTestOnly = true;
		}
		else if (Token == TEXT("-restore"))
		{
			bShouldRestoreProgress = true;
		}
		else if (Token == TEXT("-NoAutoCheckout"))
		{
			bIsSCCDisabled = true;
		}
		else if (Token == TEXT("-AutoSubmit"))
		{
			bAutoSubmit = true;
		}
		else if (Token == TEXT("-UpdateEnginePackages"))
		{
			bUpdateEnginePackages = true;
		}
		else if (Token == TEXT("-NoDelete"))
		{
			bDeleteRedirects = false;
		}
		else if (Token == TEXT("-NoDeleteUnreferenced"))
		{
			bDeleteOnlyReferencedRedirects = true;
		}
		else if ( Token.Left(1) != TEXT("-") )
		{
			FPackageName::SearchForPackageOnDisk(Token, &GRedirectCollector.FileToFixup);
		}
	}

	if (bSandboxed)
	{
		UE_LOG(LogFixupRedirectsCommandlet, Display, TEXT("Running in a sandbox."));
		bIsSCCDisabled = true;
		bAutoSubmit = false;
		bDeleteRedirects = false;
	}


	bool bShouldSkipErrors = false;

	// Setup file Output log in the Saved Directory
	const FString ProjectDir = FPaths::GetPath(FPaths::GetProjectFilePath());
	const FString LogFilename = FPaths::Combine(*ProjectDir, TEXT("Saved"), TEXT("FixupRedirect"), TEXT("FixupRedirect.log"));
	FArchive* LogOutputFile = IFileManager::Get().CreateFileWriter(*LogFilename);
	LogOutputFile->Logf(TEXT("[Redirects]"));

	/////////////////////////////////////////////////////////////////////
	// Display any script code referenced redirects
	/////////////////////////////////////////////////////////////////////

	TArray<FString> UpdatePackages;
	TArray<FString> RedirectorsThatCantBeCleaned;

	if (!bShouldRestoreProgress)
	{
		// load all string asset reference targets, and add fake redirectors for them
		GRedirectCollector.ResolveStringAssetReference();

		for (int32 RedirIndex = 0; RedirIndex < GRedirectCollector.Redirections.Num(); RedirIndex++)
		{
			FRedirection& Redir = GRedirectCollector.Redirections[RedirIndex];
			UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Boot redirector can't be cleaned: %s(%s) -> %s(%s)"), *Redir.RedirectorName, *Redir.RedirectorPackageFilename, *Redir.DestinationObjectName, *Redir.PackageFilename);
			RedirectorsThatCantBeCleaned.AddUnique(Redir.RedirectorName);
		}

		/////////////////////////////////////////////////////////////////////
		// Build a list of packages that need to be updated
		/////////////////////////////////////////////////////////////////////

		SET_WARN_COLOR(COLOR_WHITE);
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT(""));
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Generating list of tasks:"));
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("-------------------------"));
		CLEAR_WARN_COLOR();

		int32 GCIndex = 0;

		// process script code first pass, then everything else
		for( int32 PackageIndex = 0; PackageIndex < PackageList.Num(); PackageIndex++ )
		{
			const FString& Filename = PackageList[PackageIndex];

			// already loaded code, skip them, and skip autosave packages
			if (Filename.StartsWith(AutoSaveUtils::GetAutoSaveDir(), ESearchCase::IgnoreCase))
			{
				continue;
			}

			UE_LOG(LogFixupRedirectsCommandlet, Display, TEXT("Looking for redirects in %s..."), *Filename);

			// Assert if package couldn't be opened so we have no chance of messing up saving later packages.
			UPackage* Package = Cast<UPackage>(LoadPackage(NULL, *Filename, 0));

			// load all string asset reference targets, and add fake redirectors for them
			GRedirectCollector.ResolveStringAssetReference();

			if (!Package)
			{
				SET_WARN_COLOR(COLOR_RED);
				UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("   ... failed to open %s"), *Filename);
				CLEAR_WARN_COLOR();

				// if we are not in quiet mode, and the user wants to continue, go on
				const FText Message = FText::Format( NSLOCTEXT("Unreal", "PackageFailedToOpen", "The package '{0}' failed to open. Should this commandlet continue operation, ignoring further load errors? Continuing may have adverse effects (object references may be lost)."), FText::FromString( Filename ) );
				if (bShouldSkipErrors || (!bIsQuietMode && GWarn->YesNof( Message )))
				{
					bShouldSkipErrors = true;
					continue;
				}
				else
				{
					return 1;
				}
			}

			// all notices here about redirects get this color
			SET_WARN_COLOR(COLOR_DARK_GREEN);

			// look for any redirects that were followed that are referenced by this package
			// (may have been loaded earlier if this package was loaded by script code)
			// any redirectors referenced by script code can't be cleaned up
			for (int32 RedirIndex = 0; RedirIndex < GRedirectCollector.Redirections.Num(); RedirIndex++)
			{
				FRedirection& Redir = GRedirectCollector.Redirections[RedirIndex];

				if (Redir.PackageFilename == Filename)
				{
					UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("   ... references %s [-> %s]"), *Redir.RedirectorName, *Redir.DestinationObjectName);

					// put the source and dest packages in the list to be updated
					UpdatePackages.AddUnique(Redir.PackageFilename);
					UpdatePackages.AddUnique(Redir.RedirectorPackageFilename);
				}
			}

			// clear the flag for needing to collect garbage
			bool bNeedToCollectGarbage = false;

			// if this package has any redirectors, make sure we update it
			if (!GRedirectCollector.FileToFixup.Len() || GRedirectCollector.FileToFixup == FPackageName::FilenameToLongPackageName(Filename))
			{
				TArray<UObject *> ObjectsInOuter;
				GetObjectsWithOuter(Package, ObjectsInOuter);
				for( int32 Index = 0; Index < ObjectsInOuter.Num(); Index++ )
				{
					UObject* Obj = ObjectsInOuter[Index];
					UObjectRedirector* Redir = Cast<UObjectRedirector>(Obj);
					if (Redir)
					{
						// make sure this package is in the list of packages to update
						UpdatePackages.AddUnique(Filename);

						UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("   ... has redirect %s [-> %s]"), *Redir->GetPathName(), *Redir->DestinationObject->GetFullName());
						LogOutputFile->Logf(TEXT("%s = %s"), *Redir->GetPathName(), *Redir->DestinationObject->GetFullName());

						// make sure we GC if we found a redirector
						bNeedToCollectGarbage = true;
					}
				}
			}

			CLEAR_WARN_COLOR();

			// collect garbage every N packages, or if there was any redirectors in the package 
			// (to make sure that redirectors get reloaded properly and followed by the callback)
			// also, GC after loading a map package, to make sure it's unloaded so that we can track
			// other packages loading a map package as a dependency
			if (((++GCIndex) % 50) == 0 || bNeedToCollectGarbage || Package->ContainsMap())
			{
				// collect garbage to close the package
				CollectGarbage(RF_Native);
				UE_LOG(LogFixupRedirectsCommandlet, Display, TEXT("GC..."));
				// reset our counter
				GCIndex = 0;
			}
		}

		// save off restore point
		FArchive* Ar = IFileManager::Get().CreateFileWriter(*(FPaths::GameSavedDir() + TEXT("Fixup.bin")));
		*Ar << GRedirectCollector.Redirections;
		*Ar << UpdatePackages;
		*Ar << RedirectorsThatCantBeCleaned;
		delete Ar;
	}
	else
	{
		// load restore point
		FArchive* Ar = IFileManager::Get().CreateFileReader(*(FPaths::GameSavedDir() + TEXT("Fixup.bin")));
		if( Ar != NULL )
		{
			*Ar << GRedirectCollector.Redirections;
			*Ar << UpdatePackages;
			*Ar << RedirectorsThatCantBeCleaned;
			delete Ar;
		}
	}

	// unregister the callback so we stop getting redirections added
	FCoreUObjectDelegates::RedirectorFollowed.RemoveAll(&GRedirectCollector);
	
	/////////////////////////////////////////////////////////////////////
	// Explain to user what is happening
	/////////////////////////////////////////////////////////////////////
	SET_WARN_COLOR(COLOR_YELLOW);
	UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT(""));
	UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Files that need to fixed up:"));
	SET_WARN_COLOR(COLOR_DARK_YELLOW);

	// print out the working set of packages
	for (int32 PackageIndex = 0; PackageIndex < UpdatePackages.Num(); PackageIndex++)
	{
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("   %s"), *UpdatePackages[PackageIndex]);
	}

	UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT(""));
	CLEAR_WARN_COLOR();

	// if we are only testing, just just quiet before actually doing anything
	if (bIsTestOnly)
	{
		LogOutputFile->Close();
		delete LogOutputFile;
		LogOutputFile = NULL;
		return 0;
	}

	/////////////////////////////////////////////////////////////////////
	// Find redirectors that are referenced by packages we cant check out
	/////////////////////////////////////////////////////////////////////

	bool bEngineRedirectorCantBeCleaned = false;

	ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();

	// initialize source control if it hasn't been initialized yet
	if(!bIsSCCDisabled)
	{
		SourceControlProvider.Init();

		//Make sure we can check out all packages - update their state first
		SourceControlProvider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), UpdatePackages);
	}

	for( int32 PackageIndex = 0; PackageIndex < UpdatePackages.Num(); PackageIndex++ )
	{
		const FString& Filename = UpdatePackages[PackageIndex];

		bool bCanEdit = true;

		if(!bIsSCCDisabled)
		{
			FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(Filename, EStateCacheUsage::ForceUpdate);
			if(SourceControlState.IsValid() && !SourceControlState->CanEdit())
			{
				bCanEdit = false;
			}
		}
		else if(IFileManager::Get().IsReadOnly(*Filename))
		{
			bCanEdit = false;
		}

		if(!bCanEdit)
		{
			const bool bAllowCheckout = bUpdateEnginePackages || !Filename.StartsWith( FPaths::EngineDir() );

			if (!bIsSCCDisabled && bAllowCheckout)
			{
				FString PackageName(FPackageName::FilenameToLongPackageName(Filename));
				FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(Filename, EStateCacheUsage::ForceUpdate);
				if (SourceControlState.IsValid() && SourceControlState->CanCheckout())
				{
					SourceControlProvider.Execute(ISourceControlOperation::Create<FCheckOut>(), SourceControlHelpers::PackageFilename(PackageName));
					FSourceControlStatePtr NewSourceControlState = SourceControlProvider.GetState(Filename, EStateCacheUsage::ForceUpdate);
					bCanEdit = NewSourceControlState.IsValid() && NewSourceControlState->CanEdit();
				}
			}

			// if the checkout failed for any reason, we can't clean it up
			if (bAllowCheckout && !bCanEdit)
			{
				SET_WARN_COLOR(COLOR_RED);
				UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Couldn't check out/edit %s..."), *Filename);
				CLEAR_WARN_COLOR();

				// load all string asset reference targets, and add fake redirectors for them
				GRedirectCollector.ResolveStringAssetReference();

				// any redirectors that are pointed to by this read-only package can't be fixed up
				for (int32 RedirIndex = 0; RedirIndex < GRedirectCollector.Redirections.Num(); RedirIndex++)
				{
					FRedirection& Redir = GRedirectCollector.Redirections[RedirIndex];

					// any redirectors pointed to by this file we don't clean
					if (Redir.PackageFilename == Filename)
					{
						RedirectorsThatCantBeCleaned.AddUnique(Redir.RedirectorName);
						UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("   ... can't fixup references to %s"), *Redir.RedirectorName);

						// if this redirector is in an Engine package, we need to alert the user, because
						// Engine packages are shared between games, so if one game still needs the redirector
						// to be valid, then we can't check in Engine packages after another game has
						// run that could 
						if (!bEngineRedirectorCantBeCleaned)
						{
							FString PackagePath;
							FPackageName::DoesPackageExist(Redir.RedirectorPackageFilename, NULL, &PackagePath);
							if (PackagePath.StartsWith(FPaths::EngineDir()))
							{
								bEngineRedirectorCantBeCleaned = true;
							}
						}
					}
					// any redirectors in this file can't be deleted
					else if (Redir.RedirectorPackageFilename == Filename)
					{
						RedirectorsThatCantBeCleaned.AddUnique(Redir.RedirectorName);
						UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("   ... can't delete %s"), *Redir.RedirectorName);
					}
				}
			} 
			else
			{
				SET_WARN_COLOR(COLOR_DARK_GREEN);
				UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Checked out %s..."), *Filename);
				CLEAR_WARN_COLOR();
			}
		}
	}
	// warn about an engine package that can't be cleaned up (may want to revert engine packages)
	if (bEngineRedirectorCantBeCleaned)
	{
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT(""));
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Engine package redirectors are referenced by packages that can't be cleaned. If you have multiple games, it's recommended you revert any checked out Engine packages."));
		bUpdateEnginePackages=false;
	}

	UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT(""));

	/////////////////////////////////////////////////////////////////////
	// Load and save packages to save out the proper fixed up references
	/////////////////////////////////////////////////////////////////////

	SET_WARN_COLOR(COLOR_WHITE);
	UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Fixing references to point to proper objects:"));
	UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("---------------------------------------------"));
	CLEAR_WARN_COLOR();

	// keep a list of all packages that have ObjectRedirects in them. This is not needed if we aren't deleting redirects
	TArray<bool> PackagesWithRedirects;

	// Delete these packages entirely because they are empty
	TArray<bool> EmptyPackagesToDelete;

	if ( bDeleteRedirects )
	{
		PackagesWithRedirects.AddZeroed(UpdatePackages.Num());
		EmptyPackagesToDelete.AddZeroed(PackageList.Num());
	}

	bool bSCCEnabled = ISourceControlModule::Get().IsEnabled();

	// Iterate over all packages, loading and saving to remove all references to ObjectRedirectors (can't delete the Redirects yet)
	for( int32 PackageIndex = 0; PackageIndex < UpdatePackages.Num(); PackageIndex++ )
	{
		const FString& Filename = UpdatePackages[PackageIndex];

		// we can't do anything with packages that are read-only or cant be edited due to source control (we don't want to fixup the redirects)
		if(bSCCEnabled)
		{
			FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(SourceControlHelpers::PackageFilename(Filename), EStateCacheUsage::ForceUpdate);
			if(SourceControlState.IsValid() && !SourceControlState->CanEdit())
			{
				continue;
			}
		}
		else if(IFileManager::Get().IsReadOnly(*Filename))
		{
			continue;
		}
		
		// Assert if package couldn't be opened so we have no chance of messing up saving later packages.
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Cleaning %s"), *Filename);
		UPackage* Package = LoadPackage( NULL, *Filename, LOAD_NoVerify );
		// load all string asset reference targets, and add fake redirectors for them
		GRedirectCollector.ResolveStringAssetReference();

		// if it failed to open, we have already dealt with quitting if we are going to, so just skip it
		if (!Package)
		{
			continue;
		}

		if ( bDeleteOnlyReferencedRedirects && bDeleteRedirects )
		{
			TArray<UObject *> ObjectsInOuter;
			GetObjectsWithOuter(Package, ObjectsInOuter);
			for( int32 Index = 0; Index < ObjectsInOuter.Num(); Index++ )
			{
				UObject* Obj = ObjectsInOuter[Index];
				UObjectRedirector* Redir = Cast<UObjectRedirector>(Obj);
				if (Redir)
				{
					PackagesWithRedirects[PackageIndex] = 1;
					break;
				}
			}
		}

		// Don't save packages in the engine directory if we did not opt to update engine packages
		if( bUpdateEnginePackages || !Filename.StartsWith( FPaths::EngineDir() ) )
		{
			// save out the package
			UWorld* World = UWorld::FindWorldInPackage(Package);
			GEditor->SavePackage( Package, World, World ? RF_NoFlags : RF_Standalone, *Filename, GWarn );
		}
		
		// collect garbage to close the package
		UE_LOG(LogFixupRedirectsCommandlet, Display, TEXT("Collecting Garbage..."));
		CollectGarbage(RF_Native);
	}

	UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT(""));

	if ( bDeleteRedirects )
	{
		/////////////////////////////////////////////////////////////////////
		// Delete all redirects that are no longer referenced
		/////////////////////////////////////////////////////////////////////

		SET_WARN_COLOR(COLOR_WHITE);
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Deleting redirector objects:"));
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("----------------------------"));
		CLEAR_WARN_COLOR();

		const TArray<FString>& PackagesToCleaseOfRedirects = bDeleteOnlyReferencedRedirects ? UpdatePackages : PackageList;

		int32 GCIndex = 0;

		// Again, iterate over all packages, loading and saving to and this time deleting all 
		for( int32 PackageIndex = 0; PackageIndex < PackagesToCleaseOfRedirects.Num(); PackageIndex++ )
		{
			const FString& Filename = PackagesToCleaseOfRedirects[PackageIndex];
		
			if (bDeleteOnlyReferencedRedirects && PackagesWithRedirects[PackageIndex] == 0)
			{
				continue;
			}

			// The file should have already been checked out/be writable
			// If it is read only that means the checkout failed or we are not using source control and we can not write this file.
			if(bSCCEnabled)
			{
				FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(SourceControlHelpers::PackageFilename(Filename), EStateCacheUsage::ForceUpdate);
				if(SourceControlState.IsValid() && !SourceControlState->CanEdit())
				{
					UE_LOG(LogFixupRedirectsCommandlet, Display, TEXT("Skipping file: source control says we cant edit the file %s..."), *Filename);
					continue;
				}
			}
			else if( IFileManager::Get().IsReadOnly( *Filename ))
			{
				UE_LOG(LogFixupRedirectsCommandlet, Display, TEXT("Skipping read-only file %s..."), *Filename);
				continue;
			}

			UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Wiping %s..."), *Filename);
		
			UPackage* Package = LoadPackage( NULL, *Filename, 0 );
			// load all string asset reference targets, and add fake redirectors for them
			GRedirectCollector.ResolveStringAssetReference();
			// if it failed to open, we have already dealt with quitting if we are going to, so just skip it
			if (!Package)
			{
				continue;
			}

			// assume that all redirs in this file are still-referenced
			bool bIsDirty = false;

			// see if this package is completely empty other than purged redirectors and metadata
			bool bIsEmpty = true;

			// delete all ObjectRedirects now
			TArray<UObjectRedirector*> Redirs;
			// find all redirectors, put into an array so delete doesn't mess up the iterator
			TArray<UObject *> ObjectsInOuter;
			GetObjectsWithOuter(Package, ObjectsInOuter);
			for( int32 Index = 0; Index < ObjectsInOuter.Num(); Index++ )
			{
				UObject* Obj = ObjectsInOuter[Index];
				UObjectRedirector *Redir = Cast<UObjectRedirector>(Obj);

				if (Redir)
				{
					int32 Dummy;
					// if the redirector was marked as uncleanable, skip it
					if (RedirectorsThatCantBeCleaned.Find(Redir->GetFullName(), Dummy))
					{
						SET_WARN_COLOR(COLOR_DARK_RED);
						UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("   ... skipping still-referenced %s"), *Redir->GetFullName());
						CLEAR_WARN_COLOR();
						bIsEmpty = false;
						continue;
					}

					bIsDirty = true;
					SET_WARN_COLOR(COLOR_DARK_GREEN);
					UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("   ... deleting %s"), *Redir->GetFullName());
					CLEAR_WARN_COLOR();
					Redirs.Add(Redir);
				}
				else if ( (!Obj->IsA(UMetaData::StaticClass()) && !Obj->IsA(UField::StaticClass()))
					|| Obj->IsA(UUserDefinedEnum::StaticClass())) // UserDefinedEnum should not be deleted
				{
					// This package has a real object
					bIsEmpty = false;
				}
			}

			if (bIsEmpty && !bDeleteOnlyReferencedRedirects)
			{
				EmptyPackagesToDelete[PackageIndex] = 1;
			}

			// mark them for deletion.
			for (int32 RedirIndex = 0; RedirIndex < Redirs.Num(); RedirIndex++)
			{
				UObjectRedirector* Redirector = Redirs[RedirIndex];
				// Remove standalone flag and mark as pending kill.
				Redirector->ClearFlags( RF_Standalone | RF_Public );
			
				// We don't need redirectors in the root set, the references should already be fixed up
				if ( Redirector->IsRooted() )
				{
					Redirector->RemoveFromRoot();
				}

				Redirector->MarkPendingKill();
			}
			Redirs.Empty();

			if (bIsDirty)
			{
				// Don't save packages in the engine directory if we did not opt to update engine packages
				if( bUpdateEnginePackages || !Filename.StartsWith( FPaths::EngineDir() ) )
				{
					// save the package
					UWorld* World = UWorld::FindWorldInPackage(Package);
					GEditor->SavePackage( Package, World, World ? RF_NoFlags : RF_Standalone, *Filename, GWarn );
				}
			}

			// collect garbage every N packages, or if there was any redirectors in the package 
			if (((++GCIndex) % 50) == 0 || bIsDirty || Package->ContainsMap())
			{
				// collect garbage to close the package
				CollectGarbage(RF_Native);
				UE_LOG(LogFixupRedirectsCommandlet, Display, TEXT("GC..."));
				// reset our counter
				GCIndex = 0;
			}
		}

		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT(""));
	}
	else
	{
		SET_WARN_COLOR(COLOR_WHITE);
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("----------------------------"));
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Not deleting any redirector objects:"));
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("----------------------------"));
		UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT(""));
		CLEAR_WARN_COLOR();
	}

	/////////////////////////////////////////////////////////////////////
	// Clean up any unused trash
	/////////////////////////////////////////////////////////////////////

	SET_WARN_COLOR(COLOR_WHITE);
	UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Deleting empty and unused packages:"));
	UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("---------------------------------------"));
	CLEAR_WARN_COLOR();

	LogOutputFile->Logf(TEXT(""));
	LogOutputFile->Logf(TEXT("[Deleted]"));

	// Iterate over packages, attempting to delete unreferenced packages
	for( int32 PackageIndex = 0; PackageIndex < PackageList.Num(); PackageIndex++ )
	{
		bool bDelete = false;
		const FString& Filename = PackageList[PackageIndex];
		
		FString PackageName(FPackageName::FilenameToLongPackageName(Filename));

		const bool bAllowDelete = bUpdateEnginePackages || !Filename.StartsWith( FPaths::EngineDir() );
		if (bDeleteRedirects && !bDeleteOnlyReferencedRedirects && EmptyPackagesToDelete[PackageIndex] && bAllowDelete)
		{
			// this package is completely empty, delete it
			bDelete = true;
		}

		if (bDelete == true)
		{
			struct Local
			{
				static bool DeleteFromDisk(const FString& InFilename, const FString& InMessage)
				{
					SET_WARN_COLOR(COLOR_GREEN);
					UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("%s"), *InMessage);
					if (!IFileManager::Get().Delete(*InFilename, false, true))
					{
						SET_WARN_COLOR(COLOR_RED);
						UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("  ... failed to delete from disk."), *InFilename);
						CLEAR_WARN_COLOR();
						return false;
					}
					CLEAR_WARN_COLOR();
					return true;
				}
			};

			if (!bIsSCCDisabled)
			{
				// get file SCC status
				FString FileName = SourceControlHelpers::PackageFilename(PackageName);
				FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(FileName, EStateCacheUsage::ForceUpdate);

				if(SourceControlState.IsValid() && (SourceControlState->IsCheckedOut() || SourceControlState->IsAdded()) )
				{
					UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Revert '%s' from source control..."), *Filename);
					SourceControlProvider.Execute(ISourceControlOperation::Create<FRevert>(), FileName);

					SET_WARN_COLOR(COLOR_GREEN);
					UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Deleting '%s' from source control..."), *Filename);
					CLEAR_WARN_COLOR();
					SourceControlProvider.Execute(ISourceControlOperation::Create<FDelete>(), FileName);
				}
				else if(SourceControlState.IsValid() && SourceControlState->CanCheckout())
				{
					SET_WARN_COLOR(COLOR_GREEN);
					UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Deleting '%s' from source control..."), *Filename);
					CLEAR_WARN_COLOR();
					SourceControlProvider.Execute(ISourceControlOperation::Create<FDelete>(), FileName);
				}
				else if(SourceControlState.IsValid() && SourceControlState->IsCheckedOutOther())
				{
					SET_WARN_COLOR(COLOR_RED);
					UE_LOG(LogFixupRedirectsCommandlet, Warning, TEXT("Couldn't delete '%s' from source control, someone has it checked out, skipping..."), *Filename);
					CLEAR_WARN_COLOR();
				}
				else if(SourceControlState.IsValid() && !SourceControlState->IsSourceControlled())
				{
					if(Local::DeleteFromDisk(Filename, FString::Printf(TEXT("'%s' is not in source control, attempting to delete from disk..."), *Filename)))
					{
						LogOutputFile->Logf(*Filename);
					}
				}
				else
				{
					if(Local::DeleteFromDisk(Filename, FString::Printf(TEXT("'%s' is in an unknown source control state, attempting to delete from disk..."), *Filename)))
					{
						LogOutputFile->Logf(*Filename);
					}
				}
			}
			else
			{
				if(Local::DeleteFromDisk(Filename, FString::Printf(TEXT("source control disabled while deleting '%s', attempting to delete from disk..."), *Filename)))
				{
					LogOutputFile->Logf(*Filename);
				}
			}
		}
	}
	LogOutputFile->Close();
	delete LogOutputFile;
	LogOutputFile = NULL;

	if (!bIsSCCDisabled && bAutoSubmit)
	{
		/////////////////////////////////////////////////////////////////////
		// Submit the results to source control
		/////////////////////////////////////////////////////////////////////
		UE_LOG(LogFixupRedirectsCommandlet, Display, TEXT("Submiting the results to source control"));

		// Work out the list of file to check in
		TArray <FString> FilesToSubmit;

		for( int32 PackageIndex = 0; PackageIndex < PackageList.Num(); PackageIndex++ )
		{
			const FString& Filename = PackageList[PackageIndex];

			FString PackageName(FPackageName::FilenameToLongPackageName(Filename));
			FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(SourceControlHelpers::PackageFilename(Filename), EStateCacheUsage::ForceUpdate);

			if( SourceControlState.IsValid() && (SourceControlState->IsCheckedOut() || SourceControlState->IsAdded() || SourceControlState->IsDeleted()) )
			{
				// Only submit engine packages if we're requested to
				if( bUpdateEnginePackages || !Filename.StartsWith( FPaths::EngineDir() ) )
				{
					FilesToSubmit.Add(*PackageName);
				}
			}
		}

		// Check in all changed files
		const FText Description = NSLOCTEXT("FixupRedirectsCmdlet", "ChangelistDescription", "Fixed up Redirects");
		TSharedRef<FCheckIn, ESPMode::ThreadSafe> CheckInOperation = ISourceControlOperation::Create<FCheckIn>();
		CheckInOperation->SetDescription( Description );
		SourceControlProvider.Execute(CheckInOperation, SourceControlHelpers::PackageFilenames(FilesToSubmit));

		// toss the SCC manager
		ISourceControlModule::Get().GetProvider().Close();
	}
	return 0;
}
Ejemplo n.º 4
0
bool UAnimExporterITP::ExportText(const FExportObjectInnerContext* Context, UObject* Object, const TCHAR* Type, FOutputDevice& Ar, FFeedbackContext* Warn, uint32 PortFlags /*= 0*/)
{
	UAnimSequence* AnimSeq = CastChecked<UAnimSequence>(Object);

	USkeleton* Skeleton = AnimSeq->GetSkeleton();
	const FReferenceSkeleton& RefSkeleton = Skeleton->GetReferenceSkeleton();
	USkeletalMesh* SkelMesh = Skeleton->GetPreviewMesh();
	if (AnimSeq->SequenceLength == 0.f)
	{
		// something is wrong
		return false;
	}

	const float FrameRate = AnimSeq->NumFrames / AnimSeq->SequenceLength;

	// Open another archive
	FArchive* File = IFileManager::Get().CreateFileWriter(*UExporter::CurrentFilename);

	// Let's try the header...
	File->Logf(TEXT("{"));
	File->Logf(TEXT("\t\"metadata\":{"));
	File->Logf(TEXT("\t\t\"type\":\"itpanim\","));
	File->Logf(TEXT("\t\t\"version\":2"));
	File->Logf(TEXT("\t},"));

	File->Logf(TEXT("\t\"sequence\":{"));
	File->Logf(TEXT("\t\t\"frames\":%d,"), AnimSeq->NumFrames);
	File->Logf(TEXT("\t\t\"length\":%f,"), AnimSeq->SequenceLength);
	File->Logf(TEXT("\t\t\"bonecount\":%d,"), RefSkeleton.GetNum());
	File->Logf(TEXT("\t\t\"tracks\":["));

	bool firstOutput = false;

	for (int32 BoneIndex = 0; BoneIndex < RefSkeleton.GetNum(); ++BoneIndex)
	{
		//int32 BoneTreeIndex = Skeleton->GetSkeletonBoneIndexFromMeshBoneIndex(SkelMesh, BoneIndex);
		int32 BoneTrackIndex = Skeleton->GetAnimationTrackIndex(BoneIndex, AnimSeq);
		
		if (BoneTrackIndex == INDEX_NONE)
		{
			// If this sequence does not have a track for the current bone, then skip it
			continue;
		}
	
		if (firstOutput)
		{
			File->Logf(TEXT("\t\t\t},"));
		}

		firstOutput = true;

		File->Logf(TEXT("\t\t\t{"));
		File->Logf(TEXT("\t\t\t\t\"bone\":%d,"), BoneIndex);
		File->Logf(TEXT("\t\t\t\t\"transforms\":["));
		float AnimTime = 0.0f;
		float AnimEndTime = AnimSeq->SequenceLength;
		// Subtracts 1 because NumFrames includes an initial pose for 0.0 second
		double TimePerKey = (AnimSeq->SequenceLength / (AnimSeq->NumFrames - 1));
		const float AnimTimeIncrement = TimePerKey;

		bool bLastKey = false;
		// Step through each frame and add the bone's transformation data
		while (!bLastKey)
		{
			const TArray<FBoneNode>& BoneTree = Skeleton->GetBoneTree();

			FTransform BoneAtom;
			AnimSeq->GetBoneTransform(BoneAtom, BoneTrackIndex, AnimTime, true);

			bLastKey = AnimTime >= AnimEndTime;

			File->Logf(TEXT("\t\t\t\t\t{"));

			FQuat rot = BoneAtom.GetRotation();
			// For the root bone, we need to fix-up the rotation because Unreal exports
			// animations with Y-forward for some reason (maybe because Maya?)
			if (BoneIndex == 0)
			{
				FQuat addRot(FVector(0.0f, 0.0f, 1.0f), -1.57f);
				rot = addRot * rot;
			}
			File->Logf(TEXT("\t\t\t\t\t\t\"rot\":[%f,%f,%f,%f],"), rot.X, rot.Y, rot.Z, rot.W);
			FVector trans = BoneAtom.GetTranslation();

			// Sanjay: If it's skeleton retargeting, change the translation to be from the ref pose skeleton
			if (BoneTree[BoneIndex].TranslationRetargetingMode == EBoneTranslationRetargetingMode::Skeleton)
			{
				const FTransform& BoneTransform = RefSkeleton.GetRefBonePose()[BoneIndex];
				trans = BoneTransform.GetTranslation();
			}

			File->Logf(TEXT("\t\t\t\t\t\t\"trans\":[%f,%f,%f]"), trans.X, trans.Y, trans.Z);

			if (!bLastKey)
			{
				File->Logf(TEXT("\t\t\t\t\t},"));
			}
			else
			{
				File->Logf(TEXT("\t\t\t\t\t}"));
			}
			

			AnimTime += AnimTimeIncrement;
		}

		File->Logf(TEXT("\t\t\t\t]"), BoneIndex);
	}

	File->Logf(TEXT("\t\t\t}"));
	File->Logf(TEXT("\t\t]"));
	File->Logf(TEXT("\t}"));

 	File->Logf(TEXT("}"));
 	delete File;

	return true;
}
Ejemplo n.º 5
0
void FSlateStatCycleCounter::EndFrame(double CurrentTime)
{
    // Ensure the overhead is tracked at minimum even if all other detail levels are turned off.
    const int32 SLATE_STATS_DETAIL_LEVEL_FORCE_ON = INT_MIN;
    SLATE_CYCLE_COUNTER_SCOPE_FLAT_DETAILED(SLATE_STATS_DETAIL_LEVEL_FORCE_ON, GSlateStatsOverhead);

    // dump stats hierarchy if one was collected this frame.
    if (FSlateStatHierarchy::Get().GetStatEntries().Num() > 0)
    {
        // Place in the <UE4>\<GAME>\Saved\<InFolderName> folder
        FString Filename = FString::Printf(TEXT("%sSlateHierachyStats-%s.csv"), *FPaths::GameSavedDir(), *FDateTime::Now().ToString());
        UE_LOG(LogSlate, Log, TEXT("Dumping Slate Hierarchy Stats to %s..."), *Filename);
        FArchive* OutputStream = IFileManager::Get().CreateFileWriter(*Filename, EFileWrite::FILEWRITE_NoReplaceExisting);

        // some state vars used to print the path of the stat.
        const int kMaxDepth = 100;
        int32 Path[kMaxDepth] = { 0 };
        int32 PathCurrentDepth = -1;
        TCHAR PathStr[4 * kMaxDepth] = { 0 };
        TCHAR* PathStrCurrent = PathStr;
        FSlateStatHierarchy::Get().ComputeExclusiveTimes();

        for (const auto& Entry : FSlateStatHierarchy::Get().GetStatEntries())
        {
            const bool bDescending = Entry.StackDepth > PathCurrentDepth;
            const bool bAscending = Entry.StackDepth < PathCurrentDepth;
            const bool bOverflow = PathCurrentDepth >= (int32)ARRAY_COUNT(Path);
            const int PathPrevDepth = PathCurrentDepth;
            PathCurrentDepth = Entry.StackDepth;

            if (!bOverflow)
            {
                if (bDescending)
                {
                    // we always increment the ordinal, so init to -1 so the first value is zero.
                    Path[PathCurrentDepth] = -1;
                    // put a dot after the current depth and track the new position to put new depths.
                    if (PathCurrentDepth > 0)
                    {
                        while (*PathStrCurrent != 0) ++PathStrCurrent;
                        *PathStrCurrent++ = '.';
                        *PathStrCurrent = 0;
                    }
                }
                else if (bAscending)
                {
                    // back up until we find the dot matching our current depth (or get to depth zero)
                    if (PathCurrentDepth == 0)
                    {
                        // we are at level zero, so just back up all the way. Saves us from checking boundaries in the loop below when we are not at zero depth.
                        PathStrCurrent = PathStr;
                    }
                    else
                    {
                        int LevelsAscended = 0;
                        const int LevelsToAscend = PathPrevDepth - PathCurrentDepth;
                        while (LevelsAscended < LevelsToAscend)
                        {
                            // we are not back to level zero, so we know we will find a '.' eventually.
                            PathStrCurrent -= 2; // back us up before the dot marking this level
                            // keep backing up until we find the dot for the previous level.
                            while (*(PathStrCurrent - 1) != '.')
                            {
                                --PathStrCurrent;
                            }
                            ++LevelsAscended;
                        }
                    }
                }
                // increment the ordinal at this depth
                ++Path[PathCurrentDepth];
                FCString::Sprintf(PathStrCurrent, TEXT("%4d"), Path[PathCurrentDepth]);
            }

            OutputStream->Logf(TEXT("%s,%s,%s,%.8f,%.8f"), PathStr, *Entry.CounterName.ToString(), Entry.CustomName != NAME_None ? *Entry.CustomName.ToString() : TEXT(""), Entry.InclusiveTime*1000, Entry.ExclusiveTime*1000);
            //UE_LOG(LogSlate, Log, TEXT("%s,%s,%s,%.8f,%.8f"), PathStr, *Entry.CounterName.ToString(), Entry.CustomName != NAME_None ? *Entry.CustomName.ToString() : TEXT(""), Entry.InclusiveTime*1000, Entry.ExclusiveTime*1000);
        }
        OutputStream->Close();
        delete OutputStream;
        OutputStream = nullptr;
        UE_LOG(LogSlate, Log, TEXT("Done dumping Slate Hierarchy Stats!"), *Filename);
    }

    // static state for tracking when to re-average the stats
    static double Time = CurrentTime;
    static double LastTime = CurrentTime;
    static double NumFrames = 0.0;
    NumFrames += 1.0;
    Time = CurrentTime;
    const double Delta = Time - LastTime;
    // let things settle down before taking the first real sample
    static double NextDelta = GSlateStatsFlatIntervalWindowSec;
    // ensure this gets reset every frame.
    GAverageInclusiveTimesWereUpdatedThisFrame = false;

    // output flat stats if it's time to do so
    if (Delta > NextDelta)
    {
        LastTime = Time;
        NumFrames = NumFrames / 1000.0; // convert to ms.
        if (GSlateStatsFlatLogOutput > 0)
        {
            UE_LOG(LogSlate, Log, TEXT("Slate Flat Stats"));
            UE_LOG(LogSlate, Log, TEXT("================"));
        }

        // iterate over the counters, outputting their data and resetting them.
        for (const auto Counter : FSlateStatCycleCounter::GetRegisteredCounters())
        {
            Counter->LastComputedAverageInclusiveTime = Counter->InclusiveTime / NumFrames;
            if (GSlateStatsFlatLogOutput > 0)
            {
                UE_LOG(LogSlate, Log, TEXT("%s,%.8f"), *Counter->GetName().ToString(), Counter->LastComputedAverageInclusiveTime);
            }
            Counter->Reset();
        }
        // Frame time is a "virtual stat" so output it like a regular stat.
        if (GSlateStatsFlatLogOutput > 0)
        {
            UE_LOG(LogSlate, Log, TEXT("%s,%.8f"), TEXT("Frame Time"), Delta / NumFrames);
        }

        LastTime = Time;
        NumFrames = 0.0;
        NextDelta = GSlateStatsFlatIntervalWindowSec;
        GAverageInclusiveTimesWereUpdatedThisFrame = true;
    }

    // Clear the hierarchy entries, and tell the system to possibly track hierarchy entries next frame.
    FSlateStatHierarchy::Get().EndFrame(GSlateStatsHierarchyTrigger > 0);
    GSlateStatsHierarchyTrigger = 0;
}
Ejemplo n.º 6
0
/**
 * Dumps the FPS chart information to the special stats log file.
 */
void UEngine::DumpFPSChartToStatsLog( float TotalTime, float DeltaTime, int32 NumFrames, const FString& InMapName )
{
#if ALLOW_DEBUG_FILES
	const FString OutputDir = CreateOutputDirectory();
	
	// Create archive for log data.
	const FString ChartType = GetFPSChartType();
	const FString ChartName = OutputDir / CreateFileNameForChart( ChartType, InMapName, TEXT( ".log" ) );
	FArchive* OutputFile = IFileManager::Get().CreateDebugFileWriter( *ChartName, FILEWRITE_Append );

	if( OutputFile )
	{
		OutputFile->Logf(TEXT("Dumping FPS chart at %s using build %s built from changelist %i"), *FDateTime::Now().ToString(), *GEngineVersion.ToString(), GetChangeListNumberForPerfTesting() );

		// Get OS info
		FString OSMajor;
		FString OSMinor;
		FPlatformMisc::GetOSVersions(OSMajor, OSMinor);

		// Get settings info
		const Scalability::FQualityLevels& Quality = GEngine->GetGameUserSettings()->ScalabilityQuality;

		OutputFile->Logf(TEXT("Machine info:"));
		OutputFile->Logf(TEXT("\tOS: %s %s"), *OSMajor, *OSMinor);
		OutputFile->Logf(TEXT("\tCPU: %s %s"), *FPlatformMisc::GetCPUVendor(), *FPlatformMisc::GetCPUBrand());
		OutputFile->Logf(TEXT("\tGPU: %s"), *FPlatformMisc::GetPrimaryGPUBrand());
		OutputFile->Logf(TEXT("\tResolution Quality: %d"), Quality.ResolutionQuality);
		OutputFile->Logf(TEXT("\tView Distance Quality: %d"), Quality.ViewDistanceQuality);
		OutputFile->Logf(TEXT("\tAnti-Aliasing Quality: %d"), Quality.AntiAliasingQuality);
		OutputFile->Logf(TEXT("\tShadow Quality: %d"), Quality.ShadowQuality);
		OutputFile->Logf(TEXT("\tPost-Process Quality: %d"), Quality.PostProcessQuality);
		OutputFile->Logf(TEXT("\tTexture Quality: %d"), Quality.TextureQuality);
		OutputFile->Logf(TEXT("\tEffects Quality: %d"), Quality.EffectsQuality);

		int32 NumFramesBelow30 = 0; // keep track of the number of frames below 30 FPS
		float PctTimeAbove30 = 0;	// Keep track of percentage of time at 30+ FPS.
		int32 NumFramesBelow60 = 0; // keep track of the number of frames below 60 FPS
		float PctTimeAbove60 = 0;	// Keep track of percentage of time at 60+ FPS.

		// Iterate over all buckets, dumping percentages.
		for( int32 BucketIndex=0; BucketIndex<ARRAY_COUNT(GFPSChart); BucketIndex++ )
		{
			// Figure out bucket time and frame percentage.
			const float BucketTimePercentage  = 100.f * GFPSChart[BucketIndex].CummulativeTime / TotalTime;
			const float BucketFramePercentage = 100.f * GFPSChart[BucketIndex].Count / NumFrames;

			int32 StartFPS = 0;
			int32 EndFPS = 0;
			CalcQuantisedFPSRange(BucketIndex, StartFPS, EndFPS);

			// Keep track of time spent at 30+ FPS.
			if (StartFPS >= 30)
			{
				PctTimeAbove30 += BucketTimePercentage;
			}
			else
			{
				NumFramesBelow30 += GFPSChart[BucketIndex].Count;
			}
			
			// Keep track of time spent at 60+ FPS.
			if (StartFPS >= 60)
			{
				PctTimeAbove60 += BucketTimePercentage;
			}
			else
			{
				NumFramesBelow60 += GFPSChart[BucketIndex].Count;
			}

			// Log bucket index, time and frame Percentage.
			OutputFile->Logf(TEXT("Bucket: %2i - %2i  Time: %5.2f  Frame: %5.2f"), StartFPS, EndFPS, BucketTimePercentage, BucketFramePercentage);
		}

		OutputFile->Logf(TEXT("%i frames collected over %4.2f seconds, disregarding %4.2f seconds for a %4.2f FPS average, %4.2f percent of time spent > 30 FPS, %4.2f percent of time spent > 60 FPS"), 
			NumFrames, 
			DeltaTime, 
			FMath::Max<float>( 0, DeltaTime - TotalTime ), 
			NumFrames / TotalTime,
			PctTimeAbove30, PctTimeAbove60 );
		OutputFile->Logf(TEXT("Average GPU frame time: %4.2f ms"), float((GTotalGPUTime / NumFrames)*1000.0));
		OutputFile->Logf(TEXT("BoundGameThreadPct: %4.2f  BoundRenderThreadPct: %4.2f  BoundGPUPct: %4.2f PercentFrames30+: %f   PercentFrames60+: %f   BoundGameTime: %f  BoundRenderTime: %f  BoundGPUTime: %f  PctTimeAbove30: %f  PctTimeAbove60: %f ")
			, (float(GNumFramesBound_GameThread)/float(NumFrames))*100.0f
			, (float(GNumFramesBound_RenderThread)/float(NumFrames))*100.0f
			, (float(GNumFramesBound_GPU)/float(NumFrames))*100.0f
			, float(NumFrames - NumFramesBelow30) / float(NumFrames)*100.0f
			, float(NumFrames - NumFramesBelow60) / float(NumFrames)*100.0f
			, (GTotalFramesBoundTime_GameThread / DeltaTime)*100.0f
			, ((GTotalFramesBoundTime_RenderThread)/DeltaTime)*100.0f
			, ((GTotalFramesBoundTime_GPU)/DeltaTime)*100.0f
			, PctTimeAbove30
			, PctTimeAbove60
			);

		// Dump hitch data
		{
			OutputFile->Logf( TEXT( "Hitch chart:" ) );

			int32 TotalHitchCount = 0;
			int32 TotalGameThreadBoundHitches = 0;
			int32 TotalRenderThreadBoundHitches = 0;
			int32 TotalGPUBoundHitches = 0;
			for( int32 BucketIndex = 0; BucketIndex < ARRAY_COUNT( GHitchChart ); ++BucketIndex )
			{
				const float HitchThresholdInSeconds = ( float )GHitchThresholds[ BucketIndex ] * 0.001f;

				FString RangeName;
				if( BucketIndex == 0 )
				{
					// First bucket's end threshold is infinitely large
					RangeName = FString::Printf( TEXT( "%0.2fs - inf" ), HitchThresholdInSeconds );
				}
				else
				{
					const float PrevHitchThresholdInSeconds = ( float )GHitchThresholds[ BucketIndex - 1 ] * 0.001f;

					// Set range from current bucket threshold to the last bucket's threshold
					RangeName = FString::Printf( TEXT( "%0.2fs - %0.2fs" ), HitchThresholdInSeconds, PrevHitchThresholdInSeconds );
				}

				OutputFile->Logf( TEXT( "Bucket: %s  Count: %i " ), *RangeName, GHitchChart[ BucketIndex ].HitchCount );


				// Count up the total number of hitches
				TotalHitchCount += GHitchChart[ BucketIndex ].HitchCount;
				TotalGameThreadBoundHitches += GHitchChart[ BucketIndex ].GameThreadBoundHitchCount;
				TotalRenderThreadBoundHitches += GHitchChart[ BucketIndex ].RenderThreadBoundHitchCount;
				TotalGPUBoundHitches += GHitchChart[ BucketIndex ].GPUBoundHitchCount;
			}

			const int32 HitchBucketCount = STAT_FPSChart_LastHitchBucketStat - STAT_FPSChart_FirstHitchStat;
			OutputFile->Logf( TEXT( "Total hitch count (at least %ims):  %i" ), GHitchThresholds[ HitchBucketCount - 1 ], TotalHitchCount );
			OutputFile->Logf( TEXT( "Hitch frames bound by game thread:  %i  (%0.1f%%)" ), TotalGameThreadBoundHitches, TotalHitchCount > 0 ? ( ( float )TotalGameThreadBoundHitches / ( float )TotalHitchCount * 100.0f ) : 0.0f );
			OutputFile->Logf( TEXT( "Hitch frames bound by render thread:  %i  (%0.1f%%)" ), TotalRenderThreadBoundHitches, TotalHitchCount > 0 ? ( ( float )TotalRenderThreadBoundHitches / ( float )TotalHitchCount * 0.0f ) : 0.0f  );
			OutputFile->Logf( TEXT( "Hitch frames bound by GPU:  %i  (%0.1f%%)" ), TotalGPUBoundHitches, TotalHitchCount > 0 ? ( ( float )TotalGPUBoundHitches / ( float )TotalHitchCount * 100.0f ) : 0.0f );
		}

		OutputFile->Logf( LINE_TERMINATOR LINE_TERMINATOR LINE_TERMINATOR );

		// Flush, close and delete.
		delete OutputFile;

		const FString AbsolutePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead( *ChartName );

		UE_LOG( LogProfilingDebugging, Warning, TEXT( "FPS Chart (logfile) saved to %s" ), *AbsolutePath );

#if	PLATFORM_DESKTOP
		FPlatformProcess::ExploreFolder( *AbsolutePath );
#endif // PLATFORM_DESKTOP
	}
#endif // ALLOW_DEBUG_FILES
}