// 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(" element is not supported"); } title = element.GetSafeAttribute(vars, "title", "Events"); beginEvent = element.GetSafeAttribute(vars, "beginevent"); endEvent = element.GetSafeAttribute(vars, "endevent"); endOffsetPercentage = element.GetSafeAttribute(vars, "endoffsetpercent", 0.0); columns = new List(); 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(vars, "summaryStatName"); column.statName = columnEl.GetRequiredAttribute(vars, "stat").ToLower(); if (!stats.Contains(column.statName)) { stats.Add(column.statName); } column.otherStatName = columnEl.GetSafeAttribute("otherStat", "").ToLower(); column.name = columnEl.GetRequiredAttribute(vars, "name"); column.formula = columnEl.GetRequiredAttribute(vars, "formula").ToLower(); // These attributes have back compat case support. This was fixed in PRT 4.242.0 column.filterOutZeros = columnEl.GetSafeAttribute(vars, "filterOutZeros", columnEl.GetSafeAttribute(vars, "filteroutzeros", false)); column.perSecond = columnEl.GetSafeAttribute(vars, "perSecond", columnEl.GetSafeAttribute(vars, "persecond", false)); column.multiplier = columnEl.GetSafeAttribute(vars, "multiplier", 1.0); column.threshold = columnEl.GetSafeAttribute(vars, "threshold", 0.0); column.applyEndOffset = columnEl.GetSafeAttribute(vars, "applyEndOffset", true); column.frameExponent = columnEl.GetSafeAttribute(vars, "frameExponent", 4.0); column.statExponent = columnEl.GetSafeAttribute(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 frameTimes = frameTimeStat.samples; List filteredColumns = new List(); //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 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 statValues = csvStats.GetStat(col.statName).samples; List 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(" "); htmlSection.WriteLine(" "); foreach (Column col in filteredColumns) { htmlSection.WriteLine(""); } htmlSection.WriteLine(" "); htmlSection.WriteLine(" "); foreach (Column col in filteredColumns) { string bgcolor = "'#ffffff'"; if (col.colourThresholdList != null) { bgcolor = col.colourThresholdList.GetColourForValue(col.value); } htmlSection.WriteLine(""); } htmlSection.WriteLine(" "); htmlSection.WriteLine("
" + col.name + "
" + col.value.ToString("0.00") + "
"); } // 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 columns; }; }