From 2a3d213f0eafadc28eb6179e524a2480517a8bf3 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 21 Aug 2019 16:46:33 +0200 Subject: [PATCH] LoadTests. --- .../GenerateLanguages.csproj | 4 + tools/LoadTest/ClientQueryFixture.cs | 75 +++++++++ tools/LoadTest/LoadTest.csproj | 23 +++ tools/LoadTest/LoadTest.sln | 25 +++ tools/LoadTest/QueryBenchmarks.cs | 146 ++++++++++++++++++ tools/LoadTest/TestClient.cs | 34 ++++ tools/LoadTest/TestEntity.cs | 22 +++ 7 files changed, 329 insertions(+) create mode 100644 tools/LoadTest/ClientQueryFixture.cs create mode 100644 tools/LoadTest/LoadTest.csproj create mode 100644 tools/LoadTest/LoadTest.sln create mode 100644 tools/LoadTest/QueryBenchmarks.cs create mode 100644 tools/LoadTest/TestClient.cs create mode 100644 tools/LoadTest/TestEntity.cs diff --git a/tools/GenerateLanguages/GenerateLanguages.csproj b/tools/GenerateLanguages/GenerateLanguages.csproj index 135a6c0d0..6a6e46772 100644 --- a/tools/GenerateLanguages/GenerateLanguages.csproj +++ b/tools/GenerateLanguages/GenerateLanguages.csproj @@ -3,6 +3,10 @@ netcoreapp2.2 Exe + + + + ..\..\Squidex.ruleset diff --git a/tools/LoadTest/ClientQueryFixture.cs b/tools/LoadTest/ClientQueryFixture.cs new file mode 100644 index 000000000..af696f65b --- /dev/null +++ b/tools/LoadTest/ClientQueryFixture.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.ClientLibrary; +using Squidex.ClientLibrary.Management; + +namespace LoadTest +{ + public sealed class ClientQueryFixture : IDisposable + { + public SquidexClient Client { get; } = TestClient.Build(); + + public ClientQueryFixture() + { + Task.Run(async () => + { + var apps = TestClient.ClientManager.CreateAppsClient(); + + try + { + await apps.PostAppAsync(new CreateAppDto + { + Name = TestClient.TestAppName + }); + + var schemas = TestClient.ClientManager.CreateSchemasClient(); + + await schemas.PostSchemaAsync(TestClient.TestAppName, new CreateSchemaDto + { + Name = TestClient.TestSchemaName, + Fields = new List + { + new UpsertSchemaFieldDto + { + Name = TestClient.TestSchemaName, + Properties = new NumberFieldPropertiesDto() + } + }, + IsPublished = true + }); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) + { + throw; + } + } + + var contents = await Client.GetAllAsync(); + + foreach (var content in contents.Items) + { + await Client.DeleteAsync(content); + } + + for (var i = 10; i > 0; i--) + { + await Client.CreateAsync(new TestEntityData { Value = i }, true); + } + }).Wait(); + } + + public void Dispose() + { + } + } +} diff --git a/tools/LoadTest/LoadTest.csproj b/tools/LoadTest/LoadTest.csproj new file mode 100644 index 000000000..5a97587d8 --- /dev/null +++ b/tools/LoadTest/LoadTest.csproj @@ -0,0 +1,23 @@ + + + Exe + netcoreapp2.2 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + diff --git a/tools/LoadTest/LoadTest.sln b/tools/LoadTest/LoadTest.sln new file mode 100644 index 000000000..ce256f737 --- /dev/null +++ b/tools/LoadTest/LoadTest.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29123.88 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoadTest", "LoadTest.csproj", "{EC732B4F-576E-4853-880D-AA676966D017}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EC732B4F-576E-4853-880D-AA676966D017}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC732B4F-576E-4853-880D-AA676966D017}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC732B4F-576E-4853-880D-AA676966D017}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC732B4F-576E-4853-880D-AA676966D017}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8ABC073D-D6C6-47F0-AFB6-D42F177D468A} + EndGlobalSection +EndGlobal diff --git a/tools/LoadTest/QueryBenchmarks.cs b/tools/LoadTest/QueryBenchmarks.cs new file mode 100644 index 000000000..9140cc608 --- /dev/null +++ b/tools/LoadTest/QueryBenchmarks.cs @@ -0,0 +1,146 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Squidex.ClientLibrary; +using Xunit; + +namespace LoadTest +{ + public class QueryBenchmarks : IClassFixture + { + public ClientQueryFixture Fixture { get; } + + public QueryBenchmarks(ClientQueryFixture fixture) + { + Fixture = fixture; + } + + public static IEnumerable Loads() + { + int[] users = { 1, 5, 10, 20, 50, 100 }; + int[] loads = { 5, 10, 20, 50, 100 }; + + foreach (var user in users) + { + foreach (var load in loads) + { + yield return new object[] { user, load }; + } + } + } + + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_all(int numUsers, int numIterationsPerUser) + { + await Run(numUsers, numIterationsPerUser, async () => + { + await Fixture.Client.GetAsync(new ODataQuery { OrderBy = "data/value/iv asc" }); + }); + } + + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_items_with_skip(int numUsers, int numIterationsPerUser) + { + await Run(numUsers, numIterationsPerUser, async () => + { + await Fixture.Client.GetAsync(new ODataQuery { Skip = 5, OrderBy = "data/value/iv asc" }); + }); + } + + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_items_with_skip_and_top(int numUsers, int numIterationsPerUser) + { + await Run(numUsers, numIterationsPerUser, async () => + { + await Fixture.Client.GetAsync(new ODataQuery { Skip = 2, Top = 5, OrderBy = "data/value/iv asc" }); + }); + } + + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_items_with_ordering(int numUsers, int numIterationsPerUser) + { + await Run(numUsers, numIterationsPerUser, async () => + { + await Fixture.Client.GetAsync(new ODataQuery { Skip = 2, Top = 5, OrderBy = "data/value/iv desc" }); + }); + } + + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_items_with_filter(int numUsers, int numIterationsPerUser) + { + await Run(numUsers, numIterationsPerUser, async () => + { + await Fixture.Client.GetAsync(new ODataQuery { Filter = "data/value/iv gt 3 and data/value/iv lt 7", OrderBy = "data/value/iv asc" }); + }); + } + + private static async Task Run(int numUsers, int numIterationsPerUser, Func action, int expectedAvg = 100) + { + var elapsedMs = new ConcurrentBag(); + + var errors = 0; + + async Task RunAsync() + { + for (var i = 0; i < numIterationsPerUser; i++) + { + try + { + var watch = Stopwatch.StartNew(); + + await action(); + + watch.Stop(); + + elapsedMs.Add(watch.ElapsedMilliseconds); + } + catch + { + Interlocked.Increment(ref errors); + } + } + } + + var tasks = new List(); + + for (var i = 0; i < numUsers; i++) + { + tasks.Add(Task.Run(RunAsync)); + } + + await Task.WhenAll(tasks); + + var count = elapsedMs.Count; + + var max = elapsedMs.Max(); + var min = elapsedMs.Min(); + + var avg = elapsedMs.Average(); + + Assert.InRange(max, 0, expectedAvg * 10); + Assert.InRange(min, 0, expectedAvg); + + Assert.InRange(avg, 0, expectedAvg); + + Assert.Equal(0, errors); + + Assert.Equal(count, numUsers * numIterationsPerUser); + } + } +} diff --git a/tools/LoadTest/TestClient.cs b/tools/LoadTest/TestClient.cs new file mode 100644 index 000000000..f77a8977f --- /dev/null +++ b/tools/LoadTest/TestClient.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.ClientLibrary; + +namespace LoadTest +{ + public static class TestClient + { + public const string ServerUrl = "http://localhost:5000"; + + public const string ClientId = "root"; + public const string ClientSecret = "xeLd6jFxqbXJrfmNLlO2j1apagGGGSyZJhFnIuHp4I0="; + + public const string TestAppName = "integration-tests"; + public const string TestSchemaName = "numbers"; + public const string TestSchemaField = "value"; + + public static readonly SquidexClientManager ClientManager = + new SquidexClientManager("http://localhost:5000", TestAppName, ClientId, ClientSecret) + { + ReadResponseAsString = true + }; + + public static SquidexClient Build() + { + return ClientManager.GetClient(TestSchemaName); + } + } +} diff --git a/tools/LoadTest/TestEntity.cs b/tools/LoadTest/TestEntity.cs new file mode 100644 index 000000000..d6eaeeab7 --- /dev/null +++ b/tools/LoadTest/TestEntity.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.ClientLibrary; + +namespace LoadTest +{ + public sealed class TestEntity : SquidexEntityBase + { + } + + public sealed class TestEntityData + { + [JsonConverter(typeof(InvariantConverter))] + public int Value { get; set; } + } +}