From b0fa4371baec61916ef8debe83959178604f6138 Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Sat, 14 Sep 2024 11:29:19 +0100 Subject: [PATCH 1/2] Optimise memory allocations in HtmlRender. This is mainly done by avoiding creating additional `StringBuilder`s and writing direct to the `TextWriter` where possible. Where `StringBuilder`s are still needed, they're pooled and reused Saves on the amount of allocated memory and time paused for GC. ## Before > - CommandLine: "s:\ReportGenerator\src\Playground\bin\Debug\net8.0\Playground.exe" > - Runtime Version: V 8.0.824.36612 > - CLR Startup Flags: 8388611 > - Total CPU Time: 38,443 msec > - Total GC CPU Time: 1,666 msec > - Total Allocs : 8,378.712 MB > - Number of Heaps: 1 > - GC CPU MSec/MB Alloc : 0.199 MSec/MB > - Total GC Pause: 2,567.9 msec > - % Time paused for Garbage Collection: 8.1% > - % CPU Time spent Garbage Collecting: 4.3% > - Max GC Heap Size: 335.820 MB > - Peak Process Working Set: 395.698 MB > - Peak Virtual Memory Usage: 2,480,960.369 MB ## After > - CommandLine: "s:\ReportGenerator\src\Playground\bin\Debug\net8.0\Playground.exe" > - Runtime Version: V 8.0.824.36612 > - CLR Startup Flags: 8388611 > - Total CPU Time: 28,247 msec > - Total GC CPU Time: 1,183 msec > - Total Allocs : 2,938.748 MB > - Number of Heaps: 1 > - GC CPU MSec/MB Alloc : 0.403 MSec/MB > - Total GC Pause: 1,348.8 msec > - % Time paused for Garbage Collection: 6.2% > - % CPU Time spent Garbage Collecting: 4.2% > - Max GC Heap Size: 352.258 MB > - Peak Process Working Set: 417.976 MB > - Peak Virtual Memory Usage: 2,480,959.902 MB --- .../ReportGenerator.Core.csproj | 1 + .../Builders/Rendering/HtmlRenderer.cs | 766 +++++++++--------- 2 files changed, 375 insertions(+), 392 deletions(-) diff --git a/src/ReportGenerator.Core/ReportGenerator.Core.csproj b/src/ReportGenerator.Core/ReportGenerator.Core.csproj index 2f4afed6..6f05dd1a 100644 --- a/src/ReportGenerator.Core/ReportGenerator.Core.csproj +++ b/src/ReportGenerator.Core/ReportGenerator.Core.csproj @@ -87,6 +87,7 @@ + all diff --git a/src/ReportGenerator.Core/Reporting/Builders/Rendering/HtmlRenderer.cs b/src/ReportGenerator.Core/Reporting/Builders/Rendering/HtmlRenderer.cs index 7d1309d3..a29996ff 100644 --- a/src/ReportGenerator.Core/Reporting/Builders/Rendering/HtmlRenderer.cs +++ b/src/ReportGenerator.Core/Reporting/Builders/Rendering/HtmlRenderer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -6,6 +7,7 @@ using System.Net; using System.Text; using System.Text.RegularExpressions; +using Microsoft.Extensions.ObjectPool; using Palmmedia.ReportGenerator.Core.CodeAnalysis; using Palmmedia.ReportGenerator.Core.Common; using Palmmedia.ReportGenerator.Core.Logging; @@ -19,27 +21,6 @@ namespace Palmmedia.ReportGenerator.Core.Reporting.Builders.Rendering /// internal class HtmlRenderer : IHtmlRenderer, IDisposable { - /// - /// The head of each generated HTML file. - /// - private const string HtmlStart = @" - - - - - - -{0} - {1} -{2} -
"; - - /// - /// The end of each generated HTML file. - /// - private const string HtmlEnd = @"
-{0} -"; - /// /// The link to the static CSS file. /// @@ -118,7 +99,7 @@ internal HtmlRenderer( this.fileNameByClass = fileNameByClass; this.onlySummary = onlySummary; this.htmlMode = htmlMode; - this.javaScriptContent = new StringBuilder(); + this.javaScriptContent = StringBuilderCache.Get(); this.cssFileResource = cssFileResource; this.additionalCssFileResources = additionalCssFileResource == null ? new string[0] : new[] { additionalCssFileResource }; } @@ -141,7 +122,7 @@ internal HtmlRenderer( this.fileNameByClass = fileNameByClass; this.onlySummary = onlySummary; this.htmlMode = htmlMode; - this.javaScriptContent = new StringBuilder(); + this.javaScriptContent = StringBuilderCache.Get(); this.additionalCssFileResources = additionalCssFileResources ?? new string[0]; this.cssFileResource = cssFileResource; } @@ -159,14 +140,7 @@ public void BeginSummaryReport(string targetDirectory, string fileName, string t Logger.InfoFormat(Resources.WritingReportFile, targetPath); this.CreateTextWriter(targetPath); - using (var cssStream = this.GetCombinedCss()) - { - string style = this.htmlMode == HtmlMode.InlineCssAndJavaScript ? - "" - : CssLink; - - this.reportTextWriter.WriteLine(HtmlStart, WebUtility.HtmlEncode(title), WebUtility.HtmlEncode(ReportResources.CoverageReport), style); - } + this.WriteHtmlStart(this.reportTextWriter, title, ReportResources.CoverageReport); } /// @@ -179,14 +153,7 @@ public void BeginClassReport(string targetDirectory, Assembly assembly, string c Logger.DebugFormat(Resources.WritingReportFile, targetPath); this.CreateTextWriter(Path.Combine(targetDirectory, targetPath)); - using (var cssStream = this.GetCombinedCss()) - { - string style = this.htmlMode == HtmlMode.InlineCssAndJavaScript ? - "" - : CssLink; - - this.reportTextWriter.WriteLine(HtmlStart, WebUtility.HtmlEncode(classDisplayName), WebUtility.HtmlEncode(additionalTitle + ReportResources.CoverageReport), style); - } + this.WriteHtmlStart(this.reportTextWriter, classDisplayName, additionalTitle + ReportResources.CoverageReport); } /// @@ -425,7 +392,6 @@ public void BeginSummaryTable(bool branchCoverageAvailable, bool methodCoverageA this.reportTextWriter.WriteLine(""); this.reportTextWriter.WriteLine(""); this.reportTextWriter.WriteLine(""); - if (branchCoverageAvailable) { this.reportTextWriter.WriteLine(""); @@ -545,88 +511,92 @@ public void CustomSummary(IEnumerable assemblies, IEnumerable h.CodeElementCoverageQuota.GetValueOrDefault().ToString(CultureInfo.InvariantCulture))) + "]"; } - var historicCoveragesSb = new StringBuilder(); - int historicCoveragesCounter = 0; - historicCoveragesSb.Append("["); - foreach (var historicCoverage in @class.HistoricCoverages) + void WriteHistoricCoverage() { - historicCoverageExecutionTimes.Add(historicCoverage.ExecutionTime); - tagsByBistoricCoverageExecutionTime[historicCoverage.ExecutionTime] = historicCoverage.Tag; - - if (historicCoveragesCounter++ > 0) + int historicCoveragesCounter = 0; + this.javaScriptContent.Append("["); + foreach (var historicCoverage in @class.HistoricCoverages) { - historicCoveragesSb.Append(", "); - } + historicCoverageExecutionTimes.Add(historicCoverage.ExecutionTime); + tagsByBistoricCoverageExecutionTime[historicCoverage.ExecutionTime] = historicCoverage.Tag; - historicCoveragesSb.AppendFormat( - "{{ \"et\": \"{0} - {1}{2}{3}\", \"cl\": {4}, \"ucl\": {5}, \"cal\": {6}, \"tl\": {7}, \"lcq\": {8}, \"cb\": {9}, \"tb\": {10}, \"bcq\": {11}, \"cm\": {12}, \"tm\": {13}, \"mcq\": {14} }}", - historicCoverage.ExecutionTime.ToShortDateString(), - historicCoverage.ExecutionTime.ToLongTimeString(), - string.IsNullOrEmpty(historicCoverage.Tag) ? string.Empty : " - ", - historicCoverage.Tag, - historicCoverage.CoveredLines.ToString(CultureInfo.InvariantCulture), - (historicCoverage.CoverableLines - historicCoverage.CoveredLines).ToString(CultureInfo.InvariantCulture), - historicCoverage.CoverableLines.ToString(CultureInfo.InvariantCulture), - historicCoverage.TotalLines.ToString(CultureInfo.InvariantCulture), - historicCoverage.CoverageQuota.GetValueOrDefault().ToString(CultureInfo.InvariantCulture), - historicCoverage.CoveredBranches.ToString(CultureInfo.InvariantCulture), - historicCoverage.TotalBranches.ToString(CultureInfo.InvariantCulture), - historicCoverage.BranchCoverageQuota.GetValueOrDefault().ToString(CultureInfo.InvariantCulture), - methodCoverageAvailable ? historicCoverage.CoveredCodeElements.GetValueOrDefault().ToString(CultureInfo.InvariantCulture) : "0", - methodCoverageAvailable ? historicCoverage.TotalCodeElements.GetValueOrDefault().ToString(CultureInfo.InvariantCulture) : "0", - methodCoverageAvailable ? historicCoverage.CodeElementCoverageQuota.GetValueOrDefault().ToString(CultureInfo.InvariantCulture) : "0"); - } + if (historicCoveragesCounter++ > 0) + { + this.javaScriptContent.Append(", "); + } - historicCoveragesSb.Append("]"); + this.javaScriptContent.AppendFormat( + "{{ \"et\": \"{0} - {1}{2}{3}\", \"cl\": {4}, \"ucl\": {5}, \"cal\": {6}, \"tl\": {7}, \"lcq\": {8}, \"cb\": {9}, \"tb\": {10}, \"bcq\": {11}, \"cm\": {12}, \"tm\": {13}, \"mcq\": {14} }}", + historicCoverage.ExecutionTime.ToShortDateString(), + historicCoverage.ExecutionTime.ToLongTimeString(), + string.IsNullOrEmpty(historicCoverage.Tag) ? string.Empty : " - ", + historicCoverage.Tag, + historicCoverage.CoveredLines.ToString(CultureInfo.InvariantCulture), + (historicCoverage.CoverableLines - historicCoverage.CoveredLines).ToString(CultureInfo.InvariantCulture), + historicCoverage.CoverableLines.ToString(CultureInfo.InvariantCulture), + historicCoverage.TotalLines.ToString(CultureInfo.InvariantCulture), + historicCoverage.CoverageQuota.GetValueOrDefault().ToString(CultureInfo.InvariantCulture), + historicCoverage.CoveredBranches.ToString(CultureInfo.InvariantCulture), + historicCoverage.TotalBranches.ToString(CultureInfo.InvariantCulture), + historicCoverage.BranchCoverageQuota.GetValueOrDefault().ToString(CultureInfo.InvariantCulture), + methodCoverageAvailable ? historicCoverage.CoveredCodeElements.GetValueOrDefault().ToString(CultureInfo.InvariantCulture) : "0", + methodCoverageAvailable ? historicCoverage.TotalCodeElements.GetValueOrDefault().ToString(CultureInfo.InvariantCulture) : "0", + methodCoverageAvailable ? historicCoverage.CodeElementCoverageQuota.GetValueOrDefault().ToString(CultureInfo.InvariantCulture) : "0"); + } - var metricsSb = new StringBuilder(); - int metricsCounter = 0; - metricsSb.Append("{"); + this.javaScriptContent.Append("]"); + } - foreach (var metricGroup in @class.Files.SelectMany(f => f.MethodMetrics).SelectMany(m => m.Metrics).GroupBy(m => m.Name)) + void WriteMetricsCoverage() { - var firstMetric = metricGroup.First(); - metricsByName[firstMetric.Name] = firstMetric; + int metricsCounter = 0; + this.javaScriptContent.Append("{"); - if (!methodCoverageAvailable) + foreach (var metricGroup in @class.Files.SelectMany(f => f.MethodMetrics).SelectMany(m => m.Metrics).GroupBy(m => m.Name)) { - continue; - } + var firstMetric = metricGroup.First(); + metricsByName[firstMetric.Name] = firstMetric; - decimal? value = null; + if (!methodCoverageAvailable) + { + continue; + } - if (firstMetric.MetricType == MetricType.CoverageAbsolute) - { - value = metricGroup.SafeSum(m => m.Value); - } - else - { - // Show worst result on summary page - if (firstMetric.MergeOrder == MetricMergeOrder.HigherIsBetter) + decimal? value = null; + + if (firstMetric.MetricType == MetricType.CoverageAbsolute) { - value = metricGroup.Min(m => m.Value); + value = metricGroup.SafeSum(m => m.Value); } else { - value = metricGroup.Max(m => m.Value); + // Show worst result on summary page + if (firstMetric.MergeOrder == MetricMergeOrder.HigherIsBetter) + { + value = metricGroup.Min(m => m.Value); + } + else + { + value = metricGroup.Max(m => m.Value); + } } - } - if (value.HasValue) - { - if (metricsCounter++ > 0) + if (value.HasValue) { - metricsSb.Append(", "); + if (metricsCounter++ > 0) + { + this.javaScriptContent.Append(", "); + } + + this.javaScriptContent.AppendFormat( + " \"{0}\": {1}", + firstMetric.Abbreviation, + value.Value.ToString(CultureInfo.InvariantCulture)); } - - metricsSb.AppendFormat( - " \"{0}\": {1}", - firstMetric.Abbreviation, - value.Value.ToString(CultureInfo.InvariantCulture)); } - } - metricsSb.Append(" }"); + this.javaScriptContent.Append(" }"); + } this.javaScriptContent.Append(" { "); this.javaScriptContent.AppendFormat("\"name\": \"{0}\",", @class.DisplayName.Replace(@"\", @"\\")); @@ -645,8 +615,13 @@ public void CustomSummary(IEnumerable assemblies, IEnumerable historicCoverages, bool methodCo id, svgHistory); - var series = new StringBuilder(); - series.Append("["); - if (filteredHistoricCoverages.Any(h => h.CoverageQuota.HasValue)) + void WriteSeries(TextWriter series) { - for (int i = 0; i < filteredHistoricCoverages.Count; i++) + series.Write("["); + if (filteredHistoricCoverages.Any(h => h.CoverageQuota.HasValue)) { - if (i > 0) + for (int i = 0; i < filteredHistoricCoverages.Count; i++) { - series.Append(", "); - } + if (i > 0) + { + series.Write(", "); + } - if (filteredHistoricCoverages[i].CoverageQuota.HasValue) - { - series.Append("{ 'meta': "); - series.Append(i); - series.Append(", 'value': "); - series.Append(filteredHistoricCoverages[i].CoverageQuota.Value.ToString(CultureInfo.InvariantCulture)); - series.Append(" }"); - } - else - { - series.Append("null"); + if (filteredHistoricCoverages[i].CoverageQuota.HasValue) + { + series.Write("{ 'meta': "); + series.Write(i); + series.Write(", 'value': "); + series.Write(filteredHistoricCoverages[i].CoverageQuota.Value.ToString(CultureInfo.InvariantCulture)); + series.Write(" }"); + } + else + { + series.Write("null"); + } } } - } - series.AppendLine("],"); - series.Append("["); + series.WriteLine("],"); + series.Write("["); - if (filteredHistoricCoverages.Any(h => h.BranchCoverageQuota.HasValue)) - { - for (int i = 0; i < filteredHistoricCoverages.Count; i++) + if (filteredHistoricCoverages.Any(h => h.BranchCoverageQuota.HasValue)) { - if (i > 0) + for (int i = 0; i < filteredHistoricCoverages.Count; i++) { - series.Append(", "); - } + if (i > 0) + { + series.Write(", "); + } - if (filteredHistoricCoverages[i].BranchCoverageQuota.HasValue) - { - series.Append("{ 'meta': "); - series.Append(i); - series.Append(", 'value': "); - series.Append(filteredHistoricCoverages[i].BranchCoverageQuota.Value.ToString(CultureInfo.InvariantCulture)); - series.Append(" }"); - } - else - { - series.Append("null"); + if (filteredHistoricCoverages[i].BranchCoverageQuota.HasValue) + { + series.Write("{ 'meta': "); + series.Write(i); + series.Write(", 'value': "); + series.Write(filteredHistoricCoverages[i].BranchCoverageQuota.Value.ToString(CultureInfo.InvariantCulture)); + series.Write(" }"); + } + else + { + series.Write("null"); + } } } - } - series.AppendLine("],"); - series.Append("["); + series.WriteLine("],"); + series.Write("["); - if (methodCoverageAvailable && filteredHistoricCoverages.Any(h => h.CodeElementCoverageQuota.HasValue)) - { - for (int i = 0; i < filteredHistoricCoverages.Count; i++) + if (methodCoverageAvailable && filteredHistoricCoverages.Any(h => h.CodeElementCoverageQuota.HasValue)) { - if (i > 0) + for (int i = 0; i < filteredHistoricCoverages.Count; i++) { - series.Append(", "); - } + if (i > 0) + { + series.Write(", "); + } - if (filteredHistoricCoverages[i].CodeElementCoverageQuota.HasValue) - { - series.Append("{ 'meta': "); - series.Append(i); - series.Append(", 'value': "); - series.Append(filteredHistoricCoverages[i].CodeElementCoverageQuota.Value.ToString(CultureInfo.InvariantCulture)); - series.Append(" }"); - } - else - { - series.Append("null"); + if (filteredHistoricCoverages[i].CodeElementCoverageQuota.HasValue) + { + series.Write("{ 'meta': "); + series.Write(i); + series.Write(", 'value': "); + series.Write(filteredHistoricCoverages[i].CodeElementCoverageQuota.Value.ToString(CultureInfo.InvariantCulture)); + series.Write(" }"); + } + else + { + series.Write("null"); + } } } - } - series.AppendLine("]"); + series.WriteLine("]"); + } var toolTips = filteredHistoricCoverages.Select(h => string.Format( @@ -1146,9 +1123,10 @@ public void Chart(IEnumerable historicCoverages, bool methodCo this.reportTextWriter.WriteLine(""; + this.reportTextWriter.WriteLine(""); if (this.htmlMode == HtmlMode.ExternalCssAndJavaScriptWithQueryStringHandling) { // #349: Apply query string to referenced CSS and JavaScript files and links - javascript = $@""; +"); } else if (this.htmlMode == HtmlMode.InlineCssAndJavaScript) { - using (var javaScriptStream = this.GetCombinedJavascript()) - { - javascript = ""; - } + this.reportTextWriter.Write(""); + } + else + { + this.reportTextWriter.WriteLine($""); } - this.reportTextWriter.Write(HtmlEnd, javascript); + this.reportTextWriter.Write(""); } /// @@ -1954,5 +1900,41 @@ private List FilterHistoricCoverages(IEnumerable Cache = new ConcurrentDictionary(); + + public static string Get(string resourceName) => Cache.GetOrAdd(resourceName, v => + { + using (Stream stream = typeof(HtmlRenderer).Assembly.GetManifestResourceStream("Palmmedia.ReportGenerator.Core.Reporting.Builders.Rendering.resources." + resourceName)) + { + using (var reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + }); + } + + private static class StringBuilderCache + { + private static readonly DefaultObjectPool Pool = new DefaultObjectPool(new StringBuilderPooledObjectPolicy + { + InitialCapacity = 4096, + MaximumRetainedCapacity = 1 * 1024 * 1024 + }); + + internal static StringBuilder Get() => Pool.Get(); + + internal static void Return(StringBuilder builder) => Pool.Return(builder); + + internal static string ToStringAndReturnToPool(StringBuilder builder) + { + var result = builder.ToString(); + Pool.Return(builder); + return result; + } + } } } From 39597745a50f8b6950fef3138e9859d54b2d1e74 Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Thu, 26 Sep 2024 09:45:33 +0100 Subject: [PATCH 2/2] Fix bugs --- .../Builders/Rendering/HtmlRenderer.cs | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/ReportGenerator.Core/Reporting/Builders/Rendering/HtmlRenderer.cs b/src/ReportGenerator.Core/Reporting/Builders/Rendering/HtmlRenderer.cs index a29996ff..d541903b 100644 --- a/src/ReportGenerator.Core/Reporting/Builders/Rendering/HtmlRenderer.cs +++ b/src/ReportGenerator.Core/Reporting/Builders/Rendering/HtmlRenderer.cs @@ -1125,7 +1125,7 @@ void WriteSeries(TextWriter series) this.reportTextWriter.WriteLine("var historyChartData{0} = {{", id); this.reportTextWriter.Write(" \"series\" : ["); WriteSeries(this.reportTextWriter); - this.reportTextWriter.WriteLine("],\""); + this.reportTextWriter.WriteLine("],"); this.reportTextWriter.WriteLine( " \"tooltips\" : [{0}]", @@ -1580,27 +1580,33 @@ private void SaveCss(string targetDirectory) return; } - using (var fs = new FileStream(targetPath, FileMode.Create)) + if (this.htmlMode == HtmlMode.InlineCssAndJavaScript) { - if (this.htmlMode != HtmlMode.InlineCssAndJavaScript) + using (var fs = new FileStream(targetPath, FileMode.Create)) + using (var writer = new StreamWriter(fs)) { - var builder = StringBuilderCache.Get(); - using (var writer = new StringWriter(builder)) - { - this.WriteCss(writer); - } + this.WriteCss(writer); + } + } + else + { + var builder = StringBuilderCache.Get(); + using (var writer = new StringWriter(builder)) + { + this.WriteCss(writer); + } - string css = StringBuilderCache.ToStringAndReturnToPool(builder); - var matches = Regex.Matches(css, @"url\(icon_(?.+).svg\),\surl\(data:image/svg\+xml;base64,(?.+)\)"); + string css = StringBuilderCache.ToStringAndReturnToPool(builder); - foreach (Match match in matches) - { - System.IO.File.WriteAllBytes( - Path.Combine(targetDirectory, "icon_" + match.Groups["filename"].Value + ".svg"), - Convert.FromBase64String(match.Groups["base64image"].Value)); - } + System.IO.File.WriteAllText(targetPath, css); + + var matches = Regex.Matches(css, @"url\(icon_(?.+).svg\),\surl\(data:image/svg\+xml;base64,(?.+)\)"); - StringBuilderCache.Return(builder); + foreach (Match match in matches) + { + System.IO.File.WriteAllBytes( + Path.Combine(targetDirectory, "icon_" + match.Groups["filename"].Value + ".svg"), + Convert.FromBase64String(match.Groups["base64image"].Value)); } }