// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using PerfReportTool; using System.IO; namespace PerfSummaries { class TableUtil { public static string FormatStatName(string inStatName) { return inStatName.Replace("/", "/ "); } public static string SanitizeHtmlString(string str) { return str.Replace("<", "<").Replace(">", ">"); } public static string SafeTruncateHtmlTableValue(string inValue, int maxLength) { if (inValue.StartsWith("")) { // Links require special handling. Only truncate what's inside int openAnchorEndIndex = inValue.IndexOf(">"); int closeAnchorStartIndex = inValue.IndexOf(""); if (openAnchorEndIndex > 2 && closeAnchorStartIndex > openAnchorEndIndex) { string anchor = inValue.Substring(0, openAnchorEndIndex + 1); string text = inValue.Substring(openAnchorEndIndex + 1, closeAnchorStartIndex - (openAnchorEndIndex + 1)); if (text.Length > maxLength) { text = SanitizeHtmlString(text.Substring(0, maxLength)) + "..."; } return anchor + text + ""; } } return SanitizeHtmlString(inValue.Substring(0, maxLength)) + "..."; } } class SummarySectionBoundaryInfo { public SummarySectionBoundaryInfo(string inStatName, string inStartToken, string inEndToken, int inLevel, bool inInCollatedTable, bool inInFullTable) { statName = inStatName; startToken = inStartToken; endToken = inEndToken; level = inLevel; inCollatedTable = inInCollatedTable; inFullTable = inInFullTable; } public string statName; public string startToken; public string endToken; public int level; public bool inCollatedTable; public bool inFullTable; }; class SummaryTableInfo { public SummaryTableInfo(XElement tableElement) { XAttribute rowSortAt = tableElement.Attribute("rowSort"); if (rowSortAt != null) { rowSortList.AddRange(rowSortAt.Value.Split(',')); } XAttribute weightByColumnAt = tableElement.Attribute("weightByColumn"); if (weightByColumnAt != null) { weightByColumn = weightByColumnAt.Value.ToLower(); } XElement filterEl = tableElement.Element("filter"); if (filterEl != null) { columnFilterList.AddRange(filterEl.Value.Split(',')); } bReverseSortRows = tableElement.GetSafeAttibute("reverseSortRows", false); bScrollableFormatting = tableElement.GetSafeAttibute("scrollableFormatting", false); foreach (XElement sectionBoundaryEl in tableElement.Elements("sectionBoundary")) { if (sectionBoundaryEl != null) { SummarySectionBoundaryInfo sectionBoundary = new SummarySectionBoundaryInfo( sectionBoundaryEl.GetSafeAttibute("statName"), sectionBoundaryEl.GetSafeAttibute("startToken"), sectionBoundaryEl.GetSafeAttibute("endToken"), sectionBoundaryEl.GetSafeAttibute("level", 0), sectionBoundaryEl.GetSafeAttibute("inCollatedTable", true), sectionBoundaryEl.GetSafeAttibute("inFullTable", true) ); sectionBoundaries.Add(sectionBoundary); } } } public SummaryTableInfo(string filterListStr, string rowSortStr) { columnFilterList.AddRange(filterListStr.Split(',')); rowSortList.AddRange(rowSortStr.Split(',')); } public SummaryTableInfo() { } public List rowSortList = new List(); public List columnFilterList = new List(); public List sectionBoundaries = new List(); public bool bReverseSortRows; public bool bScrollableFormatting; public string weightByColumn = null; } class SummaryTableColumnFormatInfoCollection { public SummaryTableColumnFormatInfoCollection(XElement element) { foreach (XElement child in element.Elements("columnInfo")) { columnFormatInfoList.Add(new SummaryTableColumnFormatInfo(child)); } } public SummaryTableColumnFormatInfo GetFormatInfo(string columnName) { string lowerColumnName = columnName.ToLower(); if (lowerColumnName.StartsWith("avg ") || lowerColumnName.StartsWith("min ") || lowerColumnName.StartsWith("max ")) { lowerColumnName = lowerColumnName.Substring(4); } foreach (SummaryTableColumnFormatInfo columnInfo in columnFormatInfoList) { int wildcardIndex = columnInfo.name.IndexOf('*'); if (wildcardIndex == -1) { if (columnInfo.name == lowerColumnName) { return columnInfo; } } else { string prefix = columnInfo.name.Substring(0, wildcardIndex); if (lowerColumnName.StartsWith(prefix)) { return columnInfo; } } } return defaultColumnInfo; } public static SummaryTableColumnFormatInfo DefaultColumnInfo { get { return defaultColumnInfo; } } List columnFormatInfoList = new List(); static SummaryTableColumnFormatInfo defaultColumnInfo = new SummaryTableColumnFormatInfo(); }; enum AutoColorizeMode { Off, HighIsBad, LowIsBad, }; class SummaryTableColumnFormatInfo { public SummaryTableColumnFormatInfo() { name = "Default"; } public SummaryTableColumnFormatInfo(XElement element) { name = element.Attribute("name").Value.ToLower(); string autoColorizeStr = element.GetSafeAttibute("autoColorize", "highIsBad").ToLower(); var modeList = Enum.GetValues(typeof(AutoColorizeMode)); foreach (AutoColorizeMode mode in modeList) { if (mode.ToString().ToLower() == autoColorizeStr) { autoColorizeMode = mode; break; } } numericFormat = element.GetSafeAttibute("numericFormat"); } public AutoColorizeMode autoColorizeMode = AutoColorizeMode.HighIsBad; public string name; public string numericFormat; }; class SummaryTableColumn { public string name; public bool isNumeric = false; public string displayName; public bool isRowWeightColumn = false; List doubleValues = new List(); List stringValues = new List(); List toolTips = new List(); List colourThresholds = new List(); ColourThresholdList colourThresholdOverride = null; public SummaryTableColumn(string inName, bool inIsNumeric, string inDisplayName = null, bool inIsRowWeightColumn = false) { name = inName; isNumeric = inIsNumeric; displayName = inDisplayName; isRowWeightColumn = inIsRowWeightColumn; } public SummaryTableColumn Clone() { SummaryTableColumn newColumn = new SummaryTableColumn(name, isNumeric, displayName, isRowWeightColumn); newColumn.doubleValues.AddRange(doubleValues); newColumn.stringValues.AddRange(stringValues); newColumn.colourThresholds.AddRange(colourThresholds); newColumn.toolTips.AddRange(toolTips); return newColumn; } public string GetDisplayName() { if (displayName == null) { return TableUtil.FormatStatName(name); } return displayName; } public void SetValue(int index, double value) { if (!isNumeric) { // This is already a non-numeric column. Better treat this as a string value SetStringValue(index, value.ToString()); return; } // Grow to fill if necessary if (index >= doubleValues.Count) { for (int i = doubleValues.Count; i <= index; i++) { doubleValues.Add(double.MaxValue); } } doubleValues[index] = value; } void convertToStrings() { if (isNumeric) { stringValues = new List(); foreach (float f in doubleValues) { stringValues.Add(f.ToString()); } doubleValues = new List(); isNumeric = false; } } public void SetColourThresholds(int index, ColourThresholdList value) { // Grow to fill if necessary if (index >= colourThresholds.Count) { for (int i = colourThresholds.Count; i <= index; i++) { colourThresholds.Add(null); } } colourThresholds[index] = value; } public ColourThresholdList GetColourThresholds(int index) { if (index < colourThresholds.Count) { return colourThresholds[index]; } return null; } public string GetColour(int index) { ColourThresholdList thresholds = null; double value = GetValue(index); if (value == double.MaxValue) { return null; } if (colourThresholdOverride != null) { thresholds = colourThresholdOverride; } else { if (index < colourThresholds.Count) { thresholds = colourThresholds[index]; } if (thresholds == null) { return null; } } return thresholds.GetColourForValue(value); } public void ComputeAutomaticColourThresholds(AutoColorizeMode autoColorizeMode) { if (autoColorizeMode == AutoColorizeMode.Off) { return; } colourThresholds = new List(); double maxValue = -double.MaxValue; double minValue = double.MaxValue; double totalValue = 0.0f; double validCount = 0.0f; for (int i = 0; i < doubleValues.Count; i++) { double val = doubleValues[i]; if (val != double.MaxValue) { maxValue = Math.Max(val, maxValue); minValue = Math.Min(val, minValue); totalValue += val; validCount += 1.0f; } } if (minValue == maxValue) { return; } Colour green = new Colour(0.4f, 0.82f, 0.45f); Colour yellow = new Colour(1.0f, 1.0f, 0.5f); Colour red = new Colour(1.0f, 0.4f, 0.4f); double averageValue = totalValue / validCount; // TODO: Weighted average colourThresholdOverride = new ColourThresholdList(); colourThresholdOverride.Add(new ThresholdInfo(minValue, (autoColorizeMode == AutoColorizeMode.HighIsBad) ? green : red)); colourThresholdOverride.Add(new ThresholdInfo(averageValue, yellow)); colourThresholdOverride.Add(new ThresholdInfo(averageValue, yellow)); colourThresholdOverride.Add(new ThresholdInfo(maxValue, (autoColorizeMode == AutoColorizeMode.HighIsBad) ? red : green)); } public int GetCount() { return Math.Max(doubleValues.Count, stringValues.Count); } public double GetValue(int index) { if (index >= doubleValues.Count) { return double.MaxValue; } return doubleValues[index]; } public void SetStringValue(int index, string value) { if (isNumeric) { // Better convert this to a string column, since we're trying to add a string to it convertToStrings(); } // Grow to fill if necessary if (index >= stringValues.Count) { for (int i = stringValues.Count; i <= index; i++) { stringValues.Add(""); } } stringValues[index] = value; isNumeric = false; } public string GetStringValue(int index, bool roundNumericValues = false, string forceNumericFormat = null) { if (isNumeric) { if (index >= doubleValues.Count || doubleValues[index] == double.MaxValue) { return ""; } double val = doubleValues[index]; if (forceNumericFormat != null) { return val.ToString(forceNumericFormat); } else if (roundNumericValues) { double absVal = Math.Abs(val); double frac = absVal - (double)Math.Truncate(absVal); if (absVal >= 250.0f || frac < 0.0001f) { return val.ToString("0"); } if (absVal >= 50.0f) { return val.ToString("0.0"); } if (absVal >= 0.1) { return val.ToString("0.00"); } return val.ToString("0.000"); } return val.ToString(); } else { if (index >= stringValues.Count) { return ""; } if (forceNumericFormat != null) { // We're forcing a numeric format on something that's technically a string, but since we were asked, we'll try to do it anyway // Note: this is not ideal, but it's useful for collated table columns, which might get converted to non-numeric during collation try { return Convert.ToDouble(stringValues[index], System.Globalization.CultureInfo.InvariantCulture).ToString(forceNumericFormat); } catch { } // Ignore. Just fall through... } return stringValues[index]; } } public void SetToolTipValue(int index, string value) { // Grow to fill if necessary if (index >= toolTips.Count) { for (int i = toolTips.Count; i <= index; i++) { toolTips.Add(""); } } toolTips[index] = value; } public string GetToolTipValue(int index) { if (index >= toolTips.Count) { return ""; } return toolTips[index]; } }; class SummaryTable { public SummaryTable() { } public SummaryTable CollateSortedTable(List collateByList, bool addMinMaxColumns) { int numSubColumns = addMinMaxColumns ? 3 : 1; // Find all the columns in collateByList HashSet collateByColumns = new HashSet(); foreach (string collateBy in collateByList) { string key = collateBy.ToLower(); if (columnLookup.ContainsKey(key)) { collateByColumns.Add(columnLookup[key]); } } if (collateByColumns.Count == 0) { throw new Exception("None of the metadata strings were found:" + collateByList.ToString()); } // Add the new collateBy columns in the order they appear in the original column list List newColumns = new List(); List finalSortByList = new List(); foreach (SummaryTableColumn srcColumn in columns) { if (collateByColumns.Contains(srcColumn)) { newColumns.Add(new SummaryTableColumn(srcColumn.name, false, srcColumn.displayName)); finalSortByList.Add(srcColumn.name.ToLower()); // Early out if we've found all the columns if (finalSortByList.Count == collateByColumns.Count) { break; } } } newColumns.Add(new SummaryTableColumn("Count", true)); int countColumnIndex = newColumns.Count - 1; int numericColumnStartIndex = newColumns.Count; List srcToDestBaseColumnIndex = new List(); foreach (SummaryTableColumn column in columns) { // Add avg/min/max columns for this column if it's numeric and we didn't already add it above if (column.isNumeric && !collateByColumns.Contains(column)) { srcToDestBaseColumnIndex.Add(newColumns.Count); newColumns.Add(new SummaryTableColumn("Avg " + column.name, true)); if (addMinMaxColumns) { newColumns.Add(new SummaryTableColumn("Min " + column.name, true)); newColumns.Add(new SummaryTableColumn("Max " + column.name, true)); } } else { srcToDestBaseColumnIndex.Add(-1); } } List RowMaxValues = new List(); List RowTotals = new List(); List RowMinValues = new List(); List RowCounts = new List(); List RowWeights = new List(); List RowColourThresholds = new List(); // Set the initial sort key string CurrentRowSortKey = ""; foreach (string collateBy in finalSortByList) { CurrentRowSortKey += "{" + columnLookup[collateBy].GetStringValue(0) + "}"; } int destRowIndex = 0; bool reset = true; int mergedRowsCount = 0; for (int i = 0; i < rowCount; i++) { if (reset) { RowMaxValues.Clear(); RowMinValues.Clear(); RowTotals.Clear(); RowCounts.Clear(); RowWeights.Clear(); RowColourThresholds.Clear(); for (int j = 0; j < columns.Count; j++) { if (addMinMaxColumns) { RowMaxValues.Add(-double.MaxValue); RowMinValues.Add(double.MaxValue); } RowTotals.Add(0.0f); RowCounts.Add(0); RowWeights.Add(0.0); RowColourThresholds.Add(null); } mergedRowsCount = 0; reset = false; } // Compute min/max/total for all numeric columns for (int j = 0; j < columns.Count; j++) { SummaryTableColumn column = columns[j]; if (column.isNumeric) { double value = column.GetValue(i); if (value != double.MaxValue) { if (addMinMaxColumns) { RowMaxValues[j] = Math.Max(RowMaxValues[j], value); RowMinValues[j] = Math.Min(RowMinValues[j], value); } RowColourThresholds[j] = column.GetColourThresholds(i); RowCounts[j]++; double rowWeight = (rowWeightings != null) ? rowWeightings[i] : 1.0; RowWeights[j] += rowWeight; RowTotals[j] += value * rowWeight; } } } mergedRowsCount++; // Are we done? string nextSortKey = ""; if (i < rowCount - 1) { foreach (string collateBy in finalSortByList) { nextSortKey += "{" + columnLookup[collateBy].GetStringValue(i + 1) + "}"; } } // If this is the last row or if the sort key is different then write it out if (nextSortKey != CurrentRowSortKey) { for (int j = 0; j < countColumnIndex; j++) { string key = newColumns[j].name.ToLower(); newColumns[j].SetStringValue(destRowIndex, columnLookup[key].GetStringValue(i)); } // Commit the row newColumns[countColumnIndex].SetValue(destRowIndex, (double)mergedRowsCount); for (int j = 0; j < columns.Count; j++) { int destColumnBaseIndex = srcToDestBaseColumnIndex[j]; if (destColumnBaseIndex != -1 && RowCounts[j] > 0) { newColumns[destColumnBaseIndex].SetValue(destRowIndex, RowTotals[j] / RowWeights[j]); if (addMinMaxColumns) { newColumns[destColumnBaseIndex + 1].SetValue(destRowIndex, RowMinValues[j]); newColumns[destColumnBaseIndex + 2].SetValue(destRowIndex, RowMaxValues[j]); } // Set colour thresholds based on the source column ColourThresholdList Thresholds = RowColourThresholds[j]; for (int k = 0; k < numSubColumns; k++) { newColumns[destColumnBaseIndex + k].SetColourThresholds(destRowIndex, Thresholds); } } } reset = true; destRowIndex++; } CurrentRowSortKey = nextSortKey; } SummaryTable newTable = new SummaryTable(); newTable.columns = newColumns; newTable.InitColumnLookup(); newTable.rowCount = destRowIndex; newTable.firstStatColumnIndex = numericColumnStartIndex; newTable.isCollated = true; newTable.hasMinMaxColumns = addMinMaxColumns; return newTable; } public SummaryTable SortAndFilter(string customFilter, string customRowSort = "buildversion,deviceprofile", bool bReverseSort = false, string weightByColumnName = null) { return SortAndFilter(customFilter.Split(',').ToList(), customRowSort.Split(',').ToList(), bReverseSort, weightByColumnName); } public SummaryTable SortAndFilter(List columnFilterList, List rowSortList, bool bReverseSort, string weightByColumnName) { SummaryTable newTable = SortRows(rowSortList, bReverseSort); // Make a list of all unique keys List allMetadataKeys = new List(); Dictionary nameLookup = new Dictionary(); foreach (SummaryTableColumn col in newTable.columns) { string key = col.name.ToLower(); if (!nameLookup.ContainsKey(key)) { nameLookup.Add(key, col); allMetadataKeys.Add(key); } } allMetadataKeys.Sort(); // Generate the list of requested metadata keys that this table includes List orderedKeysWithDupes = new List(); // Add metadata keys from the column filter list in the order they appear foreach (string filterStr in columnFilterList) { string filterStrLower = filterStr.Trim().ToLower(); // Find all matching if (filterStrLower.EndsWith("*")) { string prefix = filterStrLower.Trim('*'); // Linear search through the sorted key list bool bFound = false; for (int wildcardSearchIndex = 0; wildcardSearchIndex < allMetadataKeys.Count; wildcardSearchIndex++) { if (allMetadataKeys[wildcardSearchIndex].StartsWith(prefix)) { orderedKeysWithDupes.Add(allMetadataKeys[wildcardSearchIndex]); bFound = true; } else if (bFound) { // Early exit: already found one key. If the pattern no longer matches then we must be done break; } } } else { string key = filterStrLower; orderedKeysWithDupes.Add(key); } } // Compute row weights if (weightByColumnName != null && nameLookup.ContainsKey(weightByColumnName)) { SummaryTableColumn rowWeightColumn = nameLookup[weightByColumnName]; newTable.rowWeightings = new List(rowWeightColumn.GetCount()); for (int i = 0; i < rowWeightColumn.GetCount(); i++) { newTable.rowWeightings.Add(rowWeightColumn.GetValue(i)); } } List newColumnList = new List(); // Add all the ordered keys that exist, ignoring duplicates foreach (string key in orderedKeysWithDupes) { if (nameLookup.ContainsKey(key)) { newColumnList.Add(nameLookup[key]); // Remove from the list so it doesn't get counted again nameLookup.Remove(key); } } newTable.columns = newColumnList; newTable.rowCount = rowCount; newTable.InitColumnLookup(); return newTable; } public void ApplyDisplayNameMapping(Dictionary statDisplaynameMapping) { // Convert to a display-friendly name foreach (SummaryTableColumn column in columns) { if (statDisplaynameMapping != null && column.displayName == null) { string name = column.name; string suffix = ""; string prefix = ""; string statName = GetStatNameWithPrefixAndSuffix(name, out prefix, out suffix); if (statDisplaynameMapping.ContainsKey(statName.ToLower())) { column.displayName = prefix + statDisplaynameMapping[statName.ToLower()] + suffix; } } } } string GetStatNameWithoutPrefixAndSuffix(string inName) { string suffix = ""; string prefix = ""; return GetStatNameWithPrefixAndSuffix(inName, out prefix, out suffix); } string GetStatNameWithPrefixAndSuffix(string inName, out string prefix, out string suffix) { suffix = ""; prefix = ""; string statName = inName; if (inName.StartsWith("Avg ") || inName.StartsWith("Max ") || inName.StartsWith("Min ")) { prefix = inName.Substring(0, 4); statName = inName.Substring(4); } if (statName.EndsWith(" Avg") || statName.EndsWith(" Max") || statName.EndsWith(" Min")) { suffix = statName.Substring(statName.Length - 4); statName = statName.Substring(0, statName.Length - 4); } return statName; } public void WriteToCSV(string csvFilename) { System.IO.StreamWriter csvFile = new System.IO.StreamWriter(csvFilename, false); List headerRow = new List(); foreach (SummaryTableColumn column in columns) { headerRow.Add(column.name); } csvFile.WriteLine(string.Join(",", headerRow)); for (int i = 0; i < rowCount; i++) { List rowStrings = new List(); foreach (SummaryTableColumn column in columns) { string cell = column.GetStringValue(i, false); // Sanitize so it opens in a spreadsheet (e.g. for buildversion) cell = cell.TrimStart('+'); rowStrings.Add(cell); } csvFile.WriteLine(string.Join(",", rowStrings)); } csvFile.Close(); } public void WriteToHTML(string htmlFilename, string VersionString, bool bSpreadsheetFriendlyStrings, List sectionBoundaries, bool bScrollableTable, bool bAddMinMaxColumns, int maxColumnStringLength, SummaryTableColumnFormatInfoCollection columnFormatInfoList, string weightByColumnName) { System.IO.StreamWriter htmlFile = new System.IO.StreamWriter(htmlFilename, false); int statColSpan = hasMinMaxColumns ? 3 : 1; int cellPadding = 2; if (isCollated) { cellPadding = 4; } htmlFile.WriteLine(""); htmlFile.WriteLine("Performance summary"); // Figure out the sticky column count int stickyColumnCount = 0; if (bScrollableTable) { stickyColumnCount = 1; if (isCollated) { for (int i = 0; i < columns.Count; i++) { if (columns[i].name == "Count") { stickyColumnCount = i + 1; break; } } } } if (bScrollableTable) { // Insert some javascript to make the columns sticky. It's not possible to do this for multiple columns with pure CSS, since you need to compute the X offset dynamically // We need to do this when the page is loaded or the window is resized htmlFile.WriteLine(""); } htmlFile.WriteLine(""); htmlFile.WriteLine(""); htmlFile.WriteLine(""); // Get format info for the columns Dictionary columnFormatInfoLookup = new Dictionary(); foreach (SummaryTableColumn column in columns) { columnFormatInfoLookup[column] = (columnFormatInfoList != null) ? columnFormatInfoList.GetFormatInfo(column.name) : SummaryTableColumnFormatInfoCollection.DefaultColumnInfo; } // Automatically colourize the table if (bScrollableTable) { foreach (SummaryTableColumn column in columns) { if (column.isNumeric) { column.ComputeAutomaticColourThresholds(columnFormatInfoLookup[column].autoColorizeMode); } } } string HeaderRow = ""; if (isCollated) { string TopHeaderRow = ""; if (bScrollableTable) { // Generate an automatic title string title = htmlFilename.Replace("_Email.html", "").Replace(".html", "").Replace("\\", "/"); title = title.Substring(title.LastIndexOf('/') + 1); TopHeaderRow += ""; } else { TopHeaderRow += ""; } if (!bAddMinMaxColumns) { TopHeaderRow = HeaderRow; } for (int i = firstStatColumnIndex; i < columns.Count; i++) { string prefix = ""; string suffix = ""; string statName = GetStatNameWithPrefixAndSuffix(columns[i].GetDisplayName(), out prefix, out suffix); if ((i - 1) % statColSpan == 0) { TopHeaderRow += ""; } HeaderRow += ""; } if (bAddMinMaxColumns) { htmlFile.WriteLine(" " + TopHeaderRow + ""); htmlFile.WriteLine(" " + HeaderRow + ""); } else { htmlFile.WriteLine(" " + TopHeaderRow + ""); } } else { foreach (SummaryTableColumn column in columns) { HeaderRow += ""; } htmlFile.WriteLine(" " + HeaderRow + ""); } string[] stripeColors = { "'#e2e2e2'", "'#ffffff'" }; // Work out which rows are major/minor section boundaries Dictionary rowSectionBoundaryLevel = new Dictionary(); if (sectionBoundaries != null) { foreach (SummarySectionBoundaryInfo sectionBoundaryInfo in sectionBoundaries) { // Skip this section boundary info if it's not in this table type if (isCollated && !sectionBoundaryInfo.inCollatedTable) { continue; } if (!isCollated && !sectionBoundaryInfo.inFullTable) { continue; } string prevSectionName = ""; for (int i = 0; i < rowCount; i++) { int boundaryLevel = 0; if (sectionBoundaryInfo != null) { // Work out the section name if we have section boundary info. When it changes, apply the sectionStart CSS class string sectionName = ""; if (sectionBoundaryInfo != null && columnLookup.ContainsKey(sectionBoundaryInfo.statName)) { // Get the section name if (!columnLookup.ContainsKey(sectionBoundaryInfo.statName)) { continue; } SummaryTableColumn col = columnLookup[sectionBoundaryInfo.statName]; sectionName = col.GetStringValue(i); // if we have a start token then strip before it if (sectionBoundaryInfo.startToken != null) { int startTokenIndex = sectionName.IndexOf(sectionBoundaryInfo.startToken); if (startTokenIndex != -1) { sectionName = sectionName.Substring(startTokenIndex + sectionBoundaryInfo.startToken.Length); } } // if we have an end token then strip after it if (sectionBoundaryInfo.endToken != null) { int endTokenIndex = sectionName.IndexOf(sectionBoundaryInfo.endToken); if (endTokenIndex != -1) { sectionName = sectionName.Substring(0, endTokenIndex); } } } if (sectionName != prevSectionName && i > 0) { // Update the row's boundary type info boundaryLevel = sectionBoundaryInfo.level; if (rowSectionBoundaryLevel.ContainsKey(i)) { // Lower level values override higher ones boundaryLevel = Math.Min(rowSectionBoundaryLevel[i], boundaryLevel); } rowSectionBoundaryLevel[i] = boundaryLevel; } prevSectionName = sectionName; } } } } // Add the rows to the table for (int i = 0; i < rowCount; i++) { string rowClassStr = ""; // Is this a major/minor section boundary if (rowSectionBoundaryLevel.ContainsKey(i)) { int sectionLevel = rowSectionBoundaryLevel[i]; if (sectionLevel < 3) { rowClassStr = " class='sectionStartLevel" + sectionLevel + "'"; } } htmlFile.Write(""); int columnIndex = 0; foreach (SummaryTableColumn column in columns) { // Add the tooltip for non-collated tables string toolTipString = ""; if (!isCollated) { string toolTip = column.GetToolTipValue(i); if (toolTip == "") { toolTip = column.GetDisplayName(); } toolTipString = " title='" + toolTip + "'"; } string colour = column.GetColour(i); // Alternating row colours are normally handled by CSS, but we need to handle it explicitly if we have frozen first columns if (columnIndex < stickyColumnCount && colour == null) { colour = stripeColors[i % 2]; } string bgColorString = (colour == null ? "" : " bgcolor=" + colour); bool bold = false; string numericFormat = columnFormatInfoLookup[column].numericFormat; string stringValue = column.GetStringValue(i, true, numericFormat); if (maxColumnStringLength > 0 && stringValue.Length > maxColumnStringLength) { stringValue = TableUtil.SafeTruncateHtmlTableValue(stringValue, maxColumnStringLength); } if (bSpreadsheetFriendlyStrings && !column.isNumeric) { stringValue = "'" + stringValue; } string columnString = " " + (bold ? "" : "") + stringValue + (bold ? "" : "") + ""; htmlFile.Write(columnString); columnIndex++; } htmlFile.WriteLine(""); } htmlFile.WriteLine("

" + title + "

"; } for (int i = 0; i < firstStatColumnIndex; i++) { HeaderRow += "" + columns[i].GetDisplayName() + "" + statName + suffix + "" + prefix.Trim() + "
" + column.GetDisplayName() + "
"); string extraString = ""; if (isCollated && weightByColumnName != null) { extraString += " - weighted avg"; //htmlFile.WriteLine("

Weighted by " + weightByColumnName +"

"); } htmlFile.WriteLine("

Created with PerfReportTool " + VersionString + extraString + "

"); htmlFile.WriteLine(""); htmlFile.Close(); } public SummaryTable SortRows(List rowSortList, bool reverseSort) { List> columnRemapping = new List>(); for (int i = 0; i < rowCount; i++) { string key = ""; foreach (string s in rowSortList) { if (columnLookup.ContainsKey(s.ToLower())) { SummaryTableColumn column = columnLookup[s.ToLower()]; key += "{" + column.GetStringValue(i) + "}"; } else { key += "{}"; } } columnRemapping.Add(new KeyValuePair(key, i)); } columnRemapping.Sort(delegate (KeyValuePair m1, KeyValuePair m2) { return m1.Key.CompareTo(m2.Key); }); // Reorder the metadata rows List newColumns = new List(); foreach (SummaryTableColumn srcCol in columns) { SummaryTableColumn destCol = new SummaryTableColumn(srcCol.name, srcCol.isNumeric); for (int i = 0; i < rowCount; i++) { int srcIndex = columnRemapping[i].Value; int destIndex = reverseSort ? rowCount - 1 - i : i; if (srcCol.isNumeric) { destCol.SetValue(destIndex, srcCol.GetValue(srcIndex)); } else { destCol.SetStringValue(destIndex, srcCol.GetStringValue(srcIndex)); } destCol.SetColourThresholds(destIndex, srcCol.GetColourThresholds(srcIndex)); destCol.SetToolTipValue(destIndex, srcCol.GetToolTipValue(srcIndex)); } newColumns.Add(destCol); } SummaryTable newTable = new SummaryTable(); newTable.columns = newColumns; newTable.rowCount = rowCount; newTable.firstStatColumnIndex = firstStatColumnIndex; newTable.isCollated = isCollated; newTable.InitColumnLookup(); return newTable; } void InitColumnLookup() { columnLookup.Clear(); foreach (SummaryTableColumn col in columns) { columnLookup.Add(col.name.ToLower(), col); } } public void AddRowData(SummaryTableRowData metadata, bool bIncludeCsvStatAverages, bool bIncludeHiddenStats) { foreach (string key in metadata.dict.Keys) { SummaryTableElement value = metadata.dict[key]; if (value.type == SummaryTableElement.Type.CsvStatAverage && !bIncludeCsvStatAverages) { continue; } if (value.GetFlag(SummaryTableElement.Flags.Hidden) && !bIncludeHiddenStats) { continue; } SummaryTableColumn column = null; if (!columnLookup.ContainsKey(key)) { column = new SummaryTableColumn(value.name, value.isNumeric); columnLookup.Add(key, column); columns.Add(column); } else { column = columnLookup[key]; } if (value.isNumeric) { column.SetValue(rowCount, (double)value.numericValue); } else { column.SetStringValue(rowCount, value.value); } column.SetColourThresholds(rowCount, value.colorThresholdList); column.SetToolTipValue(rowCount, value.tooltip); } rowCount++; } public int Count { get { return rowCount; } } Dictionary columnLookup = new Dictionary(); List columns = new List(); List rowWeightings = null; int rowCount = 0; int firstStatColumnIndex = 0; bool isCollated = false; bool hasMinMaxColumns = false; }; }