Exemplo n.º 1
0
void FabMapCalculator::WriteResultsToDisk(string datasetName, double dfTimeTaken)
{
    if(!m_bTextOutputFormat)
    {
        //If we're outputing a Matlab file, record it now.
        //If output is txt, we've been recording as we go along.
        m_full_output_path = CreateOutputDirectory(EnsurePathHasTrailingSlash(m_base_output_path));
        WriteToFile(psame, "psame", datasetName, m_full_output_path);

        #ifdef ALLOW_DATA_ASSOCIATION
            WriteToFile(m_SceneToPlace, "sceneToPlace", datasetName, m_full_output_path);
        #endif
    }
    RecordConfigParamsToFile(m_full_output_path,dfTimeTaken);
    mp_KeyframeDetector->WriteKeyframesToDisk(datasetName,m_full_output_path);
}
Exemplo n.º 2
0
void FabMapCalculator::CreateOutputFileStructures()
{
    //Creates output directories and sets up output file streams
    if(m_base_output_path != "")
    {
        //What to call it?
        string datasetName;
        if(!m_MissionReader.GetConfigurationParam("DatasetName", datasetName))
        {
            datasetName = "NoName";
        }

        //Create directories
        m_full_output_path = CreateOutputDirectory(EnsurePathHasTrailingSlash(m_base_output_path));

        //Initialize output file
        string sFileExtension = m_bSparseOutputFormat ? ".tms" : ".tmd";
        m_results_file.open((m_full_output_path + datasetName + "_psame"+sFileExtension).c_str(),ios::out);
        if(!m_results_file.is_open())
        {
            MOOSTrace("Failed to open file for output");
        }
        m_results_file << setprecision(16);

        //Write file header, if there is one
        if(m_bSparseOutputFormat)
        {
            if(!m_bTopKOutput)
                m_results_file << "SparseRecordingThreshold:" << m_dfSparseRecordingThreshold << endl;
            else
                m_results_file << "SparseRecordingThreshold: TopK" << endl;
        }

    #ifdef ALLOW_DATA_ASSOCIATION
        m_SceneToPlace_file.open((m_full_output_path + datasetName + "_sceneToPlace"+".txt").c_str(),ios::out);
        if(!m_SceneToPlace_file.is_open())
        {
            MOOSTrace("Failed to open file for sceneToPlace output");
        }
    #endif
    }
}
Exemplo n.º 3
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
}
Exemplo n.º 4
0
/**
* Dumps the FPS chart information to HTML.
*/
void UEngine::DumpFPSChartToHTML( float TotalTime, float DeltaTime, int32 NumFrames, const FString& InMapName )
{
#if ALLOW_DEBUG_FILES
	// Load the HTML building blocks from the Engine\Stats folder.
	FString FPSChartPreamble;
	FString FPSChartPostamble;
	FString FPSChartRow;
	bool	bAreAllHTMLPartsLoaded = true;

	bAreAllHTMLPartsLoaded = bAreAllHTMLPartsLoaded && FFileHelper::LoadFileToString( FPSChartPreamble,	*(FPaths::EngineContentDir() + TEXT("Stats/FPSChart_Preamble.html")	) );
	bAreAllHTMLPartsLoaded = bAreAllHTMLPartsLoaded && FFileHelper::LoadFileToString( FPSChartPostamble,	*(FPaths::EngineContentDir() + TEXT("Stats/FPSChart_Postamble.html")	) );
	bAreAllHTMLPartsLoaded = bAreAllHTMLPartsLoaded && FFileHelper::LoadFileToString( FPSChartRow,		*(FPaths::EngineContentDir() + TEXT("Stats/FPSChart_Row.html")			) );

	// Successfully loaded all HTML templates.
	if( bAreAllHTMLPartsLoaded )
	{
		// Keep track of percentage of time at 30+ FPS.
		float PctTimeAbove30 = 0;
		// Keep track of percentage of time at 60+ FPS.
		float PctTimeAbove60 = 0;

		// Iterate over all buckets, updating row 
		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;

			// Figure out bucket range. Buckets start at 5 frame intervals then change to 10.
			int32 StartFPS = 0;
			int32 EndFPS = 0;
			CalcQuantisedFPSRange(BucketIndex, StartFPS, EndFPS);

			// Keep track of time spent at 30+ FPS.
			if( StartFPS >= 30 )
			{
				PctTimeAbove30 += BucketTimePercentage;
			}

			// Keep track of time spent at 60+ FPS.
			if (StartFPS >= 60)
			{
				PctTimeAbove60 += BucketTimePercentage;
			}

			const FString SrcToken = FString::Printf(TEXT("TOKEN_%i_%i"), StartFPS, EndFPS);
			const FString DstToken = FString::Printf( TEXT("%5.2f"), BucketTimePercentage );

			// Replace token with actual values.
			FPSChartRow	= FPSChartRow.Replace( *SrcToken, *DstToken );
		}


		// Update hitch data
		{
			int32 TotalHitchCount = 0;
			int32 TotalGameThreadBoundHitches = 0;
			int32 TotalRenderThreadBoundHitches = 0;
			int32 TotalGPUBoundHitches = 0;
			for( int32 BucketIndex = 0; BucketIndex < ARRAY_COUNT( GHitchChart ); ++BucketIndex )
			{
				FString SrcToken;
				if( BucketIndex == 0 )
				{
					SrcToken = FString::Printf( TEXT("TOKEN_HITCH_%i_PLUS"), GHitchThresholds[ BucketIndex ] );
				}
				else
				{
					SrcToken = FString::Printf( TEXT("TOKEN_HITCH_%i_%i"), GHitchThresholds[ BucketIndex ], GHitchThresholds[ BucketIndex - 1 ] );
				}

				const FString DstToken = FString::Printf( TEXT("%i"), GHitchChart[ BucketIndex ].HitchCount );

				// Replace token with actual values.
				FPSChartRow	= FPSChartRow.Replace( *SrcToken, *DstToken );

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

			// Total hitch count
			FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_HITCH_TOTAL"), *FString::Printf(TEXT("%i"), TotalHitchCount), ESearchCase::CaseSensitive );
			FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_HITCH_GAME_BOUND_COUNT"), *FString::Printf(TEXT("%i"), TotalGameThreadBoundHitches), ESearchCase::CaseSensitive );
			FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_HITCH_RENDER_BOUND_COUNT"), *FString::Printf(TEXT("%i"), TotalRenderThreadBoundHitches), ESearchCase::CaseSensitive );
			FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_HITCH_GPU_BOUND_COUNT"), *FString::Printf(TEXT("%i"), TotalGPUBoundHitches), ESearchCase::CaseSensitive );
		}

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

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

		// Update non- bucket stats.
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_MAPNAME"),		    *FString::Printf(TEXT("%s"), *InMapName ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_CHANGELIST"),		*FString::Printf(TEXT("%i"), GetChangeListNumberForPerfTesting() ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_DATESTAMP"),         *FString::Printf(TEXT("%s"), *FDateTime::Now().ToString() ), ESearchCase::CaseSensitive );

		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_OS"),			    *FString::Printf(TEXT("%s %s"), *OSMajor, *OSMinor ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_CPU"),			    *FString::Printf(TEXT("%s %s"), *FPlatformMisc::GetCPUVendor(), *FPlatformMisc::GetCPUBrand() ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_GPU"),			    *FString::Printf(TEXT("%s"), *FPlatformMisc::GetPrimaryGPUBrand() ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_SETTINGS_RES"),	    *FString::Printf(TEXT("%d"), Quality.ResolutionQuality ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_SETTINGS_VD"),	    *FString::Printf(TEXT("%d"), Quality.ViewDistanceQuality ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_SETTINGS_AA"),	    *FString::Printf(TEXT("%d"), Quality.AntiAliasingQuality ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_SETTINGS_SHADOW"),   *FString::Printf(TEXT("%d"), Quality.ShadowQuality ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_SETTINGS_PP"),	    *FString::Printf(TEXT("%d"), Quality.PostProcessQuality ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_SETTINGS_TEX"),	    *FString::Printf(TEXT("%d"), Quality.TextureQuality ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_SETTINGS_FX"),	    *FString::Printf(TEXT("%d"), Quality.EffectsQuality ), ESearchCase::CaseSensitive );

		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_AVG_FPS"),			*FString::Printf(TEXT("%4.2f"), NumFrames / TotalTime), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace(TEXT("TOKEN_PCT_ABOVE_30"), *FString::Printf(TEXT("%4.2f"), PctTimeAbove30), ESearchCase::CaseSensitive);
		FPSChartRow = FPSChartRow.Replace(TEXT("TOKEN_PCT_ABOVE_60"), *FString::Printf(TEXT("%4.2f"), PctTimeAbove60), ESearchCase::CaseSensitive);
		FPSChartRow = FPSChartRow.Replace(TEXT("TOKEN_TIME_DISREGARDED"), *FString::Printf(TEXT("%4.2f"), FMath::Max<float>(0, DeltaTime - TotalTime)), ESearchCase::CaseSensitive);
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_TIME"),				*FString::Printf(TEXT("%4.2f"), DeltaTime), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_FRAMECOUNT"),		*FString::Printf(TEXT("%i"), NumFrames), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_AVG_GPUTIME"),		*FString::Printf(TEXT("%4.2f ms"), float((GTotalGPUTime / NumFrames)*1000.0) ), ESearchCase::CaseSensitive );

		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_BOUND_GAME_THREAD_PERCENT"),		*FString::Printf(TEXT("%4.2f"), (float(GNumFramesBound_GameThread)/float(NumFrames))*100.0f ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_BOUND_RENDER_THREAD_PERCENT"),		*FString::Printf(TEXT("%4.2f"), (float(GNumFramesBound_RenderThread)/float(NumFrames))*100.0f ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_BOUND_GPU_PERCENT"),		*FString::Printf(TEXT("%4.2f"), (float(GNumFramesBound_GPU)/float(NumFrames))*100.0f ), ESearchCase::CaseSensitive );

		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_BOUND_GAME_THREAD_TIME"),		*FString::Printf(TEXT("%4.2f"), (GTotalFramesBoundTime_GameThread/DeltaTime)*100.0f ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_BOUND_RENDER_THREAD_TIME"),		*FString::Printf(TEXT("%4.2f"), ((GTotalFramesBoundTime_RenderThread)/DeltaTime)*100.0f ), ESearchCase::CaseSensitive );
		FPSChartRow = FPSChartRow.Replace( TEXT("TOKEN_BOUND_GPU_TIME"),		*FString::Printf(TEXT("%4.2f"), ((GTotalFramesBoundTime_GPU)/DeltaTime)*100.0f ), ESearchCase::CaseSensitive );


		const FString OutputDir = CreateOutputDirectory();

		// Create FPS chart filename.
		const FString ChartType = GetFPSChartType();

		const FString& FPSChartFilename = OutputDir / CreateFileNameForChart( ChartType, *InMapName, TEXT( ".html" ) );
		FString FPSChart;

		// See whether file already exists and load it into string if it does.
		if( FFileHelper::LoadFileToString( FPSChart, *FPSChartFilename ) )
		{
			// Split string where we want to insert current row.
			const FString HeaderSeparator = TEXT("<UE4></UE4>");
			FString FPSChartBeforeCurrentRow, FPSChartAfterCurrentRow;
			FPSChart.Split( *HeaderSeparator, &FPSChartBeforeCurrentRow, &FPSChartAfterCurrentRow );

			// Assemble FPS chart by inserting current row at the top.
			FPSChart = FPSChartPreamble + FPSChartRow + FPSChartAfterCurrentRow;
		}
		// Assemble from scratch.
		else
		{
			FPSChart = FPSChartPreamble + FPSChartRow + FPSChartPostamble;
		}

		// Save the resulting file back to disk.
		FFileHelper::SaveStringToFile( FPSChart, *FPSChartFilename );

		UE_LOG( LogProfilingDebugging, Warning, TEXT( "FPS Chart (HTML) saved to %s" ), *IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead( *FPSChartFilename ) );
	}
	else
	{
		UE_LOG(LogChartCreation, Log, TEXT("Missing FPS chart template files."));
	}

#endif // ALLOW_DEBUG_FILES
}
Exemplo n.º 5
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
}