From 0914aae08dd8f479e11a313c45fe0122f15c912d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Gierlasi=C5=84ski?= Date: Mon, 27 Apr 2020 08:22:14 +0200 Subject: [PATCH] Gradient builder performance adjustments (#79) * Gradient builder performance adjustments, unit tests fixes * Fix unit test name * Update Xamarin.Forms dependency where HSL bug is fixed --- .../GradientBuilderTestCases.cs | 38 +++++++++ MagicGradients.Tests/GradientBuilderTests.cs | 77 +++++++++++++++++++ MagicGradients.Tests/GradientTests.cs | 26 +++---- .../Parser/CssGradientParserTests.cs | 4 +- .../Parser/CssGradientParserTestsData.cs | 32 ++++---- MagicGradients/GradientBuilder.cs | 45 ++++++++--- MagicGradients/GradientView.cs | 3 - MagicGradients/LinearGradient.cs | 3 + MagicGradients/MagicGradients.csproj | 2 +- .../ColorChannelDefinition.cs | 3 +- MagicGradients/RadialGradient.cs | 3 + 11 files changed, 191 insertions(+), 45 deletions(-) create mode 100644 MagicGradients.Tests/GradientBuilderTestCases.cs create mode 100644 MagicGradients.Tests/GradientBuilderTests.cs diff --git a/MagicGradients.Tests/GradientBuilderTestCases.cs b/MagicGradients.Tests/GradientBuilderTestCases.cs new file mode 100644 index 00000000..4985895f --- /dev/null +++ b/MagicGradients.Tests/GradientBuilderTestCases.cs @@ -0,0 +1,38 @@ +using Xamarin.Forms; +using Xunit; + +namespace MagicGradients.Tests +{ + public class GradientBuilderTestCases : TheoryData + { + public GradientBuilderTestCases() + { + Add(new LinearTestCase()); + Add(new RadialTestCase()); + } + } + + public abstract class GradientBuilderTestCase + { + public abstract void AddGradient(GradientBuilder builder); + } + + public class LinearTestCase : GradientBuilderTestCase + { + public override void AddGradient(GradientBuilder builder) + { + builder.AddLinearGradient(45); + } + } + + public class RadialTestCase : GradientBuilderTestCase + { + public override void AddGradient(GradientBuilder builder) + { + builder.AddRadialGradient( + new Point(0.5, 0.5), + RadialGradientShape.Circle, + RadialGradientSize.ClosestSide); + } + } +} diff --git a/MagicGradients.Tests/GradientBuilderTests.cs b/MagicGradients.Tests/GradientBuilderTests.cs new file mode 100644 index 00000000..bc26b4fa --- /dev/null +++ b/MagicGradients.Tests/GradientBuilderTests.cs @@ -0,0 +1,77 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using Xamarin.Forms; +using Xunit; + +namespace MagicGradients.Tests +{ + public class GradientBuilderTests + { + [Theory] + [ClassData(typeof(GradientBuilderTestCases))] + public void AddGradient_AndStops_SingleGradientWithStops(GradientBuilderTestCase testCase) + { + // Arrange + var builder = new GradientBuilder(); + + // Act + testCase.AddGradient(builder); + builder.AddStop(Color.White); + builder.AddStop(Color.Black); + + var gradients = builder.Build(); + + // Assert + using (new AssertionScope()) + { + gradients.Should().HaveCount(1); + gradients[0].Stops.Should().HaveCount(2); + } + } + + [Theory] + [ClassData(typeof(GradientBuilderTestCases))] + public void AddStops_AndGradient_AndStops_TwoGradientsWithStops(GradientBuilderTestCase testCase) + { + // Arrange + var builder = new GradientBuilder(); + + // Act + builder.AddStop(Color.Red); + testCase.AddGradient(builder); + builder.AddStop(Color.White); + builder.AddStop(Color.Black); + + var gradients = builder.Build(); + + // Assert + using (new AssertionScope()) + { + gradients.Should().HaveCount(2); + gradients[0].Stops.Should().HaveCount(1); + gradients[1].Stops.Should().HaveCount(2); + } + } + + [Fact] + public void AddOnlyStops_DefaultGradientWithStops() + { + // Arrange + var builder = new GradientBuilder(); + + // Act + builder.AddStop(Color.White); + builder.AddStop(Color.Black); + + var gradients = builder.Build(); + + // Assert + using (new AssertionScope()) + { + gradients.Should().HaveCount(1); + gradients[0].Should().BeOfType(); + gradients[0].Stops.Should().HaveCount(2); + } + } + } +} diff --git a/MagicGradients.Tests/GradientTests.cs b/MagicGradients.Tests/GradientTests.cs index 0777663d..a74245ba 100644 --- a/MagicGradients.Tests/GradientTests.cs +++ b/MagicGradients.Tests/GradientTests.cs @@ -25,8 +25,8 @@ public void SetupUndefinedOffsets_HasDefinedOffsets_NothingChanged() // Assert using (new AssertionScope()) { - gradient.Stops[0].Offset.Should().Be(0.1f); - gradient.Stops[1].Offset.Should().Be(0.2f); + gradient.Stops[0].RenderOffset.Should().Be(0.1f); + gradient.Stops[1].RenderOffset.Should().Be(0.2f); } } @@ -50,9 +50,9 @@ public void SetupUndefinedOffsets_HasUndefinedOffsets_AutomaticallySetUp() // Assert using (new AssertionScope()) { - gradient.Stops[0].Offset.Should().Be(0f); - gradient.Stops[1].Offset.Should().Be(0.5f); - gradient.Stops[2].Offset.Should().Be(1f); + gradient.Stops[0].RenderOffset.Should().Be(0f); + gradient.Stops[1].RenderOffset.Should().Be(0.5f); + gradient.Stops[2].RenderOffset.Should().Be(1f); } } @@ -81,14 +81,14 @@ public void SetupUndefinedOffsets_HasMixedOffsets_OnlySetUpUndefined() // Assert using (new AssertionScope()) { - gradient.Stops[0].Offset.Should().Be(0f); - gradient.Stops[1].Offset.Should().BeInRange(0.19f, 0.21f); - gradient.Stops[2].Offset.Should().BeInRange(0.39f, 0.41f); - gradient.Stops[3].Offset.Should().Be(0.6f); - gradient.Stops[4].Offset.Should().BeInRange(0.69f, 0.71f); - gradient.Stops[5].Offset.Should().BeInRange(0.79f, 0.81f); - gradient.Stops[6].Offset.Should().Be(0.9f); - gradient.Stops[7].Offset.Should().Be(1f); + gradient.Stops[0].RenderOffset.Should().Be(0f); + gradient.Stops[1].RenderOffset.Should().BeInRange(0.19f, 0.21f); + gradient.Stops[2].RenderOffset.Should().BeInRange(0.39f, 0.41f); + gradient.Stops[3].RenderOffset.Should().Be(0.6f); + gradient.Stops[4].RenderOffset.Should().BeInRange(0.69f, 0.71f); + gradient.Stops[5].RenderOffset.Should().BeInRange(0.79f, 0.81f); + gradient.Stops[6].RenderOffset.Should().Be(0.9f); + gradient.Stops[7].RenderOffset.Should().Be(1f); } } } diff --git a/MagicGradients.Tests/Parser/CssGradientParserTests.cs b/MagicGradients.Tests/Parser/CssGradientParserTests.cs index 225b2739..60c8846e 100644 --- a/MagicGradients.Tests/Parser/CssGradientParserTests.cs +++ b/MagicGradients.Tests/Parser/CssGradientParserTests.cs @@ -42,7 +42,7 @@ public void ParseCss_SimpleGradients_CorrectlyParsed(string css, LinearGradient // Assert gradients.Should().HaveCount(1); - gradients[0].Should().BeEquivalentTo(expected); + gradients[0].Should().BeEquivalentTo(expected, options => options.IgnoringCyclicReferences()); } [Theory] @@ -58,7 +58,7 @@ public void ParseCss_GradientsWithoutOffsets_AutomaticallyAssignedOffsets(string // Assert gradients.Should().HaveCount(1); - gradients[0].Should().BeEquivalentTo(expected); + gradients[0].Should().BeEquivalentTo(expected, options => options.IgnoringCyclicReferences()); } [Fact] diff --git a/MagicGradients.Tests/Parser/CssGradientParserTestsData.cs b/MagicGradients.Tests/Parser/CssGradientParserTestsData.cs index 68fba232..67e67e64 100644 --- a/MagicGradients.Tests/Parser/CssGradientParserTestsData.cs +++ b/MagicGradients.Tests/Parser/CssGradientParserTestsData.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using Xamarin.Forms; using static MagicGradients.Parser.CssHelpers; @@ -10,15 +9,20 @@ public class CssGradientParserTestData { public static string ComplexGradientsCss = "linear-gradient(242deg, rgba(195, 195, 195, 0.02) 0%, rgba(195, 195, 195, 0.02) 16.667%,rgba(91, 91, 91, 0.02) 16.667%, rgba(91, 91, 91, 0.02) 33.334%,rgba(230, 230, 230, 0.02) 33.334%, rgba(230, 230, 230, 0.02) 50.001000000000005%,rgba(18, 18, 18, 0.02) 50.001%, rgba(18, 18, 18, 0.02) 66.668%,rgba(163, 163, 163, 0.02) 66.668%, rgba(163, 163, 163, 0.02) 83.33500000000001%,rgba(140, 140, 140, 0.02) 83.335%, rgba(140, 140, 140, 0.02) 100.002%),linear-gradient(152deg, rgba(151, 151, 151, 0.02) 0%, rgba(151, 151, 151, 0.02) 16.667%,rgba(11, 11, 11, 0.02) 16.667%, rgba(11, 11, 11, 0.02) 33.334%,rgba(162, 162, 162, 0.02) 33.334%, rgba(162, 162, 162, 0.02) 50.001000000000005%,rgba(171, 171, 171, 0.02) 50.001%, rgba(171, 171, 171, 0.02) 66.668%,rgba(119, 119, 119, 0.02) 66.668%, rgba(119, 119, 119, 0.02) 83.33500000000001%,rgba(106, 106, 106, 0.02) 83.335%, rgba(106, 106, 106, 0.02) 100.002%),linear-gradient(11deg, rgba(245, 245, 245, 0.01) 0%, rgba(245, 245, 245, 0.01) 16.667%,rgba(23, 23, 23, 0.01) 16.667%, rgba(23, 23, 23, 0.01) 33.334%,rgba(96, 96, 96, 0.01) 33.334%, rgba(96, 96, 96, 0.01) 50.001000000000005%,rgba(140, 140, 140, 0.01) 50.001%, rgba(140, 140, 140, 0.01) 66.668%,rgba(120, 120, 120, 0.01) 66.668%, rgba(120, 120, 120, 0.01) 83.33500000000001%,rgba(48, 48, 48, 0.01) 83.335%, rgba(48, 48, 48, 0.01) 100.002%),linear-gradient(27deg, rgba(106, 106, 106, 0.03) 0%, rgba(106, 106, 106, 0.03) 14.286%,rgba(203, 203, 203, 0.03) 14.286%, rgba(203, 203, 203, 0.03) 28.572%,rgba(54, 54, 54, 0.03) 28.572%, rgba(54, 54, 54, 0.03) 42.858%,rgba(75, 75, 75, 0.03) 42.858%, rgba(75, 75, 75, 0.03) 57.144%,rgba(216, 216, 216, 0.03) 57.144%, rgba(216, 216, 216, 0.03) 71.42999999999999%,rgba(39, 39, 39, 0.03) 71.43%, rgba(39, 39, 39, 0.03) 85.71600000000001%,rgba(246, 246, 246, 0.03) 85.716%, rgba(246, 246, 246, 0.03) 100.002%),linear-gradient(317deg, rgba(215, 215, 215, 0.01) 0%, rgba(215, 215, 215, 0.01) 16.667%,rgba(72, 72, 72, 0.01) 16.667%, rgba(72, 72, 72, 0.01) 33.334%,rgba(253, 253, 253, 0.01) 33.334%, rgba(253, 253, 253, 0.01) 50.001000000000005%,rgba(4, 4, 4, 0.01) 50.001%, rgba(4, 4, 4, 0.01) 66.668%,rgba(183, 183, 183, 0.01) 66.668%, rgba(183, 183, 183, 0.01) 83.33500000000001%,rgba(17, 17, 17, 0.01) 83.335%, rgba(17, 17, 17, 0.01) 100.002%),linear-gradient(128deg, rgba(119, 119, 119, 0.03) 0%, rgba(119, 119, 119, 0.03) 12.5%,rgba(91, 91, 91, 0.03) 12.5%, rgba(91, 91, 91, 0.03) 25%,rgba(45, 45, 45, 0.03) 25%, rgba(45, 45, 45, 0.03) 37.5%,rgba(182, 182, 182, 0.03) 37.5%, rgba(182, 182, 182, 0.03) 50%,rgba(243, 243, 243, 0.03) 50%, rgba(243, 243, 243, 0.03) 62.5%,rgba(162, 162, 162, 0.03) 62.5%, rgba(162, 162, 162, 0.03) 75%,rgba(190, 190, 190, 0.03) 75%, rgba(190, 190, 190, 0.03) 87.5%,rgba(148, 148, 148, 0.03) 87.5%, rgba(148, 148, 148, 0.03) 100%),linear-gradient(90deg, rgb(185, 139, 80),rgb(176, 26, 6))"; + private static GradientElements CreateStops(int count) + { + return new GradientElements(Enumerable.Repeat(new GradientStop(), count)); + } + public static LinearGradient[] ComplexGradientsExpected = new[] { - new LinearGradient{ Angle = FromDegrees(242), Stops = new GradientElements(new GradientStop[12])}, - new LinearGradient{ Angle = FromDegrees(152), Stops = new GradientElements(new GradientStop[12])}, - new LinearGradient{ Angle = FromDegrees(11), Stops = new GradientElements(new GradientStop[12])}, - new LinearGradient{ Angle = FromDegrees(27), Stops = new GradientElements(new GradientStop[14])}, - new LinearGradient{ Angle = FromDegrees(317), Stops = new GradientElements(new GradientStop[12])}, - new LinearGradient{ Angle = FromDegrees(128), Stops = new GradientElements(new GradientStop[16])}, - new LinearGradient{ Angle = FromDegrees(90), Stops = new GradientElements(new GradientStop[2])} + new LinearGradient{ Angle = FromDegrees(242), Stops = CreateStops(12)}, + new LinearGradient{ Angle = FromDegrees(152), Stops = CreateStops(12)}, + new LinearGradient{ Angle = FromDegrees(11), Stops = CreateStops(12)}, + new LinearGradient{ Angle = FromDegrees(27), Stops = CreateStops(14)}, + new LinearGradient{ Angle = FromDegrees(317), Stops = CreateStops(12)}, + new LinearGradient{ Angle = FromDegrees(128), Stops = CreateStops(16)}, + new LinearGradient{ Angle = FromDegrees(90), Stops = CreateStops(2)} }.Reverse().ToArray(); public static IEnumerable SimpleGradients() @@ -94,7 +98,7 @@ public static IEnumerable GradientsWithoutOffsets() { Angle = FromDegrees(224), Stops = new GradientElements { - new GradientStop { Color = Color.Black, Offset = 0f }, + new GradientStop { Color = Color.Black, RenderOffset = 0f }, } } }; @@ -104,8 +108,8 @@ public static IEnumerable GradientsWithoutOffsets() { Angle = FromDegrees(224), Stops = new GradientElements { - new GradientStop { Color = Color.Black, Offset = 0f }, - new GradientStop { Color = Color.Black, Offset = 1f } + new GradientStop { Color = Color.Black, RenderOffset = 0f }, + new GradientStop { Color = Color.Black, RenderOffset = 1f } } } }; @@ -115,9 +119,9 @@ public static IEnumerable GradientsWithoutOffsets() { Angle = FromDegrees(224), Stops = new GradientElements { - new GradientStop { Color = Color.Black, Offset = 0f }, - new GradientStop { Color = Color.Black, Offset = 0.5f }, - new GradientStop { Color = Color.Black, Offset = 1f } + new GradientStop { Color = Color.Black, RenderOffset = 0f }, + new GradientStop { Color = Color.Black, RenderOffset = 0.5f }, + new GradientStop { Color = Color.Black, RenderOffset = 1f } } } }; diff --git a/MagicGradients/GradientBuilder.cs b/MagicGradients/GradientBuilder.cs index 2cd6b402..701b178e 100644 --- a/MagicGradients/GradientBuilder.cs +++ b/MagicGradients/GradientBuilder.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Xamarin.Forms; namespace MagicGradients @@ -6,17 +7,19 @@ namespace MagicGradients public class GradientBuilder { private readonly List _gradients = new List(); - private Gradient _lastGradient; + private readonly List _stops = new List(); public GradientBuilder AddLinearGradient(double angle, bool isRepeating = false) { - _lastGradient = new LinearGradient + AddCachedStopsToLast(); + + var linearGradient = new LinearGradient { Angle = angle, IsRepeating = isRepeating }; - _gradients.Add(_lastGradient); + _gradients.Add(linearGradient); return this; } @@ -28,7 +31,9 @@ public GradientBuilder AddRadialGradient( RadialGradientFlags flags = RadialGradientFlags.PositionProportional, bool isRepeating = false) { - _lastGradient = new RadialGradient + AddCachedStopsToLast(); + + var radialGradient = new RadialGradient { Center = center, Shape = shape, @@ -37,25 +42,20 @@ public GradientBuilder AddRadialGradient( IsRepeating = isRepeating }; - _gradients.Add(_lastGradient); + _gradients.Add(radialGradient); return this; } public GradientBuilder AddStop(Color color, float? offset = null) { - if (_lastGradient == null) - { - AddLinearGradient(0); - } - var stop = new GradientStop { Color = color, Offset = offset ?? -1 }; - _lastGradient.Stops.Add(stop); + _stops.Add(stop); return this; } @@ -70,8 +70,31 @@ public GradientBuilder AddStops(Color color, IEnumerable offsets) return this; } + private void AddCachedStopsToLast() + { + if (!_stops.Any()) + return; + + var lastGradient = _gradients.LastOrDefault(); + if (lastGradient == null) + { + lastGradient = CreateDefaultGradient(); + _gradients.Add(lastGradient); + } + lastGradient.Stops = new GradientElements(_stops); + + _stops.Clear(); + } + + private Gradient CreateDefaultGradient() => new LinearGradient + { + Angle = 0, + IsRepeating = false + }; + public Gradient[] Build() { + AddCachedStopsToLast(); return _gradients.ToArray(); } } diff --git a/MagicGradients/GradientView.cs b/MagicGradients/GradientView.cs index a23e46ca..a21abfa4 100644 --- a/MagicGradients/GradientView.cs +++ b/MagicGradients/GradientView.cs @@ -65,9 +65,6 @@ protected override void OnPaintSurface(SKPaintSurfaceEventArgs e) foreach (var gradient in GradientSource.GetGradients()) { -#if DEBUG_RENDER - System.Diagnostics.Debug.WriteLine($"Rendering Gradient with {gradient.Stops.Count} stops"); -#endif gradient.Measure(e.Info.Width, e.Info.Height); gradient.Render(context); } diff --git a/MagicGradients/LinearGradient.cs b/MagicGradients/LinearGradient.cs index 606d6acf..2c041102 100644 --- a/MagicGradients/LinearGradient.cs +++ b/MagicGradients/LinearGradient.cs @@ -49,6 +49,9 @@ private float GetOffsetFromPixels(float offset, int width, int height) public override void Render(RenderContext context) { +#if DEBUG_RENDER + System.Diagnostics.Debug.WriteLine($"Rendering Linear Gradient with {Stops.Count} stops"); +#endif _renderer.Render(context); } } diff --git a/MagicGradients/MagicGradients.csproj b/MagicGradients/MagicGradients.csproj index 4ef4d71a..29ff2758 100644 --- a/MagicGradients/MagicGradients.csproj +++ b/MagicGradients/MagicGradients.csproj @@ -18,7 +18,7 @@ - + diff --git a/MagicGradients/Parser/TokenDefinitions/ColorChannelDefinition.cs b/MagicGradients/Parser/TokenDefinitions/ColorChannelDefinition.cs index ff0f87c8..32ac9a35 100644 --- a/MagicGradients/Parser/TokenDefinitions/ColorChannelDefinition.cs +++ b/MagicGradients/Parser/TokenDefinitions/ColorChannelDefinition.cs @@ -16,7 +16,8 @@ public bool IsMatch(string token) => public void Parse(CssReader reader, GradientBuilder builder) { - var color = (Color)ColorConverter.ConvertFromInvariantString(GetColorString(reader)); + var colorString = GetColorString(reader); + var color = (Color)ColorConverter.ConvertFromInvariantString(colorString); var parts = reader.ReadNext().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (parts.TryConvertOffsets(out var offsets)) diff --git a/MagicGradients/RadialGradient.cs b/MagicGradients/RadialGradient.cs index 04669565..15b41ea6 100644 --- a/MagicGradients/RadialGradient.cs +++ b/MagicGradients/RadialGradient.cs @@ -68,6 +68,9 @@ public RadialGradient() public override void Render(RenderContext context) { +#if DEBUG_RENDER + System.Diagnostics.Debug.WriteLine($"Rendering Radial Gradient with {Stops.Count} stops"); +#endif _renderer.Render(context); } }