diff --git a/src/EdgeDB.Net.Driver/Binary/Builders/ObjectBuilder.cs b/src/EdgeDB.Net.Driver/Binary/Builders/ObjectBuilder.cs index 0a185f45..784b322f 100644 --- a/src/EdgeDB.Net.Driver/Binary/Builders/ObjectBuilder.cs +++ b/src/EdgeDB.Net.Driver/Binary/Builders/ObjectBuilder.cs @@ -10,67 +10,63 @@ namespace EdgeDB { internal sealed class ObjectBuilder { - private static readonly Dictionary _codecVisitorStateTable = new(); + private static readonly ConcurrentDictionary _codecVisitorStateTable = new(); private static readonly object _visitorLock = new(); - public static TType? BuildResult(EdgeDBBinaryClient client, ICodec codec, in ReadOnlyMemory data) + public readonly struct PreheatedCodec { - // TO INVESTIGATE: since a codec can only be "visited" or "mutated" for - // one type at a time, we have to ensure that the codec is ready to deserialize - // TType, we can store the states of the codecs here for building result - // to achieve this. - var typeCodec = codec; - - bool wasSkipped = false; - lock(_visitorLock) - { - // Since the supplied codec could be a template, we walk the codec and cache based on the supplied type - // if the codec hasn't been walked OR the result type is an object we walk it and cache it if its not an - // object codec. - if (typeof(TType) != typeof(object) && _codecVisitorStateTable.TryGetValue(typeof(TType), out var info)) - { - if(codec.GetHashCode() == info.Version) - { - typeCodec = info.Codec; - wasSkipped = true; - } - } - - if (!wasSkipped) - { - var version = codec.GetHashCode(); - - var visitor = new TypeVisitor(client); + public readonly ICodec Codec; - visitor.SetTargetType(typeof(TType)); + public PreheatedCodec(ICodec codec) + { + Codec = codec; + } + } - visitor.Visit(ref codec); + public static PreheatedCodec PreheatCodec(EdgeDBBinaryClient client, ICodec codec) + { + // if the codec has been visited before and we have the most up-to-date version, return it. + if ( + typeof(T) != typeof(object) && + _codecVisitorStateTable.TryGetValue(typeof(T), out var info) && + codec.GetHashCode() == info.Version) + { + client.Logger.SkippingCodecVisiting(typeof(T), codec); + return new(info.Codec); + } - if (typeof(TType) != typeof(object)) - _codecVisitorStateTable[typeof(TType)] = (version, codec); + var version = codec.GetHashCode(); - typeCodec = codec; + var visitor = new TypeVisitor(client); + visitor.SetTargetType(typeof(T)); + visitor.Visit(ref codec); - client.Logger.ObjectDeserializationPrep(CodecFormatter.Format(typeCodec).ToString()); - } - } + if (typeof(T) != typeof(object)) + _codecVisitorStateTable[typeof(T)] = (version, codec); - if(wasSkipped) + if (client.Logger.IsEnabled(LogLevel.Debug)) { - client.Logger.SkippingCodecVisiting(typeof(TType), codec); + client.Logger.ObjectDeserializationPrep(CodecFormatter.Format(codec).ToString()); } - - if (typeCodec is ObjectCodec objectCodec) + return new(codec); + } + + public static T? BuildResult(EdgeDBBinaryClient client, in PreheatedCodec preheated, in ReadOnlyMemory data) + { + if (preheated.Codec is ObjectCodec objectCodec) { - return (TType?)TypeBuilder.BuildObject(client, typeof(TType), objectCodec, in data); + return (T?)TypeBuilder.BuildObject(client, typeof(T), objectCodec, in data); } - var value = typeCodec.Deserialize(client, in data); + var value = preheated.Codec.Deserialize(client, in data); - return (TType?)ConvertTo(typeof(TType), value); + return (T?)ConvertTo(typeof(T), value); } + public static T? BuildResult(EdgeDBBinaryClient client, ICodec codec, in ReadOnlyMemory data) + => BuildResult(client, PreheatCodec(client, codec), data); + public static object? ConvertTo(Type type, object? value) { if (value is null) @@ -102,7 +98,7 @@ internal sealed class ObjectBuilder { return ConvertCollection(type, valueType, value); } - + // check for edgeql types //if (TypeBuilder.IsValidObjectType(type) && value is IDictionary dict) // return TypeBuilder.BuildObject(type, dict); @@ -162,13 +158,13 @@ internal sealed class ObjectBuilder foreach (var val in (IEnumerable)value) { converted.Add(strongInnerType is not null ? ConvertTo(strongInnerType, val) : val); - + //if (val is IDictionary raw) //{ // converted.Add(strongInnerType is not null ? TypeBuilder.BuildObject(strongInnerType, raw) : val); //} //else - + } diff --git a/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs b/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs index b3685a80..a8aff9be 100644 --- a/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs +++ b/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs @@ -27,11 +27,11 @@ public static class TypeBuilder /// using this naming strategy, the naming convention of the dotnet type will be preserved. /// /// - /// If the naming strategy doesn't find a match, the + /// If the naming strategy doesn't find a match, the /// will be used. /// public static INamingStrategy SchemaNamingStrategy { get; set; } - + internal readonly static ConcurrentDictionary TypeInfo = new(); internal readonly static ConcurrentDictionary TypeConverters = new(); internal static readonly INamingStrategy AttributeNamingStrategy; @@ -56,7 +56,7 @@ public static void AddOrUpdateTypeBuilder( if(!EdgeDBTypeConstructorInfo.TryGetConstructorInfo(typeof(TType), out var ctorInfo) || ctorInfo.EmptyConstructor is null) throw new TargetInvocationException($"Cannot create an instance of {typeof(TType).Name}: no empty constructor found", null); - object Factory(ref ObjectEnumerator enumerator) + object? Factory(ref ObjectEnumerator enumerator) { var instance = (TType)ctorInfo.EmptyConstructor.Invoke(Array.Empty()); @@ -127,7 +127,7 @@ public static bool TryRemoveTypeFactory([MaybeNullWhen(false)]out TypeDes internal static bool TryGetTypeDeserializerInfo(Type type, [MaybeNullWhen(false)] out EdgeDBTypeDeserializeInfo info) { info = null; - + if (!IsValidObjectType(type)) return false; @@ -138,15 +138,15 @@ internal static bool TryGetTypeDeserializerInfo(Type type, [MaybeNullWhen(false) } else info = typeInfo; - + return info is not null; } - + internal static object? BuildObject(EdgeDBBinaryClient client, Type type, Binary.Codecs.ObjectCodec codec, in ReadOnlyMemory data) { if (!IsValidObjectType(type)) throw new InvalidOperationException($"Cannot deserialize data to {type.Name}"); - + if (!TypeInfo.TryGetValue(type, out EdgeDBTypeDeserializeInfo? info)) { info = TypeInfo.AddOrUpdate(type, new EdgeDBTypeDeserializeInfo(type), (_, v) => v); @@ -197,7 +197,7 @@ internal static bool IsValidObjectType(Type type) } return - type == typeof(object) || + type == typeof(object) || type.IsAssignableTo(typeof(ITuple)) || type.IsAbstract || type.IsRecord() || @@ -226,7 +226,7 @@ private static object CreateDynamicList(Array arr, Type elementType) return inst; } - + internal static bool TryGetCustomBuilder(this Type objectType, out MethodInfo? info) { info = null; diff --git a/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs b/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs index 474038ae..57f14c15 100644 --- a/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs +++ b/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs @@ -14,6 +14,7 @@ using EdgeDB.Binary.Protocol; using ProtocolExecuteResult = EdgeDB.Binary.Protocol.ExecuteResult; using EdgeDB.Binary.Protocol.Common; +using System.Diagnostics; namespace EdgeDB { @@ -32,7 +33,7 @@ internal abstract class EdgeDBBinaryClient : BaseEdgeDBClient remove => OnDisconnectInternal.Remove((c) => value()); } #endregion - + /// /// Gets whether or not this connection is idle. /// @@ -54,7 +55,7 @@ internal ref Guid StateDescriptorId internal byte[] ServerKey; internal int? SuggestedPoolConcurrency; - + internal readonly ILogger Logger; internal readonly TimeSpan MessageTimeout; internal readonly TimeSpan ConnectionTimeout; @@ -62,10 +63,10 @@ internal ref Guid StateDescriptorId internal EdgeDBConfig ClientConfig => _config; - + protected CancellationToken DisconnectCancelToken => Duplexer.DisconnectToken; - + private ICodec? _stateCodec; private Guid _stateDescriptorId; @@ -136,6 +137,7 @@ public async Task SyncAsync(CancellationToken token = default) /// A codec could not be found for the given input arguments or the result. internal virtual async Task ExecuteInternalAsync(string query, IDictionary? args = null, Cardinality? cardinality = null, Capabilities? capabilities = Capabilities.Modifications, IOFormat format = IOFormat.Binary, bool isRetry = false, bool implicitTypeName = false, + Func? preheat = null, CancellationToken token = default) { // if the current client is not connected, reconnect it @@ -148,7 +150,7 @@ internal virtual async Task ExecuteInternalAsync(string q await _semaphore.WaitAsync(linkedTokenSource.Token).ConfigureAwait(false); await _readySource.Task; - + IsIdle = false; bool released = false; @@ -158,6 +160,33 @@ internal virtual async Task ExecuteInternalAsync(string q { var parseResult = await _protocolProvider.ParseQueryAsync(arguments, linkedTokenSource.Token); + if (preheat is not null) + { + var executeTask = + _protocolProvider.ExecuteQueryAsync(arguments, parseResult, linkedTokenSource.Token); + +#if DEBUG + async Task PreheatWithTrace(ParseResult p) + { + var stopwatch = Stopwatch.StartNew(); + await preheat(p); + stopwatch.Stop(); + Logger.LogDebug("Preheating of codecs took {@PreheatTime}ms", Math.Round(stopwatch.Elapsed.TotalMilliseconds, 4)); + } + + var preheatTask = PreheatWithTrace(parseResult); +#else + var preheatTask = preheat(parseResult); +#endif + + await Task.WhenAll( + preheatTask, + executeTask + ); + + return executeTask.Result; + } + return await _protocolProvider.ExecuteQueryAsync(arguments, parseResult, linkedTokenSource.Token); } catch (OperationCanceledException ce) @@ -178,14 +207,14 @@ internal virtual async Task ExecuteInternalAsync(string q _semaphore.Release(); released = true; - return await ExecuteInternalAsync(query, args, cardinality, capabilities, format, true, implicitTypeName, token).ConfigureAwait(false); + return await ExecuteInternalAsync(query, args, cardinality, capabilities, format, true, implicitTypeName, preheat, token).ConfigureAwait(false); } catch (EdgeDBException x) when (x.ShouldRetry && !isRetry) { _semaphore.Release(); released = true; - return await ExecuteInternalAsync(query, args, cardinality, capabilities, format, true, implicitTypeName, token).ConfigureAwait(false); + return await ExecuteInternalAsync(query, args, cardinality, capabilities, format, true, implicitTypeName, preheat, token).ConfigureAwait(false); } catch (Exception x) { @@ -231,6 +260,7 @@ public override async Task ExecuteAsync(string query, IDictionary Task.Run(() => ObjectBuilder.PreheatCodec(this, parseResult.OutCodecInfo.Codec), token), token: token); var array = new TResult?[result.Data.Length]; @@ -259,13 +289,14 @@ public override async Task ExecuteAsync(string query, IDictionary Task.Run(() => ObjectBuilder.PreheatCodec(this, parseResult.OutCodecInfo.Codec), token), token: token); if (result.Data.Length > 1) @@ -289,19 +320,20 @@ public override async Task QueryRequiredSingleAsync(string que Capabilities? capabilities = Capabilities.Modifications, CancellationToken token = default) { var implicitTypeName = TypeBuilder.TryGetTypeDeserializerInfo(typeof(TResult), out var info) && info.RequiresTypeName; - + var result = await ExecuteInternalAsync( query, args, Cardinality.AtMostOne, capabilities, implicitTypeName: implicitTypeName, + preheat: (parseResult) => Task.Run(() => ObjectBuilder.PreheatCodec(this, parseResult.OutCodecInfo.Codec), token), token: token); if (result.Data.Length is > 1 or 0) throw new ResultCardinalityMismatchException(Cardinality.One, result.Data.Length > 1 ? Cardinality.Many : Cardinality.AtMostOne); - + return result.Data.Length != 1 ? throw new MissingRequiredException() : ObjectBuilder.BuildResult(this, result.OutCodecInfo.Codec, in result.Data[0])!; @@ -316,7 +348,7 @@ public override async Task QueryRequiredSingleAsync(string que public override async Task QueryJsonAsync(string query, IDictionary? args = null, Capabilities? capabilities = Capabilities.Modifications, CancellationToken token = default) { var result = await ExecuteInternalAsync(query, args, Cardinality.Many, capabilities, IOFormat.Json, token: token); - + return result.Data.Length == 1 ? (string)result.OutCodecInfo.Codec.Deserialize(this, in result.Data[0])! : "[]"; @@ -411,8 +443,8 @@ internal void UpdateStateCodec(ICodec codec, in Guid stateCodecId) /// Connects and authenticates this client. /// /// - /// This task waits for the underlying connection to receive a - /// ready message indicating the client + /// This task waits for the underlying connection to receive a + /// ready message indicating the client /// can start to preform queries. /// /// @@ -522,12 +554,12 @@ private async Task ConnectInternalAsync(int attempts = 0, CancellationToken toke await ConnectInternalAsync(attempts, token); return; } - } + } if(Duplexer is StreamDuplexer streamDuplexer) streamDuplexer.Init(stream); - + // send handshake await Duplexer.SendAsync(token, _protocolProvider.Handshake()).ConfigureAwait(false); } @@ -593,8 +625,8 @@ public void Dispose() /// /// A cancellation token used to cancel the asynchronous operation. /// - /// A task representing the asynchronous operation of opening or - /// initializing a stream; the result of the task is a stream the + /// A task representing the asynchronous operation of opening or + /// initializing a stream; the result of the task is a stream the /// binary client can use to read from/write to the database with. /// protected abstract ValueTask GetStreamAsync(CancellationToken token = default); @@ -604,7 +636,7 @@ public void Dispose() /// /// A cancellation token used to cancel the asynchronous operation. /// - /// A task that represents the asynchronous closing operation of + /// A task that represents the asynchronous closing operation of /// the stream. /// protected abstract ValueTask CloseStreamAsync(CancellationToken token = default); diff --git a/src/EdgeDB.Net.Driver/EdgeDB.Net.Driver.csproj b/src/EdgeDB.Net.Driver/EdgeDB.Net.Driver.csproj index 84b40e44..af3526d4 100644 --- a/src/EdgeDB.Net.Driver/EdgeDB.Net.Driver.csproj +++ b/src/EdgeDB.Net.Driver/EdgeDB.Net.Driver.csproj @@ -22,9 +22,15 @@ + - + + + + + + diff --git a/tests/EdgeDB.Tests.Benchmarks/EdgeDB.Tests.Benchmarks.csproj b/tests/EdgeDB.Tests.Benchmarks/EdgeDB.Tests.Benchmarks.csproj index bedb62e0..add345b0 100644 --- a/tests/EdgeDB.Tests.Benchmarks/EdgeDB.Tests.Benchmarks.csproj +++ b/tests/EdgeDB.Tests.Benchmarks/EdgeDB.Tests.Benchmarks.csproj @@ -10,7 +10,8 @@ - + + diff --git a/tests/EdgeDB.Tests.Benchmarks/FullExecuteBenchmark.cs b/tests/EdgeDB.Tests.Benchmarks/FullExecuteBenchmark.cs index ccd29cc3..1af3817f 100644 --- a/tests/EdgeDB.Tests.Benchmarks/FullExecuteBenchmark.cs +++ b/tests/EdgeDB.Tests.Benchmarks/FullExecuteBenchmark.cs @@ -1,13 +1,10 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnostics.dotTrace; using EdgeDB.Tests.Benchmarks.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace EdgeDB.Tests.Benchmarks { + [DotTraceDiagnoser] public class FullExecuteBenchmark { public EdgeDBClient? Client; diff --git a/tests/EdgeDB.Tests.Benchmarks/Program.cs b/tests/EdgeDB.Tests.Benchmarks/Program.cs index 2cc40237..a230348b 100644 --- a/tests/EdgeDB.Tests.Benchmarks/Program.cs +++ b/tests/EdgeDB.Tests.Benchmarks/Program.cs @@ -2,6 +2,6 @@ using BenchmarkDotNet.Running; using EdgeDB.Tests.Benchmarks; -BenchmarkRunner.Run(); +BenchmarkRunner.Run(); await Task.Delay(-1);