// 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(vars, "fps"); hitchThreshold = (float)element.GetRequiredAttribute(vars, "hitchThreshold"); bUseEngineHitchMetric = element.GetSafeAttribute(vars, "useEngineHitchMetric", false); if (bUseEngineHitchMetric) { engineHitchToNonHitchRatio = element.GetSafeAttribute(vars, "engineHitchToNonHitchRatio", 1.5f); engineMinTimeBetweenHitchesMs = element.GetSafeAttribute(vars, "engineMinTimeBetweenHitchesMs", 200.0f); } bIgnoreHitchTimePercent = element.GetSafeAttribute(vars, "ignoreHitchTimePercent", false); bIgnoreMVP = element.GetSafeAttribute(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 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 frameTimes = csvStats.Stats["frametime"].samples; FpsChartData fpsChartData = ComputeFPSChartDataForFrames(frameTimes, true); // Write the averages List Columns = new List(); 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 += "Section Name"; ValueRow += "Entire Run"; foreach (ColumnInfo column in Columns) { string columnName = column.Name; if (columnName.ToLower().EndsWith("time")) { columnName += " (ms)"; } if (column.Details != null) { columnName += " ("+ column.Details + ")"; } HeaderRow += "" + TableUtil.FormatStatName(columnName) + ""; ValueRow += "" + column.Value.ToString("0.00") + ""; } htmlSection.WriteLine(""); htmlSection.WriteLine(" " + HeaderRow + ""); htmlSection.WriteLine(" " + ValueRow + ""); } // Output CSV if (statsCsvFile != null) { List ColumnNames = new List(); List ColumnValues = new List(); List ColumnColors = new List(); 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 ColumnDict = new Dictionary(); 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 += ""; foreach (ColumnInfo column in Columns) { ValueRow += ""; } htmlSection.WriteLine(" " + ValueRow + ""); } // Output CSV if (statsCsvFile != null) { List ColumnValues = new List(); List ColumnColors = new List(); 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("
" + CapRange.name + "" + column.Value.ToString("0.00") + "
"); htmlSection.WriteLine("

Engine hitch metric: " + (bUseEngineHitchMetric ? "enabled" : "disabled") + "

"); } 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; }; }