diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f9da92..abc1dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.1.3 + +Released on Saturday, September 12 2026 + +- Improved pseudo-class matching to skip a per-element `ConditionalWeakTable` probe when no state has ever been forced via `SetPseudoClass` + # 1.1.2 Released on Friday, September 11 2026 diff --git a/src/AngleSharp.Css.Docs/package.json b/src/AngleSharp.Css.Docs/package.json index d68f95c..1459308 100644 --- a/src/AngleSharp.Css.Docs/package.json +++ b/src/AngleSharp.Css.Docs/package.json @@ -1,6 +1,6 @@ { "name": "@anglesharp/css", - "version": "1.1.2", + "version": "1.1.3", "preview": true, "description": "The doclet for the AngleSharp.Css documentation.", "keywords": [ diff --git a/src/AngleSharp.Css/PseudoClassStateStore.cs b/src/AngleSharp.Css/PseudoClassStateStore.cs index 1dd69ab..448ee59 100644 --- a/src/AngleSharp.Css/PseudoClassStateStore.cs +++ b/src/AngleSharp.Css/PseudoClassStateStore.cs @@ -14,12 +14,24 @@ static class PseudoClassStateStore { private static readonly ConditionalWeakTable> _states = new(); - public static void Set(IElement element, String pseudoClass, Boolean value) => + // WithCss() wraps every non-:focus pseudo-class selector in ForcingPseudoClassSelector, so + // TryGet runs once per pseudo-class match attempt on every element - a page that never + // calls SetPseudoClass still paid a ConditionalWeakTable probe for every :hover, :disabled, + // :checked, ... match. This flag only ever moves from "nothing forced" to "something + // forced": Remove/Clear cannot prove every forced state everywhere has been undone (there + // is no per-element or per-process count to check), so it deliberately never goes back to + // false. Once anything has been forced, the process pays the old per-match cost again. + private static volatile Boolean _anyForced; + + public static void Set(IElement element, String pseudoClass, Boolean value) + { + _anyForced = true; _states.GetValue(element, _ => new Dictionary(StringComparer.OrdinalIgnoreCase))[pseudoClass] = value; + } public static Boolean TryGet(IElement element, String pseudoClass, out Boolean value) { - if (_states.TryGetValue(element, out var state) && state.TryGetValue(pseudoClass, out value)) + if (_anyForced && _states.TryGetValue(element, out var state) && state.TryGetValue(pseudoClass, out value)) { return true; } diff --git a/src/AngleSharp.Performance.Css/CssCascadeBenchmarks.cs b/src/AngleSharp.Performance.Css/CssCascadeBenchmarks.cs index 06965c2..5043005 100644 --- a/src/AngleSharp.Performance.Css/CssCascadeBenchmarks.cs +++ b/src/AngleSharp.Performance.Css/CssCascadeBenchmarks.cs @@ -124,6 +124,20 @@ private static String BuildDocument() sb.Append("h1,h2,h3,p.big,a:hover,.c-1 span{font-weight:bold;letter-spacing:0.02em;}"); sb.Append(".hidden{display:none}.big{font-size:24px}a{color:blue}"); + + // WithCss() wraps every non-:focus pseudo-class selector so a caller-forced state + // (ElementExtensions.SetPseudoClass) can override it - so every rule below probes a + // ConditionalWeakTable per candidate element even though this benchmark never forces + // anything. One rule per child class per interaction pseudo-class keeps every sampled + // "div.child-*" reaching that probe instead of being short-circuited by an earlier, + // non-matching simple selector. + for (var i = 0; i < 20; i++) + { + sb.Append(".child-").Append(i).Append(":hover{outline:1px solid red;}"); + sb.Append(".child-").Append(i).Append(":active{outline:1px solid green;}"); + sb.Append(".child-").Append(i).Append(":disabled{outline:1px solid blue;}"); + } + sb.Append("
"); for (var s = 0; s < 25; s++) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index beb3e0d..04ce268 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ Extends the CSSOM from the core AngleSharp library. AngleSharp.Css - 1.1.2 + 1.1.3 enable latest true