Skip to content

Commit 1cf18e8

Browse files
Phase 1: Optimize String.Split() operations with span-based parsing
Co-authored-by: SebastianStehle <1236435+SebastianStehle@users.noreply.github.com>
1 parent 5aeab66 commit 1cf18e8

5 files changed

Lines changed: 147 additions & 15 deletions

File tree

Mjml.Net/BindingHelper.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,48 @@ public static (string? Top, string? Right, string? Bottom, string? Left) ParseSh
3131
return (null, null, null, null);
3232
}
3333

34+
#if NET8_0_OR_GREATER
35+
// Optimize: Use stackalloc to avoid heap allocation for .NET 8+
36+
Span<Range> ranges = stackalloc Range[4];
37+
var span = value.AsSpan();
38+
var count = span.Split(ranges, ' ', StringSplitOptions.RemoveEmptyEntries);
39+
40+
string? t = null;
41+
string? r = null;
42+
string? b = null;
43+
string? l = null;
44+
45+
switch (count)
46+
{
47+
case 1:
48+
t = value[ranges[0]];
49+
r = value[ranges[0]];
50+
b = value[ranges[0]];
51+
l = value[ranges[0]];
52+
break;
53+
case 2:
54+
t = value[ranges[0]];
55+
r = value[ranges[1]];
56+
b = value[ranges[0]];
57+
l = value[ranges[1]];
58+
break;
59+
case 3:
60+
t = value[ranges[0]];
61+
r = value[ranges[1]];
62+
b = value[ranges[2]];
63+
l = value[ranges[1]];
64+
break;
65+
case >= 4:
66+
t = value[ranges[0]];
67+
r = value[ranges[1]];
68+
b = value[ranges[2]];
69+
l = value[ranges[3]];
70+
break;
71+
}
72+
73+
return (t, r, b, l);
74+
#else
75+
// For .NET 6/7, use standard Split with StringSplitOptions
3476
var parts = value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
3577

3678
string? t = null;
@@ -58,7 +100,7 @@ public static (string? Top, string? Right, string? Bottom, string? Left) ParseSh
58100
b = parts[2];
59101
l = parts[1];
60102
break;
61-
case 4:
103+
case >= 4:
62104
t = parts[0];
63105
r = parts[1];
64106
b = parts[2];
@@ -67,5 +109,6 @@ public static (string? Top, string? Right, string? Bottom, string? Left) ParseSh
67109
}
68110

69111
return (t, r, b, l);
112+
#endif
70113
}
71114
}

Mjml.Net/Components/Body/MsoButtonComponent.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,22 @@ private void RenderMso(IHtmlRenderer renderer)
4343

4444
if (stroked)
4545
{
46+
#if NET8_0_OR_GREATER
47+
// Optimize: Use stackalloc to avoid heap allocation (max 3 parts) on .NET 8+
48+
Span<Range> ranges = stackalloc Range[3];
49+
var count = Border.AsSpan().Split(ranges, ' ');
50+
51+
borderWeight = count > 0 ? Border[ranges[0]] : borderWeight;
52+
borderStyle = count > 1 ? AdaptBorderStyle(Border[ranges[1]]) : borderStyle;
53+
borderColor = count >= 3 ? Border[ranges[2]] : borderColor;
54+
#else
55+
// For .NET 6/7, use standard Split
4656
var border = Border.Split(" ");
4757

4858
borderWeight = border.Length > 0 ? border[0] : borderWeight;
4959
borderStyle = border.Length > 1 ? AdaptBorderStyle(border[1]) : borderStyle;
50-
borderColor = border.Length == 3 ? border[2] : borderColor;
60+
borderColor = border.Length >= 3 ? border[2] : borderColor;
61+
#endif
5162
}
5263

5364
renderer.Content("<!--[if mso]>");

Mjml.Net/Components/Body/SectionComponent.cs

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -354,17 +354,34 @@ private void RenderSectionWithBackground(IHtmlRenderer renderer, GlobalContext c
354354
}
355355
else if (!isBackgroundSizeAuto)
356356
{
357+
#if NET8_0_OR_GREATER
358+
// Optimize: Use stackalloc for small expected arrays (max 2 parts) on .NET 8+
359+
Span<Range> ranges = stackalloc Range[2];
360+
var count = BackgroundSize.AsSpan().Split(ranges, ' ');
361+
362+
if (count == 1)
363+
{
364+
vmlSize = BackgroundSize[ranges[0]];
365+
vmlAspect = "atmost";
366+
}
367+
else if (count >= 2)
368+
{
369+
vmlSize = $"{BackgroundSize[ranges[0]]},{BackgroundSize[ranges[1]]}";
370+
}
371+
#else
372+
// For .NET 6/7, use standard Split
357373
var positions = BackgroundSize.Split(' ');
358374

359375
if (positions.Length == 1)
360376
{
361377
vmlSize = positions[0];
362378
vmlAspect = "atmost";
363379
}
364-
else
380+
else if (positions.Length >= 2)
365381
{
366-
vmlSize = string.Join(',', positions);
382+
vmlSize = $"{positions[0]},{positions[1]}";
367383
}
384+
#endif
368385
}
369386

370387
if (isBackgroundSizeAuto)
@@ -491,30 +508,64 @@ protected virtual void RenderWrappedChildren(IHtmlRenderer renderer, GlobalConte
491508

492509
private (string X, string Y) ParseBackgroundPosition()
493510
{
494-
var positions = BackgroundPosition.Split(' ');
511+
#if NET8_0_OR_GREATER
512+
// Optimize: Use stackalloc to avoid heap allocation (max 2 parts) on .NET 8+
513+
Span<Range> ranges = stackalloc Range[2];
514+
var count = BackgroundPosition.AsSpan().Split(ranges, ' ');
495515

496-
static bool IsTopOrBottom(ref string axis)
516+
static bool IsTopOrBottom(ReadOnlySpan<char> axis)
497517
{
498-
if (axis.Equals("top", StringComparison.OrdinalIgnoreCase) || axis.Equals("bottom", StringComparison.OrdinalIgnoreCase))
499-
{
500-
return true;
501-
}
518+
return axis.Equals("top", StringComparison.OrdinalIgnoreCase) ||
519+
axis.Equals("bottom", StringComparison.OrdinalIgnoreCase);
520+
}
521+
522+
switch (count)
523+
{
524+
case 1:
525+
var pos0 = BackgroundPosition.AsSpan(ranges[0]);
526+
if (IsTopOrBottom(pos0))
527+
{
528+
return ("center", BackgroundPosition[ranges[0]]);
529+
}
530+
531+
return (BackgroundPosition[ranges[0]], "center");
532+
533+
case >= 2:
534+
var pos0_2 = BackgroundPosition.AsSpan(ranges[0]);
535+
var pos1 = BackgroundPosition.AsSpan(ranges[1]);
536+
537+
if (IsTopOrBottom(pos0_2) || (pos0_2.Equals("center", StringComparison.OrdinalIgnoreCase) && IsTopOrBottom(pos1)))
538+
{
539+
return (BackgroundPosition[ranges[1]], BackgroundPosition[ranges[0]]);
540+
}
502541

503-
return false;
542+
return (BackgroundPosition[ranges[0]], BackgroundPosition[ranges[1]]);
543+
544+
default:
545+
return ("center", "top");
546+
}
547+
#else
548+
// For .NET 6/7, use standard Split
549+
var positions = BackgroundPosition.Split(' ');
550+
551+
static bool IsTopOrBottom(string axis)
552+
{
553+
return axis.Equals("top", StringComparison.OrdinalIgnoreCase) ||
554+
axis.Equals("bottom", StringComparison.OrdinalIgnoreCase);
504555
}
505556

506557
switch (positions.Length)
507558
{
508559
case 1:
509-
if (IsTopOrBottom(ref positions[0]))
560+
if (IsTopOrBottom(positions[0]))
510561
{
511562
return ("center", positions[0]);
512563
}
513564

514565
return (positions[0], "center");
515566

516-
case 2:
517-
if (IsTopOrBottom(ref positions[0]) || (positions[0].Equals("center", StringComparison.OrdinalIgnoreCase) && IsTopOrBottom(ref positions[1])))
567+
case >= 2:
568+
if (IsTopOrBottom(positions[0]) || (positions[0].Equals("center", StringComparison.OrdinalIgnoreCase) && IsTopOrBottom(positions[1])))
518569
{
519570
return (positions[1], positions[0]);
520571
}
@@ -524,6 +575,7 @@ static bool IsTopOrBottom(ref string axis)
524575
default:
525576
return ("center", "top");
526577
}
578+
#endif
527579
}
528580

529581
private static (string XPercent, string YPercent) GetBackgroundPositionAsPercentage(string backgroundPositionX, string backgroundPositionY)

Mjml.Net/Internal/Binder.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public string[] ClassNames
1717
{
1818
if (attributes.TryGetValue(Constants.MjClass, out var classNames))
1919
{
20-
currentClasses = classNames.Split(' ');
20+
// Optimize: Use RemoveEmptyEntries to avoid empty strings
21+
currentClasses = classNames.Split(' ', StringSplitOptions.RemoveEmptyEntries);
2122
}
2223
else
2324
{

Mjml.Net/Types/ManyType.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,30 @@ public sealed class ManyType(IType unit, int min, int max) : IType
1010

1111
public bool Validate(string value, ref ValidationContext context)
1212
{
13+
#if NET8_0_OR_GREATER
14+
// Optimize: Use stackalloc to avoid heap allocation (max is typically 4)
15+
// Allocate one extra slot to detect when count exceeds max
16+
Span<Range> ranges = stackalloc Range[Math.Min(max + 1, 17)];
17+
var span = value.AsSpan();
18+
var count = span.Split(ranges, ' ', StringSplitOptions.RemoveEmptyEntries);
19+
20+
if (count < min || count > max)
21+
{
22+
return false;
23+
}
24+
25+
for (int i = 0; i < count; i++)
26+
{
27+
var part = value[ranges[i]];
28+
if (!unit.Validate(part, ref context))
29+
{
30+
return false;
31+
}
32+
}
33+
34+
return true;
35+
#else
36+
// For .NET 6/7, use standard Split
1337
var parts = value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
1438

1539
if (parts.Length < min || parts.Length > max)
@@ -26,5 +50,6 @@ public bool Validate(string value, ref ValidationContext context)
2650
}
2751

2852
return true;
53+
#endif
2954
}
3055
}

0 commit comments

Comments
 (0)