diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index eb80910..595282a 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -579,7 +579,7 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory if (additionalCommandState is not null && additionalCommandState.HasCommandProperties) { sb.Indent() - .NewLine().Append("var cmd = TryReuse(ref Storage, sql, commandType, args);") + .NewLine().Append("var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool);") .NewLine().Append("if (cmd is null)").Indent() .NewLine().Append("cmd = base.GetCommand(connection, sql, commandType, args);"); WriteCommandProperties(ctx, sb, "cmd", additionalCommandState.CommandProperties); @@ -587,22 +587,26 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory } else { - sb.Indent(false).NewLine().Append(" => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);").Outdent(false); + sb.Indent(false).NewLine().Append(" => TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args);").Outdent(false); } - sb.NewLine().NewLine().Append("public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);").NewLine(); + sb.NewLine().NewLine().Append("public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);").NewLine(); if (cacheCount == 1) { + sb.Append("private static readonly DbCommandCache _cmdPool = new();").NewLine(); + sb.Append("[global::System.ThreadStatic] // note this works correctly with ref").NewLine(); sb.Append("private static global::System.Data.Common.DbCommand? Storage;").NewLine(); } else { + sb.Append("private readonly DbCommandCache _cmdPool = new(); // note: per cache instance").NewLine(); sb.Append("protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}").NewLine().NewLine(); for (int i = 0; i < cacheCount; i++) { sb.Append("internal sealed class Cached").Append(i).Append(" : CommandFactory").Append(index).Indent().NewLine() .Append("protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;").NewLine() + .Append("[global::System.ThreadStatic] // note this works correctly with ref-return").NewLine() .Append("private static global::System.Data.Common.DbCommand? s_Storage;").NewLine() .Outdent().NewLine(); } diff --git a/src/Dapper.AOT/CommandFactory.cs b/src/Dapper.AOT/CommandFactory.cs index b4331f9..cc60a6e 100644 --- a/src/Dapper.AOT/CommandFactory.cs +++ b/src/Dapper.AOT/CommandFactory.cs @@ -1,5 +1,6 @@ using Dapper.Internal; using System; +using System.Collections.Concurrent; using System.Data; using System.Data.Common; using System.Diagnostics; @@ -175,12 +176,53 @@ protected static void SetValueWithDefaultSize(DbParameter parameter, string? val /// /// Provides an opportunity to recycle and reuse command instances /// - protected static bool TryRecycle(ref DbCommand? storage, DbCommand command) + protected static bool TryRecycleInterlocked(ref DbCommand? storage, DbCommand command, DbCommandCache? cache = null) { // detach and recycle command.Connection = null; command.Transaction = null; - return Interlocked.CompareExchange(ref storage, command, null) is null; + if (Interlocked.CompareExchange(ref storage, command, null) is null) + { + return true; + } + return cache is not null && cache.TryPut(command); + } + + /// + /// Provides an opportunity to recycle and reuse command instances + /// + protected static bool TryRecycleThreadStatic(ref DbCommand? storage, DbCommand command, DbCommandCache? cache = null) + { + // detach and recycle + command.Connection = null; + command.Transaction = null; + if (storage is null) + { + storage = command; + return true; + } + return cache is not null && cache.TryPut(command); + } + + + /// + /// A simple store for command re-use. + /// + protected sealed class DbCommandCache(int capacity = 16) + { + private readonly ConcurrentQueue store = []; + internal bool TryPut(DbCommand command) + { + if (store.Count < capacity) // not exact - inherent race condition + { + store.Enqueue(command); + return true; + } + return false; + } + + internal DbCommand? TryTake() + => store.TryDequeue(out var cmd) ? cmd : null; } } @@ -252,9 +294,26 @@ public virtual void UpdateParameters(in UnifiedCommand command, T args) /// /// Provides an opportunity to recycle and reuse command instances /// - protected DbCommand? TryReuse(ref DbCommand? storage, string sql, CommandType commandType, T args) + protected DbCommand? TryReuseThreadStatic(ref DbCommand? storage, string sql, CommandType commandType, T args, DbCommandCache? cache = null) + { + var cmd = storage ?? cache?.TryTake(); + storage = null; + if (cmd is not null) + { + // try to avoid any dirty detection in the setters + if (cmd.CommandText != sql) cmd.CommandText = sql; + if (cmd.CommandType != commandType) cmd.CommandType = commandType; + UpdateParameters(new(cmd), args); + } + return cmd; + } + + /// + /// Provides an opportunity to recycle and reuse command instances + /// + protected DbCommand? TryReuseInterlocked(ref DbCommand? storage, string sql, CommandType commandType, T args, DbCommandCache? cache = null) { - var cmd = Interlocked.Exchange(ref storage, null); + var cmd = Interlocked.Exchange(ref storage, null) ?? cache?.TryTake(); if (cmd is not null) { // try to avoid any dirty detection in the setters diff --git a/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.cs b/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.cs index f3a94ae..0786934 100644 --- a/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.cs @@ -118,20 +118,23 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, object? args) - => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args); + => TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args); - public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command); + public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool); + private readonly DbCommandCache _cmdPool = new(); // note: per cache instance protected abstract ref global::System.Data.Common.DbCommand? Storage {get;} internal sealed class Cached0 : CommandFactory0 { protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage; + [global::System.ThreadStatic] // note this works correctly with ref-return private static global::System.Data.Common.DbCommand? s_Storage; } internal sealed class Cached1 : CommandFactory0 { protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage; + [global::System.ThreadStatic] // note this works correctly with ref-return private static global::System.Data.Common.DbCommand? s_Storage; } @@ -165,9 +168,11 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, object? args) - => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args); + => TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args); - public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command); + public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool); + private static readonly DbCommandCache _cmdPool = new(); + [global::System.ThreadStatic] // note this works correctly with ref private static global::System.Data.Common.DbCommand? Storage; } diff --git a/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.netfx.cs index f3a94ae..0786934 100644 --- a/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.netfx.cs @@ -118,20 +118,23 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, object? args) - => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args); + => TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args); - public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command); + public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool); + private readonly DbCommandCache _cmdPool = new(); // note: per cache instance protected abstract ref global::System.Data.Common.DbCommand? Storage {get;} internal sealed class Cached0 : CommandFactory0 { protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage; + [global::System.ThreadStatic] // note this works correctly with ref-return private static global::System.Data.Common.DbCommand? s_Storage; } internal sealed class Cached1 : CommandFactory0 { protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage; + [global::System.ThreadStatic] // note this works correctly with ref-return private static global::System.Data.Common.DbCommand? s_Storage; } @@ -165,9 +168,11 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, object? args) - => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args); + => TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args); - public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command); + public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool); + private static readonly DbCommandCache _cmdPool = new(); + [global::System.ThreadStatic] // note this works correctly with ref private static global::System.Data.Common.DbCommand? Storage; } diff --git a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs index c57fae0..fd44a52 100644 --- a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs @@ -199,7 +199,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, object? args) { - var cmd = TryReuse(ref Storage, sql, commandType, args); + var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool); if (cmd is null) { cmd = base.GetCommand(connection, sql, commandType, args); @@ -212,7 +212,9 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje return cmd; } - public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command); + public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool); + private readonly DbCommandCache _cmdPool = new(); + [global::System.ThreadStatic] // note this works correctly with ref private static global::System.Data.Common.DbCommand? Storage; } @@ -256,7 +258,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, object? args) { - var cmd = TryReuse(ref Storage, sql, commandType, args); + var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool); if (cmd is null) { cmd = base.GetCommand(connection, sql, commandType, args); @@ -269,18 +271,21 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje return cmd; } - public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command); + public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool); + private readonly DbCommandCache _cmdPool = new(); protected abstract ref global::System.Data.Common.DbCommand? Storage {get;} internal sealed class Cached0 : CommandFactory1 { protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage; + [global::System.ThreadStatic] // note this works correctly with ref-return private static global::System.Data.Common.DbCommand? s_Storage; } internal sealed class Cached1 : CommandFactory1 { protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage; + [global::System.ThreadStatic] // note this works correctly with ref-return private static global::System.Data.Common.DbCommand? s_Storage; } diff --git a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs index c57fae0..fd44a52 100644 --- a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs @@ -199,7 +199,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, object? args) { - var cmd = TryReuse(ref Storage, sql, commandType, args); + var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool); if (cmd is null) { cmd = base.GetCommand(connection, sql, commandType, args); @@ -212,7 +212,9 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje return cmd; } - public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command); + public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool); + private readonly DbCommandCache _cmdPool = new(); + [global::System.ThreadStatic] // note this works correctly with ref private static global::System.Data.Common.DbCommand? Storage; } @@ -256,7 +258,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, object? args) { - var cmd = TryReuse(ref Storage, sql, commandType, args); + var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool); if (cmd is null) { cmd = base.GetCommand(connection, sql, commandType, args); @@ -269,18 +271,21 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje return cmd; } - public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command); + public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool); + private readonly DbCommandCache _cmdPool = new(); protected abstract ref global::System.Data.Common.DbCommand? Storage {get;} internal sealed class Cached0 : CommandFactory1 { protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage; + [global::System.ThreadStatic] // note this works correctly with ref-return private static global::System.Data.Common.DbCommand? s_Storage; } internal sealed class Cached1 : CommandFactory1 { protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage; + [global::System.ThreadStatic] // note this works correctly with ref-return private static global::System.Data.Common.DbCommand? s_Storage; }