Skip to content

Commit 9c9803e

Browse files
authored
Merge pull request #212 from jafin/perf/css-parser-optimizations-clean
Optimize CSS parser performance
2 parents c434e6e + 525ff33 commit 9c9803e

8 files changed

Lines changed: 214 additions & 119 deletions

File tree

src/AngleSharp.Css/Converters/DictionaryValueConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public ICssValue Convert(StringSource source)
2525

2626
if (ident != null && _values.TryGetValue(ident, out mode))
2727
{
28-
return new CssConstantValue<T>(ident.ToLowerInvariant(), mode);
28+
return new CssConstantValue<T>(ident.ToLowerFast(), mode);
2929
}
3030

3131
source.BackTo(pos);

src/AngleSharp.Css/Converters/PeriodicValueConverter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public ICssValue Convert(StringSource source)
3434

3535
if (length > 0)
3636
{
37+
if (length == 4)
38+
{
39+
return new CssPeriodicValue(options);
40+
}
41+
3742
var values = new ICssValue[length];
3843
Array.Copy(options, values, length);
3944
return new CssPeriodicValue(values);

src/AngleSharp.Css/Dom/Internal/CssProperty.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal class CssProperty : ICssProperty
2929

3030
internal CssProperty(String name, IValueConverter converter, PropertyFlags flags = PropertyFlags.None, ICssValue value = null, Boolean important = false)
3131
{
32-
_name = name.StartsWith("--") ? name : name.ToLowerInvariant();
32+
_name = name.StartsWith("--") ? name : name.ToLowerFast();
3333
_converter = converter;
3434
_flags = flags;
3535
_value = value;

src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ sealed class CssStyleDeclaration : ICssStyleDeclaration
1919
#region Fields
2020

2121
private readonly List<ICssProperty> _declarations;
22+
private readonly Dictionary<String, Int32> _declarationIndex;
2223
private readonly IBrowsingContext _context;
2324
private ICssRule _parent;
2425
private Boolean _updating;
@@ -36,6 +37,7 @@ sealed class CssStyleDeclaration : ICssStyleDeclaration
3637
public CssStyleDeclaration(IBrowsingContext context)
3738
{
3839
_declarations = new List<ICssProperty>();
40+
_declarationIndex = new Dictionary<String, Int32>(StringComparer.OrdinalIgnoreCase);
3941
_context = context;
4042
}
4143

@@ -77,9 +79,9 @@ public String CssText
7779

7880
public ICssProperty GetProperty(String name)
7981
{
80-
for (var i = 0; i < _declarations.Count; i++)
82+
if (_declarationIndex.TryGetValue(name, out var index) && index < _declarations.Count)
8183
{
82-
var declaration = _declarations[i];
84+
var declaration = _declarations[index];
8385

8486
if (declaration.Name.Isi(name))
8587
{
@@ -100,6 +102,7 @@ public void Update(String value)
100102
if (!_updating)
101103
{
102104
_declarations.Clear();
105+
_declarationIndex.Clear();
103106

104107
if (!String.IsNullOrEmpty(value))
105108
{
@@ -109,6 +112,7 @@ public void Update(String value)
109112
if (decl != null)
110113
{
111114
_declarations.AddRange(decl);
115+
RebuildIndex();
112116
}
113117
}
114118
}
@@ -304,12 +308,14 @@ public void SetProperty(String propertyName, String propertyValue, String priori
304308

305309
public void AddProperty(ICssProperty declaration)
306310
{
311+
_declarationIndex[declaration.Name] = _declarations.Count;
307312
_declarations.Add(declaration);
308313
}
309314

310315
public void RemoveProperty(ICssProperty declaration)
311316
{
312317
_declarations.Remove(declaration);
318+
RebuildIndex();
313319
}
314320

315321
#endregion
@@ -359,14 +365,24 @@ private void RemovePropertyByName(String propertyName)
359365
var info = _context.GetDeclarationInfo(propertyName);
360366
var longhands = info.Longhands;
361367

362-
for (var i = 0; i < _declarations.Count; i++)
368+
if (_declarationIndex.TryGetValue(propertyName, out var index) && index < _declarations.Count &&
369+
_declarations[index].Name.Is(propertyName))
363370
{
364-
var declaration = _declarations[i];
365-
366-
if (declaration.Name.Is(propertyName))
371+
_declarations.RemoveAt(index);
372+
RebuildIndex();
373+
}
374+
else
375+
{
376+
for (var i = 0; i < _declarations.Count; i++)
367377
{
368-
_declarations.RemoveAt(i);
369-
break;
378+
var declaration = _declarations[i];
379+
380+
if (declaration.Name.Is(propertyName))
381+
{
382+
_declarations.RemoveAt(i);
383+
RebuildIndex();
384+
break;
385+
}
370386
}
371387
}
372388

@@ -389,23 +405,39 @@ private void ChangeDeclarations(IEnumerable<ICssProperty> decls, Predicate<ICssP
389405
{
390406
var skip = defaultSkip(newdecl);
391407

392-
for (var i = 0; i < _declarations.Count; i++)
408+
if (_declarationIndex.TryGetValue(newdecl.Name, out var idx) && idx < _declarations.Count &&
409+
_declarations[idx].Name.Is(newdecl.Name))
393410
{
394-
var olddecl = _declarations[i];
395-
396-
if (olddecl.Name.Is(newdecl.Name))
411+
if (removeExisting.Invoke(_declarations[idx], newdecl))
397412
{
398-
if (removeExisting.Invoke(olddecl, newdecl))
399-
{
400-
_declarations.RemoveAt(i);
401-
skip = false;
402-
}
403-
else
413+
_declarations.RemoveAt(idx);
414+
skip = false;
415+
}
416+
else
417+
{
418+
skip = true;
419+
}
420+
}
421+
else
422+
{
423+
for (var i = 0; i < _declarations.Count; i++)
424+
{
425+
var olddecl = _declarations[i];
426+
427+
if (olddecl.Name.Is(newdecl.Name))
404428
{
405-
skip = true;
406-
}
429+
if (removeExisting.Invoke(olddecl, newdecl))
430+
{
431+
_declarations.RemoveAt(i);
432+
skip = false;
433+
}
434+
else
435+
{
436+
skip = true;
437+
}
407438

408-
break;
439+
break;
440+
}
409441
}
410442
}
411443

@@ -416,13 +448,14 @@ private void ChangeDeclarations(IEnumerable<ICssProperty> decls, Predicate<ICssP
416448
}
417449

418450
_declarations.AddRange(declarations);
451+
RebuildIndex();
419452
}
420453

421454
private void SetLonghand(ICssProperty property)
422455
{
423-
for (var i = 0; i < _declarations.Count; i++)
456+
if (_declarationIndex.TryGetValue(property.Name, out var index) && index < _declarations.Count)
424457
{
425-
var declaration = _declarations[i];
458+
var declaration = _declarations[index];
426459

427460
if (declaration.Name.Is(property.Name))
428461
{
@@ -431,11 +464,12 @@ private void SetLonghand(ICssProperty property)
431464
return;
432465
}
433466

434-
_declarations[i] = property;
467+
_declarations[index] = property;
435468
return;
436469
}
437470
}
438471

472+
_declarationIndex[property.Name] = _declarations.Count;
439473
_declarations.Add(property);
440474
}
441475

@@ -452,6 +486,16 @@ private void SetShorthand(ICssProperty shorthand)
452486
}
453487
}
454488

489+
private void RebuildIndex()
490+
{
491+
_declarationIndex.Clear();
492+
493+
for (var i = 0; i < _declarations.Count; i++)
494+
{
495+
_declarationIndex[_declarations[i].Name] = i;
496+
}
497+
}
498+
455499
private void RaiseChanged()
456500
{
457501
if (!_updating)

src/AngleSharp.Css/Extensions/StringExtensions.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,24 @@ public static String CssUrl(this String value)
8787
var argument = value.CssString();
8888
return Keywords.Url.CssFunction(argument);
8989
}
90+
91+
/// <summary>
92+
/// Converts to lowercase, returning the original string if it is already lowercase.
93+
/// Avoids allocation for the common case of already-lowercase CSS identifiers.
94+
/// </summary>
95+
internal static String ToLowerFast(this String value)
96+
{
97+
for (var i = 0; i < value.Length; i++)
98+
{
99+
var c = value[i];
100+
101+
if (c >= 'A' && c <= 'Z')
102+
{
103+
return value.ToLowerInvariant();
104+
}
105+
}
106+
107+
return value;
108+
}
90109
}
91110
}

src/AngleSharp.Css/Parser/CssBuilder.cs

Lines changed: 47 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
#nullable disable
22
namespace AngleSharp.Css.Parser
33
{
4+
using AngleSharp.Common;
45
using AngleSharp.Css.Dom;
56
using AngleSharp.Css.Parser.Tokens;
67
using AngleSharp.Dom;
78
using AngleSharp.Text;
89
using System;
10+
using System.Collections.Generic;
911

1012
/// <summary>
1113
/// See http://dev.w3.org/csswg/css-syntax/#parsing for details.
@@ -14,6 +16,22 @@ sealed class CssBuilder
1416
{
1517
#region Fields
1618

19+
private static readonly Dictionary<String, Int32> AtRuleMap = new Dictionary<String, Int32>(StringComparer.OrdinalIgnoreCase)
20+
{
21+
{ RuleNames.Media, 1 },
22+
{ RuleNames.FontFace, 2 },
23+
{ RuleNames.Keyframes, 3 },
24+
{ RuleNames.Import, 4 },
25+
{ RuleNames.Charset, 5 },
26+
{ RuleNames.Namespace, 6 },
27+
{ RuleNames.Page, 7 },
28+
{ RuleNames.Supports, 8 },
29+
{ RuleNames.ViewPort, 9 },
30+
{ RuleNames.Document, 10 },
31+
{ RuleNames.CounterStyle, 11 },
32+
{ RuleNames.FontFeatureValues, 12 },
33+
};
34+
1735
private readonly CssTokenizer _tokenizer;
1836
private readonly CssParserOptions _options;
1937
private readonly IBrowsingContext _context;
@@ -67,67 +85,26 @@ private ICssRule CreateStyleRule(ICssStyleSheet sheet, CssToken token)
6785

6886
private ICssRule CreateAtRule(ICssStyleSheet sheet, CssToken token)
6987
{
70-
if (token.Data.Is(RuleNames.Media))
71-
{
72-
var rule = new CssMediaRule(sheet);
73-
return CreateMedia(rule, token);
74-
}
75-
else if (token.Data.Is(RuleNames.FontFace))
76-
{
77-
var rule = new CssFontFaceRule(sheet);
78-
return CreateFontFace(rule, token);
79-
}
80-
else if (token.Data.Is(RuleNames.Keyframes))
81-
{
82-
var rule = new CssKeyframesRule(sheet);
83-
return CreateKeyframes(rule, token);
84-
}
85-
else if (token.Data.Is(RuleNames.Import))
86-
{
87-
var rule = new CssImportRule(sheet);
88-
return CreateImport(rule, token);
89-
}
90-
else if (token.Data.Is(RuleNames.Charset))
91-
{
92-
var rule = new CssCharsetRule(sheet);
93-
return CreateCharset(rule, token);
94-
}
95-
else if (token.Data.Is(RuleNames.Namespace))
96-
{
97-
var rule = new CssNamespaceRule(sheet);
98-
return CreateNamespace(rule, token);
99-
}
100-
else if (token.Data.Is(RuleNames.Page))
101-
{
102-
var rule = new CssPageRule(sheet);
103-
return CreatePage(rule, token);
104-
}
105-
else if (token.Data.Is(RuleNames.Supports))
88+
if (AtRuleMap.TryGetValue(token.Data, out var ruleId))
10689
{
107-
var rule = new CssSupportsRule(sheet);
108-
return CreateSupports(rule, token);
109-
}
110-
else if (token.Data.Is(RuleNames.ViewPort))
111-
{
112-
var rule = new CssViewportRule(sheet);
113-
return CreateViewport(rule, token);
114-
}
115-
else if (token.Data.Is(RuleNames.Document))
116-
{
117-
var rule = new CssDocumentRule(sheet);
118-
return CreateDocument(rule, token);
119-
}
120-
else if (token.Data.Is(RuleNames.CounterStyle))
121-
{
122-
var rule = new CssCounterStyleRule(sheet);
123-
return CreateCounterStyle(rule, token);
124-
}
125-
else if (token.Data.Is(RuleNames.FontFeatureValues))
126-
{
127-
var rule = new CssFontFeatureValuesRule(sheet);
128-
return CreateFontFeatureValues(rule, token);
90+
switch (ruleId)
91+
{
92+
case 1: return CreateMedia(new CssMediaRule(sheet), token);
93+
case 2: return CreateFontFace(new CssFontFaceRule(sheet), token);
94+
case 3: return CreateKeyframes(new CssKeyframesRule(sheet), token);
95+
case 4: return CreateImport(new CssImportRule(sheet), token);
96+
case 5: return CreateCharset(new CssCharsetRule(sheet), token);
97+
case 6: return CreateNamespace(new CssNamespaceRule(sheet), token);
98+
case 7: return CreatePage(new CssPageRule(sheet), token);
99+
case 8: return CreateSupports(new CssSupportsRule(sheet), token);
100+
case 9: return CreateViewport(new CssViewportRule(sheet), token);
101+
case 10: return CreateDocument(new CssDocumentRule(sheet), token);
102+
case 11: return CreateCounterStyle(new CssCounterStyleRule(sheet), token);
103+
case 12: return CreateFontFeatureValues(new CssFontFeatureValuesRule(sheet), token);
104+
}
129105
}
130-
else if (_options.IsIncludingUnknownRules)
106+
107+
if (_options.IsIncludingUnknownRules)
131108
{
132109
return CreateUnknownAtRule(sheet, token);
133110
}
@@ -556,10 +533,18 @@ public void CreateDeclarationWith(ICssProperties properties, ref CssToken token)
556533
{
557534
var name = token.Data;
558535

559-
while (token.Type == CssTokenType.Delim)
536+
if (token.Type == CssTokenType.Delim)
560537
{
561-
token = NextToken();
562-
name += token.Data;
538+
var sb = StringBuilderPool.Obtain();
539+
sb.Append(name);
540+
541+
while (token.Type == CssTokenType.Delim)
542+
{
543+
token = NextToken();
544+
sb.Append(token.Data);
545+
}
546+
547+
name = sb.ToPool();
563548
}
564549

565550
token = NextToken();

0 commit comments

Comments
 (0)