Files
UnrealEngineUWP/Engine/Source/Programs/CSVTools/PerfReportTool/PerfReportTool.cs
tyler staples 48925f6220 PerfReportTool - Added an "unweighted_average" formula to BoundedStatValueSummaries as weighted averages can sometimes be incorrect.
BoundedStatValueSummaries weights the stat averages by the frametimes. Normally it's not an issue to use the same value for both the value and the weight as they'll end up canceling each other out resulting in an unweighted average. However, in the case where some values are much larger than others this can skew the value resulting in a value much different from the weighted average. This is the case for server frametimes as there can be a few very large spikes that end up skewing the weights.

Since it's not trivial to detect which stats are actually "frametime" stats as they can go by different names, the simplest solution is adding a new "unweighted_average" formula. This also allows using it in other cases where it might make more sense, but these should be very rare.

#rnx
#rb andrew.ladenberger
[FYI] ben.woodhouse

[CL 26644283 by tyler staples in ue5-main branch]
2023-07-27 12:35:32 -04:00

2057 lines
72 KiB
C#

// 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.Xml.Linq;
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 System.Globalization;
using CSVTools;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace PerfReportTool
{
class Version
{
// Format: Major.Minor.Bugfix
private static string VersionString = "4.107.0";
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 <filename> or -csvdir <directory path> or -summaryTableCacheIn <directory path> or\n" +
" -csvList <comma separated> or -prcList <comma separated>\n" +
" -o <dir name>: output directory (will be created if necessary)\n" +
"\n" +
"Optional Args:\n" +
" -reportType <e.g. flythrough, playthrough, playthroughmemory>\n" +
" -reportTypeCompatCheck : do a compatibility if when specifying a report type (rather than forcing)\n" +
" -graphXML <xmlfilename>\n" +
" -reportXML <xmlfilename>\n" +
" -reportxmlbasedir <folder>\n" +
" -title <name> - title for detailed reports\n" +
" -summaryTitle <name> - title for summary tables\n" +
" -maxy <value> - 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 <filename> : 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" +
"\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" +
"Deprecated performance args:\n" +
" -csvToSvgProcesses : Use separate processes for csvToSVG instead of threads (slower)\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 <frameNumber>\n" +
" -maxx <frameNumber>\n" +
" -beginEvent <event> : strip data before this event\n" +
" -endEvent <event> : 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 <pattern>, e.g -searchpattern csvprofile*\n" +
" -customTable <comma separated fields>\n" +
" -customTableSort <comma separated field row sort order> (use with -customTable)\n" +
" -noDetailedReports : skips individual report generation\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 <XML filename>\n" +
" -summaryTable <name> :\n" +
" Selects a custom summary table type from the list in reportTypes.xml \n" +
" (if not specified, 'default' will be used)\n" +
" -condensedSummaryTable <name> :\n" +
" Selects a custom condensed summary table type from the list in reportTypes.xml \n" +
" (if not specified, 'condensed' will be used)\n" +
" -summaryTableFilename <name> : use the specified filename for the summary table (instead of SummaryTable.html)\n" +
" -metadataFilter <query> or <key0=value0,key1=value1...>: 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" +
" -externalGraphs : enables external graphs (off by default)\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" +
" -scrollableTable [0|1]: makes the summary table scrollable, with frozen first rows and columns (set 0 to force off)\n" +
" -colorizeTable [off|budget|auto]: selects the table colorization mode. If omitted, uses the default in the summary\n" +
" xml table if set.\n" +
" -maxSummaryTableStringLength <n>: 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 <path> : Make report links relative to this\n" +
" -csvLinkRootPath <path> : 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:<CSV ID>}}\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 <n> : ignore CSVs without at least this number of valid frames\n" +
" -maxFileAgeDays <n> : max file age in days. CSV or PRC files older than this will be ignored\n" +
" -summaryTableStatThreshold <n> : stat/metric columns in the summarytable will be filtered out if all values are\n" +
" less than the threshold\n" +
" -summaryTableXmlSubst <find1>=<replace1>,<find2>=<replace2>... : replace summarytable XML row and filter entries\n" +
" -summaryTableXmlAppend <list,of,stats> : append these stats to the summary table's filter list\n" +
" -summaryTableXmlRowSortAppend <list,of,stats> : 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" +
"Diff rows\n" +
" -addDiffRows : adds diff rows after the first two rows\n" +
" -sortColumnsByDiff : sorts columns by the max of its diff scores (use with -addDiffRows).\n" +
" Notes: Diff score corresponds to the value of a column's diff row; the sign is reversed if LowIsBad\n" +
" Stats are sorted by prefixes first, e.g GPU/\n" +
" -columnDiffDisplayThreshold <value> : if specified, hides columns with max diff value below this threshold\n"+
"\n" +
"Optional Column Filters\n" +
" -debugShowFilteredColumns : grays out filtered columns instead of removing. Column tooltip will show filtered reason.\n" +
" -hideMetadataColumns : filters out metadata columns from the table (excluding those used in row sort).\n" +
"\n" +
"Regression Column Filtering\n" +
" -onlyShowRegressedColumns : enables regression filtering. Only shows columns where the most recent row group\n" +
" (see -regressionJoinRowsByName) is outside the given stddiv threshold from the mean of the previous rows.\n" +
" -regressionJoinRowsByName <statName> : a stat name to join rows by for aggregation. (default: no aggregation)\n"+
" -regressionStdDevThreshold <n> (default = 2) : the stddiv threshold for filtering \n"+
" -regressionOutlierStdDevThreshold <n> (default = 4) : stddiv threshold for outliers (these are ignored)\n"+
"\n" +
"Json serialization:\n" +
" -summaryTableToJson <path> : path (usually a json filename) to write summary table row data to\n" +
" -summaryTableToJsonSeparateFiles : writes separate files. -summaryTableToJson specifies the directory name\n" +
" -summaryTableToJsonFastMode : exit after serializing json data (skips making summary tables)\n" +
" -summaryTableToJsonWriteAllElementData : write all element data, including tooltips, flags\n" +
" -summaryTableToJsonMetadataOnly : only write CsvMetadata elements to json\n" +
" -summaryTableToJsonFileStream : use a file stream to write Json. Experimental but can avoid OOMs\n" +
" -summaryTableToJsonNoIndent : don't indent json output files\n" +
" -jsonToPrcs <json filename> : write PRCs. PRC files will be written to -summaryTableCache folder\n" +
"\n" +
"Performance args for bulk mode:\n" +
" -precacheCount <n> : number of CSV files to precache in the lookahead cache (0 for no precache)\n" +
" -precacheThreads <n> : number of threads to use for the CSV lookahead cache (default 8)\n" +
" -summaryTableCache <dir> : specifies a directory for summary table data to be cached.\n" +
" This avoids processing csvs on subsequent runs when -noDetailedReports is specified\n" +
" Note: Enables -readAllStats implicitly. \n" +
" -summaryTableCacheInvalidate : regenerates summary table disk cache entries (ie write only)\n" +
" -summaryTableCacheReadOnly : only read from the cache, never write\n" +
" -summaryTableCachePurgeInvalid : Purges invalid PRCs from the cache folder\n" +
" -summaryTableCacheIn <dir> : reads data directly from the summary table cache instead of from CSVs\n" +
" -summaryTableCacheUseOnlyCsvID : only use the CSV ID for the summary table cacheID, ignoringthe report type hash\n" +
" Use this if you want to avoid cache data being invalidated by report changes\n" +
" -noCsvCacheFiles: disables usage of .csv.cache files. Cache files can be much faster if filtering on metadata\n" +
"";
/*
"Note on custom tables:\n" +
" The -customTable and -customTableSort args allow you to generate a custom summary table\n" +
" This is an alternative to using preset summary tables (see -summarytable)\n" +
" Example:\n"+
" -customTableSort \"deviceprofile,buildversion\" -customTable \"deviceprofile,buildversion,memoryfreeMB*\" \n"+
" This outputs a table containing deviceprofile, buildversion and memoryfree stats, sorted by deviceprofile and then buildversion\n" +
""
*/
Dictionary<string, string> statDisplaynameMapping;
ReportXML reportXML;
string GetBaseDirectory()
{
string location = System.Reflection.Assembly.GetEntryAssembly().Location.ToLower();
string baseDirectory = location;
baseDirectory = baseDirectory.Replace("perfreporttool.exe", "");
string debugSubDir = "\\bin\\debug\\";
if (baseDirectory.ToLower().EndsWith(debugSubDir))
{
// Might be best to use the CSVToSVG from source, but that might not be built, so use the one checked into binaries instead
baseDirectory = baseDirectory.Substring(0, baseDirectory.Length - debugSubDir.Length);
baseDirectory += "\\..\\..\\..\\..\\Binaries\\DotNET\\CsvTools";
}
return baseDirectory;
}
void Run(string[] args)
{
System.Globalization.CultureInfo.DefaultThreadCurrentCulture = System.Globalization.CultureInfo.InvariantCulture;
// Read the command line
if (args.Length < 1)
{
WriteLine("Invalid args");
WriteLine(formatString);
return;
}
WriteLine("PerfReportTool v" + Version.Get());
ReadCommandLine(args);
PerfLog perfLog = new PerfLog(GetBoolArg("perfLog"));
SummaryFactory.Init();
// Handle converting json to PRCs if requested
string jsonToPrcFilename = GetArg("jsonToPrcs", null);
if (jsonToPrcFilename != null)
{
string prcOutputDir = GetArg("summaryTableCache", null);
if (prcOutputDir == null)
{
throw new Exception("-jsonToPRCs requires -summaryTableCache!");
}
ConvertJsonToPrcs(jsonToPrcFilename, prcOutputDir);
return;
}
string csvDir = null;
bool bBulkMode = false;
bool bSummaryTableCacheOnlyMode = false;
// Read CSV filenames from a directory or list
string[] csvFilenames;
if (args.Length == 1)
{
// Simple mode: just pass a csv name
csvFilenames = new string[] { args[0] };
}
else
{
csvDir = GetArg("csvDir");
int maxFileAgeDays = GetIntArg("maxFileAgeDays", -1);
string summaryTableCacheInDir = GetArg("summaryTableCacheIn");
string csvListStr = GetArg("csvList");
string prcListStr = GetArg("prcList");
if (csvDir.Length > 0)
{
bool recurse = GetBoolArg("recurse");
string searchPattern = GetArg("searchPattern", false);
if (searchPattern == "")
{
searchPattern = "*.csv;*.csv.bin";
}
else if (!searchPattern.Contains('.'))
{
searchPattern += ".csv;*.csv.bin";
}
System.IO.FileInfo[] files = GetFilesWithSearchPattern(csvDir, searchPattern, recurse, maxFileAgeDays);
csvFilenames = new string[files.Length];
int i = 0;
foreach (FileInfo csvFile in files)
{
csvFilenames[i] = csvFile.FullName;
i++;
}
// We don't write summary CSVs in bulk mode
bBulkMode = true;
perfLog.LogTiming("DirectoryScan");
}
else if (summaryTableCacheInDir.Length > 0)
{
bool recurse = GetBoolArg("recurse");
System.IO.FileInfo[] files = GetFilesWithSearchPattern(summaryTableCacheInDir, "*.prc", recurse, maxFileAgeDays);
csvFilenames = new string[files.Length];
int i = 0;
foreach (FileInfo csvFile in files)
{
csvFilenames[i] = csvFile.FullName;
i++;
}
bBulkMode = true;
bSummaryTableCacheOnlyMode = true;
perfLog.LogTiming("DirectoryScan");
}
else if (csvListStr.Length > 0)
{
csvFilenames = csvListStr.Split(',');
bBulkMode = true;
}
else if (prcListStr.Length > 0)
{
csvFilenames = prcListStr.Split(',');
bBulkMode = true;
bSummaryTableCacheOnlyMode = true;
}
else
{
string csvFilenamesStr = GetArg("csv");
if (csvFilenamesStr.Length == 0)
{
csvFilenamesStr = GetArg("csvs", true);
if (csvFilenamesStr.Length == 0)
{
if (!GetBoolArg("listSummaryTables"))
{
System.Console.Write(formatString);
return;
}
}
}
csvFilenames = csvFilenamesStr.Split(';');
}
}
// Load the report + graph XML data
reportXML = new ReportXML(
GetArg("graphxml", false),
GetArg("reportxml", false),
GetArg("reportxmlbasedir", false),
GetArg("summaryTableXml", false),
GetArg("summaryTableXmlSubst", false),
GetArg("summaryTableXmlAppend", false),
GetArg("summaryTableXmlRowSortAppend", false)
);
if (GetBoolArg("listSummaryTables"))
{
Console.WriteLine("Listing summary tables:");
List<string> summaryTableNames = reportXML.GetSummaryTableNames();
foreach (string name in summaryTableNames)
{
Console.WriteLine(" " + name);
}
return;
}
statDisplaynameMapping = reportXML.GetDisplayNameMapping();
// If we're outputting row data to json, create the dict
SummaryTableDataJsonWriteHelper summaryTableJsonHelper = null;
string summaryJsonOutPath = GetArg("summaryTableToJson", null);
if (summaryJsonOutPath != null)
{
summaryTableJsonHelper = new SummaryTableDataJsonWriteHelper(summaryJsonOutPath,
GetBoolArg("summaryTableToJsonSeparateFiles"),
GetBoolArg("summaryTableToJsonMetadataOnly"),
GetBoolArg("summaryTableToJsonWriteAllElementData"),
!GetBoolArg("summaryTableToJsonNoIndent"));
}
SummaryTableCacheStats summaryTableCacheStats = new SummaryTableCacheStats();
perfLog.LogTiming("Initialization");
string summaryTableCacheDir = null;
if (bBulkMode)
{
summaryTableCacheDir = GetArg("summaryTableCache", null);
if (summaryTableCacheDir != null)
{
// Check for incompatible options. Could just feed these into the metadata key eventually
string incompatibleOptionsStr = "minx,maxx,beginevent,endevent,noStripEvents";
string[] incompatibleOptionsList = incompatibleOptionsStr.Split(',');
List<string> badOptions = new List<string>();
foreach (string option in incompatibleOptionsList)
{
if (GetArg(option, null) != null)
{
badOptions.Add(option);
}
}
if (badOptions.Count > 0)
{
Console.WriteLine("Warning: Summary Table cache disabled due to incompatible options (" + string.Join(", ", badOptions) + "). See help for details.");
summaryTableCacheDir = null;
}
else
{
Console.WriteLine("Using summary table cache: " + summaryTableCacheDir);
Directory.CreateDirectory(summaryTableCacheDir);
if (GetBoolArg("summaryTableCachePurgeInvalid"))
{
Console.WriteLine("Purging invalid data from the summary table cache.");
DirectoryInfo di = new DirectoryInfo(summaryTableCacheDir);
FileInfo[] files = di.GetFiles("*.prc", SearchOption.TopDirectoryOnly);
int numFilesDeleted = 0;
foreach (FileInfo file in files)
{
if (SummaryTableRowData.TryReadFromCache(summaryTableCacheDir, file.Name.Substring(0, file.Name.Length - 4)) == null)
{
File.Delete(file.FullName);
numFilesDeleted++;
}
}
summaryTableCacheStats.PurgeCount = numFilesDeleted;
Console.WriteLine(numFilesDeleted + " of " + files.Length + " cache entries deleted");
perfLog.LogTiming("PurgeSummaryTableCache");
}
}
}
}
// Create the output directory if requested
string outputDir = GetArg("o", false).ToLower();
if (!string.IsNullOrEmpty(outputDir))
{
if (!Directory.Exists(outputDir))
{
Directory.CreateDirectory(outputDir);
}
}
int precacheCount = GetIntArg("precacheCount", 8);
int precacheThreads = GetIntArg("precacheThreads", 8);
bool bBatchedGraphs = true;
if (GetBoolArg("noBatchedGraphs"))
{
bBatchedGraphs = false;
}
if (bBatchedGraphs)
{
WriteLine("Batched graph generation enabled.");
}
else
{
WriteLine("Batched graph generation disabled.");
}
// Read the metadata filter string
string metadataFilterString = GetArg("metadataFilter", null);
QueryExpression metadataQuery = null;
if (metadataFilterString != null)
{
metadataQuery = MetadataQueryBuilder.BuildQueryExpressionTree(metadataFilterString);
}
bool writeDetailedReports = !GetBoolArg("noDetailedReports");
// A csv hash for now can just be a filename/size. Replace with metadata later
// Make PerfSummaryCache from: CSV hash + reporttype hash. If the cache is enabled then always -readAllStats
bool bReadAllStats = GetBoolArg("readAllStats");
bool bSummaryTableCacheReadonly = GetBoolArg("summaryTableCacheReadOnly");
bool bSummaryTableCacheInvalidate = GetBoolArg("summaryTableCacheInvalidate");
string cleanCsvOutputFilename = GetArg("cleanCsvOut", null);
if (cleanCsvOutputFilename != null && bBulkMode)
{
throw new Exception("-cleanCsvOut is not compatible with bulk mode. Pass one csv with -csv <filename>");
}
bool bShowHiddenStats = GetBoolArg("showHiddenStats");
string customSummaryTableFilter = GetArg("customTable");
if (customSummaryTableFilter.Length > 0)
{
bShowHiddenStats = true;
}
string summaryTableCacheForRead = summaryTableCacheDir;
if (bSummaryTableCacheInvalidate || writeDetailedReports)
{
// Don't read from the summary metadata cache if we're generating full reports
summaryTableCacheForRead = null;
}
if (bSummaryTableCacheOnlyMode)
{
// Override these options in summaryTableCacheOnly mode
bSummaryTableCacheReadonly = true;
summaryTableCacheForRead = null;
bSummaryTableCacheInvalidate = false;
}
ReportTypeParams reportTypeParams = new ReportTypeParams
{
reportTypeOverride = GetArg("reportType", false).ToLower(),
forceReportType = !GetBoolArg("reportTypeCompatCheck")
};
bool bRemoveDuplicates = !GetBoolArg("allowDuplicateCSVs");
bool bSummaryTableCacheUseOnlyCsvID = GetBoolArg("summaryTableCacheUseOnlyCsvID");
bool bRequireMetadata = GetBoolArg("requireMetadata");
bool bListFilesMode = GetBoolArg("listFiles");
int frameCountThreshold = GetIntArg("minFrameCount", 0);
if (bListFilesMode)
{
writeDetailedReports = false;
}
CsvFileCache csvFileCache = new CsvFileCache(
csvFilenames,
precacheCount,
precacheThreads,
!GetBoolArg("noCsvCacheFiles"),
metadataQuery,
reportXML,
reportTypeParams,
bBulkMode,
bSummaryTableCacheOnlyMode,
bSummaryTableCacheUseOnlyCsvID,
bRemoveDuplicates,
bRequireMetadata,
summaryTableCacheForRead,
bListFilesMode);
SummaryTable summaryTable = new SummaryTable();
bool bWriteToSummaryTableCache = summaryTableCacheDir != null && !bSummaryTableCacheReadonly;
int csvCount = csvFilenames.Length;
for (int i = 0; i < csvCount; i++)
{
try
{
CachedCsvFile cachedCsvFile = csvFileCache.GetNextCachedCsvFile();
if (cachedCsvFile == null)
{
continue;
}
Console.WriteLine("-------------------------------------------------");
Console.WriteLine("CSV " + (i + 1) + "/" + csvFilenames.Length);
Console.WriteLine(cachedCsvFile.filename);
perfLog.LogTiming(" CsvCacheRead");
if (cachedCsvFile == null)
{
Console.WriteLine("Skipped!");
}
else
{
SummaryTableRowData rowData = cachedCsvFile.cachedSummaryTableRowData;
if (rowData == null)
{
if (summaryTableCacheForRead != null)
{
summaryTableCacheStats.MissCount++;
}
if (bBulkMode)
{
rowData = new SummaryTableRowData();
}
if (cleanCsvOutputFilename != null)
{
WriteCleanCsv(cachedCsvFile, cleanCsvOutputFilename, cachedCsvFile.reportTypeInfo);
perfLog.LogTiming(" WriteCleanCsv");
}
else
{
GenerateReport(cachedCsvFile, outputDir, bBulkMode, rowData, bBatchedGraphs, writeDetailedReports, bReadAllStats || bWriteToSummaryTableCache, cachedCsvFile.reportTypeInfo, csvDir);
perfLog.LogTiming(" GenerateReport");
if (rowData != null && bWriteToSummaryTableCache)
{
if (rowData.WriteToCache(summaryTableCacheDir, cachedCsvFile.summaryTableCacheId))
{
Console.WriteLine("Cached summary rowData for CSV: " + cachedCsvFile.filename);
summaryTableCacheStats.WriteCount++;
perfLog.LogTiming(" WriteSummaryTableCache");
}
}
}
}
else
{
summaryTableCacheStats.HitCount++;
}
if (rowData != null)
{
// Filter row based on framecount if minFrameCount is specified
bool bIncludeRowData = true;
if (frameCountThreshold > 0 && rowData.GetFrameCount() < frameCountThreshold)
{
Console.WriteLine("CSV frame count below the threshold. Excluding from summary table:" + cachedCsvFile.filename);
bIncludeRowData = false;
}
if (bIncludeRowData)
{
summaryTable.AddRowData(rowData, bReadAllStats, bShowHiddenStats);
if (summaryTableJsonHelper != null)
{
summaryTableJsonHelper.AddRowData(rowData);
}
}
perfLog.LogTiming(" AddRowData");
}
}
}
catch (Exception e)
{
if (bBulkMode)
{
Console.Out.WriteLine("[ERROR] : " + e.Message);
}
else
{
// If we're not in bulk mode, exceptions are fatal
throw e;
}
}
}
if (summaryTableJsonHelper != null)
{
summaryTableJsonHelper.WriteToJson(GetBoolArg("summaryTableToJsonFileStream"));
perfLog.LogTiming("WriteSummaryDataJson");
if (GetBoolArg("summaryTableToJsonFastMode"))
{
perfLog.LogTotalTiming();
return;
}
}
Console.WriteLine("-------------------------------------------------");
// Write out the summary table, if there is one
if (summaryTable.Count > 0)
{
// Pre-sort the summary table to ensure determinism
summaryTable = summaryTable.SortRows(new List<string>(new string[] { "csvfilename" }), true);
perfLog.LogTiming("PreSort Summary table");
string summaryTableFilename = GetArg("summaryTableFilename", "SummaryTable");
if (summaryTableFilename.ToLower().EndsWith(".html"))
{
summaryTableFilename = summaryTableFilename.Substring(0, summaryTableFilename.Length - 5);
}
bool bCsvTable = GetBoolArg("csvTable");
bool bCollateTable = GetBoolArg("collateTable");
bool bCollateTableOnly = GetBoolArg("collateTableOnly");
bCollateTable |= bCollateTableOnly;
string collatedTableFilename = summaryTableFilename + ( bCollateTableOnly ? "" : "_Collated" );
bool bSpreadsheetFriendlyStrings = GetBoolArg("spreadsheetFriendly");
string weightByColumnName = GetArg("weightByColumn", null);
if (customSummaryTableFilter.Length > 0)
{
string customSummaryTableRowSort = GetArg("customTableSort");
if (customSummaryTableRowSort.Length == 0)
{
customSummaryTableRowSort = "buildversion,deviceprofile";
}
if (!bCollateTableOnly)
{
WriteSummaryTableReport(outputDir, summaryTableFilename, summaryTable, customSummaryTableFilter.Split(',').ToList(), customSummaryTableRowSort.Split(',').ToList(), false, bCsvTable, bSpreadsheetFriendlyStrings, null, null);
}
if (bCollateTable)
{
WriteSummaryTableReport(outputDir, collatedTableFilename, summaryTable, customSummaryTableFilter.Split(',').ToList(), customSummaryTableRowSort.Split(',').ToList(), true, bCsvTable, bSpreadsheetFriendlyStrings, null, weightByColumnName);
}
}
else
{
string summaryTableName = GetArg("summaryTable");
if (summaryTableName.Length == 0)
{
summaryTableName = "default";
}
SummaryTableInfo tableInfo = reportXML.GetSummaryTable(summaryTableName);
if (!bCollateTableOnly)
{
WriteSummaryTableReport(outputDir, summaryTableFilename, summaryTable, tableInfo, false, bCsvTable, bSpreadsheetFriendlyStrings, null);
}
if (bCollateTable)
{
WriteSummaryTableReport(outputDir, collatedTableFilename, summaryTable, tableInfo, true, bCsvTable, bSpreadsheetFriendlyStrings, weightByColumnName);
}
}
// EmailTable is hardcoded to use the condensed type
string condensedSummaryTable = GetArg("condensedSummaryTable", null);
if (GetBoolArg("emailSummary") || GetBoolArg("emailTable") || condensedSummaryTable != null)
{
SummaryTableInfo tableInfo = reportXML.GetSummaryTable(condensedSummaryTable == null ? "condensed" : condensedSummaryTable);
WriteSummaryTableReport(outputDir, summaryTableFilename + "_Email", summaryTable, tableInfo, true, false, bSpreadsheetFriendlyStrings, weightByColumnName);
}
perfLog.LogTiming("WriteSummaryTable");
}
if (summaryTableCacheDir != null)
{
summaryTableCacheStats.LogStats();
}
Console.WriteLine("Duplicate CSVs skipped: " + csvFileCache.duplicateCount);
perfLog.LogTotalTiming();
}
void WriteSummaryTableReport(string outputDir, string filenameWithoutExtension, SummaryTable table, List<string> columnFilterList, List<string> rowSortList, bool bCollated, bool bToCSV, bool bSpreadsheetFriendlyStrings, List<SummarySectionBoundaryInfo> sectionBoundaries, string weightByColumnName)
{
SummaryTableInfo tableInfo = new SummaryTableInfo();
tableInfo.columnFilterList = columnFilterList;
tableInfo.rowSortList = rowSortList;
WriteSummaryTableReport(outputDir, filenameWithoutExtension, table, tableInfo, bCollated, bToCSV, bSpreadsheetFriendlyStrings, weightByColumnName);
}
void WriteSummaryTableReport(string outputDir, string filenameWithoutExtension, SummaryTable table, SummaryTableInfo tableInfo, bool bCollated, bool bToCSV, bool bSpreadsheetFriendlyStrings, string weightByColumnNameOverride)
{
string weightByColumnName = weightByColumnNameOverride != null ? weightByColumnNameOverride : tableInfo.weightByColumn;
if (GetBoolArg("noWeightedAvg"))
{
weightByColumnName = null;
}
bool bTransposeFullSummaryTable = GetBoolArg("transposeTable");
bool bTransposeCollatedSummaryTable = bTransposeFullSummaryTable | GetBoolArg("transposeCollatedTable");
// Check params and any commandline overrides
bool bReverseTable = tableInfo.bReverseSortRows;
bool? bReverseTableOption = GetOptionalBoolArg("reverseTable");
if (bReverseTableOption != null)
{
bReverseTable = (bool)bReverseTableOption;
}
bool bScrollableTable = tableInfo.bScrollableFormatting;
bool? bScrollableTableOption = GetOptionalBoolArg("scrollableTable");
if (bScrollableTableOption != null)
{
bScrollableTable = (bool)bScrollableTableOption;
}
// The colorize mode is initially set to whatever is in the summary table xml file.
// We then override that value if -colorizeTable is set to auto, off or budget.
// If -colorizeTable isn't specified then we use the default from the xml.
// If -colorizeTable isn't specified and it's not set in the summary table xml it uses the default set in the class initializer.
TableColorizeMode colorizeMode = tableInfo.tableColorizeMode;
string colorizeArg = GetArg("colorizeTable","").ToLower();
if ( GetBoolArg("autoColorizeTable")) // Legacy support for the -autoColorizeTable arg
{
colorizeMode = TableColorizeMode.Auto;
}
if ( colorizeArg != "" )
{
if (colorizeArg == "auto")
{
colorizeMode = TableColorizeMode.Auto;
}
else if (colorizeArg == "off")
{
colorizeMode = TableColorizeMode.Off;
}
else if (colorizeArg == "budget")
{
colorizeMode = TableColorizeMode.Budget;
}
}
bool addMinMaxColumns = !GetBoolArg("noSummaryMinMax") && !bTransposeCollatedSummaryTable;
if (!string.IsNullOrEmpty(outputDir))
{
filenameWithoutExtension = Path.Combine(outputDir, filenameWithoutExtension);
}
IEnumerable<ISummaryTableColumnFilter> additionalColumnFilters = MakeAdditionalColumnFilters(tableInfo);
bool showFilteredColumns = GetBoolArg("debugShowFilteredColumns");
// Set format info for the columns as some of the info is needed for the filters.
// TODO: would be better if we could determine HighIsBad without the format info and store it directly in the column.
table.SetColumnFormatInfo(reportXML.columnFormatInfoList);
SummaryTable filteredTable = table.SortAndFilter(tableInfo.columnFilterList, tableInfo.rowSortList, bReverseTable, weightByColumnName, showFilteredColumns, additionalColumnFilters);
if (bCollated)
{
filteredTable = filteredTable.CollateSortedTable(tableInfo.rowSortList, addMinMaxColumns);
}
if (bToCSV)
{
filteredTable.WriteToCSV(filenameWithoutExtension + ".csv");
}
else
{
filteredTable.ApplyDisplayNameMapping(statDisplaynameMapping);
string VersionString = GetBoolArg("noWatermarks") ? "" : Version.Get();
string summaryTitle = GetArg("summaryTitle", null);
if (GetBoolArg("addDiffRows"))
{
filteredTable.AddDiffRows(GetBoolArg("sortColumnsByDiff"), GetFloatArg("columnDiffDisplayThreshold", 0.0f));
}
// Run again to add format info for any new columns that were added (eg. count).
filteredTable.SetColumnFormatInfo(reportXML.columnFormatInfoList);
filteredTable.WriteToHTML(
filenameWithoutExtension + ".html",
VersionString,
bSpreadsheetFriendlyStrings,
tableInfo.sectionBoundaries,
bScrollableTable,
colorizeMode,
addMinMaxColumns,
tableInfo.hideStatPrefix,
GetIntArg("maxSummaryTableStringLength", Int32.MaxValue),
weightByColumnName,
summaryTitle,
bCollated ? bTransposeCollatedSummaryTable : bTransposeFullSummaryTable,
showFilteredColumns
);
}
}
IEnumerable<ISummaryTableColumnFilter> MakeAdditionalColumnFilters(SummaryTableInfo tableInfo)
{
List<ISummaryTableColumnFilter> additionalColumnFilters = new List<ISummaryTableColumnFilter>();
additionalColumnFilters.Add(new StatThresholdColumnFilter(GetFloatArg("summaryTableStatThreshold", tableInfo.statThreshold)));
if (GetBoolArg("hideMetadataColumns"))
{
additionalColumnFilters.Add(new MetadataColumnFilter(tableInfo.rowSortList));
}
if (GetBoolArg("onlyShowRegressedColumns"))
{
string joinByStatName = GetArg("regressionJoinRowsByName");
float stdDevThreshold = GetFloatArg("regressionStdDevThreshold", 2.0f);
float outlierStdDevThreshold = GetFloatArg("regressionOutlierStdDevThreshold", 4.0f);
additionalColumnFilters.Add(new RegressionColumnFilter(joinByStatName, stdDevThreshold, outlierStdDevThreshold));
}
return additionalColumnFilters;
}
string ReplaceFileExtension(string path, string newExtension)
{
// Special case for .bin.csv
if (path.ToLower().EndsWith(".csv.bin"))
{
return path.Substring(0, path.Length - 8) + newExtension;
}
int lastDotIndex = path.LastIndexOf('.');
if (path.EndsWith("\""))
{
newExtension = newExtension + "\"";
if (lastDotIndex == -1)
{
lastDotIndex = path.Length - 1;
}
}
else if (lastDotIndex == -1)
{
lastDotIndex = path.Length;
}
return path.Substring(0, lastDotIndex) + newExtension;
}
static Dictionary<string, bool> UniqueHTMLFilemameLookup = new Dictionary<string, bool>();
void WriteCleanCsv(CachedCsvFile csvFile, string outCsvFilename, ReportTypeInfo reportTypeInfo)
{
if (File.Exists(outCsvFilename))
{
throw new Exception("Clean csv file " + outCsvFilename + " already exists!");
}
Console.WriteLine("Writing clean (standard format, event stripped) csv file to " + outCsvFilename);
int minX = GetIntArg("minx", 0);
int maxX = GetIntArg("maxx", Int32.MaxValue);
// Check if we're stripping stats
bool bStripStatsByEvents = reportTypeInfo.bStripEvents;
if (GetBoolArg("noStripEvents"))
{
bStripStatsByEvents = false;
}
int numFramesStripped;
CsvStats csvStatsUnstripped;
CsvStats csvStats = ProcessCsv(csvFile, out numFramesStripped, out csvStatsUnstripped, minX, maxX, null, bStripStatsByEvents);
csvStats.WriteToCSV(outCsvFilename, false);
}
CsvStats ProcessCsv(CachedCsvFile csvFile, out int numFramesStripped, out CsvStats csvStatsUnstripped, int minX = 0, int maxX = Int32.MaxValue, PerfLog perfLog = null, bool bStripStatsByEvents = true)
{
numFramesStripped = 0;
CsvStats csvStats = ReadCsvStats(csvFile, minX, maxX);
csvStatsUnstripped = csvStats;
if (perfLog != null)
{
perfLog.LogTiming(" ReadCsvStats");
}
if (bStripStatsByEvents)
{
CsvStats strippedCsvStats = StripCsvStatsByEvents(csvStatsUnstripped, out numFramesStripped);
csvStats = strippedCsvStats;
}
if (perfLog != null)
{
perfLog.LogTiming(" FilterStats");
}
return csvStats;
}
void GenerateReport(CachedCsvFile csvFile, string outputDir, bool bBulkMode, SummaryTableRowData rowData, bool bBatchedGraphs, bool writeDetailedReport, bool bReadAllStats, ReportTypeInfo reportTypeInfo, string csvDir)
{
PerfLog perfLog = new PerfLog(GetBoolArg("perfLog"));
string shortName = ReplaceFileExtension(MakeShortFilename(csvFile.filename), "");
string title = GetArg("title", false);
if (title.Length == 0)
{
title = shortName;
}
char c = title[0];
c = char.ToUpper(c);
title = c + title.Substring(1);
int minX = GetIntArg("minx", 0);
int maxX = GetIntArg("maxx", Int32.MaxValue);
string htmlFilename = null;
if (writeDetailedReport)
{
htmlFilename = shortName;
// Make sure the HTML filename is unique
if (bBulkMode)
{
int index = 0;
while (UniqueHTMLFilemameLookup.ContainsKey(htmlFilename.Trim().ToLower()))
{
if (htmlFilename.EndsWith("]") && htmlFilename.Contains('['))
{
int strIndex = htmlFilename.LastIndexOf('[');
htmlFilename = htmlFilename.Substring(0, strIndex);
}
htmlFilename += "[" + index.ToString() + "]";
index++;
}
UniqueHTMLFilemameLookup.Add(htmlFilename.Trim().ToLower(), true);
}
htmlFilename += ".html";
}
bool bCsvToSvgProcesses = GetBoolArg("csvToSvgProcesses");
bool bCsvToSvgMultiThreaded = !GetBoolArg("csvToSvgSequential");
int minWorker, minIOC;
// Get the current settings.
ThreadPool.GetMinThreads(out minWorker, out minIOC);
float thickness = 1.0f;
List<string> csvToSvgCommandlines = new List<string>();
List<string> svgFilenames = new List<string>();
string responseFilename = null;
List<Process> csvToSvgProcesses = new List<Process>();
List<Task> csvToSvgTasks = new List<Task>();
if (writeDetailedReport)
{
GraphGenerator graphGenerator = null;
if (!bCsvToSvgProcesses)
{
graphGenerator = new GraphGenerator(csvFile.GetFinalCsv(), csvFile.filename);
}
// Generate all the graphs asyncronously
foreach (ReportGraph graph in reportTypeInfo.graphs)
{
string svgFilename = String.Empty;
if (graph.isExternal && !GetBoolArg("externalGraphs"))
{
svgFilenames.Add(svgFilename);
continue;
}
bool bFoundStat = false;
foreach (string statString in graph.settings.statString.value.Split(','))
{
List<StatSamples> matchingStats = csvFile.dummyCsvStats.GetStatsMatchingString(statString);
if (matchingStats.Count > 0)
{
bFoundStat = true;
break;
}
}
if (bFoundStat)
{
svgFilename = GetTempFilename(csvFile.filename) + ".svg";
if (graphGenerator != null)
{
GraphParams graphParams = GetCsvToSvgGraphParams(csvFile.filename, graph, thickness, minX, maxX, false, svgFilenames.Count);
if (bCsvToSvgMultiThreaded)
{
csvToSvgTasks.Add(graphGenerator.MakeGraphAsync(graphParams, svgFilename, true, false));
}
else
{
graphGenerator.MakeGraph(graphParams, svgFilename, true, false);
}
}
else
{
string args = GetCsvToSvgArgs(csvFile.filename, svgFilename, graph, thickness, minX, maxX, false, svgFilenames.Count);
if (bBatchedGraphs)
{
csvToSvgCommandlines.Add(args);
}
else
{
Process csvToSvgProcess = LaunchCsvToSvgAsync(args);
csvToSvgProcesses.Add(csvToSvgProcess);
}
}
}
svgFilenames.Add(svgFilename);
}
if (bCsvToSvgProcesses && bBatchedGraphs)
{
// Save the response file
responseFilename = GetTempFilename(csvFile.filename) + "_response.txt";
System.IO.File.WriteAllLines(responseFilename, csvToSvgCommandlines);
Process csvToSvgProcess = LaunchCsvToSvgAsync("-batchCommands \"" + responseFilename + "\" -mt " + GetIntArg("graphThreads", Environment.ProcessorCount / 2).ToString());
csvToSvgProcesses.Add(csvToSvgProcess);
}
}
perfLog.LogTiming(" Initial Processing");
// Check if we're stripping stats
bool bStripStatsByEvents = reportTypeInfo.bStripEvents;
if (GetBoolArg("noStripEvents"))
{
bStripStatsByEvents = false;
}
if (writeDetailedReport && csvToSvgTasks.Count > 0)
{
// wait on the graph tasks to complete
// Note that we have to do this before we can call ProcessCSV, since this modifies the CsvStats object the graph tasks are reading
foreach (Task task in csvToSvgTasks)
{
task.Wait();
}
perfLog.LogTiming(" WaitForAsyncGraphs");
}
// Read the full csv while we wait for the graph processes to complete (this is only safe for CsvToSVG processes, not task threads)
int numFramesStripped;
CsvStats csvStatsUnstripped;
CsvStats csvStats = ProcessCsv(csvFile, out numFramesStripped, out csvStatsUnstripped, minX, maxX, perfLog, bStripStatsByEvents);
if (writeDetailedReport && csvToSvgProcesses.Count > 0)
{
// wait on the graph processes to complete
foreach (Process process in csvToSvgProcesses)
{
process.WaitForExit();
}
perfLog.LogTiming(" WaitForAsyncGraphs");
}
// Generate CSV metadata
if (rowData != null)
{
Uri currentDirUri = new Uri(Directory.GetCurrentDirectory() + "/", UriKind.Absolute);
if (outputDir.Length > 0 && !outputDir.EndsWith("/"))
{
outputDir += "/";
}
Uri optionalDirUri = new Uri(outputDir, UriKind.RelativeOrAbsolute);
// Make a Csv URI that's relative to the report directory
Uri finalDirUri;
if (optionalDirUri.IsAbsoluteUri)
{
finalDirUri = optionalDirUri;
}
else
{
finalDirUri = new Uri(currentDirUri, outputDir);
}
Uri csvFileUri = new Uri(csvFile.filename, UriKind.Absolute);
Uri relativeCsvUri = finalDirUri.MakeRelativeUri(csvFileUri);
string csvPath = relativeCsvUri.ToString();
bool bLinkTemplates = GetBoolArg("linkTemplates");
string csvId = null;
if (csvStats.metaData != null)
{
csvId = csvStats.metaData.GetValue("csvid", null);
}
// re-root the CSV path if requested
string csvLinkRootPath = GetArg("csvLinkRootPath", null);
if (csvDir != null && csvLinkRootPath != null)
{
string csvDirFinal = csvDir.Replace("\\", "/");
csvDirFinal += csvDirFinal.EndsWith("/") ? "" : "/";
Uri csvDirUri = new Uri(csvDirFinal, UriKind.Absolute);
Uri csvRelativeToCsvDirUri = csvDirUri.MakeRelativeUri(csvFileUri);
csvPath = Path.Combine(csvLinkRootPath, csvRelativeToCsvDirUri.ToString());
csvPath = new Uri(csvPath, UriKind.Absolute).ToString();
}
string csvLink = "<a href='" + csvPath + "'>" + shortName + ".csv" + "</a>";
if (bLinkTemplates)
{
if (!csvPath.StartsWith("http://") && !csvPath.StartsWith("https://"))
{
csvLink = "{LinkTemplate:Csv:" + (csvId ?? "0") + "}";
}
}
rowData.Add(SummaryTableElement.Type.ToolMetadata, "Csv File", csvLink, null, csvPath);
rowData.Add(SummaryTableElement.Type.ToolMetadata, "ReportType", reportTypeInfo.name);
rowData.Add(SummaryTableElement.Type.ToolMetadata, "ReportTypeID", reportTypeInfo.summaryTableCacheID);
if (htmlFilename != null)
{
string htmlUrl = htmlFilename;
string reportLinkRootPath = GetArg("reportLinkRootPath", null);
if (reportLinkRootPath != null)
{
htmlUrl = reportLinkRootPath + htmlFilename;
}
string reportLink = "<a href='" + htmlUrl + "'>Link</a>";
if (bLinkTemplates)
{
if (!htmlUrl.StartsWith("http://") && !htmlUrl.StartsWith("https://"))
{
reportLink = "{LinkTemplate:Report:" + (csvId ?? "0") + "}";
}
}
rowData.Add(SummaryTableElement.Type.ToolMetadata, "Report", reportLink);
}
// Pass through all the metadata from the CSV
if (csvStats.metaData != null)
{
foreach (KeyValuePair<string, string> pair in csvStats.metaData.Values.ToList())
{
rowData.Add(SummaryTableElement.Type.CsvMetadata, pair.Key.ToLower(), pair.Value);
}
}
if (bReadAllStats)
{
// Add every stat avg value to the metadata
foreach (StatSamples stat in csvStats.Stats.Values)
{
rowData.Add(SummaryTableElement.Type.CsvStatAverage, stat.Name, (double)stat.average);
}
}
}
if (htmlFilename != null && !string.IsNullOrEmpty(outputDir))
{
htmlFilename = Path.Combine(outputDir, htmlFilename);
}
// Write the report
WriteReport(htmlFilename, title, svgFilenames, reportTypeInfo, csvStats, csvStatsUnstripped, numFramesStripped, minX, maxX, bBulkMode, rowData);
perfLog.LogTiming(" WriteReport");
// Delete the temp files
foreach (string svgFilename in svgFilenames)
{
if (svgFilename != String.Empty && File.Exists(svgFilename))
{
File.Delete(svgFilename);
}
}
if (responseFilename != null && File.Exists(responseFilename))
{
File.Delete(responseFilename);
}
}
CsvStats ReadCsvStats(CachedCsvFile csvFile, int minX, int maxX)
{
CsvStats csvStats = csvFile.GetFinalCsv();
reportXML.ApplyDerivedMetadata(csvStats.metaData);
if (csvStats.metaData == null)
{
csvStats.metaData = new CsvMetadata();
}
csvStats.metaData.Values.Add("csvfilename", csvFile.filename);
// Adjust min/max x based on the event delimiters
string beginEventStr = GetArg("beginEvent").ToLower();
if (beginEventStr != "")
{
foreach (CsvEvent ev in csvStats.Events)
{
if (ev.Name.ToLower() == beginEventStr)
{
minX = Math.Max(minX, ev.Frame);
break;
}
}
}
string endEventStr = GetArg("endEvent").ToLower();
if (endEventStr != "")
{
for (int i = csvStats.Events.Count - 1; i >= 0; i--)
{
CsvEvent ev = csvStats.Events[i];
if (ev.Name.ToLower() == endEventStr)
{
maxX = Math.Min(maxX, ev.Frame);
break;
}
}
}
// Strip out all stats with a zero total
List<StatSamples> allStats = new List<StatSamples>();
foreach (StatSamples stat in csvStats.Stats.Values)
{
allStats.Add(stat);
}
csvStats.Stats.Clear();
foreach (StatSamples stat in allStats)
{
if (stat.total != 0.0f)
{
csvStats.AddStat(stat);
}
}
// Crop the stats to the range
csvStats.CropStats(minX, maxX);
return csvStats;
}
CsvStats StripCsvStatsByEvents(CsvStats csvStats, out int numFramesStripped)
{
numFramesStripped = 0;
List<CsvEventStripInfo> eventsToStrip = reportXML.GetCsvEventsToStrip();
// We want to run the mask apply in parallel if -nodetailedreports is specified. Otherwise leave cores free for graph generation
bool doParallelMaskApply = GetBoolArg("noDetailedReports");
CsvStats strippedStats = csvStats;
if (eventsToStrip != null)
{
BitArray sampleMask = null;
foreach (CsvEventStripInfo eventStripInfo in eventsToStrip)
{
csvStats.ComputeEventStripSampleMask(eventStripInfo.beginName, eventStripInfo.endName, ref sampleMask);
}
if (sampleMask != null)
{
numFramesStripped = sampleMask.Cast<bool>().Count(l => !l);
strippedStats = csvStats.ApplySampleMask(sampleMask, doParallelMaskApply);
}
}
if (numFramesStripped > 0)
{
Console.WriteLine("CSV frames excluded : " + numFramesStripped);
}
return strippedStats;
}
void WriteReport(string htmlFilename, string title, List<string> svgFilenames, ReportTypeInfo reportTypeInfo, CsvStats csvStats, CsvStats csvStatsUnstripped, int numFramesStripped, int minX, int maxX, bool bBulkMode, SummaryTableRowData summaryRowData)
{
ReportGraph[] graphs = reportTypeInfo.graphs.ToArray();
string titleStr = reportTypeInfo.title + " : " + title;
System.IO.StreamWriter htmlFile = null;
if (htmlFilename != null)
{
htmlFile = new System.IO.StreamWriter(htmlFilename);
htmlFile.WriteLine("<html>");
htmlFile.WriteLine(" <head>");
htmlFile.WriteLine(" <meta http-equiv='X-UA-Compatible' content='IE=edge'/>");
if (GetBoolArg("noWatermarks"))
{
htmlFile.WriteLine(" <![CDATA[ \nCreated with PerfReportTool");
}
else
{
htmlFile.WriteLine(" <![CDATA[ \nCreated with PerfReportTool " + Version.Get() + " with commandline:");
htmlFile.WriteLine(commandLine.GetCommandLine());
}
htmlFile.WriteLine(" ]]>");
htmlFile.WriteLine(" <title>" + titleStr + "</title>");
htmlFile.WriteLine(" <style type='text/css'>");
htmlFile.WriteLine(" table, th, td { border: 2px solid black; border-collapse: collapse; padding: 3px; vertical-align: top; font-family: 'Verdana', Times, serif; font-size: 12px;}");
htmlFile.WriteLine(" p { font-family: 'Verdana', Times, serif; font-size: 12px }");
htmlFile.WriteLine(" h1 { font-family: 'Verdana', Times, serif; font-size: 20px; padding-top:10px }");
htmlFile.WriteLine(" h2 { font-family: 'Verdana', Times, serif; font-size: 18px; padding-top:20px }");
htmlFile.WriteLine(" h3 { font-family: 'Verdana', Times, serif; font-size: 16px; padding-top:20px }");
htmlFile.WriteLine(" </style>");
htmlFile.WriteLine(" </head>");
htmlFile.WriteLine(" <body><font face='verdana'>");
htmlFile.WriteLine(" <h1>" + titleStr + "</h1>");
// show the range
if (minX > 0 || maxX < Int32.MaxValue)
{
htmlFile.WriteLine("<br><br><font size='1.5'>(CSV cropped to range " + minX + "-");
if (maxX < Int32.MaxValue)
{
htmlFile.WriteLine(maxX);
}
htmlFile.WriteLine(")</font>");
}
htmlFile.WriteLine(" <h2>Summary</h2>");
htmlFile.WriteLine("<table style='width:800'>");
if (reportTypeInfo.metadataToShowList != null)
{
Dictionary<string, string> 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("<tr><td bgcolor='#F0F0F0'>" + friendlyName + "</td><td><b>" + value + "</b></td></tr>");
}
}
}
htmlFile.WriteLine("<tr><td bgcolor='#F0F0F0'>Frame count</td><td>" + csvStats.SampleCount + " (" + numFramesStripped + " excluded)</td></tr>");
htmlFile.WriteLine("</table>");
}
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<Summary> summaries = new List<Summary>(reportTypeInfo.summaries);
bool bExtraLinksSummary = GetBoolArg("extraLinksSummary");
if (bExtraLinksSummary)
{
bool bLinkTemplates = GetBoolArg("linkTemplates");
summaries.Insert(0, new ExtraLinksSummary(null, null, bLinkTemplates));
}
// If the reporttype has summary info, then write out the summary]
PeakSummary peakSummary = null;
foreach (Summary summary in summaries)
{
summary.WriteSummaryData(htmlFile, summary.useUnstrippedCsvStats ? csvStatsUnstripped : csvStats, csvStatsUnstripped, bWriteSummaryCsv, summaryRowData, htmlFilename);
if (summary.GetType() == typeof(PeakSummary))
{
peakSummary = (PeakSummary)summary;
}
}
if (htmlFile != null)
{
// Output the list of graphs
htmlFile.WriteLine("<h2>Graphs</h2>");
// TODO: support sections for graphs
List<string> sections = new List<string>();
//// 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("<ul>");
string currentCategory = sections[index];
if (currentCategory.Length > 0)
{
htmlFile.WriteLine("<h4>" + currentCategory + " Graphs</h4>");
}
for (int i = 0; i < svgFilenames.Count(); i++)
{
string svgFilename = svgFilenames[i];
if (string.IsNullOrEmpty(svgFilename))
{
continue;
}
ReportGraph graph = graphs[i];
string svgTitle = graph.title;
//if (reportTypeInfo.summary.stats[i].ToLower().StartsWith(currentCategory))
{
htmlFile.WriteLine("<li><a href='#" + StripSpaces(svgTitle) + "'>" + svgTitle + "</a></li>");
}
}
htmlFile.WriteLine("</ul>");
}
// Output the Graphs
for (int svgFileIndex = 0; svgFileIndex < svgFilenames.Count; svgFileIndex++)
{
string svgFilename = svgFilenames[svgFileIndex];
if (String.IsNullOrEmpty(svgFilename))
{
continue;
}
ReportGraph graph = graphs[svgFileIndex];
string svgTitle = graph.title;
htmlFile.WriteLine(" <br><a name='" + StripSpaces(svgTitle) + "'></a> <h2>" + svgTitle + "</h2>");
if (graph.isExternal)
{
string outFilename = htmlFilename.Replace(".html", "_" + svgTitle.Replace(" ", "_") + ".svg");
File.Copy(svgFilename, outFilename, true);
htmlFile.WriteLine("<a href='" + outFilename + "'>" + svgTitle + " (external)</a>");
}
else
{
string[] svgLines = ReadLinesFromFile(svgFilename);
foreach (string line in svgLines)
{
string modLine = line.Replace("__MAKEUNIQUE__", "U_" + svgFileIndex.ToString());
htmlFile.WriteLine(modLine);
}
}
}
if (GetBoolArg("noWatermarks"))
{
htmlFile.WriteLine("<p style='font-size:8'>Created with PerfReportTool</p>");
}
else
{
htmlFile.WriteLine("<p style='font-size:8'>Created with PerfReportTool " + Version.Get() + "</p>");
}
htmlFile.WriteLine(" </font>");
htmlFile.WriteLine(" </body>");
htmlFile.WriteLine("</html>");
htmlFile.Close();
string ForEmail = GetArg("foremail", false);
if (ForEmail != "")
{
WriteEmail(htmlFilename, title, svgFilenames, reportTypeInfo, csvStats, csvStatsUnstripped, minX, maxX, bBulkMode);
}
}
}
void WriteEmail(string htmlFilename, string title, List<string> svgFilenames, 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("<html>");
htmlFile.WriteLine(" <head>");
htmlFile.WriteLine(" <meta http-equiv='X-UA-Compatible' content='IE=edge'/>");
if (GetBoolArg("noWatermarks"))
{
htmlFile.WriteLine(" <![CDATA[ \nCreated with PerfReportTool");
}
else
{
htmlFile.WriteLine(" <![CDATA[ \nCreated with PerfReportTool " + Version.Get() + " with commandline:");
htmlFile.WriteLine(commandLine.GetCommandLine());
}
htmlFile.WriteLine(" ]]>");
htmlFile.WriteLine(" <title>" + titleStr + "</title>");
htmlFile.WriteLine(" </head>");
htmlFile.WriteLine(" <body><font face='verdana'>");
htmlFile.WriteLine(" <h1>" + titleStr + "</h1>");
// show the range
if (minX > 0 || maxX < Int32.MaxValue)
{
htmlFile.WriteLine("<br><br><font size='1.5'>(CSV cropped to range " + minX + "-");
if (maxX < Int32.MaxValue)
{
htmlFile.WriteLine(maxX);
}
htmlFile.WriteLine(")</font>");
}
htmlFile.WriteLine("<a href=\"[Report Link Here]\">Click here for Report w/ interactive SVGs.</a>");
htmlFile.WriteLine(" <h2>Summary</h2>");
htmlFile.WriteLine("Overall Runtime: [Replace Me With Runtime]");
bool bWriteSummaryCsv = GetBoolArg("writeSummaryCsv") && !bBulkMode;
// If the reporttype has summary info, then write out the summary]
PeakSummary peakSummary = null;
foreach (Summary summary in reportTypeInfo.summaries)
{
summary.WriteSummaryData(htmlFile, csvStats, csvStatsUnstripped, bWriteSummaryCsv, null, htmlFilename);
if (summary.GetType() == typeof(PeakSummary))
{
peakSummary = (PeakSummary)summary;
}
}
htmlFile.WriteLine(" </font></body>");
htmlFile.WriteLine("</html>");
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));
}
string GetCsvToSvgArgs(string csvFilename, string svgFilename, ReportGraph graph, double thicknessMultiplier, int minx, int maxx, bool multipleCSVs, int graphIndex, float scaleby = 1.0f)
{
string title = graph.title;
GraphSettings graphSettings = graph.settings;
string[] statStringTokens = graphSettings.statString.value.Split(',');
IEnumerable<string> quoteWrappedStatStrings = statStringTokens.Select(token => '"' + token + '"');
string statString = String.Join(" ", quoteWrappedStatStrings);
double thickness = graphSettings.thickness.value * thicknessMultiplier;
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;
string additionalArgs = "";
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 smoothParams = "";
if (smooth)
{
smoothParams = " -smooth";
if (smoothKernelPercent >= 0.0f)
{
smoothParams += " -smoothKernelPercent " + smoothKernelPercent.ToString();
}
if (smoothKernelSize >= 0.0f)
{
smoothParams += " -smoothKernelSize " + smoothKernelSize.ToString();
}
}
string highlightEventRegions = "";
if (!GetBoolArg("noStripEvents"))
{
List<CsvEventStripInfo> 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;
highlightEventRegions += eventsToStrip[i].beginName + "," + endEvent;
}
highlightEventRegions += "\"";
}
}
OptionalDouble minFilterStatValueSetting = graph.minFilterStatValue.isSet ? graph.minFilterStatValue : graphSettings.minFilterStatValue;
string args =
" -csvs \"" + csvFilename + "\"" +
" -title \"" + title + "\"" +
" -o \"" + svgFilename + "\"" +
" -stats " + statString +
" -width " + (width * scaleby).ToString() +
" -height " + (height * scaleby).ToString() +
OptionalHelper.GetDoubleSetting(graph.budget, " -budget ") +
" -maxy " + maxy.ToString() +
" -uniqueID Graph_" + graphIndex.ToString() +
" -lineDecimalPlaces " + lineDecimalPlaces.ToString() +
" -nocommandlineEmbed " +
((statMultiplier != 1.0) ? " -statMultiplier " + statMultiplier.ToString("0.0000000000000000000000") : "") +
(hideEventNames ? " -hideeventNames 1" : "") +
((minx > 0) ? (" -minx " + minx.ToString()) : "") +
((maxx != Int32.MaxValue) ? (" -maxx " + maxx.ToString()) : "") +
OptionalHelper.GetDoubleSetting(graphSettings.miny, " -miny ") +
OptionalHelper.GetDoubleSetting(graphSettings.threshold, " -threshold ") +
OptionalHelper.GetDoubleSetting(graphSettings.averageThreshold, " -averageThreshold ") +
OptionalHelper.GetDoubleSetting(minFilterStatValueSetting, " -minFilterStatValue ") +
OptionalHelper.GetStringSetting(graphSettings.minFilterStatName, " -minFilterStatName ") +
(compression > 0.0 ? " -compression " + compression.ToString() : "") +
(thickness > 0.0 ? " -thickness " + thickness.ToString() : "") +
smoothParams +
(interactive ? " -interactive" : "") +
(stacked ? " -stacked -forceLegendSort" : "") +
(showAverages ? " -showAverages" : "") +
(snapToPeaks ? "" : " -nosnap") +
(filterOutZeros ? " -filterOutZeros" : "") +
(maxHierarchyDepth >= 0 ? " -maxHierarchyDepth " + maxHierarchyDepth.ToString() : "") +
(hideStatPrefix.Length > 0 ? " -hideStatPrefix " + hideStatPrefix : "") +
(graphSettings.mainStat.isSet ? " -stacktotalstat " + graphSettings.mainStat.value : "") +
(showEvents.Length > 0 ? " -showevents " + showEvents : "") +
(highlightEventRegions.Length > 0 ? " -highlightEventRegions " + highlightEventRegions : "") +
(graphSettings.legendAverageThreshold.isSet ? " -legendAverageThreshold " + graphSettings.legendAverageThreshold.value : "") +
(graphSettings.ignoreStats.isSet ? " -ignoreStats " + graphSettings.ignoreStats.value : "") +
" " + additionalArgs;
return args;
}
GraphParams GetCsvToSvgGraphParams(string csvFilename, 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(graphSettings.statString.value.Split(','));
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.showEventNameText = false;
}
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;
}
graphParams.interactive = true;
if (!GetBoolArg("noStripEvents"))
{
List<CsvEventStripInfo> eventsToStrip = reportXML.GetCsvEventsToStrip();
if (eventsToStrip != null)
{
for (int i = 0; i < eventsToStrip.Count; i++)
{
graphParams.highlightEventRegions.Add((eventsToStrip[i].beginName == 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.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 = GetBaseDirectory() + "/CSVToSVG.exe";
string binary = csvToolPath;
// run mono on non-Windows hosts
if (Host != HostPlatform.Windows)
{
// note, on Mac mono will not be on path
binary = Host == HostPlatform.Linux ? "mono" : "/Library/Frameworks/Mono.framework/Versions/Current/Commands/mono";
args = csvToolPath + " " + args;
}
// 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.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<System.IO.FileInfo> fileList = new List<FileInfo>();
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<string, dynamic> jsonDict = JsonToDynamicDict(jsonText);
Console.WriteLine("Writing PRCs");
foreach (string csvId in jsonDict.Keys)
{
Dictionary<string, dynamic> srcDict = jsonDict[csvId];
SummaryTableRowData rowData = new SummaryTableRowData(srcDict);
rowData.WriteToCache(prcOutputDir, csvId);
}
}
Dictionary<string, dynamic> JsonToDynamicDict(string jsonStr)
{
JsonElement RootElement = JsonSerializer.Deserialize<JsonElement>((string)jsonStr, null);
Dictionary<string, dynamic> 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<dynamic> ArrayValue = new List<dynamic>();
foreach( JsonElement element in jsonElement.EnumerateArray())
{
ArrayValue.Add(GetJsonValue(element));
}
return ArrayValue;
case JsonValueKind.Object:
Dictionary<string, dynamic> DictValue = new Dictionary<string, dynamic>();
foreach ( JsonProperty property in jsonElement.EnumerateObject() )
{
DictValue[property.Name] = GetJsonValue(property.Value);
}
return DictValue;
}
return null;
}
}
static class Extensions
{
public static T GetSafeAttibute<T>(this XElement element, string attributeName, T defaultValue = default(T))
{
XAttribute attribute = element.Attribute(attributeName);
if (attribute == null)
{
return defaultValue;
}
try
{
switch (Type.GetTypeCode(typeof(T)))
{
case TypeCode.Boolean:
return (T)Convert.ChangeType(Convert.ChangeType(attribute.Value, typeof(int)), typeof(bool));
case TypeCode.Single:
case TypeCode.Double:
case TypeCode.Decimal:
return (T)Convert.ChangeType(attribute.Value, typeof(T), CultureInfo.InvariantCulture.NumberFormat);
default:
return (T)Convert.ChangeType(attribute.Value, typeof(T));
}
}
catch (FormatException e)
{
Console.WriteLine(string.Format("[Warning] Failed to convert XML attribute '{0}' ({1})", attributeName, e.Message));
return defaultValue;
}
}
};
}