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

513 lines
17 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using PerfReportTool;
using CSVStats;
namespace PerfSummaries
{
class BoundedStatValuesSummary : Summary
{
class Column
{
public string name;
public string formula;
public double value;
public string summaryStatName;
public string statName;
public string otherStatName;
public bool perSecond;
public bool filterOutZeros;
public bool applyEndOffset;
public double multiplier;
public double threshold;
public double frameExponent; // Exponent for relative frame time (0-1) in streamingstressmetric formula
public double statExponent; // Exponent for stat value in streamingstressmetric formula
public ColourThresholdList colourThresholdList;
public Column() { }
public Column(Column inCol)
{
name = inCol.name;
formula = inCol.formula;
value = inCol.value;
summaryStatName = inCol.summaryStatName;
statName = inCol.statName;
otherStatName = inCol.otherStatName;
perSecond = inCol.perSecond;
filterOutZeros = inCol.filterOutZeros;
applyEndOffset = inCol.applyEndOffset;
multiplier = inCol.multiplier;
threshold = inCol.threshold;
frameExponent = inCol.frameExponent;
statExponent = inCol.statExponent;
colourThresholdList = inCol.colourThresholdList;
}
};
public BoundedStatValuesSummary(XElement element, XmlVariableMappings vars, string baseXmlDirectory)
{
ReadStatsFromXML(element, vars);
if (stats.Count != 0)
{
throw new Exception("<stats> element is not supported");
}
title = element.GetSafeAttribute(vars, "title", "Events");
beginEvent = element.GetSafeAttribute<string>(vars, "beginevent");
endEvent = element.GetSafeAttribute<string>(vars, "endevent");
endOffsetPercentage = element.GetSafeAttribute<double>(vars, "endoffsetpercent", 0.0);
columns = new List<Column>();
foreach (XElement columnEl in element.Elements("column"))
{
Column column = new Column();
double[] colourThresholds = ReadColourThresholdsXML(columnEl.Element("colourThresholds"), vars);
if (colourThresholds != null)
{
column.colourThresholdList = new ColourThresholdList();
for (int i = 0; i < colourThresholds.Length; i++)
{
column.colourThresholdList.Add(new ThresholdInfo(colourThresholds[i], null));
}
}
column.summaryStatName = columnEl.GetSafeAttribute<string>(vars, "summaryStatName");
column.statName = columnEl.GetRequiredAttribute<string>(vars, "stat").ToLower();
if (!stats.Contains(column.statName))
{
stats.Add(column.statName);
}
column.otherStatName = columnEl.GetSafeAttribute<string>("otherStat", "").ToLower();
column.name = columnEl.GetRequiredAttribute<string>(vars, "name");
column.formula = columnEl.GetRequiredAttribute<string>(vars, "formula").ToLower();
// These attributes have back compat case support. This was fixed in PRT 4.242.0
column.filterOutZeros = columnEl.GetSafeAttribute<bool>(vars, "filterOutZeros", columnEl.GetSafeAttribute<bool>(vars, "filteroutzeros", false));
column.perSecond = columnEl.GetSafeAttribute<bool>(vars, "perSecond", columnEl.GetSafeAttribute<bool>(vars, "persecond", false));
column.multiplier = columnEl.GetSafeAttribute<double>(vars, "multiplier", 1.0);
column.threshold = columnEl.GetSafeAttribute<double>(vars, "threshold", 0.0);
column.applyEndOffset = columnEl.GetSafeAttribute<bool>(vars, "applyEndOffset", true);
column.frameExponent = columnEl.GetSafeAttribute<double>(vars, "frameExponent", 4.0);
column.statExponent = columnEl.GetSafeAttribute<double>(vars, "statExponent", 0.25);
columns.Add(column);
}
}
public BoundedStatValuesSummary() { }
public override string GetName() { return "boundedstatvalues"; }
public override HtmlSection WriteSummaryData(bool bWriteHtml, CsvStats csvStats, CsvStats csvStatsUnstripped, bool bWriteSummaryCsv, SummaryTableRowData rowData, string htmlFileName)
{
HtmlSection htmlSection = null;
int startFrame = -1;
int endFrame = int.MaxValue;
// Find the start and end frames based on the events
if (beginEvent != null)
{
foreach (CsvEvent ev in csvStats.Events)
{
if (CsvStats.DoesSearchStringMatch(ev.Name, beginEvent))
{
startFrame = ev.Frame;
break;
}
}
if (startFrame == -1)
{
Console.WriteLine("BoundedStatValuesSummary: Begin event " + beginEvent + " was not found");
return htmlSection;
}
}
if (endEvent != null)
{
foreach (CsvEvent ev in csvStats.Events)
{
if (CsvStats.DoesSearchStringMatch(ev.Name, endEvent))
{
endFrame = ev.Frame;
if (endFrame > startFrame)
{
break;
}
}
}
if (endFrame == int.MaxValue)
{
Console.WriteLine("BoundedStatValuesSummary: End event " + endEvent + " was not found");
return htmlSection;
}
}
if (startFrame >= endFrame)
{
Console.WriteLine("Warning: BoundedStatValuesSummary: end event "+ endEvent + " appeared before the start event "+beginEvent);
return htmlSection;
}
endFrame = Math.Min(endFrame, csvStats.SampleCount - 1);
startFrame = Math.Max(startFrame, 0);
// Adjust the end frame based on the specified offset percentage, but cache the old value (some columns may need the unmodified one)
int endEventFrame = Math.Min(csvStats.SampleCount, endFrame + 1);
if (endOffsetPercentage > 0.0)
{
double multiplier = endOffsetPercentage / 100.0;
endFrame += (int)((double)(endFrame - startFrame) * multiplier);
}
endFrame = Math.Min(csvStats.SampleCount, endFrame + 1);
StatSamples frameTimeStat = csvStats.GetStat("frametime");
List<float> frameTimes = frameTimeStat.samples;
List<Column> filteredColumns = new List<Column>();
//Filtering and wildcard logic
//This adds new columns for each stat which matches a wildcard column, and also filters the columns to ones
//that have a valid stat associated with them. The original wildcard column will be removed as well
foreach (Column col in columns)
{
bool nameIsWildcard = col.name != null && col.name.Contains('*');
bool summaryStatNameIsWildcard = col.summaryStatName != null && col.summaryStatName.Contains('*');
bool statNameIsWildcard = col.statName != null && col.statName.Contains('*');
bool otherStatNameIsSet = col.otherStatName != null && col.otherStatName.Length > 0;
bool otherStatNameIsWildcard = col.otherStatName != null && col.otherStatName.Contains('*');
bool anyNameWildcard = nameIsWildcard || summaryStatNameIsWildcard || statNameIsWildcard || otherStatNameIsWildcard;
bool allNameWildcard = nameIsWildcard && summaryStatNameIsWildcard && statNameIsWildcard && (otherStatNameIsSet ? otherStatNameIsWildcard : true);
//Check all wildcard fields are set, or none of them should be set
if (anyNameWildcard && !allNameWildcard)
{
string errorString = "Warning: BoundedStatValuesSummary: Skipping column because wildcard * was found in some column parameters but not all. name: " + col.name + " summaryStatName: " + col.summaryStatName + " statName: " + col.statName;
if(otherStatNameIsSet)
{
errorString += " otherStatName: " + col.otherStatName;
}
Console.WriteLine(errorString);
continue;
}
//Generate new columns based on the wildcard definition
if (anyNameWildcard && allNameWildcard)
{
string[] splitStr = col.statName.Split('*', 2);
if(splitStr.Length == 0)
{
Console.WriteLine("Warning: Skipping stat due to error: StatName is a wildcard and should contain a * symbol: " + col.statName);
continue;
}
//Find each stat that matches the wildcard in statName
string statPrefix = splitStr[0];
foreach (KeyValuePair<string, StatSamples> foundStat in csvStats.Stats)
{
string foundStatName = foundStat.Key;
if (foundStatName.ToLower().Contains(statPrefix.ToLower()) && !foundStatName.Equals(col.statName))
{
//Create a new column as a copy of the Wildcard Definition Column
Column newCol = new Column(col);
string[] splitString = foundStatName.Split(statPrefix, 2);
if(splitString.Length <= 1)
{
Console.WriteLine("Warning: Skipping stat due to error: FoundStatName(" + foundStatName + ") does not contain expected prefix: " + statPrefix);
continue;
}
string statPostfix = splitString[1];
newCol.name = col.name.Substring(0, col.name.IndexOf('*')) + statPostfix;
newCol.summaryStatName = col.summaryStatName.Substring(0, col.summaryStatName.IndexOf('*')) + statPostfix;
newCol.statName = col.statName.Substring(0, col.statName.IndexOf('*')) + statPostfix;
if(csvStats.GetStat(newCol.statName) == null)
{
Console.WriteLine("Warning: Skipping stat due to error: StatName not found in csvstats: " + newCol.statName);
continue;
}
if (otherStatNameIsSet)
{
newCol.otherStatName = col.otherStatName.Substring(0, col.otherStatName.IndexOf('*')) + statPostfix;
if (csvStats.GetStat(newCol.otherStatName) == null)
{
Console.WriteLine("Warning: Skipping stat due to error: StatName not found in csvstats: " + newCol.otherStatName);
continue;
}
}
//Add newly formed column to the filteredColumns list
filteredColumns.Add(newCol);
}
}
}
else
{
// This is a non-wildcard column, so add it as usual
// Filter only columns with stats that exist in the CSV
if (csvStats.GetStat(col.statName) != null
&& (String.IsNullOrWhiteSpace(col.otherStatName) || csvStats.GetStat(col.otherStatName) != null))
{
filteredColumns.Add(col);
}
}
}
// Nothing to report, so bail out!
if (filteredColumns.Count == 0)
{
return htmlSection;
}
// Process the column values
foreach (Column col in filteredColumns)
{
List<float> statValues = csvStats.GetStat(col.statName).samples;
List<float> otherStatValues = String.IsNullOrWhiteSpace(col.otherStatName) ? null : csvStats.GetStat(col.otherStatName).samples;
double value = 0.0;
double totalFrameWeight = 0.0;
int colEndFrame = col.applyEndOffset ? endFrame : endEventFrame;
if (col.formula == "average")
{
for (int i = startFrame; i < colEndFrame; i++)
{
if (col.filterOutZeros == false || statValues[i] > 0)
{
value += statValues[i] * frameTimes[i];
totalFrameWeight += frameTimes[i];
}
}
}
else if (col.formula == "unweighted_average")
{
for (int i = startFrame; i < colEndFrame; i++)
{
if (col.filterOutZeros == false || statValues[i] > 0)
{
value += statValues[i];
++totalFrameWeight;
}
}
}
else if (col.formula == "maximum")
{
for (int i = startFrame; i < colEndFrame; i++)
{
if (col.filterOutZeros == false || statValues[i] > 0)
{
value = statValues[i] > value ? statValues[i] : value;
}
}
totalFrameWeight = 1.0;
}
else if (col.formula == "minimum")
{
value = double.MaxValue;
for (int i = startFrame; i < colEndFrame; i++)
{
if (col.filterOutZeros == false || statValues[i] > 0)
{
value = statValues[i] < value ? statValues[i] : value;
}
}
totalFrameWeight = 1.0;
}
else if (col.formula == "percentoverthreshold")
{
for (int i = startFrame; i < colEndFrame; i++)
{
if (statValues[i] > col.threshold)
{
value += frameTimes[i];
}
totalFrameWeight += frameTimes[i];
}
value *= 100.0;
}
else if (col.formula == "percentunderthreshold")
{
for (int i = startFrame; i < colEndFrame; i++)
{
if (statValues[i] < col.threshold)
{
value += frameTimes[i];
}
totalFrameWeight += frameTimes[i];
}
value *= 100.0;
}
else if (col.formula == "sum")
{
for (int i = startFrame; i < colEndFrame; i++)
{
value += statValues[i];
}
if (col.perSecond)
{
double totalTimeMS = 0.0;
for (int i = startFrame; i < colEndFrame; i++)
{
if (col.filterOutZeros == false || statValues[i] > 0)
{
totalTimeMS += frameTimes[i];
}
}
value /= (totalTimeMS / 1000.0);
}
totalFrameWeight = 1.0;
}
else if (col.formula == "sumwhenotheroverthreshold")
{
for (int i = startFrame; i < colEndFrame; i++)
{
if (otherStatValues[i] > col.threshold)
{
value += statValues[i];
}
}
if (col.perSecond)
{
double totalTimeMS = 0.0;
for (int i = startFrame; i < colEndFrame; i++)
{
if ((col.filterOutZeros == false || statValues[i] > 0) && otherStatValues[i] > col.threshold)
{
totalTimeMS += frameTimes[i];
}
}
value /= (totalTimeMS / 1000.0);
}
totalFrameWeight = 1.0;
}
else if (col.formula == "sumwhenotherunderthreshold")
{
for (int i = startFrame; i < colEndFrame; i++)
{
if (otherStatValues[i] < col.threshold)
{
value += statValues[i];
}
}
if (col.perSecond)
{
double totalTimeMS = 0.0;
for (int i = startFrame; i < colEndFrame; i++)
{
if ((col.filterOutZeros == false || statValues[i] > 0) && otherStatValues[i] < col.threshold)
{
totalTimeMS += frameTimes[i];
}
}
value /= (totalTimeMS / 1000.0);
}
totalFrameWeight = 1.0;
}
else if (col.formula == "streamingstressmetric")
{
// Note: tInc is scaled such that it hits 1.0 on the event frame, regardless of the offset
double tInc = 1.0 / (double)(endEventFrame - startFrame);
double t = tInc * 0.5;
for (int i = startFrame; i < colEndFrame; i++)
{
if (col.filterOutZeros == false || statValues[i] > 0)
{
// Frame weighting is scaled to heavily favor final frames. Note that t can exceed 1 after the event frame if an offset percentage is specified, so we clamp it
double frameWeight = Math.Pow(Math.Min(t, 1.0), col.frameExponent) * frameTimes[i];
// If we're past the end event frame, apply a linear falloff to the weight
if (i >= endEventFrame)
{
double falloff = 1.0 - (double)(i - endEventFrame) / (colEndFrame - endEventFrame);
frameWeight *= falloff;
}
// The frame score takes into account the queue depth, but it's not massively significant
double frameScore = Math.Pow(statValues[i], col.statExponent);
value += frameScore * frameWeight;
totalFrameWeight += frameWeight;
}
t += tInc;
}
}
else if (col.formula == "ratio")
{
double numerator = 0.0;
for (int i = startFrame; i < colEndFrame; i++)
{
numerator += statValues[i];
}
double denominator = 0.0;
for (int i = startFrame; i < colEndFrame; i++)
{
denominator += otherStatValues[i];
}
// Guard against divide by 0 as json serialization cannot handle Inf.
value = denominator != 0.0 ? numerator / denominator : 0.0;
totalFrameWeight = 1.0;
}
else
{
throw new Exception("BoundedStatValuesSummary: unexpected formula " + col.formula);
}
value *= col.multiplier;
col.value = totalFrameWeight != 0.0 ? value / totalFrameWeight : value;
}
// Output HTML
if (bWriteHtml)
{
htmlSection = new HtmlSection(title, bStartCollapsed);
htmlSection.WriteLine(" <table border='0' style='width:1400'>");
htmlSection.WriteLine(" <tr>");
foreach (Column col in filteredColumns)
{
htmlSection.WriteLine("<th>" + col.name + "</th>");
}
htmlSection.WriteLine(" </tr>");
htmlSection.WriteLine(" <tr>");
foreach (Column col in filteredColumns)
{
string bgcolor = "'#ffffff'";
if (col.colourThresholdList != null)
{
bgcolor = col.colourThresholdList.GetColourForValue(col.value);
}
htmlSection.WriteLine("<td bgcolor=" + bgcolor + ">" + col.value.ToString("0.00") + "</td>");
}
htmlSection.WriteLine(" </tr>");
htmlSection.WriteLine(" </table>");
}
// Output summary table row data
if (rowData != null)
{
foreach (Column col in filteredColumns)
{
if (col.summaryStatName != null)
{
rowData.Add(SummaryTableElement.Type.SummaryTableMetric, col.summaryStatName, col.value, col.colourThresholdList);
}
}
}
return htmlSection;
}
public override void PostInit(ReportTypeInfo reportTypeInfo, CsvStats csvStats)
{
}
string title;
string beginEvent;
string endEvent;
double endOffsetPercentage;
List<Column> columns;
};
}