Files
UnrealEngine/Engine/Source/Programs/CSVTools/PerfReportTool/Summaries/FPSChartSummary.cs
2025-05-18 13:04:45 +08:00

504 lines
16 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using PerfReportTool;
using CSVStats;
namespace PerfSummaries
{
class FPSChartSummary : Summary
{
public FPSChartSummary(XElement element, XmlVariableMappings vars, string baseXmlDirectory)
{
ReadStatsFromXML(element, vars);
fps = element.GetRequiredAttribute<int>(vars, "fps");
hitchThreshold = (float)element.GetRequiredAttribute<double>(vars, "hitchThreshold");
bUseEngineHitchMetric = element.GetSafeAttribute<bool>(vars, "useEngineHitchMetric", false);
if (bUseEngineHitchMetric)
{
engineHitchToNonHitchRatio = element.GetSafeAttribute<float>(vars, "engineHitchToNonHitchRatio", 1.5f);
engineMinTimeBetweenHitchesMs = element.GetSafeAttribute<float>(vars, "engineMinTimeBetweenHitchesMs", 200.0f);
}
bIgnoreHitchTimePercent = element.GetSafeAttribute<bool>(vars, "ignoreHitchTimePercent", false);
bIgnoreMVP = element.GetSafeAttribute<bool>(vars, "ignoreMVP", false);
}
public FPSChartSummary() { }
public override string GetName() { return "fpschart"; }
float GetEngineHitchToNonHitchRatio()
{
float MinimumRatio = 1.0f;
float targetFrameTime = 1000.0f / fps;
float MaximumRatio = hitchThreshold / targetFrameTime;
return Math.Min(Math.Max(engineHitchToNonHitchRatio, MinimumRatio), MaximumRatio);
}
struct FpsChartData
{
public float MVP;
public float HitchesPerMinute;
public float HitchTimePercent;
public int HitchCount;
public float TotalTimeSeconds;
};
FpsChartData ComputeFPSChartDataForFrames(List<float> frameTimes, bool skiplastFrame)
{
double totalFrametime = 0.0;
int hitchCount = 0;
double totalHitchTime = 0.0;
int frameCount = skiplastFrame ? frameTimes.Count - 1 : frameTimes.Count;
// Count hitches
if (bUseEngineHitchMetric)
{
// Minimum time passed before we'll record a new hitch
double CurrentTime = 0.0;
double LastHitchTime = float.MinValue;
double LastFrameTime = float.MinValue;
float HitchMultiplierAmount = GetEngineHitchToNonHitchRatio();
for (int i = 0; i < frameCount; i++)
{
float frametime = frameTimes[i];
// How long has it been since the last hitch we detected?
if (frametime >= hitchThreshold)
{
double TimeSinceLastHitch = (CurrentTime - LastHitchTime);
if (TimeSinceLastHitch >= engineMinTimeBetweenHitchesMs)
{
// For the current frame to be considered a hitch, it must have run at least this many times slower than
// the previous frame
// If our frame time is much larger than our last frame time, we'll count this as a hitch!
if (frametime > (LastFrameTime * HitchMultiplierAmount))
{
LastHitchTime = CurrentTime;
hitchCount++;
}
}
totalHitchTime += frametime;
}
LastFrameTime = frametime;
CurrentTime += (double)frametime;
}
totalFrametime = CurrentTime;
}
else
{
for (int i = 0; i < frameCount; i++)
{
float frametime = frameTimes[i];
totalFrametime += frametime;
if (frametime >= hitchThreshold)
{
hitchCount++;
}
}
}
float TotalSeconds = (float)totalFrametime / 1000.0f;
float TotalMinutes = TotalSeconds / 60.0f;
FpsChartData outData = new FpsChartData();
outData.HitchCount = hitchCount;
outData.TotalTimeSeconds = TotalSeconds;
outData.HitchesPerMinute = (float)hitchCount / TotalMinutes;
// subtract hitch threshold to weight larger hitches
totalHitchTime -= (hitchCount * hitchThreshold);
outData.HitchTimePercent = (float)(totalHitchTime / totalFrametime) * 100.0f;
// If frame count is low enough this can get rounded down to 0
int TotalTargetFrames = (int)((double)fps * (TotalSeconds));
if (TotalTargetFrames > 0)
{
int MissedFrames = Math.Max(TotalTargetFrames - frameTimes.Count, 0);
outData.MVP = (((float)MissedFrames * 100.0f) / (float)TotalTargetFrames);
}
else
{
outData.MVP = 0;
}
return outData;
}
class ColumnInfo
{
public ColumnInfo(string inName, double inValue, ColourThresholdList inColorThresholds, string inDetails=null, bool inIsAvgValue=false)
{
Name = inName;
Value = inValue;
ColorThresholds = inColorThresholds;
isAverageValue = inIsAvgValue;
Details = inDetails;
UpdateColor();
}
public void UpdateColor()
{
Color = ColourThresholdList.GetSafeColourForValue(ColorThresholds, Value);
}
public string Name;
public double Value;
public string Color;
public ColourThresholdList ColorThresholds;
public string Details;
public bool isAverageValue;
};
private ColourThresholdList ComputeFrameTimeColorThresholdsFromMVP( int fps )
{
ColourThresholdList mvpColorThresholdList = GetStatColourThresholdList("MVP"+ fps.ToString());
ColourThresholdList colorThresholdListOut = null;
if (mvpColorThresholdList != null && mvpColorThresholdList.Count == 4)
{
colorThresholdListOut = new ColourThresholdList();
double idealFrameTime = 1000.0 / (double)fps;
for (int i = 0; i < 4; i++)
{
// MVP = (1/idealFrameTime - 1/avgFrameTime) * idealFrameTime
// avgFrameTime = idealFrameTime / (1 - MVP)
ThresholdInfo mvpThreshold = mvpColorThresholdList.Thresholds[i];
double frameTimeThreshold = idealFrameTime / (1.0 - mvpThreshold.value / 100.0);
colorThresholdListOut.Thresholds.Add(new ThresholdInfo(frameTimeThreshold, mvpThreshold.colour));
}
}
return colorThresholdListOut;
}
public override HtmlSection WriteSummaryData(bool bWriteHtml, CsvStats csvStats, CsvStats csvStatsUnstripped, bool bWriteSummaryCsv, SummaryTableRowData rowData, string htmlFileName)
{
HtmlSection htmlSection = null;
System.IO.StreamWriter statsCsvFile = null;
if (bWriteSummaryCsv)
{
string csvPath = Path.Combine(Path.GetDirectoryName(htmlFileName), "FrameStats_colored.csv");
statsCsvFile = new System.IO.StreamWriter(csvPath, false);
}
// Compute MVP30 and MVP60. Note: we ignore the last frame because fpscharts can hitch
List<float> frameTimes = csvStats.Stats["frametime"].samples;
FpsChartData fpsChartData = ComputeFPSChartDataForFrames(frameTimes, true);
// Write the averages
List<ColumnInfo> Columns = new List<ColumnInfo>();
Columns.Add( new ColumnInfo("Total Time (s)", fpsChartData.TotalTimeSeconds, new ColourThresholdList() ) );
Columns.Add(new ColumnInfo("Hitches/Min", fpsChartData.HitchesPerMinute, GetStatColourThresholdList("Hitches/Min") ) );
if (!bIgnoreHitchTimePercent)
{
Columns.Add(new ColumnInfo("HitchTimePercent", fpsChartData.HitchTimePercent, GetStatColourThresholdList("HitchTimePercent")));
}
string MvpStatName = "MVP" + fps.ToString();
if (!bIgnoreMVP)
{
Columns.Add(new ColumnInfo(MvpStatName, fpsChartData.MVP, GetStatColourThresholdList(MvpStatName)));
}
// Output CSV stats
int csvStatColumnStartIndex = Columns.Count;
foreach (string statName in stats)
{
string baseStatName = statName;
string [] statAttributes=new string[0];
int bracketIndex = statName.IndexOf('(');
if (bracketIndex != -1)
{
baseStatName = statName.Substring(0,bracketIndex);
int rBracketIndex = statName.LastIndexOf(')');
if (rBracketIndex > bracketIndex)
{
string attributesStr = statName.Substring(bracketIndex + 1, rBracketIndex - (bracketIndex + 1)).ToLower();
statAttributes = attributesStr.Split(' ');
}
}
string ValueType = " Avg";
bool bIsAvg = false;
if (!csvStats.Stats.ContainsKey(baseStatName.ToLower()))
{
continue;
}
bool bUnstripped = statAttributes.Contains("unstripped");
StatSamples stat = bUnstripped ? csvStatsUnstripped.Stats[baseStatName.ToLower()] : csvStats.Stats[baseStatName.ToLower()];
float value;
if (statAttributes.Contains("min"))
{
value = stat.ComputeMinValue();
ValueType = " Min";
}
else if (statAttributes.Contains("max"))
{
value = stat.ComputeMaxValue();
ValueType = " Max";
}
else
{
value = stat.average;
bIsAvg = true;
}
string detailStr = null;
if (bUnstripped)
{
detailStr = "all frames";
}
ColourThresholdList colorThresholdList = GetStatColourThresholdList(statName);
// If the frametime color thresholds are not specified then compute them based on MVP
if (!bIgnoreMVP && colorThresholdList == null && baseStatName.ToLower() == "frametime" && fps > 0)
{
colorThresholdList = ComputeFrameTimeColorThresholdsFromMVP(fps);
}
Columns.Add(new ColumnInfo(baseStatName + ValueType, value, colorThresholdList, detailStr, bIsAvg));
}
// Output summary table row data
if (rowData != null)
{
foreach (ColumnInfo column in Columns)
{
string columnName = column.Name;
// Output simply MVP to rowData instead of MVP30 etc
if (columnName.StartsWith("MVP"))
{
columnName = "MVP";
}
// Hide pre-existing stats with the same name
if (column.isAverageValue && columnName.EndsWith(" Avg"))
{
string originalStatName = columnName.Substring(0, columnName.Length - 4).ToLower();
SummaryTableElement smv;
if (rowData.dict.TryGetValue(originalStatName, out smv))
{
if (smv.type == SummaryTableElement.Type.CsvStatAverage)
{
smv.SetFlag(SummaryTableElement.Flags.Hidden, true);
}
}
}
rowData.Add(SummaryTableElement.Type.SummaryTableMetric, columnName, column.Value, column.ColorThresholds);
}
rowData.Add(SummaryTableElement.Type.SummaryTableMetric, "TargetFPS", (double)fps);
}
// Output HTML
if (bWriteHtml)
{
htmlSection = new HtmlSection("FPSChart", bStartCollapsed);
string HeaderRow = "";
string ValueRow = "";
HeaderRow += "<th>Section Name</th>";
ValueRow += "<td>Entire Run</td>";
foreach (ColumnInfo column in Columns)
{
string columnName = column.Name;
if (columnName.ToLower().EndsWith("time"))
{
columnName += " (ms)";
}
if (column.Details != null)
{
columnName += " ("+ column.Details + ")";
}
HeaderRow += "<th>" + TableUtil.FormatStatName(columnName) + "</th>";
ValueRow += "<td bgcolor=" + column.Color + ">" + column.Value.ToString("0.00") + "</td>";
}
htmlSection.WriteLine("<table border='0' style='width:400'>");
htmlSection.WriteLine(" <tr>" + HeaderRow + "</tr>");
htmlSection.WriteLine(" <tr>" + ValueRow + "</tr>");
}
// Output CSV
if (statsCsvFile != null)
{
List<string> ColumnNames = new List<string>();
List<double> ColumnValues = new List<double>();
List<string> ColumnColors = new List<string>();
foreach (ColumnInfo column in Columns)
{
ColumnNames.Add(column.Name);
ColumnValues.Add(column.Value);
ColumnColors.Add(column.Color);
}
statsCsvFile.Write("Section Name,");
statsCsvFile.WriteLine(string.Join(",", ColumnNames));
statsCsvFile.Write("Entire Run,");
statsCsvFile.WriteLine(string.Join(",", ColumnValues));
// Pass through color data as part of database-friendly stuff.
statsCsvFile.Write("Entire Run BGColors,");
statsCsvFile.WriteLine(string.Join(",", ColumnColors));
}
if (csvStats.Events.Count > 0)
{
Dictionary<string, ColumnInfo> ColumnDict = new Dictionary<string, ColumnInfo>();
foreach (ColumnInfo column in Columns)
{
ColumnDict[column.Name] = column;
}
// Per-event breakdown
foreach (CaptureRange CapRange in captures)
{
CaptureData CaptureFrameTimes = GetFramesForCapture(CapRange, frameTimes, csvStats.Events);
if (CaptureFrameTimes == null)
{
continue;
}
FpsChartData captureFpsChartData = ComputeFPSChartDataForFrames(CaptureFrameTimes.Frames, true);
if (captureFpsChartData.TotalTimeSeconds == 0.0f)
{
continue;
}
ColumnDict["Total Time (s)"].Value = captureFpsChartData.TotalTimeSeconds;
ColumnDict["Hitches/Min"].Value = captureFpsChartData.HitchesPerMinute;
if (!bIgnoreHitchTimePercent)
{
ColumnDict["HitchTimePercent"].Value = captureFpsChartData.HitchTimePercent;
}
if (!bIgnoreMVP)
{
ColumnDict[MvpStatName].Value = captureFpsChartData.MVP;
}
// Update the CSV stat values
int columnIndex = csvStatColumnStartIndex;
foreach (string statName in stats)
{
string StatToCheck = statName.Split('(')[0];
if (!csvStats.Stats.ContainsKey(StatToCheck.ToLower()))
{
continue;
}
string[] StatTokens = statName.Split('(');
float value = 0;
if (StatTokens.Length > 1 && StatTokens[1].ToLower().Contains("min"))
{
value = csvStats.Stats[StatTokens[0].ToLower()].ComputeMinValue(CaptureFrameTimes.startIndex, CaptureFrameTimes.endIndex);
}
else if (StatTokens.Length > 1 && StatTokens[1].ToLower().Contains("max"))
{
value = csvStats.Stats[StatTokens[0].ToLower()].ComputeMaxValue(CaptureFrameTimes.startIndex, CaptureFrameTimes.endIndex);
}
else
{
value = csvStats.Stats[StatTokens[0].ToLower()].ComputeAverage(CaptureFrameTimes.startIndex, CaptureFrameTimes.endIndex);
}
Columns[columnIndex].Value = value;
columnIndex++;
}
// Recompute colors
foreach (ColumnInfo column in Columns)
{
column.UpdateColor();
}
// Write out data per capture range to summary table row data
if (rowData != null)
{
if (!bIgnoreHitchTimePercent)
{
rowData.Add(SummaryTableElement.Type.SummaryTableMetric, CapRange.name + "_HitchTimePercent", captureFpsChartData.HitchTimePercent, GetStatColourThresholdList("HitchTimePercent"));
}
if (!bIgnoreMVP)
{
rowData.Add(SummaryTableElement.Type.SummaryTableMetric, CapRange.name + "_MVP", captureFpsChartData.MVP, GetStatColourThresholdList(MvpStatName));
}
rowData.Add(SummaryTableElement.Type.SummaryTableMetric, CapRange.name + "_HPM", captureFpsChartData.HitchesPerMinute, GetStatColourThresholdList("Hitches/Min"));
rowData.Add(SummaryTableElement.Type.SummaryTableMetric, CapRange.name + "_HitchCount", captureFpsChartData.HitchCount);
rowData.Add(SummaryTableElement.Type.SummaryTableMetric, CapRange.name + "_TotalTimeSeconds", captureFpsChartData.TotalTimeSeconds);
}
// Output HTML
if (htmlSection != null)
{
string ValueRow = "";
ValueRow += "<td>" + CapRange.name + "</td>";
foreach (ColumnInfo column in Columns)
{
ValueRow += "<td bgcolor=" + column.Color + ">" + column.Value.ToString("0.00") + "</td>";
}
htmlSection.WriteLine(" <tr>" + ValueRow + "</tr>");
}
// Output CSV
if (statsCsvFile != null)
{
List<double> ColumnValues = new List<double>();
List<string> ColumnColors = new List<string>();
foreach (ColumnInfo column in Columns)
{
ColumnValues.Add(column.Value);
ColumnColors.Add(column.Color);
}
statsCsvFile.Write(CapRange.name + ",");
statsCsvFile.WriteLine(string.Join(",", ColumnValues));
// Pass through color data as part of database-friendly stuff.
statsCsvFile.Write(CapRange.name + " colors,");
statsCsvFile.WriteLine(string.Join(",", ColumnColors));
}
}
}
if (htmlSection != null)
{
htmlSection.WriteLine("</table>");
htmlSection.WriteLine("<p style='font-size:8'>Engine hitch metric: " + (bUseEngineHitchMetric ? "enabled" : "disabled") + "</p>");
}
if (statsCsvFile != null)
{
statsCsvFile.Close();
}
return htmlSection;
}
public override void PostInit(ReportTypeInfo reportTypeInfo, CsvStats csvStats)
{
}
int fps;
float hitchThreshold;
bool bUseEngineHitchMetric;
bool bIgnoreHitchTimePercent;
bool bIgnoreMVP;
float engineHitchToNonHitchRatio;
float engineMinTimeBetweenHitchesMs;
};
}