Browse Source

Performance improvements.

pull/194/head
Sebastian Stehle 8 years ago
parent
commit
3104ecf660
  1. 16
      Squidex.sln
  2. 12
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
  3. 71
      src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
  4. 38
      src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs
  5. 99
      src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs
  6. 166
      src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs
  7. 23
      src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs
  8. 8
      src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs
  9. 31
      src/Squidex.Infrastructure.MongoDb/States/MongoState.cs
  10. 44
      src/Squidex.Infrastructure.MongoDb/States/MongoStateStore.cs
  11. 4
      src/Squidex.Infrastructure/States/StateHolder.cs
  12. 26
      tests/Benchmarks/Benchmarks.csproj
  13. 19
      tests/Benchmarks/IBenchmark.cs
  14. 90
      tests/Benchmarks/Program.cs
  15. 7
      tests/Benchmarks/Properties/launchSettings.json
  16. 140
      tests/Benchmarks/Services.cs
  17. 53
      tests/Benchmarks/Tests/AppendToEventStore.cs
  18. 52
      tests/Benchmarks/Tests/AppendToEventStoreWithManyWriters.cs
  19. 63
      tests/Benchmarks/Tests/HandleEvents.cs
  20. 70
      tests/Benchmarks/Tests/HandleEventsWithManyWriters.cs
  21. 102
      tests/Benchmarks/Tests/ReadSchemaState.cs
  22. 21
      tests/Benchmarks/Tests/TestData/MyAppState.cs
  23. 19
      tests/Benchmarks/Tests/TestData/MyEvent.cs
  24. 79
      tests/Benchmarks/Tests/TestData/MyEventConsumer.cs
  25. 21
      tests/Benchmarks/Utils/Helper.cs

16
Squidex.sln

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2006
VisualStudioVersion = 15.0.27004.2009
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex", "src\Squidex\Squidex.csproj", "{61F6BBCE-A080-4400-B194-70E2F5D2096E}"
EndProject
@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Mo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Operations", "src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj", "{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "tests\Benchmarks\Benchmarks.csproj", "{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -301,6 +303,18 @@ Global
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x64.Build.0 = Release|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x86.ActiveCfg = Release|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x86.Build.0 = Release|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x64.ActiveCfg = Debug|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x64.Build.0 = Debug|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x86.ActiveCfg = Debug|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x86.Build.0 = Debug|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|Any CPU.Build.0 = Release|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x64.ActiveCfg = Release|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x64.Build.0 = Release|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x86.ActiveCfg = Release|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

12
src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs

@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
{
public sealed class JsonSchemaModel
{
private static readonly Field[] Empty = new Field[0];
[JsonProperty]
public string Name { get; set; }
@ -54,12 +56,16 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
public Schema ToSchema(FieldRegistry fieldRegistry)
{
var fields = new List<Field>();
Field[] fields = Empty;
if (Fields != null)
{
foreach (var fieldModel in Fields)
fields = new Field[Fields.Count];
for (var i = 0; i < fields.Length; i++)
{
var fieldModel = Fields[i];
var parititonKey = new Partitioning(fieldModel.Partitioning);
var field = fieldRegistry.CreateField(fieldModel.Id, fieldModel.Name, parititonKey, fieldModel.Properties);
@ -79,7 +85,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
field = field.Hide();
}
fields.Add(field);
fields[i] = field;
}
}

71
src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs

@ -18,9 +18,9 @@ namespace Squidex.Domain.Apps.Core.Schemas
public sealed class Schema : Cloneable<Schema>
{
private readonly string name;
private ImmutableList<Field> fieldsOrdered = ImmutableList<Field>.Empty;
private ImmutableDictionary<long, Field> fieldsById = ImmutableDictionary<long, Field>.Empty;
private ImmutableDictionary<string, Field> fieldsByName = ImmutableDictionary<string, Field>.Empty;
private ImmutableArray<Field> fieldsOrdered = ImmutableArray<Field>.Empty;
private ImmutableDictionary<long, Field> fieldsById;
private ImmutableDictionary<string, Field> fieldsByName;
private SchemaProperties properties;
private bool isPublished;
@ -41,12 +41,42 @@ namespace Squidex.Domain.Apps.Core.Schemas
public IReadOnlyDictionary<long, Field> FieldsById
{
get { return fieldsById; }
get
{
if (fieldsById == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsById = ImmutableDictionary<long, Field>.Empty;
}
else
{
fieldsById = fieldsOrdered.ToImmutableDictionary(x => x.Id);
}
}
return fieldsById;
}
}
public IReadOnlyDictionary<string, Field> FieldsByName
{
get { return fieldsByName; }
get
{
if (fieldsByName == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsByName = ImmutableDictionary<string, Field>.Empty;
}
else
{
fieldsByName = fieldsOrdered.ToImmutableDictionary(x => x.Name);
}
}
return fieldsByName;
}
}
public SchemaProperties Properties
@ -62,34 +92,17 @@ namespace Squidex.Domain.Apps.Core.Schemas
this.properties = properties ?? new SchemaProperties();
this.properties.Freeze();
OnCloned();
}
public Schema(string name, IEnumerable<Field> fields, SchemaProperties properties, bool isPublished)
public Schema(string name, Field[] fields, SchemaProperties properties, bool isPublished)
: this(name, properties)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(fields, nameof(fields));
this.isPublished = isPublished;
fieldsOrdered = ImmutableList<Field>.Empty.AddRange(fields);
OnCloned();
}
protected override void OnCloned()
{
if (fieldsOrdered.Count > 0)
{
fieldsById = fieldsOrdered.ToImmutableDictionary(x => x.Id);
fieldsByName = fieldsOrdered.ToImmutableDictionary(x => x.Name);
}
else
{
fieldsById = ImmutableDictionary<long, Field>.Empty;
fieldsByName = ImmutableDictionary<string, Field>.Empty;
}
fieldsOrdered = ImmutableArray.Create(fields);
}
[Pure]
@ -195,14 +208,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
Guard.NotNull(ids, nameof(ids));
if (ids.Count != fieldsOrdered.Count || ids.Any(x => !fieldsById.ContainsKey(x)))
if (ids.Count != fieldsOrdered.Length || ids.Any(x => !FieldsById.ContainsKey(x)))
{
throw new ArgumentException("Ids must cover all fields.", nameof(ids));
}
return Clone(clone =>
{
clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToImmutableList();
clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToImmutableArray();
});
}
@ -211,7 +224,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
Guard.NotNull(field, nameof(field));
if (fieldsByName.ContainsKey(field.Name) || fieldsById.ContainsKey(field.Id))
if (FieldsByName.ContainsKey(field.Name) || FieldsById.ContainsKey(field.Id))
{
throw new ArgumentException($"A field with name '{field.Name}' and id {field.Id} already exists.", nameof(field));
}
@ -227,7 +240,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
Guard.NotNull(updater, nameof(updater));
if (!fieldsById.TryGetValue(fieldId, out var field))
if (!FieldsById.TryGetValue(fieldId, out var field))
{
return this;
}

38
src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs

@ -0,0 +1,38 @@
// ==========================================================================
// BsonHelper.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.MongoDb
{
public static class BsonHelper
{
public static string UnescapeBson(this string value)
{
return ReplaceFirstCharacter(value, '§', '$');
}
public static string EscapeJson(this string value)
{
return ReplaceFirstCharacter(value, '$', '§');
}
private static string ReplaceFirstCharacter(string value, char toReplace, char replacement)
{
if (value.Length == 0 || value[0] != toReplace)
{
return value;
}
if (value.Length == 1)
{
return toReplace.ToString();
}
return replacement + value.Substring(1);
}
}
}

99
src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs

@ -0,0 +1,99 @@
// ==========================================================================
// BsonJsonReader.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using MongoDB.Bson;
using MongoDB.Bson.IO;
using NewtonsoftJsonReader = Newtonsoft.Json.JsonReader;
using NewtonsoftJsonToken = Newtonsoft.Json.JsonToken;
namespace Squidex.Infrastructure.MongoDb
{
public sealed class BsonJsonReader : NewtonsoftJsonReader
{
private readonly IBsonReader bsonReader;
public BsonJsonReader(IBsonReader bsonReader)
{
Guard.NotNull(bsonReader, nameof(bsonReader));
this.bsonReader = bsonReader;
}
public override bool Read()
{
if (bsonReader.State == BsonReaderState.Type)
{
bsonReader.ReadBsonType();
}
if (bsonReader.State == BsonReaderState.Name)
{
SetToken(NewtonsoftJsonToken.PropertyName, bsonReader.ReadName().UnescapeBson());
}
else if (bsonReader.State == BsonReaderState.EndOfDocument)
{
SetToken(NewtonsoftJsonToken.EndObject);
bsonReader.ReadEndDocument();
}
else if (bsonReader.State == BsonReaderState.EndOfArray)
{
SetToken(NewtonsoftJsonToken.EndArray);
bsonReader.ReadEndArray();
}
else if (bsonReader.State == BsonReaderState.Value)
{
switch (bsonReader.CurrentBsonType)
{
case BsonType.Document:
SetToken(NewtonsoftJsonToken.StartObject);
bsonReader.ReadStartDocument();
break;
case BsonType.Array:
SetToken(NewtonsoftJsonToken.StartArray);
bsonReader.ReadStartArray();
break;
case BsonType.Undefined:
SetToken(NewtonsoftJsonToken.Undefined);
break;
case BsonType.Null:
SetToken(NewtonsoftJsonToken.Null);
break;
case BsonType.String:
SetToken(NewtonsoftJsonToken.String, bsonReader.ReadString());
break;
case BsonType.Binary:
SetToken(NewtonsoftJsonToken.Bytes, bsonReader.ReadBinaryData().Bytes);
break;
case BsonType.Boolean:
SetToken(NewtonsoftJsonToken.Boolean, bsonReader.ReadBoolean());
break;
case BsonType.DateTime:
SetToken(NewtonsoftJsonToken.Date, bsonReader.ReadDateTime());
break;
case BsonType.Int32:
SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt32());
break;
case BsonType.Int64:
SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt64());
break;
case BsonType.Double:
SetToken(NewtonsoftJsonToken.Float, bsonReader.ReadDouble());
break;
case BsonType.Decimal128:
SetToken(NewtonsoftJsonToken.Float, Decimal128.ToDouble(bsonReader.ReadDecimal128()));
break;
default:
throw new NotSupportedException();
}
}
return !bsonReader.IsAtEndOfFile();
}
}
}

166
src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs

@ -0,0 +1,166 @@
// ==========================================================================
// BsonJsonWriter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using MongoDB.Bson.IO;
using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter;
namespace Squidex.Infrastructure.MongoDb
{
public sealed class BsonJsonWriter : NewtonsoftJSonWriter
{
private readonly IBsonWriter bsonWriter;
public BsonJsonWriter(IBsonWriter bsonWriter)
{
Guard.NotNull(bsonWriter, nameof(bsonWriter));
this.bsonWriter = bsonWriter;
}
public override void WritePropertyName(string name, bool escape)
{
bsonWriter.WriteName(name.EscapeJson());
}
public override void WriteStartArray()
{
bsonWriter.WriteStartArray();
}
public override void WriteEndArray()
{
bsonWriter.WriteEndArray();
}
public override void WriteStartObject()
{
bsonWriter.WriteStartDocument();
}
public override void WriteEndObject()
{
bsonWriter.WriteEndDocument();
}
public override void WriteNull()
{
bsonWriter.WriteNull();
}
public override void WriteUndefined()
{
bsonWriter.WriteUndefined();
}
public override void WriteValue(string value)
{
bsonWriter.WriteString(value);
}
public override void WriteValue(int value)
{
bsonWriter.WriteInt32(value);
}
public override void WriteValue(uint value)
{
bsonWriter.WriteInt32((int)value);
}
public override void WriteValue(long value)
{
bsonWriter.WriteInt64(value);
}
public override void WriteValue(ulong value)
{
bsonWriter.WriteInt64((long)value);
}
public override void WriteValue(float value)
{
bsonWriter.WriteDouble(value);
}
public override void WriteValue(double value)
{
bsonWriter.WriteDouble(value);
}
public override void WriteValue(bool value)
{
bsonWriter.WriteBoolean(value);
}
public override void WriteValue(short value)
{
bsonWriter.WriteInt32(value);
}
public override void WriteValue(ushort value)
{
bsonWriter.WriteInt32(value);
}
public override void WriteValue(char value)
{
bsonWriter.WriteInt32(value);
}
public override void WriteValue(byte value)
{
bsonWriter.WriteInt32(value);
}
public override void WriteValue(sbyte value)
{
bsonWriter.WriteInt32(value);
}
public override void WriteValue(decimal value)
{
bsonWriter.WriteDecimal128(value);
}
public override void WriteValue(DateTime value)
{
bsonWriter.WriteString(value.ToString());
}
public override void WriteValue(DateTimeOffset value)
{
bsonWriter.WriteString(value.ToString());
}
public override void WriteValue(byte[] value)
{
bsonWriter.WriteBytes(value);
}
public override void WriteValue(TimeSpan value)
{
bsonWriter.WriteString(value.ToString());
}
public override void WriteValue(Guid value)
{
bsonWriter.WriteString(value.ToString());
}
public override void WriteValue(Uri value)
{
bsonWriter.WriteString(value.ToString());
}
public override void Flush()
{
bsonWriter.Flush();
}
}
}

23
src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs

@ -20,9 +20,7 @@ namespace Squidex.Infrastructure.MongoDb
foreach (var property in source)
{
var key = ReplaceFirstCharacter(property.Key, '$', '§');
result.Add(key, property.Value.ToBson());
result.Add(property.Key.EscapeJson(), property.Value.ToBson());
}
return result;
@ -34,9 +32,7 @@ namespace Squidex.Infrastructure.MongoDb
foreach (var property in source)
{
var key = ReplaceFirstCharacter(property.Name, '§', '$');
result.Add(key, property.Value.ToJson());
result.Add(property.Name.UnescapeBson(), property.Value.ToJson());
}
return result;
@ -133,20 +129,5 @@ namespace Squidex.Infrastructure.MongoDb
throw new NotSupportedException($"Cannot convert {source.GetType()} to Json.");
}
private static string ReplaceFirstCharacter(string value, char toReplace, char replacement)
{
if (value.Length == 0 || value[0] != toReplace)
{
return value;
}
if (value.Length == 1)
{
return toReplace.ToString();
}
return replacement + value.Substring(1);
}
}
}

8
src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs

@ -27,12 +27,16 @@ namespace Squidex.Infrastructure.MongoDb
protected override T DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args)
{
return BsonSerializer.Deserialize<BsonDocument>(context.Reader).ToJson().ToObject<T>(serializer);
var jsonReader = new BsonJsonReader(context.Reader);
return serializer.Deserialize<T>(jsonReader);
}
protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, T value)
{
BsonSerializer.Serialize(context.Writer, JObject.FromObject(value, serializer).ToBson());
var jsonWriter = new BsonJsonWriter(context.Writer);
serializer.Serialize(jsonWriter, value);
}
}
}

31
src/Squidex.Infrastructure.MongoDb/States/MongoState.cs

@ -0,0 +1,31 @@
// ==========================================================================
// MongoState.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.States
{
public sealed class MongoState<T>
{
[BsonId]
[BsonElement]
[BsonRepresentation(BsonType.String)]
public string Id { get; set; }
[BsonRequired]
[BsonElement]
public string Etag { get; set; }
[BsonRequired]
[BsonElement]
[BsonJson]
public T Doc { get; set; }
}
}

44
src/Squidex.Infrastructure.MongoDb/States/MongoStateStore.cs

@ -8,23 +8,14 @@
using System;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.States
{
public sealed class MongoStateStore : IStateStore, IExternalSystem
{
private const string FieldId = "_id";
private const string FieldDoc = "_doc";
private const string FieldEtag = "_etag";
private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true };
private static readonly FilterDefinitionBuilder<BsonDocument> Filter = Builders<BsonDocument>.Filter;
private static readonly UpdateDefinitionBuilder<BsonDocument> Update = Builders<BsonDocument>.Update;
private static readonly ProjectionDefinitionBuilder<BsonDocument> Projection = Builders<BsonDocument>.Projection;
private readonly IMongoDatabase database;
private readonly JsonSerializer serializer;
@ -54,14 +45,12 @@ namespace Squidex.Infrastructure.States
var collection = GetCollection<T>();
var existing =
await collection.Find(Filter.Eq(FieldId, key))
await collection.Find(x => x.Id == key)
.FirstOrDefaultAsync();
if (existing != null)
{
var value = existing[FieldDoc].AsBsonDocument.ToJson().ToObject<T>(serializer);
return (value, existing[FieldEtag].AsString);
return (existing.Doc, existing.Etag);
}
return (default(T), null);
@ -71,31 +60,26 @@ namespace Squidex.Infrastructure.States
{
var collection = GetCollection<T>();
var newData = JToken.FromObject(value, serializer).ToBson();
try
{
await collection.UpdateOneAsync(
Filter.And(
Filter.Eq(FieldId, key),
Filter.Eq(FieldEtag, oldEtag)
),
Update
.Set(FieldEtag, newEtag)
.Set(FieldDoc, newData),
Upsert);
await collection.InsertOneAsync(
/*Builders<MongoState<T>>.Filter.And(
Builders<MongoState<T>>.Filter.Eq(nameof(MongoState<T>.Id), key),
Builders<MongoState<T>>.Filter.Eq(nameof(MongoState<T>.Etag), oldEtag)
),*/
new MongoState<T> { Id = key, Etag = newEtag, Doc = value });
}
catch (MongoWriteException ex)
{
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
var existingEtag =
await collection.Find(Filter.Eq(FieldId, key))
.Project<BsonDocument>(Projection.Exclude(FieldDoc)).FirstOrDefaultAsync();
await collection.Find(x => x.Id == key)
.Project<MongoState<T>>(Builders<MongoState<T>>.Projection.Exclude(x => x.Id)).FirstOrDefaultAsync();
if (existingEtag != null && existingEtag.Contains(FieldEtag))
if (existingEtag != null)
{
throw new InconsistentStateException(existingEtag[FieldEtag].AsString, oldEtag, ex);
throw new InconsistentStateException(existingEtag.Etag, oldEtag, ex);
}
}
else
@ -105,9 +89,9 @@ namespace Squidex.Infrastructure.States
}
}
private IMongoCollection<BsonDocument> GetCollection<T>()
private IMongoCollection<MongoState<T>> GetCollection<T>()
{
return database.GetCollection<BsonDocument>($"States_{typeof(T).Name}");
return database.GetCollection<MongoState<T>>($"States_{typeof(T).Name}");
}
}
}

4
src/Squidex.Infrastructure/States/StateHolder.cs

@ -20,11 +20,11 @@ namespace Squidex.Infrastructure.States
public T State { get; set; }
internal StateHolder(string key, Action written, IStateStore store)
public StateHolder(string key, Action written, IStateStore store)
{
this.key = key;
this.written = written;
this.store = store;
this.written = written;
}
public async Task ReadAsync()

26
tests/Benchmarks/Benchmarks.csproj

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Read\Squidex.Domain.Apps.Read.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure.MongoDb\Squidex.Infrastructure.MongoDb.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.4.4" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.4.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.DependencyInjection">
<HintPath>C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.extensions.dependencyinjection\2.0.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.dll</HintPath>
</Reference>
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

19
tests/Benchmarks/IBenchmark.cs

@ -0,0 +1,19 @@
// ==========================================================================
// IBenchmark.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Benchmarks
{
public interface IBenchmark
{
void RunInitialize();
long Run();
void RunCleanup();
}
}

90
tests/Benchmarks/Program.cs

@ -0,0 +1,90 @@
// ==========================================================================
// Program.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Benchmarks.Tests;
namespace Benchmarks
{
public static class Program
{
private static readonly List<(string Name, IBenchmark Benchmark)> Benchmarks = new IBenchmark[]
{
new AppendToEventStore(),
new AppendToEventStoreWithManyWriters(),
new HandleEvents(),
new HandleEventsWithManyWriters(),
new ReadSchemaState()
}.Select(x => (x.GetType().Name, x)).ToList();
public static void Main(string[] args)
{
var name = "ReadSchemaState";
var selected = Benchmarks.Find(x => x.Name == name);
if (selected.Benchmark == null)
{
Console.WriteLine($"'{name}' is not a valid benchmark, please try: ");
foreach (var b in Benchmarks)
{
Console.WriteLine($" * {b.Name}");
}
}
else
{
const int numRuns = 3;
try
{
var elapsed = 0d;
var count = 0L;
Console.WriteLine($"{selected.Name}: Initialized");
for (var run = 0; run < numRuns; run++)
{
try
{
selected.Benchmark.RunInitialize();
var watch = Stopwatch.StartNew();
count += selected.Benchmark.Run();
watch.Stop();
elapsed += watch.ElapsedMilliseconds;
Console.WriteLine($"{selected.Name}: Run {run + 1} finished");
}
finally
{
selected.Benchmark.RunCleanup();
}
}
var averageElapsed = TimeSpan.FromMilliseconds(elapsed / numRuns);
var averageSeconds = Math.Round(count / (numRuns * averageElapsed.TotalSeconds), 2);
Console.WriteLine($"{selected.Name}: Completed after {averageElapsed}, {averageSeconds} items/s");
}
catch (Exception e)
{
Console.WriteLine($"Benchmark failed with '{e.Message}'");
}
}
Console.ReadLine();
}
}
}

7
tests/Benchmarks/Properties/launchSettings.json

@ -0,0 +1,7 @@
{
"profiles": {
"Benchmarks": {
"commandName": "Project"
}
}
}

140
tests/Benchmarks/Services.cs

@ -0,0 +1,140 @@
// ==========================================================================
// Services.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Benchmarks.Tests.TestData;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using NodaTime;
using NodaTime.Serialization.JsonNet;
using Squidex.Domain.Apps.Core.Apps.Json;
using Squidex.Domain.Apps.Core.Rules.Json;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Schemas.Json;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.States;
namespace Benchmarks
{
public static class Services
{
public static IServiceProvider Create()
{
var services = new ServiceCollection();
services.AddSingleton(CreateTypeNameRegistry());
services.AddSingleton<EventDataFormatter>();
services.AddSingleton<FieldRegistry>();
services.AddTransient<MyAppState>();
services.AddSingleton<IMongoClient>(
new MongoClient("mongodb://localhost"));
services.AddSingleton<ISemanticLog>(
new SemanticLog(new ILogChannel[0], new ILogAppender[0], () => new JsonLogWriter()));
services.AddSingleton<IMemoryCache>(
new MemoryCache(Options.Create(new MemoryCacheOptions())));
services.AddSingleton<IPubSub,
InMemoryPubSub>();
services.AddSingleton<IEventNotifier,
DefaultEventNotifier>();
services.AddSingleton<IEventStore,
MongoEventStore>();
services.AddSingleton<IStateStore,
MongoStateStore>();
services.AddSingleton<IStateFactory,
StateFactory>();
services.AddSingleton<JsonSerializer>(c =>
JsonSerializer.Create(c.GetRequiredService<JsonSerializerSettings>()));
services.AddSingleton<JsonSerializerSettings>(c =>
CreateJsonSerializerSettings(c.GetRequiredService<TypeNameRegistry>(), c.GetRequiredService<FieldRegistry>()));
services.AddSingleton(c =>
c.GetRequiredService<IMongoClient>().GetDatabase(Guid.NewGuid().ToString()));
return services.BuildServiceProvider();
}
public static void Cleanup(this IServiceProvider services)
{
var mongoClient = services.GetRequiredService<IMongoClient>();
var mongoDatabase = services.GetRequiredService<IMongoDatabase>();
mongoClient.DropDatabase(mongoDatabase.DatabaseNamespace.DatabaseName);
if (services is IDisposable disposable)
{
disposable.Dispose();
}
}
private static TypeNameRegistry CreateTypeNameRegistry()
{
var result = new TypeNameRegistry();
result.Map(typeof(MyEvent));
return result;
}
private static JsonSerializerSettings CreateJsonSerializerSettings(TypeNameRegistry typeNameRegistry, FieldRegistry fieldRegistry)
{
var settings = new JsonSerializerSettings();
settings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry);
settings.ContractResolver = new ConverterContractResolver(
new AppClientsConverter(),
new AppContributorsConverter(),
new ClaimsPrincipalConverter(),
new InstantConverter(),
new LanguageConverter(),
new LanguagesConfigConverter(),
new NamedGuidIdConverter(),
new NamedLongIdConverter(),
new NamedStringIdConverter(),
new PropertiesBagConverter<EnvelopeHeaders>(),
new PropertiesBagConverter<PropertiesBag>(),
new RefTokenConverter(),
new RuleConverter(),
new SchemaConverter(fieldRegistry),
new StringEnumConverter());
settings.NullValueHandling = NullValueHandling.Ignore;
settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
settings.DateParseHandling = DateParseHandling.None;
settings.TypeNameHandling = TypeNameHandling.Auto;
settings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
BsonJsonConvention.Register(JsonSerializer.Create(settings));
return settings;
}
}
}

53
tests/Benchmarks/Tests/AppendToEventStore.cs

@ -0,0 +1,53 @@
// ==========================================================================
// AppendToEventStore.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Benchmarks.Utils;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.CQRS.Events;
namespace Benchmarks.Tests
{
public sealed class AppendToEventStore : IBenchmark
{
private IServiceProvider services;
private IEventStore eventStore;
public void RunInitialize()
{
services = Services.Create();
eventStore = services.GetRequiredService<IEventStore>();
}
public long Run()
{
const long numCommits = 100;
const long numStreams = 20;
for (var streamId = 0; streamId < numStreams; streamId++)
{
var eventOffset = -1;
var streamName = streamId.ToString();
for (var commitId = 0; commitId < numCommits; commitId++)
{
eventStore.AppendEventsAsync(Guid.NewGuid(), streamName, eventOffset, new[] { Helper.CreateEventData() }).Wait();
eventOffset++;
}
}
return numCommits * numStreams;
}
public void RunCleanup()
{
services.Cleanup();
}
}
}

52
tests/Benchmarks/Tests/AppendToEventStoreWithManyWriters.cs

@ -0,0 +1,52 @@
// ==========================================================================
// AppendToEventStoreWithManyWriters.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Benchmarks.Utils;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.CQRS.Events;
namespace Benchmarks.Tests
{
public sealed class AppendToEventStoreWithManyWriters : IBenchmark
{
private IServiceProvider services;
private IEventStore eventStore;
public void RunInitialize()
{
services = Services.Create();
eventStore = services.GetRequiredService<IEventStore>();
}
public long Run()
{
const long numCommits = 200;
const long numStreams = 100;
Parallel.For(0, numStreams, streamId =>
{
var streamName = streamId.ToString();
for (var commitId = 0; commitId < numCommits; commitId++)
{
eventStore.AppendEventsAsync(Guid.NewGuid(), streamName, new[] { Helper.CreateEventData() }).Wait();
}
});
return numCommits * numStreams;
}
public void RunCleanup()
{
services.Cleanup();
}
}
}

63
tests/Benchmarks/Tests/HandleEvents.cs

@ -0,0 +1,63 @@
// ==========================================================================
// HandleEvents.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Benchmarks.Tests.TestData;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Events.Actors;
using Squidex.Infrastructure.States;
namespace Benchmarks.Tests
{
public sealed class HandleEvents : IBenchmark
{
private const int NumEvents = 5000;
private IServiceProvider services;
private IEventStore eventStore;
private EventConsumerActor eventConsumerActor;
private EventDataFormatter eventDataFormatter;
private MyEventConsumer eventConsumer;
public void RunInitialize()
{
services = Services.Create();
eventConsumer = new MyEventConsumer(NumEvents);
eventStore = services.GetRequiredService<IEventStore>();
eventDataFormatter = services.GetRequiredService<EventDataFormatter>();
eventConsumerActor = services.GetRequiredService<EventConsumerActor>();
eventConsumerActor.ActivateAsync(services.GetRequiredService<StateHolder<EventConsumerState>>()).Wait();
eventConsumerActor.Activate(eventConsumer);
}
public long Run()
{
var streamName = Guid.NewGuid().ToString();
for (var eventId = 0; eventId < NumEvents; eventId++)
{
var eventData = eventDataFormatter.ToEventData(new Envelope<IEvent>(new MyEvent { EventNumber = eventId + 1 }), Guid.NewGuid());
eventStore.AppendEventsAsync(Guid.NewGuid(), streamName, eventId - 1, new[] { eventData }).Wait();
}
eventConsumer.WaitAndVerify();
return NumEvents;
}
public void RunCleanup()
{
services.Cleanup();
}
}
}

70
tests/Benchmarks/Tests/HandleEventsWithManyWriters.cs

@ -0,0 +1,70 @@
// ==========================================================================
// HandleEventsWithManyWriters.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Benchmarks.Tests.TestData;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Events.Actors;
using Squidex.Infrastructure.States;
namespace Benchmarks.Tests
{
public sealed class HandleEventsWithManyWriters : IBenchmark
{
private const int NumCommits = 200;
private const int NumStreams = 10;
private IServiceProvider services;
private IEventStore eventStore;
private EventConsumerActor eventConsumerActor;
private EventDataFormatter eventDataFormatter;
private MyEventConsumer eventConsumer;
public void RunInitialize()
{
services = Services.Create();
eventConsumer = new MyEventConsumer(NumStreams * NumCommits);
eventStore = services.GetRequiredService<IEventStore>();
eventDataFormatter = services.GetRequiredService<EventDataFormatter>();
eventConsumerActor = services.GetRequiredService<EventConsumerActor>();
eventConsumerActor.ActivateAsync(services.GetRequiredService<StateHolder<EventConsumerState>>()).Wait();
eventConsumerActor.Activate(eventConsumer);
}
public long Run()
{
Parallel.For(0, NumStreams, streamId =>
{
var eventOffset = -1;
var streamName = streamId.ToString();
for (var commitId = 0; commitId < NumCommits; commitId++)
{
var eventData = eventDataFormatter.ToEventData(new Envelope<IEvent>(new MyEvent()), Guid.NewGuid());
eventStore.AppendEventsAsync(Guid.NewGuid(), streamName, eventOffset - 1, new[] { eventData }).Wait();
eventOffset++;
}
});
eventConsumer.WaitAndVerify();
return NumStreams * NumCommits;
}
public void RunCleanup()
{
services.Cleanup();
}
}
}

102
tests/Benchmarks/Tests/ReadSchemaState.cs

@ -0,0 +1,102 @@
// ==========================================================================
// ReadSchemaState.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Benchmarks.Tests.TestData;
using Microsoft.Extensions.DependencyInjection;
using NodaTime;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Read.State.Orleans.Grains;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
namespace Benchmarks.Tests
{
public class ReadSchemaState : IBenchmark
{
private IServiceProvider services;
private MyAppState grain;
public void RunInitialize()
{
services = Services.Create();
grain = services.GetRequiredService<IStateFactory>().GetAsync<MyAppState, AppStateGrainState>("DEFAULT").Result;
var state = new AppStateGrainState
{
App = new JsonAppEntity
{
Id = Guid.NewGuid()
}
};
state.Schemas = new Dictionary<Guid, JsonSchemaEntity>();
for (var i = 1; i <= 100; i++)
{
var schema = new JsonSchemaEntity
{
Id = Guid.NewGuid(),
Created = SystemClock.Instance.GetCurrentInstant(),
CreatedBy = new RefToken("user", "1"),
LastModified = SystemClock.Instance.GetCurrentInstant(),
LastModifiedBy = new RefToken("user", "1"),
SchemaDef = new Schema("Name")
};
for (var j = 1; j < 30; j++)
{
schema.SchemaDef = schema.SchemaDef.AddField(new StringField(j, j.ToString(), Partitioning.Invariant));
}
state.Schemas.Add(schema.Id, schema);
}
state.Rules = new Dictionary<Guid, JsonRuleEntity>();
for (var i = 0; i < 100; i++)
{
var rule = new JsonRuleEntity
{
Id = Guid.NewGuid(),
Created = SystemClock.Instance.GetCurrentInstant(),
CreatedBy = new RefToken("user", "1"),
LastModified = SystemClock.Instance.GetCurrentInstant(),
LastModifiedBy = new RefToken("user", "1"),
RuleDef = new Rule(new ContentChangedTrigger(), new WebhookAction())
};
state.Rules.Add(rule.Id, rule);
}
grain.SetState(state);
grain.WriteStateAsync().Wait();
}
public long Run()
{
for (var i = 0; i < 10; i++)
{
grain.ReadStateAsync().Wait();
}
return 10;
}
public void RunCleanup()
{
services.Cleanup();
}
}
}

21
tests/Benchmarks/Tests/TestData/MyAppState.cs

@ -0,0 +1,21 @@
// ==========================================================================
// ReadSchemaState.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Read.State.Orleans.Grains;
using Squidex.Infrastructure.States;
namespace Benchmarks.Tests.TestData
{
public sealed class MyAppState : StatefulObject<AppStateGrainState>
{
public void SetState(AppStateGrainState state)
{
State = state;
}
}
}

19
tests/Benchmarks/Tests/TestData/MyEvent.cs

@ -0,0 +1,19 @@
// ==========================================================================
// MyEvent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Benchmarks.Tests.TestData
{
[TypeName("MyEvent")]
public sealed class MyEvent : IEvent
{
public int EventNumber { get; set; }
}
}

79
tests/Benchmarks/Tests/TestData/MyEventConsumer.cs

@ -0,0 +1,79 @@
// ==========================================================================
// MyEventConsumer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Tasks;
namespace Benchmarks.Tests.TestData
{
public sealed class MyEventConsumer : IEventConsumer
{
private readonly TaskCompletionSource<object> completion = new TaskCompletionSource<object>();
private readonly int numEvents;
public List<int> EventNumbers { get; } = new List<int>();
public string Name
{
get { return typeof(MyEventConsumer).Name; }
}
public string EventsFilter
{
get { return string.Empty; }
}
public MyEventConsumer(int numEvents)
{
this.numEvents = numEvents;
}
public Task ClearAsync()
{
return TaskHelper.Done;
}
public void WaitAndVerify()
{
completion.Task.Wait();
if (EventNumbers.Count != numEvents)
{
throw new InvalidOperationException($"{EventNumbers.Count} Events have been handled");
}
for (var i = 0; i < EventNumbers.Count; i++)
{
var value = EventNumbers[i];
if (value != i + 1)
{
throw new InvalidOperationException($"Event[{i}] != value");
}
}
}
public Task On(Envelope<IEvent> @event)
{
if (@event.Payload is MyEvent myEvent)
{
EventNumbers.Add(myEvent.EventNumber);
if (myEvent.EventNumber == numEvents)
{
completion.SetResult(true);
}
}
return TaskHelper.Done;
}
}
}

21
tests/Benchmarks/Utils/Helper.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Helper.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure.CQRS.Events;
namespace Benchmarks.Utils
{
public static class Helper
{
public static EventData CreateEventData()
{
return new EventData { EventId = Guid.NewGuid(), Metadata = "EventMetdata", Payload = "EventPayload", Type = "MyEvent" };
}
}
}
Loading…
Cancel
Save