From 9ef2ae9d2810d0967239588edcd9c2521021c2c9 Mon Sep 17 00:00:00 2001 From: Jason Finch Date: Sun, 29 Mar 2026 09:19:10 +1000 Subject: [PATCH 1/2] Fix performance benchmark TFM compatibility Multi-target AngleSharp.Performance.Css to net472;net10.0 so that Alba.CsCss (which only supports net462/net472) is excluded from the net10.0 build. Update ExCSS from 2.0.6 to 4.3.1 and adapt to its renamed StylesheetParser API. --- .../AngleSharp.Performance.Css.csproj | 9 ++++++--- src/AngleSharp.Performance.Css/CsCssParser.cs | 4 +++- src/AngleSharp.Performance.Css/ExCssParser.cs | 4 ++-- src/AngleSharp.Performance.Css/Program.cs | 2 ++ 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/AngleSharp.Performance.Css/AngleSharp.Performance.Css.csproj b/src/AngleSharp.Performance.Css/AngleSharp.Performance.Css.csproj index e48c8dde..9670b169 100644 --- a/src/AngleSharp.Performance.Css/AngleSharp.Performance.Css.csproj +++ b/src/AngleSharp.Performance.Css/AngleSharp.Performance.Css.csproj @@ -1,6 +1,6 @@ - net10.0 + net472;net10.0 Exe @@ -26,8 +26,11 @@ - - + + + + + \ No newline at end of file diff --git a/src/AngleSharp.Performance.Css/CsCssParser.cs b/src/AngleSharp.Performance.Css/CsCssParser.cs index 915051e0..dcf4b197 100644 --- a/src/AngleSharp.Performance.Css/CsCssParser.cs +++ b/src/AngleSharp.Performance.Css/CsCssParser.cs @@ -1,4 +1,5 @@ -namespace AngleSharp.Performance.Css +#if NET472 +namespace AngleSharp.Performance.Css { using Alba.CsCss.Style; using System; @@ -24,3 +25,4 @@ public void Run(String source) } } } +#endif diff --git a/src/AngleSharp.Performance.Css/ExCssParser.cs b/src/AngleSharp.Performance.Css/ExCssParser.cs index e389aa8c..c73d0cdb 100644 --- a/src/AngleSharp.Performance.Css/ExCssParser.cs +++ b/src/AngleSharp.Performance.Css/ExCssParser.cs @@ -7,11 +7,11 @@ class ExCssParser : ITestee { public String Name => "ExCSS"; - public Type Library => typeof(Parser); + public Type Library => typeof(StylesheetParser); public void Run(String source) { - var parser = new Parser(); + var parser = new StylesheetParser(); parser.Parse(source); } } diff --git a/src/AngleSharp.Performance.Css/Program.cs b/src/AngleSharp.Performance.Css/Program.cs index 27571603..11898bd1 100644 --- a/src/AngleSharp.Performance.Css/Program.cs +++ b/src/AngleSharp.Performance.Css/Program.cs @@ -16,7 +16,9 @@ static void Main(String[] args) { new AngleSharpParser(), new ExCssParser(), +#if NET472 new CsCssParser(), +#endif }; var testsuite = new TestSuite(parsers, stylesheets.Tests, new Output(), new Warmup()) From d4213e64d27d4a644f81caf6d0e660cec7a321eb Mon Sep 17 00:00:00 2001 From: Jason Finch Date: Sun, 29 Mar 2026 09:34:20 +1000 Subject: [PATCH 2/2] Convert performance benchmarks to BenchmarkDotNet Replace the custom TestSuite/ITestee infrastructure with BenchmarkDotNet for statistically rigorous benchmarking with memory diagnostics. Inline parser logic into a single CssParserBenchmarks class parameterized by CSS sample file. Add benchmark usage instructions to README. --- README.md | 14 ++++ .../AngleSharp.Performance.Css.csproj | 17 ++-- .../AngleSharpParser.cs | 26 ------- src/AngleSharp.Performance.Css/CsCssParser.cs | 28 ------- .../CssParserBenchmarks.cs | 77 +++++++++++++++++++ src/AngleSharp.Performance.Css/ExCssParser.cs | 18 ----- src/AngleSharp.Performance.Css/Program.cs | 27 +------ 7 files changed, 99 insertions(+), 108 deletions(-) delete mode 100644 src/AngleSharp.Performance.Css/AngleSharpParser.cs delete mode 100644 src/AngleSharp.Performance.Css/CsCssParser.cs create mode 100644 src/AngleSharp.Performance.Css/CssParserBenchmarks.cs delete mode 100644 src/AngleSharp.Performance.Css/ExCssParser.cs diff --git a/README.md b/README.md index 431b3aa8..4ecccc76 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,20 @@ The main idea behind AngleSharp.Css is to expose the CSSOM as it would be in the - Calculated values (i.e., `calc(20px + 50%)`) - Window-based declaration calculations, see `window.GetComputedStyle` +## Benchmarks + +The `AngleSharp.Performance.Css` project uses [BenchmarkDotNet](https://benchmarkdotnet.org/) to compare CSS parsing performance across libraries. Run the benchmarks in Release mode: + +```bash +dotnet run --project src/AngleSharp.Performance.Css/AngleSharp.Performance.Css.csproj -c Release --framework net10.0 +``` + +To run a quick smoke test instead of a full benchmark: + +```bash +dotnet run --project src/AngleSharp.Performance.Css/AngleSharp.Performance.Css.csproj -c Release --framework net10.0 -- --job short +``` + ## Participating Participation in the project is highly welcome. For this project the same rules as for the AngleSharp core project may be applied. diff --git a/src/AngleSharp.Performance.Css/AngleSharp.Performance.Css.csproj b/src/AngleSharp.Performance.Css/AngleSharp.Performance.Css.csproj index 9670b169..0891ee5c 100644 --- a/src/AngleSharp.Performance.Css/AngleSharp.Performance.Css.csproj +++ b/src/AngleSharp.Performance.Css/AngleSharp.Performance.Css.csproj @@ -1,14 +1,8 @@ net472;net10.0 - Exe - - false - default - - - AnyCPU + 9 @@ -18,8 +12,6 @@ - - netstandard2.0 @@ -27,10 +19,11 @@ - + + - + - \ No newline at end of file + diff --git a/src/AngleSharp.Performance.Css/AngleSharpParser.cs b/src/AngleSharp.Performance.Css/AngleSharpParser.cs deleted file mode 100644 index b1b30ded..00000000 --- a/src/AngleSharp.Performance.Css/AngleSharpParser.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace AngleSharp.Performance.Css -{ - using AngleSharp; - using AngleSharp.Css.Parser; - using System; - - class AngleSharpParser : ITestee - { - private static readonly CssParserOptions options = new CssParserOptions - { - IsIncludingUnknownDeclarations = true, - IsIncludingUnknownRules = true, - IsToleratingInvalidSelectors = true, - }; - private static readonly CssParser parser = new CssParser(options); - - public String Name => "AngleSharp"; - - public Type Library => typeof(BrowsingContext); - - public void Run(String source) - { - parser.ParseStyleSheet(source); - } - } -} diff --git a/src/AngleSharp.Performance.Css/CsCssParser.cs b/src/AngleSharp.Performance.Css/CsCssParser.cs deleted file mode 100644 index dcf4b197..00000000 --- a/src/AngleSharp.Performance.Css/CsCssParser.cs +++ /dev/null @@ -1,28 +0,0 @@ -#if NET472 -namespace AngleSharp.Performance.Css -{ - using Alba.CsCss.Style; - using System; - - class CsCssParser : ITestee - { - public String Name => "CsCss"; - - public Type Library => typeof(CssLoader); - - public void Run(String source) - { - var parser = new CssLoader { Compatibility = Alba.CsCss.BrowserCompatibility.FullStandards }; - - try - { - parser.ParseSheet(source, new Uri("http://localhost/foo.css"), new Uri("http://localhost")); - } - catch - { - // May crash otherwise if invalid input detected ... - } - } - } -} -#endif diff --git a/src/AngleSharp.Performance.Css/CssParserBenchmarks.cs b/src/AngleSharp.Performance.Css/CssParserBenchmarks.cs new file mode 100644 index 00000000..771cb0a4 --- /dev/null +++ b/src/AngleSharp.Performance.Css/CssParserBenchmarks.cs @@ -0,0 +1,77 @@ +namespace AngleSharp.Performance.Css +{ + using AngleSharp.Css.Parser; + using BenchmarkDotNet.Attributes; + using ExCSS; + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + + [MemoryDiagnoser] + public class CssParserBenchmarks + { + private static readonly CssParserOptions AngleSharpOptions = new CssParserOptions + { + IsIncludingUnknownDeclarations = true, + IsIncludingUnknownRules = true, + IsToleratingInvalidSelectors = true, + }; + + private static readonly CssParser AngleSharpParser = new CssParser(AngleSharpOptions); + + private string _source = null!; + + [ParamsSource(nameof(CssFileNames))] + public string CssFileName { get; set; } = null!; + + public static IEnumerable CssFileNames() + { + var dir = Path.Combine(AppContext.BaseDirectory, "Samples"); + + return Directory.GetFiles(dir, "*.css") + .OrderBy(f => f) + .Select(f => Path.GetFileNameWithoutExtension(f)); + } + + [GlobalSetup] + public void Setup() + { + var path = Path.Combine(AppContext.BaseDirectory, "Samples", CssFileName + ".css"); + _source = File.ReadAllText(path); + } + + [Benchmark(Baseline = true)] + public object AngleSharp() + { + return AngleSharpParser.ParseStyleSheet(_source); + } + + [Benchmark] + public object ExCss() + { + var parser = new StylesheetParser(); + return parser.Parse(_source); + } + +#if NET472 + [Benchmark] + public object CsCss() + { + var parser = new Alba.CsCss.Style.CssLoader + { + Compatibility = Alba.CsCss.BrowserCompatibility.FullStandards + }; + + try + { + return parser.ParseSheet(_source, new Uri("http://localhost/foo.css"), new Uri("http://localhost")); + } + catch + { + return null!; + } + } +#endif + } +} diff --git a/src/AngleSharp.Performance.Css/ExCssParser.cs b/src/AngleSharp.Performance.Css/ExCssParser.cs deleted file mode 100644 index c73d0cdb..00000000 --- a/src/AngleSharp.Performance.Css/ExCssParser.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace AngleSharp.Performance.Css -{ - using System; - using ExCSS; - - class ExCssParser : ITestee - { - public String Name => "ExCSS"; - - public Type Library => typeof(StylesheetParser); - - public void Run(String source) - { - var parser = new StylesheetParser(); - parser.Parse(source); - } - } -} diff --git a/src/AngleSharp.Performance.Css/Program.cs b/src/AngleSharp.Performance.Css/Program.cs index 11898bd1..7dde577c 100644 --- a/src/AngleSharp.Performance.Css/Program.cs +++ b/src/AngleSharp.Performance.Css/Program.cs @@ -1,33 +1,12 @@ namespace AngleSharp.Performance.Css { - using System; - using System.Collections.Generic; - using System.IO; + using BenchmarkDotNet.Running; class Program { - static void Main(String[] args) + static void Main(string[] args) { - var samplesDir = Path.Combine(AppContext.BaseDirectory, "Samples"); - var stylesheets = new FileTests() - .IncludeFromDirectory(samplesDir); - - var parsers = new List - { - new AngleSharpParser(), - new ExCssParser(), -#if NET472 - new CsCssParser(), -#endif - }; - - var testsuite = new TestSuite(parsers, stylesheets.Tests, new Output(), new Warmup()) - { - NumberOfRepeats = 5, - NumberOfReRuns = 1, - }; - - testsuite.Run(); + BenchmarkRunner.Run(args: args); } } }