// Copyright (C) Microsoft. All rights reserved. // Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Diagnostics; using CSVStats; using System.Collections; using System.Security.Cryptography; using System.Threading.Tasks; using System.Threading; using PerfSummaries; using CSVTools; using System.Text.Json; namespace PerfReportTool { class Version { // Format: Major.Minor.Bugfix private static string VersionString = "4.253.3"; public static string Get() { return VersionString; } }; class HashHelper { public static string StringToHashStr(string strIn, int maxCharsOut=-1) { HashAlgorithm algorithm = SHA256.Create(); StringBuilder sb = new StringBuilder(); byte[] hash = algorithm.ComputeHash(Encoding.UTF8.GetBytes(strIn)); StringBuilder sbOut = new StringBuilder(); foreach (byte b in hash) { sbOut.Append(b.ToString("X2")); } string strOut = sbOut.ToString(); if (maxCharsOut > 0) { return strOut.Substring(0, maxCharsOut); } return strOut; } } class SummaryTableCacheStats { public int WriteCount = 0; public int HitCount = 0; public int MissCount = 0; public int PurgeCount = 0; public void LogStats() { Console.WriteLine("Summary Table Cache stats:"); Console.WriteLine(" Cache hits : " + HitCount); Console.WriteLine(" Cache misses : " + MissCount); Console.WriteLine(" Cache writes : " + WriteCount); if (PurgeCount > 0) { Console.WriteLine(" Files purged : " + PurgeCount); } if (HitCount > 0 || MissCount > 0) { Console.WriteLine(" Hit percentage : " + ((float)HitCount * 100.0f / ((float)MissCount+(float)HitCount)).ToString("0.0") + "%"); } } }; class Program : CommandLineTool { static string formatString = "PerfReportTool v" + Version.Get() + "\n" + "\n" + "Format: \n" + " -csv or -csvdir or -summaryTableCacheIn or\n" + " -csvList or -prcList \n" + " -o : output directory (will be created if necessary)\n" + "\n" + "Optional Args:\n" + " -reportType \n" + " -reportTypeCompatCheck : do a compatibility if when specifying a report type (rather than forcing)\n" + " -graphXML \n" + " -reportXML \n" + " -reportxmlbasedir \n" + " -title - title for detailed reports\n" + " -summaryTitle - title for summary tables\n" + " -maxy - forces all graphs to use this value\n" + " -writeSummaryCsv : if specified, a csv file containing summary information will be generated.\n" + " Not available in bulk mode.\n" + " -noWatermarks : don't embed the commandline or version in reports\n" + " -cleanCsvOut : write a standard format CSV after event stripping with metadata stripped out.\n" + " Not available in bulk mode.\n" + " -noSmooth : disable smoothing on all graphs\n" + " -listSummaryTables: lists available summary tables from the current report XML\n" + " -dumpVariables | -dumpVariablesAll : dumps variables to the log for each CSV (all includes metadata)\n" + " -dumpVariablesToJson | -dumpAllVariablesToJson : path (usually a json filename) to write variables (like budgets) to json (all includes metadata)\n" + " -metadataProxy : when dumping variables can be used in place of a csv, proxy data can be specified in a csvMetadataProxies section in report XML\n" + " -overrideMetadata : when dumping variables use to specify/override metadata: -overrideMetadata platform=abc,targetframerate=60\n" + "\n" + "Performance args:\n" + " -perfLog : output performance logging information\n" + " -graphThreads : use with -batchedGraphs to control the number of threads per CsvToSVG instance \n" + " (default: PC core count/2)\n" + " -csvToSvgSequential : Run CsvToSvg sequentially\n" + " -useEmbeddedGraphUrl : Insert a script to fetch the graph from the specified endpoint on page load rather than embedding the full graph\n" + " -embeddedGraphUrlRoot : The url to fetch the graph from if -useEmbeddedGraphUrl is specified. CsvToSvg graph args are provided as get params\n" + "Deprecated performance args:\n" + " -csvToSvgProcesses : Use separate processes for csvToSVG instead of threads (slower)\n" + " -embedGraphCommandline : if -csvToSvgProcesses is specified, embeds the commandline for debugging purposes\n" + " -noBatchedGraphs : disable batched/multithreaded graph generation (use with -csvToSvgProcesses. Default is enabled)\n" + "\n" + "Options to truncate or filter source data:\n" + "Warning: these options disable Summary Table caching\n" + " -minx \n" + " -maxx \n" + " -beginEvent : strip data before this event\n" + " -endEvent : strip data after this event\n" + " -noStripEvents : if specified, don't strip out samples between excluded events from the stats\n" + "\n" + "Optional bulk mode args: (use with -csvdir, -summaryTableCacheIn, -csvList, -prcList)\n" + " -recurse \n" + " -searchpattern , e.g -searchpattern csvprofile*\n" + " -customTable - sets or overrides the summary table metrics filter\n" + " -customTableSort - overrides the summary table row sort\n" + " -noDetailedReports : skips individual report generation\n" + " -noReports : skips generating reports\n" + " -collateTable : writes a collated table in addition to the main one, merging by row sort\n" + " -collateTableOnly : as -collateTable, but doesn't write the standard summary table.\n" + " -emailTable : writes a condensed email-friendly table (see the 'condensed' summary table)\n" + " -csvTable : writes the summary table in CSV format instead of html\n" + " -summaryTableXML \n" + " -summaryTable :\n" + " Selects a custom summary table type from the list in reportTypes.xml \n" + " (if not specified, 'default' will be used)\n" + " -condensedSummaryTable :\n" + " Selects a custom condensed summary table type from the list in reportTypes.xml \n" + " (if not specified, 'condensed' will be used)\n" + " -summaryTableFilename : use the specified filename for the summary table (instead of SummaryTable.html)\n" + " -metadataFilter or : filters based on CSV metadata,\n" + " e.g \"platform=ps4 AND deviceprofile=ps4_60\" \n" + " -readAllStats : allows any CSV stat avg to appear in the summary table, not just those referenced in summaries\n" + " -showHiddenStats : shows stats which have been automatically hidden (typically duplicate csv unit stats)\n" + " -spreadsheetfriendly: outputs a single quote before non-numeric entries in summary tables\n" + " -noSummaryMinMax: don't make min/max columns for each stat in a condensed summary\n" + " -reverseTable [0|1]: Reverses the order of summary tables (set 0 to force off)\n" + " -sortTrailingDigitsAsNumeric : detects trailing digits and pads them when sorting summary table\n"+ " -scrollableTable [0|1]: makes the summary table scrollable, with frozen first rows and columns (set 0 to force off)\n" + " -colorizeTable : selects the table colorization mode. If omitted, uses the default in the summary\n" + " xml table if set.\n" + " -maxSummaryTableStringLength : strings longer than this will get truncated\n" + " -allowDuplicateCSVs : doesn't remove duplicate CSVs (Note: can cause summary table cache file locking issues)\n" + " -requireMetadata : ignores CSVs without metadata\n" + " -listFiles : just list all files that pass the metadata query. Don't generate any reports.\n" + " -reportLinkRootPath : Make report links relative to this\n" + " -csvLinkRootPath : Make CSV file links relative to this\n" + " -linkTemplates : insert templates in place of relative links that can be replaced later\n" + " e.g {{LinkTemplate:Report:}}\n" + " -weightByColumn : weight collated table averages by this column (overrides value specified in the report XML)\n" + " -noWeightedAvg : Don't use weighted averages for the collated table\n" + " -minFrameCount : ignore CSVs without at least this number of valid frames\n" + " -maxFileAgeDays : max file age in days. CSV or PRC files older than this will be ignored\n" + " -summaryTableStatThreshold : stat/metric columns in the summarytable will be filtered out if all values are\n" + " less than the threshold\n" + " -summaryTableXmlSubst =;=... : replace summarytable XML row and filter entries\n" + " Note: one to many is supported (using , separators)\n" + " -summaryTableXmlAppend : append these stats to the summary table's filter list\n" + " -summaryTableXmlRowSortAppend : append these stats to the summary table's row sort list\n" + " -transposeTable : write the summary tables transposed\n" + " -transposeCollatedTable : write the collated summary table transposed (disables min/max columns)\n" + " -externalMetadataSources : specify paths for external metadata key=value text files\n" + " these resolve from existing metadata to form the final path. e.g D:\\ExtMeta\\{{platform}}\\{{buildversion}}.txt\n" + " -collatedStringVisibility : sets visibility of strings in collated summary tables (default=auto)\n" + " -collatedDateVisibility : sets visibility of dates in collated summary tables (default=newest)\n" + " -columnSortMode : sorting mode for columns\n" + " -summaryTableJsData : embeds javascript row and column data in the "); // CSS htmlFile.WriteLine(" "); htmlFile.WriteLine(" "); htmlFile.WriteLine(" "); htmlFile.WriteLine("

" + titleStr + "

"); // show the range if (minX > 0 || maxX < Int32.MaxValue) { htmlFile.WriteLine("

(CSV cropped to range " + minX + "-"); if (maxX < Int32.MaxValue) { htmlFile.WriteLine(maxX); } htmlFile.WriteLine(")"); } // Output the metadata table htmlFile.WriteLine(""); if (reportTypeInfo.metadataToShowList != null) { Dictionary displayNameMapping = reportXML.GetDisplayNameMapping(); foreach (string metadataStr in reportTypeInfo.metadataToShowList) { string value = csvStats.metaData.GetValue(metadataStr, null); if (value != null) { string friendlyName = metadataStr; if (displayNameMapping.ContainsKey(metadataStr.ToLower())) { friendlyName = displayNameMapping[metadataStr]; } htmlFile.WriteLine(""); } } } htmlFile.WriteLine(""); htmlFile.WriteLine("
" + friendlyName + "" + value + "
Frame count" + csvStats.SampleCount + " (" + numFramesStripped + " excluded)
"); // Output the top level nav htmlFile.WriteLine("
"); htmlFile.WriteLine("
"); htmlFile.WriteLine(""); htmlFile.WriteLine(""); htmlFile.WriteLine(""); htmlFile.WriteLine("
"); } if (summaryRowData != null) { summaryRowData.Add(SummaryTableElement.Type.ToolMetadata, "framecount", csvStats.SampleCount.ToString()); if (numFramesStripped > 0) { summaryRowData.Add(SummaryTableElement.Type.ToolMetadata, "framecountExcluded", numFramesStripped.ToString()); } } bool bWriteSummaryCsv = GetBoolArg("writeSummaryCsv") && !bBulkMode; List summaries = new List(reportTypeInfo.summaries); bool bExtraLinksSummary = GetBoolArg("extraLinksSummary"); if (bExtraLinksSummary) { bool bLinkTemplates = GetBoolArg("linkTemplates"); summaries.Insert(0, new ExtraLinksSummary(null, reportTypeInfo.vars, null, bLinkTemplates)); } // If the reporttype has summary info, then write out the summary] foreach (Summary summary in summaries) { bool bWriteHtml = htmlFile != null && !summary.bHideInDetailedReport; HtmlSection htmlSection = summary.WriteSummaryData(bWriteHtml, summary.useUnstrippedCsvStats ? csvStatsUnstripped : csvStats, csvStatsUnstripped, bWriteSummaryCsv, summaryRowData, htmlFilename); if (htmlSection != null) { htmlSection.WriteToFile(htmlFile); } } if (htmlFile != null) { // Output the list of graphs htmlFile.WriteLine("
"); htmlFile.WriteLine("

Graphs

"); // TODO: support sections for graphs List sections = new List(); //// We have to at least have the empty string in this array so that we can print the list of links. if (sections.Count() == 0) { sections.Add(""); } for (int index = 0; index < sections.Count; index++) { htmlFile.WriteLine("
    "); string currentCategory = sections[index]; if (currentCategory.Length > 0) { htmlFile.WriteLine("

    " + currentCategory + " Graphs

    "); } foreach (CsvSvgInfo csvSvgInfo in csvSvgInfoList) { string svgTitle = csvSvgInfo.Graph.title; // TODO: Check if this graph belongs in this section. htmlFile.WriteLine("
  • " + svgTitle + "
  • "); } htmlFile.WriteLine("
"); } htmlFile.WriteLine("Back to top \u2191"); // Output the Graphs for (int svgFileIndex = 0; svgFileIndex < csvSvgInfoList.Count; svgFileIndex++) { CsvSvgInfo csvSvgInfo = csvSvgInfoList[svgFileIndex]; ReportGraph graph = csvSvgInfo.Graph; string svgTitle = graph.title; HtmlSection htmlSection = new HtmlSection(svgTitle, false, StripSpaces(svgTitle), 3); if (csvSvgInfo.Format == CsvSvgInfo.GraphFormat.Inline) { string[] svgLines = ReadLinesFromFile(csvSvgInfo.SvgFilename); foreach (string line in svgLines) { string modLine = line.Replace("__MAKEUNIQUE__", "U_" + svgFileIndex.ToString()); htmlSection.WriteLine(modLine); } } else if (csvSvgInfo.Format == CsvSvgInfo.GraphFormat.Url) { string graphArgs = GetCsvToSvgArgs(null, null, graph, 1.0, minX, maxX, false, svgFileIndex, CsvToSvgArgFormat.Url); string csvId = csvStats.metaData?.GetValue("csvid", null); if (csvId == null) { throw new Exception("Failed to generate embeddedGraphUrl since no valid csvId was found."); } string graphUrlRoot = GetArg("embeddedGraphUrlRoot", mandatory: true); string graphUrl = $"{graphUrlRoot}?csvs={csvId}&{graphArgs}"; string script = $"
\n"; script += $""; htmlSection.WriteLine(script); string expandedGraphUrl = graphUrl + "&tohtml=true"; htmlSection.WriteLine($"View "); } else { throw new Exception("Unsupported graph output format."); } htmlSection.WriteToFile(htmlFile); } htmlFile.WriteLine("Back to top \u2191"); if (GetBoolArg("noWatermarks")) { htmlFile.WriteLine("

Created with PerfReportTool

"); } else { htmlFile.WriteLine("

Created with PerfReportTool " + Version.Get() + "

"); } htmlFile.WriteLine(" "); htmlFile.WriteLine(""); htmlFile.Close(); string ForEmail = GetArg("foremail", false); if (ForEmail != "") { WriteEmail(htmlFilename, title, csvSvgInfoList, reportTypeInfo, csvStats, csvStatsUnstripped, minX, maxX, bBulkMode); } } } void WriteEmail(string htmlFilename, string title, List csvSvgInfoList, ReportTypeInfo reportTypeInfo, CsvStats csvStats, CsvStats csvStatsUnstripped, int minX, int maxX, bool bBulkMode) { if (htmlFilename == null) { return; } ReportGraph[] graphs = reportTypeInfo.graphs.ToArray(); string titleStr = reportTypeInfo.title + " : " + title; System.IO.StreamWriter htmlFile; htmlFile = new System.IO.StreamWriter(htmlFilename + "email"); htmlFile.WriteLine(""); htmlFile.WriteLine(" "); htmlFile.WriteLine(" "); if (GetBoolArg("noWatermarks")) { htmlFile.WriteLine(" "); htmlFile.WriteLine(" " + titleStr + ""); htmlFile.WriteLine(" "); htmlFile.WriteLine(" "); htmlFile.WriteLine("

" + titleStr + "

"); // show the range if (minX > 0 || maxX < Int32.MaxValue) { htmlFile.WriteLine("

(CSV cropped to range " + minX + "-"); if (maxX < Int32.MaxValue) { htmlFile.WriteLine(maxX); } htmlFile.WriteLine(")"); } htmlFile.WriteLine("Click here for Report w/ interactive SVGs."); htmlFile.WriteLine("

Summary

"); htmlFile.WriteLine("Overall Runtime: [Replace Me With Runtime]"); bool bWriteSummaryCsv = GetBoolArg("writeSummaryCsv") && !bBulkMode; // If the reporttype has summary info, then write out the summary] foreach (Summary summary in reportTypeInfo.summaries) { bool bWriteHtml = htmlFile != null && !summary.bHideInDetailedReport; HtmlSection htmlSection = summary.WriteSummaryData(bWriteHtml, csvStats, csvStatsUnstripped, bWriteSummaryCsv, null, htmlFilename); if (htmlSection != null) { htmlSection.WriteToFile(htmlFile); } } htmlFile.WriteLine("
"); htmlFile.WriteLine(""); htmlFile.Close(); } string StripSpaces(string str) { return str.Replace(" ", ""); } string GetTempFilename(string csvFilename) { string shortFileName = MakeShortFilename(csvFilename).Replace(" ", "_"); return Path.Combine(Path.GetTempPath(), shortFileName + "_" + Guid.NewGuid().ToString().Substring(26)); } enum CsvToSvgArgFormat { CommandLine, Url } string GetCsvToSvgArgs(string csvFilename, string svgFilename, ReportGraph graph, double thicknessMultiplier, int minx, int maxx, bool multipleCSVs, int graphIndex, CsvToSvgArgFormat argFormat, float scaleby = 1.0f) { string title = graph.title; GraphSettings graphSettings = graph.settings; float maxy = GetFloatArg("maxY", (float)graphSettings.maxY.value); bool smooth = graphSettings.smooth.value && !GetBoolArg("nosmooth"); double smoothKernelPercent = graphSettings.smoothKernelPercent.value; double smoothKernelSize = graphSettings.smoothKernelSize.value; double compression = graphSettings.compression.value; int width = graphSettings.width.value; int height = graphSettings.height.value; bool stacked = graphSettings.stacked.value; bool showAverages = graphSettings.showAverages.value; bool filterOutZeros = graphSettings.filterOutZeros.value; bool snapToPeaks = false; if (graphSettings.snapToPeaks.isSet) { snapToPeaks = graphSettings.snapToPeaks.value; } int lineDecimalPlaces = graphSettings.lineDecimalPlaces.isSet ? graphSettings.lineDecimalPlaces.value : 1; int maxHierarchyDepth = graphSettings.maxHierarchyDepth.value; string hideStatPrefix = graphSettings.hideStatPrefix.value; string showEvents = graphSettings.showEvents.value; double statMultiplier = graphSettings.statMultiplier.isSet ? graphSettings.statMultiplier.value : 1.0; bool hideEventNames = false; if (multipleCSVs) { showEvents = "CSV:*"; hideEventNames = true; } bool interactive = true; string highlightEventRegions = ""; if (!GetBoolArg("noStripEvents")) { List eventsToStrip = reportXML.GetCsvEventsToStrip(); if (eventsToStrip != null) { highlightEventRegions += "\""; for (int i = 0; i < eventsToStrip.Count; i++) { if (i > 0) { highlightEventRegions += ";"; } string endEvent = (eventsToStrip[i].endName == null) ? "{null}" : eventsToStrip[i].endName; string beginEvent = (eventsToStrip[i].beginName == null) ? "{null}" : eventsToStrip[i].beginName; highlightEventRegions += beginEvent + ";" + endEvent; } highlightEventRegions += "\""; } } Optional minFilterStatValueSetting = graph.minFilterStatValue.isSet ? graph.minFilterStatValue : graphSettings.minFilterStatValue; string Quote(string s) { return "\"" + s + "\""; } Dictionary args = new(); void AddOptionalArg(string name, Optional value) { if (value.isSet) { args[name] = value.value.ToString(); } } void AddConditionalArg(string name, bool condition, T value) { if (condition) { args[name] = value.ToString(); } } void AddConditionalFlag(string name, bool condition) { if (condition) { args[name] = ""; } } args["title"] = Quote(title); AddConditionalArg("width", width > 0, (width * scaleby)); AddConditionalArg("height", height > 0, (height * scaleby)); AddOptionalArg("budget", graph.budget); AddConditionalArg("maxy", maxy > 0, maxy); args["uniqueID"] = "Graph_" + graphIndex.ToString(); args["lineDecimalPlaces"] = lineDecimalPlaces.ToString(); AddConditionalFlag("nocommandlineEmbed", GetBoolArg("embedGraphCommandline")); AddConditionalArg("statMultiplier", statMultiplier != 1.0, statMultiplier.ToString("0.0000000000000000000000")); AddConditionalArg("hideeventNames", hideEventNames, 1); AddConditionalArg("minx", minx > 0, minx); AddConditionalArg("maxx", maxx != Int32.MaxValue, maxx); AddOptionalArg("miny", graphSettings.minY); AddOptionalArg("maxAutoMaxY", graphSettings.maxAutoMaxY); AddOptionalArg("threshold", graphSettings.threshold); AddOptionalArg("averageThreshold", graphSettings.averageThreshold); AddOptionalArg("minFilterStatValue", minFilterStatValueSetting); AddConditionalArg("minFilterStatName", graphSettings.minFilterStatName.isSet, graphSettings.minFilterStatName.value); AddConditionalArg("compression", compression > 0.0, compression); AddConditionalArg("thickness", graphSettings.thickness.isSet, graphSettings.thickness.value * thicknessMultiplier); // Smoothing AddConditionalFlag("smooth", smooth); AddConditionalArg("smoothKernelPercent", smooth && smoothKernelPercent >= 0.0f, smoothKernelPercent); AddConditionalArg("smoothKernelSize", smooth && smoothKernelSize >= 0.0f, smoothKernelSize); AddConditionalFlag("interactive", interactive); AddConditionalFlag("stacked", stacked); AddConditionalFlag("forceLegendSort", stacked); // based on the stacked flag AddConditionalFlag("showAverages", showAverages); AddConditionalFlag("nosnap", !snapToPeaks); AddConditionalFlag("filterOutZeros", filterOutZeros); AddConditionalArg("maxHierarchyDepth", maxHierarchyDepth > 0, maxHierarchyDepth); AddConditionalArg("hideStatPrefix", hideStatPrefix.Length > 0, hideStatPrefix); AddConditionalArg("stacktotalstat", graphSettings.mainStat.isSet, graphSettings.mainStat.value); AddOptionalArg("legendAverageThreshold", graphSettings.legendAverageThreshold); AddConditionalArg("ignoreStats", graphSettings.ignoreStats.isSet, graphSettings.ignoreStats.value); AddConditionalArg("startEvent", graphSettings.startEvent.isSet, graphSettings.startEvent.value); AddConditionalArg("startEventOffset", graphSettings.startEventOffset.isSet, graphSettings.startEventOffset.value); AddConditionalArg("endEvent", graphSettings.endEvent.isSet, graphSettings.endEvent.value); AddConditionalArg("endEventOffset", graphSettings.endEventOffset.isSet, graphSettings.endEventOffset.value); List statList = CsvStats.SplitStringListWithBracketGroups(graphSettings.statString.value,','); string argString = string.Empty; if (argFormat == CsvToSvgArgFormat.Url) { string FormatStringList(string inStringList, string splitStr) { string[] tokens = inStringList .Split(splitStr) .Select(token => token.Trim()) .ToArray(); return String.Join(";", tokens); } // Exclude csvs from the args as that is a separate argument that the caller must setup. // Any multi-value args need to be delimited with a semi-colon. // Derived csv stat definitions can include equals, so encode it args["stats"] = String.Join(";", statList).Replace("=", "%3D"); AddConditionalArg("showevents", showEvents.Length > 0, FormatStringList(showEvents, " ")); AddConditionalArg("highlightEventRegions", highlightEventRegions.Length > 0, FormatStringList(highlightEventRegions, ",")); List argList = args.Select(a => string.IsNullOrEmpty(a.Value) // Empty value means it's a flag ? $"{a.Key}=true" : $"{a.Key}={a.Value.Replace("\"", "")}") // Strip quotes .ToList(); argString = string.Join("&", argList); } else { args["csvs"] = Quote(csvFilename); args["o"] = Quote(svgFilename); AddConditionalArg("showevents", showEvents.Length > 0, showEvents); AddConditionalArg("highlightEventRegions", highlightEventRegions.Length > 0, highlightEventRegions); IEnumerable quoteWrappedStatStrings = statList.Select(token => '"' + token + '"'); args["stats"] = String.Join(" ", quoteWrappedStatStrings); List argList = args.Select(a => string.IsNullOrEmpty(a.Value) ? $"-{a.Key}" : $"-{a.Key} {a.Value}") .ToList(); argString = string.Join(" ", argList); } return argString; } GraphParams GetCsvToSvgGraphParams(ReportGraph graph, double thicknessMultiplier, int minx, int maxx, bool multipleCSVs, int graphIndex, float scaleby = 1.0f) { GraphParams graphParams = new GraphParams(); graphParams.title = graph.title; GraphSettings graphSettings = graph.settings; graphParams.statNames.AddRange(CsvStats.SplitStringListWithBracketGroups(graphSettings.statString.value,',')); graphParams.lineThickness = (float)(graphSettings.thickness.value * thicknessMultiplier); graphParams.smooth = graphSettings.smooth.value && !GetBoolArg("nosmooth"); if (graphParams.smooth) { if (graphSettings.smoothKernelPercent.isSet && graphSettings.smoothKernelPercent.value > 0) { graphParams.smoothKernelPercent = (float)graphSettings.smoothKernelPercent.value; } if (graphSettings.smoothKernelSize.isSet && graphSettings.smoothKernelSize.value > 0) { graphParams.smoothKernelSize = (int)(graphSettings.smoothKernelSize.value); } } if (graphSettings.compression.isSet) { graphParams.compression = (float)graphSettings.compression.value; } graphParams.width = (int)(graphSettings.width.value * scaleby); graphParams.height = (int)(graphSettings.height.value * scaleby); if (graphSettings.stacked.isSet) { graphParams.stacked = graphSettings.stacked.value; if (graphParams.stacked) { graphParams.forceLegendSort = true; if (graphSettings.mainStat.isSet) { graphParams.stackTotalStat = graphSettings.mainStat.value; } } } if (graphSettings.showAverages.isSet) { graphParams.showAverages = graphSettings.showAverages.value; } if (graphSettings.filterOutZeros.isSet) { graphParams.filterOutZeros = graphSettings.filterOutZeros.value; } graphParams.snapToPeaks = false; if (graphSettings.snapToPeaks.isSet) { graphParams.snapToPeaks = graphSettings.snapToPeaks.value; } graphParams.lineDecimalPlaces = graphSettings.lineDecimalPlaces.isSet ? graphSettings.lineDecimalPlaces.value : 1; if (graphSettings.maxHierarchyDepth.isSet) { graphParams.maxHierarchyDepth = graphSettings.maxHierarchyDepth.value; } if (graphSettings.hideStatPrefix.isSet && graphSettings.hideStatPrefix.value.Length > 0) { graphParams.hideStatPrefixes.AddRange(graphSettings.hideStatPrefix.value.Split(' ', ';')); } if (multipleCSVs) { graphParams.showEventNames.Add("CSV:*"); graphParams.showEventNameTextMode = ShowEventTextMode.Hide; } else { if (graphSettings.showEvents.isSet && graphSettings.showEvents.value.Length > 0) { graphParams.showEventNames.AddRange(graphSettings.showEvents.value.Split(' ', ';')); } } if (graphSettings.statMultiplier.isSet) { graphParams.statMultiplier = (float)graphSettings.statMultiplier.value; } if (graphSettings.startEvent.isSet) { graphParams.startEvent = graphSettings.startEvent.value; if (graphSettings.startEventOffset.isSet) { graphParams.startEventOffset = graphSettings.startEventOffset.value; } } if (graphSettings.endEvent.isSet) { graphParams.endEvent = graphSettings.endEvent.value; if (graphSettings.endEventOffset.isSet) { graphParams.endEventOffset = graphSettings.endEventOffset.value; } } graphParams.interactive = true; if (!GetBoolArg("noStripEvents")) { List eventsToStrip = reportXML.GetCsvEventsToStrip(); if (eventsToStrip != null) { for (int i = 0; i < eventsToStrip.Count; i++) { graphParams.highlightEventRegions.Add((eventsToStrip[i].beginName == null) ? "{null}" : eventsToStrip[i].beginName); graphParams.highlightEventRegions.Add((eventsToStrip[i].endName == null) ? "{null}" : eventsToStrip[i].endName); } } } if (graph.minFilterStatValue.isSet) { graphParams.minFilterStatValue = (float)graph.minFilterStatValue.value; } if (graphSettings.minFilterStatName.isSet) { graphParams.minFilterStatName = graphSettings.minFilterStatName.value; } if (graph.budget.isSet) { graphParams.budget = (float)graph.budget.value; } graphParams.uniqueId = "Graph_" + graphIndex.ToString(); if (minx > 0) { graphParams.minX = minx; } if (maxx != Int32.MaxValue) { graphParams.maxX = maxx; } if (graphSettings.minY.isSet) { graphParams.minY = (float)graphSettings.minY.value; } graphParams.maxY = GetFloatArg("maxY", (float)graphSettings.maxY.value); if (graphSettings.maxAutoMaxY.isSet) { graphParams.maxAutoMaxY = (float)graphSettings.maxAutoMaxY.value; } if (graphSettings.threshold.isSet) { graphParams.threshold = (float)graphSettings.threshold.value; } if (graphSettings.averageThreshold.isSet) { graphParams.averageThreshold = (float)graphSettings.averageThreshold.value; } if (graphSettings.legendAverageThreshold.isSet) { graphParams.legendAverageThreshold = (float)graphSettings.legendAverageThreshold.value; } if (graphSettings.ignoreStats.isSet && graphSettings.ignoreStats.value.Length > 0) { graphParams.ignoreStats.AddRange(graphSettings.ignoreStats.value.Split(' ', ';')); } return graphParams; } Process LaunchCsvToSvgAsync(string args) { string csvToolPath = Path.Combine(GetBaseDirectory(), "CSVToSVG.exe"); string binary = csvToolPath; if (Host == HostPlatform.Windows) { if (!File.Exists(binary) && binary.ToLower().Contains("\\perfreporttool\\")) { binary = binary.Replace("\\perfreporttool\\", "\\CsvToSVG\\", StringComparison.InvariantCultureIgnoreCase); } } else { throw new NotImplementedException("-CsvToSvgProcesses is deprecated and only supported on Windows"); } // Generate the SVGs, multithreaded ProcessStartInfo startInfo = new ProcessStartInfo(binary); startInfo.Arguments = args; startInfo.CreateNoWindow = true; startInfo.UseShellExecute = false; Process process = Process.Start(startInfo); return process; } int CountCSVs(CsvStats csvStats) { // Count the CSVs int csvCount = 0; foreach (CsvEvent ev in csvStats.Events) { string eventName = ev.Name; if (eventName.Length > 0) { if (eventName.Contains("CSV:") && eventName.ToLower().Contains(".csv")) { csvCount++; } } } if (csvCount == 0) { csvCount = 1; } return csvCount; } static int Main(string[] args) { Program program = new Program(); if (Debugger.IsAttached) { program.Run(args); } else { try { program.Run(args); } catch (System.Exception e) { Console.Error.WriteLine("[ERROR] " + e.Message); return 1; } } return 0; } bool matchesPattern(string str, string pattern) { string[] patternSections = pattern.ToLower().Split('*'); // Check the substrings appear in order string remStr = str.ToLower(); for (int i = 0; i < patternSections.Length; i++) { int idx = remStr.IndexOf(patternSections[i]); if (idx == -1) { return false; } remStr = remStr.Substring(idx + patternSections[i].Length); } return remStr.Length == 0; } System.IO.FileInfo[] GetFilesWithSearchPattern(string directory, string searchPatternStr, bool recurse, int maxFileAgeDays = -1) { List fileList = new List(); string[] searchPatterns = searchPatternStr.Split(';'); DirectoryInfo di = new DirectoryInfo(directory); foreach (string searchPattern in searchPatterns) { System.IO.FileInfo[] files = di.GetFiles("*.*", recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); foreach (FileInfo file in files) { if (maxFileAgeDays >= 0) { DateTime fileModifiedTime = file.LastWriteTimeUtc; DateTime currentTime = DateTime.UtcNow; TimeSpan elapsed = currentTime.Subtract(fileModifiedTime); if (elapsed.TotalHours > (double)maxFileAgeDays * 24.0) { continue; } } if (matchesPattern(file.FullName, searchPattern)) { fileList.Add(file); } } } return fileList.Distinct().ToArray(); } void ConvertJsonToPrcs(string jsonFilename, string prcOutputDir) { Console.WriteLine("Converting " + jsonFilename + " to PRCs. Output folder: " + prcOutputDir); if (!Directory.Exists(prcOutputDir)) { Directory.CreateDirectory(prcOutputDir); } Console.WriteLine("Reading "+jsonFilename); string jsonText = File.ReadAllText(jsonFilename); Console.WriteLine("Parsing json"); Dictionary jsonDict = JsonToDynamicDict(jsonText); Console.WriteLine("Writing PRCs"); foreach (string csvId in jsonDict.Keys) { Dictionary srcDict = jsonDict[csvId]; SummaryTableRowData rowData = new SummaryTableRowData(srcDict); rowData.WriteToCache(prcOutputDir, csvId); } } Dictionary JsonToDynamicDict(string jsonStr) { JsonElement RootElement = JsonSerializer.Deserialize((string)jsonStr); Dictionary RootElementValue = GetJsonValue(RootElement); return RootElementValue; } // .net Json support is poor, so we have to do stuff like this if we just want to read a json file to a dictionary dynamic GetJsonValue(JsonElement jsonElement) { string jsonStr = jsonElement.GetRawText(); switch (jsonElement.ValueKind) { case JsonValueKind.Number: return jsonElement.GetDouble(); case JsonValueKind.Null: return null; case JsonValueKind.True: return true; case JsonValueKind.False: return false; case JsonValueKind.String: return jsonElement.GetString(); case JsonValueKind.Undefined: return null; case JsonValueKind.Array: List ArrayValue = new List(); foreach( JsonElement element in jsonElement.EnumerateArray()) { ArrayValue.Add(GetJsonValue(element)); } return ArrayValue; case JsonValueKind.Object: Dictionary DictValue = new Dictionary(); foreach ( JsonProperty property in jsonElement.EnumerateObject() ) { DictValue[property.Name] = GetJsonValue(property.Value); } return DictValue; } return null; } } }