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

640 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 System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using CSVStats;
namespace PerfSummaries
{
class SummaryTableDataJsonWriteHelper
{
public SummaryTableDataJsonWriteHelper(string InJsonFilenameOrDirectory, bool bInSeparateFiles, bool bInCsvMetadataOnly, bool bInWriteAllElementData, bool bInWriteIndented=false)
{
if (bInSeparateFiles)
{
JsonDirectory = InJsonFilenameOrDirectory;
}
else
{
JsonFilename = InJsonFilenameOrDirectory;
}
bCsvMetadataOnly = bInCsvMetadataOnly;
bWriteAllElementData = bInWriteAllElementData;
bWriteIndented = bInWriteIndented;
}
public void AddRowData(SummaryTableRowData rowData)
{
if (!rowData.dict.ContainsKey("csvid"))
{
Console.WriteLine("Warning: SummaryTableDataJsonHelper.AddRowData - Row data contains no CSV ID! Skipping");
return;
}
Dictionary<string, dynamic> RowDict = rowData.ToJsonDict(bCsvMetadataOnly, bWriteAllElementData);
Dict.Add(rowData.dict["csvid"].value, RowDict);
}
public void WriteToJson(bool bSerializeToStream)
{
if (JsonDirectory != null)
{
Console.WriteLine("Writing summary table row data to directory : " + JsonDirectory);
foreach (string csvId in Dict.Keys)
{
string filename = Path.Combine(JsonDirectory, csvId+".json");
WriteJsonFile(bSerializeToStream, filename, Dict[csvId], bWriteIndented);
}
}
else
{
Console.WriteLine("Writing summary table row data to file: " + JsonFilename);
WriteJsonFile(bSerializeToStream, JsonFilename, Dict, bWriteIndented);
}
}
private static void WriteJsonFile(bool bSerializeToStream, string filename, Dictionary<string, dynamic> dictToSerialize, bool bWriteIndented)
{
JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = bWriteIndented, NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals };
if (bSerializeToStream)
{
Task task = WriteJsonFileAsyncPrivate(options, filename, dictToSerialize);
task.Wait();
}
else
{
string jsonString = JsonSerializer.Serialize(dictToSerialize, options);
File.WriteAllText(filename, jsonString);
}
}
private static async Task WriteJsonFileAsyncPrivate(JsonSerializerOptions options, string Filename, Dictionary<string, dynamic> DictToSerialize)
{
// serialize JSON directly to a file
FileStream createStream = File.Create(Filename);
await JsonSerializer.SerializeAsync(createStream, DictToSerialize);
createStream.Dispose(); // DisposeAsync doesn't exist in .Net 4.8 so we can't await it
}
Dictionary<string, dynamic> Dict = new Dictionary<string, dynamic>();
string JsonFilename = null;
string JsonDirectory = null;
bool bCsvMetadataOnly;
bool bWriteAllElementData;
bool bWriteIndented;
}
class SummaryTableElement
{
// Bump this when making changes!
public static int CacheVersion = 1;
// NOTE: this is serialized. Don't change the order!
public enum Type
{
CsvStatAverage,
CsvMetadata,
SummaryTableMetric,
ToolMetadata,
ExternalMetadata,
COUNT,
ANY=COUNT
};
public enum Flags
{
Hidden = 0x01
};
private SummaryTableElement()
{
}
public SummaryTableElement(Type inType, string inName, double inValue, ColourThresholdList inColorThresholdList, string inToolTip, uint inFlags = 0)
{
if (inName == "")
{
throw new Exception("Name cannot be empty!");
}
type = inType;
name = inName;
isNumeric = true;
numericValue = inValue;
value = inValue.ToString();
colorThresholdList = inColorThresholdList;
tooltip = inToolTip;
flags = inFlags;
}
public SummaryTableElement(Type inType, string inName, string inValue, ColourThresholdList inColorThresholdList, string inToolTip, uint inFlags = 0)
{
if (inName == "")
{
throw new Exception("Name cannot be empty!");
}
type = inType;
name = inName;
numericValue = 0.0;
isNumeric = false;
colorThresholdList = inColorThresholdList;
value = inValue;
tooltip = inToolTip;
flags = inFlags;
}
public static SummaryTableElement ReadFromCache(BinaryReader reader)
{
SummaryTableElement val = new SummaryTableElement();
val.type = (Type)reader.ReadUInt32();
val.name = reader.ReadString();
val.value = reader.ReadString();
val.tooltip = reader.ReadString();
val.numericValue = reader.ReadDouble();
val.isNumeric = reader.ReadBoolean();
val.flags = reader.ReadUInt32();
bool hasThresholdList = reader.ReadBoolean();
if (hasThresholdList)
{
int thresholdCount = reader.ReadInt32();
val.colorThresholdList = new ColourThresholdList();
// TODO: support serializing ColourThresholdList.LerpColours. This will require adding backwards compat to the serialization system.
for (int i = 0; i < thresholdCount; i++)
{
bool bHasColour = reader.ReadBoolean();
Colour thresholdColour = null;
if (bHasColour)
{
thresholdColour = new Colour(reader.ReadString());
}
double thresholdValue = reader.ReadDouble();
ThresholdInfo info = new ThresholdInfo(thresholdValue, thresholdColour);
val.colorThresholdList.Add(info);
}
}
return val;
}
public void WriteToCache(BinaryWriter writer)
{
writer.Write((uint)type);
writer.Write(name);
writer.Write(value);
writer.Write(tooltip);
writer.Write(numericValue);
writer.Write(isNumeric);
writer.Write(flags);
writer.Write(colorThresholdList != null);
if (colorThresholdList != null)
{
writer.Write((int)colorThresholdList.Count);
// TODO: support serializing ColourThresholdList.LerpColours. This will require adding backwards compat to the serialization system.
foreach (ThresholdInfo thresholdInfo in colorThresholdList.Thresholds)
{
writer.Write(thresholdInfo.colour != null);
if (thresholdInfo.colour != null)
{
writer.Write(thresholdInfo.colour.ToString());
}
writer.Write(thresholdInfo.value);
}
}
}
public SummaryTableElement Clone()
{
return (SummaryTableElement)MemberwiseClone();
}
public void SetFlag(Flags flag, bool value)
{
if (value)
{
flags |= (uint)flag;
}
else
{
flags &= ~(uint)flag;
}
}
public bool GetFlag(Flags flag)
{
return (flags & (uint)flag) != 0;
}
public SummaryTableElement(Type typeIn, string elementName, Dictionary<string, dynamic> jsonDict)
{
name = elementName;
type = typeIn;
dynamic jsonValue = jsonDict["value"];
isNumeric = !(jsonValue is string);
if (isNumeric)
{
numericValue = (double)jsonValue;
value = numericValue.ToString();
}
else
{
value = jsonValue;
}
if (jsonDict.TryGetValue("colorThresholdList", out dynamic colorThresholdListDynamic))
{
Dictionary<string, dynamic> colorThresholdDict = colorThresholdListDynamic;
colorThresholdList = new ColourThresholdList(colorThresholdDict);
}
tooltip = "";
if ( jsonDict.TryGetValue("tooltip", out dynamic tooltipDynamic ) )
{
tooltip = tooltipDynamic;
}
if (jsonDict.TryGetValue("flags", out dynamic flagsDynamic))
{
List<dynamic> flagStrings = flagsDynamic;
flags = 0;
foreach (string flagStr in flagStrings)
{
if ( Enum.TryParse<Flags>(flagStr, out Flags flag ) )
{
flags |= (uint)flag;
}
}
}
}
public Dictionary<string, dynamic> ToJsonDict(bool bWriteType)
{
Dictionary<string, dynamic> Dict = new Dictionary<string, dynamic>();
if (bWriteType)
{
Dict.Add("type", type.ToString());
}
if (isNumeric)
{
try
{
// Serialize as Decimal to avoid 0.1 being serialized as 0.1000000000000001 etc
Dict.Add("value", new Decimal(numericValue));
}
catch (OverflowException)
{
// if Decimal constructor threw then this is a value it doesn't like such as NaN, inf, etc. In those
// cases just pass through the value as a double
Dict.Add("value", numericValue);
}
}
else
{
Dict.Add("value", value);
}
if (!string.IsNullOrEmpty(tooltip))
{
Dict.Add("tooltip", tooltip);
}
if ( colorThresholdList != null)
{
Dict.Add("colorThresholdList", colorThresholdList.ToJsonDict());
}
List<string> FlagStrings = new List<string>();
var FlagValues = Enum.GetValues(typeof(Flags));
foreach (var FlagValue in FlagValues)
{
if ( (flags & (uint)(int)FlagValue) != 0)
{
FlagStrings.Add(FlagValue.ToString());
}
}
if (FlagStrings.Count > 0)
{
Dict.Add("flags", FlagStrings);
}
return Dict;
}
public dynamic DynamicValue
{
get
{
if (isNumeric)
{
return numericValue;
}
return value;
}
}
public Type type;
public string name;
public string value;
public string tooltip;
public ColourThresholdList colorThresholdList;
public double numericValue;
public bool isNumeric;
public uint flags;
}
class SummaryTableRowData
{
public SummaryTableRowData()
{
}
// TODO: If this is bumped beyond 6, we need to implement backwards compatibility
static int CacheVersion = 6;
public static SummaryTableRowData TryReadFromCache(string summaryTableCacheDir, string csvId)
{
string filename = Path.Combine(summaryTableCacheDir, csvId + ".prc");
return TryReadFromCacheFile(filename);
}
public CsvMetadata ReadCsvMetadata()
{
CsvMetadata metadata = null;
foreach (KeyValuePair<string, SummaryTableElement> entry in dict)
{
if (entry.Value.type == SummaryTableElement.Type.CsvMetadata)
{
if (metadata == null)
{
metadata = new CsvMetadata();
}
metadata.Values.Add(entry.Key, entry.Value.value);
}
if (entry.Value.type == SummaryTableElement.Type.CsvStatAverage)
{
// Metadata values appear first so we can early out
break;
}
}
return metadata;
}
public static SummaryTableRowData TryReadFromCacheFile(string filename, bool bReadJustInitialMetadata = false)
{
SummaryTableRowData metaData = null;
if (!File.Exists(filename))
{
return null;
}
try
{
using (FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read))
{
BinaryReader reader = new BinaryReader(fileStream);
int version = reader.ReadInt32();
int metadataValueVersion = reader.ReadInt32();
if (version == CacheVersion && metadataValueVersion == SummaryTableElement.CacheVersion)
{
bool bEarlyOut = false;
metaData = new SummaryTableRowData();
int dictEntryCount = reader.ReadInt32();
for (int i = 0; i < dictEntryCount; i++)
{
string key = reader.ReadString();
SummaryTableElement value = SummaryTableElement.ReadFromCache(reader);
// If we're just reading initial metadata then skip everything after ToolMetadata and CsvMetadata
if (bReadJustInitialMetadata && value.type != SummaryTableElement.Type.ToolMetadata && value.type != SummaryTableElement.Type.CsvMetadata)
{
bEarlyOut = true;
break;
}
metaData.dict.Add(key, value);
}
if (!bEarlyOut)
{
string endString = reader.ReadString();
if (endString != "END")
{
Console.WriteLine("Corruption detected in " + filename + ". Skipping read");
metaData = null;
}
}
}
reader.Close();
}
}
catch (Exception e)
{
metaData = null;
Console.WriteLine("Error reading from cache file " + filename + ": " + e.Message);
}
return metaData;
}
// Filter out external entries for caching purposes
List<KeyValuePair<string, SummaryTableElement>> GetCacheableEntries()
{
List<KeyValuePair<string, SummaryTableElement>> cacheableEntries = new List<KeyValuePair<string, SummaryTableElement>>();
foreach (KeyValuePair<string, SummaryTableElement> entry in dict)
{
if (entry.Value.type != SummaryTableElement.Type.ExternalMetadata)
{
cacheableEntries.Add(entry);
}
}
return cacheableEntries;
}
public bool WriteToCache(string summaryTableCacheDir, string csvId)
{
string filename = Path.Combine(summaryTableCacheDir, csvId + ".prc");
try
{
using (FileStream fileStream = new FileStream(filename, FileMode.Create))
{
BinaryWriter writer = new BinaryWriter(fileStream);
writer.Write(CacheVersion);
writer.Write(SummaryTableElement.CacheVersion);
// Only write non-external entries to the cache
List<KeyValuePair<string, SummaryTableElement>> cacheableEntries = GetCacheableEntries();
writer.Write(cacheableEntries.Count);
foreach (KeyValuePair<string, SummaryTableElement> entry in cacheableEntries)
{
writer.Write(entry.Key);
entry.Value.WriteToCache(writer);
}
writer.Write("END");
writer.Close();
}
}
catch (IOException)
{
Console.WriteLine("Failed to write to cache file " + filename + ".");
return false;
}
return true;
}
public int GetFrameCount()
{
if (!dict.ContainsKey("framecount"))
{
return 0;
}
return (int)dict["framecount"].numericValue;
}
public bool Contains(string key)
{
return dict.ContainsKey(key);
}
public SummaryTableElement Get(string key)
{
if ( dict.TryGetValue(key, out SummaryTableElement elementOut) )
{
return elementOut;
}
return null;
}
public void RemoveSafe(string name)
{
string key = name.ToLower();
if (dict.ContainsKey(key))
{
dict.Remove(key);
}
}
public void Add(SummaryTableElement.Type type, string name, double value, ColourThresholdList colorThresholdList = null, string tooltip = "", uint flags = 0)
{
string key = name.ToLower();
SummaryTableElement metadataValue = new SummaryTableElement(type, name, value, colorThresholdList, tooltip, flags);
try
{
dict.Add(key, metadataValue);
}
catch (System.ArgumentException)
{
//throw new Exception("Summary metadata key " + key + " has already been added");
Console.WriteLine("Warning: Key " + key + " has already been added. Ignoring...");
}
}
public void Add(SummaryTableElement.Type type, string name, string value, ColourThresholdList colorThresholdList = null, string tooltip = "", uint flags = 0)
{
string key = name.ToLower();
double numericValue = double.MaxValue;
try
{
numericValue = Convert.ToDouble(value, System.Globalization.CultureInfo.InvariantCulture);
}
catch { }
SummaryTableElement metadataValue = null;
if (numericValue != double.MaxValue)
{
metadataValue = new SummaryTableElement(type, name, numericValue, colorThresholdList, tooltip, flags);
}
else
{
metadataValue = new SummaryTableElement(type, name, value, colorThresholdList, tooltip, flags);
}
try
{
dict.Add(key, metadataValue);
}
catch (System.ArgumentException)
{
//throw new Exception("Summary metadata key " + key + " has already been added");
Console.WriteLine("Warning: Key " + key + " has already been added. Ignoring...");
}
}
public void Add(SummaryTableElement element)
{
dict.Add(element.name.ToLower(), element);
}
public void AddString(SummaryTableElement.Type type, string name, string value, ColourThresholdList colorThresholdList = null, string tooltip = "")
{
string key = name.ToLower();
SummaryTableElement metadataValue = new SummaryTableElement(type, name, value, colorThresholdList, tooltip);
dict.Add(key, metadataValue);
}
public SummaryTableRowData(Dictionary<string, dynamic> jsonDict)
{
foreach (string dataTypeStr in jsonDict.Keys)
{
if ( Enum.TryParse(dataTypeStr, out SummaryTableElement.Type dataType) )
{
Dictionary<string, dynamic> DataTypeDict = jsonDict[dataTypeStr];
foreach(string elementName in DataTypeDict.Keys)
{
SummaryTableElement newElement = new SummaryTableElement(dataType, elementName, DataTypeDict[elementName]);
Add(newElement);
}
}
}
}
public Dictionary<string, dynamic> ToJsonDict(bool bCsvMetadataOnly, bool bWriteAllElementData)
{
Dictionary<string, dynamic> DictOut = new Dictionary<string, dynamic>();
// Make a dictionary for each data type
if (bCsvMetadataOnly)
{
DictOut[SummaryTableElement.Type.CsvMetadata.ToString()] = new Dictionary<string, dynamic>();
}
else
{
var DataTypes = Enum.GetValues(typeof(SummaryTableElement.Type));
foreach (SummaryTableElement.Type dataType in DataTypes)
{
// External metadata is never cached
if (dataType != SummaryTableElement.Type.COUNT && dataType != SummaryTableElement.Type.ExternalMetadata)
{
DictOut[dataType.ToString()] = new Dictionary<string, dynamic>();
}
}
}
foreach (string key in dict.Keys)
{
SummaryTableElement Element = dict[key];
if (bCsvMetadataOnly && Element.type != SummaryTableElement.Type.CsvMetadata)
{
continue;
}
// External metadata is never cached
if (Element.type == SummaryTableElement.Type.ExternalMetadata)
{
continue;
}
if (bWriteAllElementData)
{
DictOut[Element.type.ToString()][Element.name] = Element.ToJsonDict(false);
}
else
{
DictOut[Element.type.ToString()][Element.name] = Element.DynamicValue;
}
}
return DictOut;
}
public Dictionary<string, SummaryTableElement> dict = new Dictionary<string, SummaryTableElement>();
};
}