From 256ea2fa438aabce808a1081cacaab559c19cc16 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Thu, 24 Oct 2024 22:43:08 -0300 Subject: [PATCH 01/36] Adds FbTransactionInfo.GetTransactionId(). --- .../FbTransactionTests.cs | 18 ++++++++++++++++++ .../Common/IscHelper.cs | 4 ++++ .../FirebirdClient/FbTransactionInfo.cs | 9 +++++++++ 3 files changed, 31 insertions(+) diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbTransactionTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbTransactionTests.cs index d3a7d9908..144375b74 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbTransactionTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbTransactionTests.cs @@ -139,4 +139,22 @@ public async Task SnapshotAtNumber() } } } + + [Test] + public async Task CanGetTransactionId() + { + if (!EnsureServerVersionAtLeast(new Version(2, 5, 0, 0))) + return; + + await using (var transaction1 = await Connection.BeginTransactionAsync()) + { + var idFromInfo = await new FbTransactionInfo(transaction1).GetTransactionIdAsync(); + Assert.NotZero(idFromInfo); + + var command = new FbCommand("SELECT current_transaction FROM rdb$database", Connection, transaction1); + var idFromSql = await command.ExecuteScalarAsync(); + + Assert.AreEqual(idFromInfo, idFromSql); + } + } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/IscHelper.cs b/src/FirebirdSql.Data.FirebirdClient/Common/IscHelper.cs index 578895f7a..3cf67ac76 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/IscHelper.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/IscHelper.cs @@ -206,6 +206,10 @@ public static List ParseTransactionInfo(byte[] buffer, Charset charset) case IscCodes.isc_info_error: throw FbException.Create("Received error response."); + case IscCodes.isc_info_tra_id: + info.Add(VaxInteger(buffer, pos, length)); + break; + case IscCodes.fb_info_tra_snapshot_number: info.Add(VaxInteger(buffer, pos, length)); break; diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbTransactionInfo.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbTransactionInfo.cs index df3cb6192..41c928474 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbTransactionInfo.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbTransactionInfo.cs @@ -33,6 +33,15 @@ public sealed class FbTransactionInfo #region Methods + public long GetTransactionId() + { + return GetValue(IscCodes.isc_info_tra_id); + } + public Task GetTransactionIdAsync(CancellationToken cancellationToken = default) + { + return GetValueAsync(IscCodes.isc_info_tra_id, cancellationToken); + } + public long GetTransactionSnapshotNumber() { return GetValue(IscCodes.fb_info_tra_snapshot_number); From 7dd65e8320ebc1210dfa4c9e5a83524db3bef011 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Thu, 24 Oct 2024 22:45:59 -0300 Subject: [PATCH 02/36] Adds .NET distributed tracing instrumentation. --- .../FirebirdClient/FbCommand.cs | 125 +++++++++++----- .../FirebirdSql.Data.FirebirdClient.csproj | 1 + .../Trace/FbActivitySource.cs | 137 ++++++++++++++++++ 3 files changed, 227 insertions(+), 36 deletions(-) create mode 100644 src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs index 551c6ea64..cf0b0e2dd 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs @@ -20,11 +20,13 @@ using System.ComponentModel; using System.Data; using System.Data.Common; +using System.Diagnostics; using System.Text; using System.Threading; using System.Threading.Tasks; using FirebirdSql.Data.Common; using FirebirdSql.Data.Logging; +using FirebirdSql.Data.Trace; using Microsoft.Extensions.Logging; namespace FirebirdSql.Data.FirebirdClient; @@ -50,6 +52,7 @@ public sealed class FbCommand : DbCommand, IFbPreparedCommand, IDescriptorFiller private int? _commandTimeout; private int _fetchSize; private Type[] _expectedColumnTypes; + private Activity _currentActivity; #endregion @@ -1064,6 +1067,13 @@ internal void Release() _statement.Dispose2(); _statement = null; } + + if (_currentActivity != null) + { + // Do not set status to Ok: https://opentelemetry.io/docs/concepts/signals/traces/#span-status + _currentActivity.Dispose(); + _currentActivity = null; + } } Task IFbPreparedCommand.ReleaseAsync(CancellationToken cancellationToken) => ReleaseAsync(cancellationToken); internal async Task ReleaseAsync(CancellationToken cancellationToken = default) @@ -1082,6 +1092,13 @@ internal async Task ReleaseAsync(CancellationToken cancellationToken = default) await _statement.Dispose2Async(cancellationToken).ConfigureAwait(false); _statement = null; } + + if (_currentActivity != null) + { + // Do not set status to Ok: https://opentelemetry.io/docs/concepts/signals/traces/#span-status + _currentActivity.Dispose(); + _currentActivity = null; + } } void IFbPreparedCommand.TransactionCompleted() => TransactionCompleted(); @@ -1302,6 +1319,26 @@ private async ValueTask UpdateParameterValuesAsync(Descriptor descriptor, Cancel #endregion + #region Tracing + + private void TraceCommandStart() + { + Debug.Assert(_currentActivity == null); + if (FbActivitySource.Source.HasListeners()) + _currentActivity = FbActivitySource.CommandStart(this); + } + + private void TraceCommandException(Exception e) + { + if (_currentActivity != null) + { + FbActivitySource.CommandException(_currentActivity, e); + _currentActivity = null; + } + } + + #endregion Tracing + #region Private Methods private void Prepare(bool returnsSet) @@ -1446,57 +1483,73 @@ private async Task PrepareAsync(bool returnsSet, CancellationToken cancellationT private void ExecuteCommand(CommandBehavior behavior, bool returnsSet) { LogMessages.CommandExecution(Log, this); + TraceCommandStart(); + try + { + Prepare(returnsSet); - Prepare(returnsSet); + if ((behavior & CommandBehavior.SequentialAccess) == CommandBehavior.SequentialAccess || + (behavior & CommandBehavior.SingleResult) == CommandBehavior.SingleResult || + (behavior & CommandBehavior.SingleRow) == CommandBehavior.SingleRow || + (behavior & CommandBehavior.CloseConnection) == CommandBehavior.CloseConnection || + behavior == CommandBehavior.Default) + { + // Set the fetch size + _statement.FetchSize = _fetchSize; - if ((behavior & CommandBehavior.SequentialAccess) == CommandBehavior.SequentialAccess || - (behavior & CommandBehavior.SingleResult) == CommandBehavior.SingleResult || - (behavior & CommandBehavior.SingleRow) == CommandBehavior.SingleRow || - (behavior & CommandBehavior.CloseConnection) == CommandBehavior.CloseConnection || - behavior == CommandBehavior.Default) - { - // Set the fetch size - _statement.FetchSize = _fetchSize; + // Set if it's needed the Records Affected information + _statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected; - // Set if it's needed the Records Affected information - _statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected; + // Validate input parameter count + if (_namedParameters.Count > 0 && !HasParameters) + { + throw FbException.Create("Must declare command parameters."); + } - // Validate input parameter count - if (_namedParameters.Count > 0 && !HasParameters) - { - throw FbException.Create("Must declare command parameters."); + // Execute + _statement.Execute(CommandTimeout * 1000, this); } - - // Execute - _statement.Execute(CommandTimeout * 1000, this); + } + catch (Exception e) + { + TraceCommandException(e); + throw; } } private async Task ExecuteCommandAsync(CommandBehavior behavior, bool returnsSet, CancellationToken cancellationToken = default) { LogMessages.CommandExecution(Log, this); + TraceCommandStart(); + try + { + await PrepareAsync(returnsSet, cancellationToken).ConfigureAwait(false); - await PrepareAsync(returnsSet, cancellationToken).ConfigureAwait(false); + if ((behavior & CommandBehavior.SequentialAccess) == CommandBehavior.SequentialAccess || + (behavior & CommandBehavior.SingleResult) == CommandBehavior.SingleResult || + (behavior & CommandBehavior.SingleRow) == CommandBehavior.SingleRow || + (behavior & CommandBehavior.CloseConnection) == CommandBehavior.CloseConnection || + behavior == CommandBehavior.Default) + { + // Set the fetch size + _statement.FetchSize = _fetchSize; - if ((behavior & CommandBehavior.SequentialAccess) == CommandBehavior.SequentialAccess || - (behavior & CommandBehavior.SingleResult) == CommandBehavior.SingleResult || - (behavior & CommandBehavior.SingleRow) == CommandBehavior.SingleRow || - (behavior & CommandBehavior.CloseConnection) == CommandBehavior.CloseConnection || - behavior == CommandBehavior.Default) - { - // Set the fetch size - _statement.FetchSize = _fetchSize; + // Set if it's needed the Records Affected information + _statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected; - // Set if it's needed the Records Affected information - _statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected; + // Validate input parameter count + if (_namedParameters.Count > 0 && !HasParameters) + { + throw FbException.Create("Must declare command parameters."); + } - // Validate input parameter count - if (_namedParameters.Count > 0 && !HasParameters) - { - throw FbException.Create("Must declare command parameters."); + // Execute + await _statement.ExecuteAsync(CommandTimeout * 1000, this, cancellationToken).ConfigureAwait(false); } - - // Execute - await _statement.ExecuteAsync(CommandTimeout * 1000, this, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + TraceCommandException(e); + throw; } } diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj b/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj index a040e0613..0ebf0442b 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj @@ -52,6 +52,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs new file mode 100644 index 000000000..1a21fe53c --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -0,0 +1,137 @@ +using System; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using FirebirdSql.Data.FirebirdClient; + +namespace FirebirdSql.Data.Trace +{ + internal static class FbActivitySource + { + internal static readonly ActivitySource Source = new("FirebirdSql.Data", "1.0.0"); + + internal static Activity CommandStart(FbCommand command) + { + // Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md + var dbName = command.Connection.Database; + + string dbOperationName = null; + string dbCollectionName = null; + string activityName; + + switch (command.CommandType) + { + case CommandType.StoredProcedure: + dbOperationName = "EXECUTE PROCEDURE"; + activityName = $"{dbOperationName} {command.CommandText}"; + break; + + case CommandType.TableDirect: + dbOperationName = "SELECT"; + dbCollectionName = command.CommandText; + activityName = $"{dbOperationName} {dbCollectionName}"; + break; + + case CommandType.Text: + activityName = dbName; + break; + + default: + throw new InvalidEnumArgumentException($"Invalid value for 'System.Data.CommandType' ({(int)command.CommandType})."); + } + + var activity = Source.StartActivity(activityName, ActivityKind.Client); + if (activity.IsAllDataRequested) + { + activity.SetTag("db.system", "firebird"); + + if (dbCollectionName != null) + { + activity.SetTag("db.collection.name", dbCollectionName); + } + + // db.namespace + + if (dbOperationName != null) + { + activity.SetTag("db.operation.name", dbOperationName); + } + + // db.response.status_code + + // error.type (handled by RecordException) + + // server.port + + // db.operation.batch.size + + // db.query_summary + + activity.SetTag("db.query.text", command.CommandText); + + // network.peer.address + + // network.peer.port + + if (command.Connection.DataSource != null) + { + activity.SetTag("server.address", command.Connection.DataSource); + } + + foreach (FbParameter p in command.Parameters) + { + var name = p.ParameterName; + var value = NormalizeDbNull(p.InternalValue); + activity.SetTag($"db.query.parameter.{name}", value); + + } + + // Only for explicit transactions. + if (command.Transaction != null) + { + FbTransactionInfo fbInfo = new FbTransactionInfo(command.Transaction); + + var transactionId = fbInfo.GetTransactionId(); + activity.SetTag($"db.transaction_id", transactionId); + + // TODO: Firebird 4+ only (or remove?) + /* + var snapshotId = fbInfo.GetTransactionSnapshotNumber(); + if (snapshotId != 0) + { + activity.SetTag($"db.snapshot_id", snapshotId); + } + */ + } + } + + return activity; + } + + internal static void CommandException(Activity activity, Exception exception, bool escaped = true) + { + // Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md + activity.AddEvent( + new("exception", tags: new() + { + { "exception.message", exception.Message }, + { "exception.type", exception.GetType().FullName }, + { "exception.escaped", escaped }, + { "exception.stacktrace", exception.ToString() }, + }) + ); + + string errorDescription = exception is FbException fbException + ? fbException.SQLSTATE + : exception.Message; + + activity.SetStatus(ActivityStatusCode.Error, errorDescription); + activity.Dispose(); + } + + private static object NormalizeDbNull(object value) => + value == DBNull.Value || value == null + ? null + : value; + } +} From 7fe1327af03485057ac170de422f926069ce2a3e Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sat, 26 Oct 2024 03:26:56 -0300 Subject: [PATCH 03/36] Add metrics. --- .../FirebirdClient/FbCommand.cs | 32 +++-- .../FirebirdClient/FbConnection.cs | 15 +++ .../FirebirdClient/FbConnectionPoolManager.cs | 10 ++ .../Metrics/FbMetricsStore.cs | 124 ++++++++++++++++++ 4 files changed, 169 insertions(+), 12 deletions(-) create mode 100644 src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs index cf0b0e2dd..c352df5bf 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs @@ -26,6 +26,7 @@ using System.Threading.Tasks; using FirebirdSql.Data.Common; using FirebirdSql.Data.Logging; +using FirebirdSql.Data.Metrics; using FirebirdSql.Data.Trace; using Microsoft.Extensions.Logging; @@ -53,6 +54,7 @@ public sealed class FbCommand : DbCommand, IFbPreparedCommand, IDescriptorFiller private int _fetchSize; private Type[] _expectedColumnTypes; private Activity _currentActivity; + private long _startedAtTicks; #endregion @@ -1068,13 +1070,9 @@ internal void Release() _statement = null; } - if (_currentActivity != null) - { - // Do not set status to Ok: https://opentelemetry.io/docs/concepts/signals/traces/#span-status - _currentActivity.Dispose(); - _currentActivity = null; - } + TraceCommandStop(); } + Task IFbPreparedCommand.ReleaseAsync(CancellationToken cancellationToken) => ReleaseAsync(cancellationToken); internal async Task ReleaseAsync(CancellationToken cancellationToken = default) { @@ -1093,12 +1091,7 @@ internal async Task ReleaseAsync(CancellationToken cancellationToken = default) _statement = null; } - if (_currentActivity != null) - { - // Do not set status to Ok: https://opentelemetry.io/docs/concepts/signals/traces/#span-status - _currentActivity.Dispose(); - _currentActivity = null; - } + TraceCommandStop(); } void IFbPreparedCommand.TransactionCompleted() => TransactionCompleted(); @@ -1326,6 +1319,21 @@ private void TraceCommandStart() Debug.Assert(_currentActivity == null); if (FbActivitySource.Source.HasListeners()) _currentActivity = FbActivitySource.CommandStart(this); + + _startedAtTicks = FbMetricsStore.CommandStart(); + } + + private void TraceCommandStop() + { + if (_currentActivity != null) + { + // Do not set status to Ok: https://opentelemetry.io/docs/concepts/signals/traces/#span-status + _currentActivity.Dispose(); + _currentActivity = null; + } + + FbMetricsStore.CommandStop(_startedAtTicks, Connection); + _startedAtTicks = 0; } private void TraceCommandException(Exception e) diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs index 0a75039aa..102dc2beb 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs @@ -16,6 +16,7 @@ //$Authors = Carlos Guzman Alvarez, Jiri Cincura (jiri@cincura.net) using System; +using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Data.Common; @@ -23,6 +24,7 @@ using System.Threading.Tasks; using FirebirdSql.Data.Common; using FirebirdSql.Data.Logging; +using FirebirdSql.Data.Metrics; using Microsoft.Extensions.Logging; namespace FirebirdSql.Data.FirebirdClient; @@ -190,6 +192,12 @@ public override string ConnectionString _options = new ConnectionString(value); _options.Validate(); _connectionString = value; + + MetricsConnectionAttributes = [ + new("db.system", "firebird"), + new("db.namespace", _options.Database), + new("server.address", $"{_options.DataSource}:{_options.Port}") + ]; } } } @@ -270,6 +278,8 @@ internal bool IsClosed get { return _state == ConnectionState.Closed; } } + internal KeyValuePair[] MetricsConnectionAttributes; + #endregion #region Protected Properties @@ -524,6 +534,7 @@ public override async Task ChangeDatabaseAsync(string databaseName, Cancellation public override void Open() { LogMessages.ConnectionOpening(Log, this); + var startedAtTicks = FbMetricsStore.ConnectionOpening(); if (string.IsNullOrEmpty(_connectionString)) { @@ -616,10 +627,13 @@ public override void Open() } LogMessages.ConnectionOpened(Log, this); + FbMetricsStore.ConnectionOpened(startedAtTicks, this._options.NormalizedConnectionString); } + public override async Task OpenAsync(CancellationToken cancellationToken) { LogMessages.ConnectionOpening(Log, this); + var startedAtTicks = FbMetricsStore.ConnectionOpening(); if (string.IsNullOrEmpty(_connectionString)) { @@ -712,6 +726,7 @@ public override async Task OpenAsync(CancellationToken cancellationToken) } LogMessages.ConnectionOpened(Log, this); + FbMetricsStore.ConnectionOpened(startedAtTicks, this._options.NormalizedConnectionString); } public override void Close() diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionPoolManager.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionPoolManager.cs index 59924b468..a72bd7669 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionPoolManager.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionPoolManager.cs @@ -167,6 +167,10 @@ static long GetTicks() var ticks = Environment.TickCount; return ticks + -(long)int.MinValue; } + + internal int AvailableCount => _available.Count; + internal int BusyCount => _busy.Count; + internal int MaxSize => _connectionString.MaxPoolSize; } int _disposed; @@ -220,6 +224,12 @@ internal void ClearPool(ConnectionString connectionString) } } + internal Dictionary GetMetrics() => + _pools.ToDictionary( + kvp => kvp.Key, + kvp => (kvp.Value.AvailableCount, kvp.Value.BusyCount, kvp.Value.MaxSize) + ); + public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) == 1) diff --git a/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs new file mode 100644 index 000000000..d4470a2af --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Linq; +using FirebirdSql.Data.FirebirdClient; + +namespace FirebirdSql.Data.Metrics +{ + internal static class FbMetricsStore + { + private const string ConnectionPoolNameAttributeName = "db.client.connection.pool.name"; + private const string ConnectionStateAttributeName = "db.client.connection.state"; + private const string ConnectionStateIdleValue = "idle"; + private const string ConnectionStateUsedValue = "used"; + + internal static readonly Meter Source = new("FirebirdSql.Data", "1.0.0"); + + static readonly Histogram OperationDuration; + static readonly Histogram ConnectionCreateTime; + + static FbMetricsStore() + { + // Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md + + OperationDuration = Source.CreateHistogram( + "db.client.operation.duration", + unit: "s", + description: "Duration of database client operations." + ); + + Source.CreateObservableUpDownCounter( + "db.client.connection.count", + GetConnectionCount, + unit: "{connection}", + description: "The number of connections that are currently in state described by the 'state' attribute." + ); + + // db.client.connection.idle.max + // The maximum number of idle open connections allowed + + // db.client.connection.idle.min + // The minimum number of idle open connections allowed + + Source.CreateObservableUpDownCounter( + "db.client.connection.max", + GetConnectionMax, + unit: "{connection}", + description: "The maximum number of open connections allowed." + ); + + // db.client.connection.pending_requests + // The number of current pending requests for an open connection + + // db.client.connection.timeouts + // The number of connection timeouts that have occurred trying to obtain a connection from the pool + + ConnectionCreateTime = Source.CreateHistogram( + "db.client.connection.create_time", + unit: "s", + description: "The time it took to create a new connection." + ); + + // db.client.connection.wait_time + // The time it took to obtain an open connection from the pool + + // db.client.connection.use_time + // The time between borrowing a connection and returning it to the pool + } + + internal static long CommandStart() => Stopwatch.GetTimestamp(); + + internal static void CommandStop(long startedAtTicks, FbConnection connection) + { + if (OperationDuration.Enabled && startedAtTicks > 0) + { + var elapsedTicks = Stopwatch.GetTimestamp() - startedAtTicks; + var elapsedSeconds = TimeSpan.FromTicks(elapsedTicks).TotalSeconds; + + OperationDuration.Record(elapsedSeconds, connection.MetricsConnectionAttributes); + } + } + + internal static long ConnectionOpening() => Stopwatch.GetTimestamp(); + + internal static void ConnectionOpened(long startedAtTicks, string poolName) + { + if (ConnectionCreateTime.Enabled && startedAtTicks > 0) + { + var elapsedTicks = Stopwatch.GetTimestamp() - startedAtTicks; + var elapsedSeconds = TimeSpan.FromTicks(elapsedTicks).TotalSeconds; + + ConnectionCreateTime.Record(elapsedSeconds, [new(ConnectionPoolNameAttributeName, poolName)]); + } + } + + static IEnumerable> GetConnectionCount() => + FbConnectionPoolManager.Instance.GetMetrics() + .SelectMany(kvp => new List> + { + new( + kvp.Value.idleCount, + new(ConnectionPoolNameAttributeName, kvp.Key), + new(ConnectionStateAttributeName, ConnectionStateIdleValue) + ), + + new( + kvp.Value.busyCount, + new(ConnectionPoolNameAttributeName, kvp.Key), + new(ConnectionStateAttributeName, ConnectionStateUsedValue) + ), + }); + + static IEnumerable> GetConnectionMax() => + FbConnectionPoolManager.Instance.GetMetrics() + .SelectMany(kvp => new List> + { + new( + kvp.Value.maxSize, + [new(ConnectionPoolNameAttributeName, kvp.Key)] + ), + }); + } +} From 2968ea306883903415d73dde8772733b07c776d6 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:27:19 -0300 Subject: [PATCH 04/36] Fix NullReferenceException when StartActivity returns null StartActivity() returns null when no listeners are interested in the activity. The code accessed activity.IsAllDataRequested without a null check, causing a NullReferenceException. Use the Npgsql-style pattern 'activity is not { IsAllDataRequested: true }' for early return. --- .../Trace/FbActivitySource.cs | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs index 1a21fe53c..c02b72320 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -41,68 +41,67 @@ internal static Activity CommandStart(FbCommand command) } var activity = Source.StartActivity(activityName, ActivityKind.Client); - if (activity.IsAllDataRequested) + if (activity is not { IsAllDataRequested: true }) + return activity; + + activity.SetTag("db.system", "firebird"); + + if (dbCollectionName != null) { - activity.SetTag("db.system", "firebird"); + activity.SetTag("db.collection.name", dbCollectionName); + } - if (dbCollectionName != null) - { - activity.SetTag("db.collection.name", dbCollectionName); - } + // db.namespace - // db.namespace + if (dbOperationName != null) + { + activity.SetTag("db.operation.name", dbOperationName); + } - if (dbOperationName != null) - { - activity.SetTag("db.operation.name", dbOperationName); - } + // db.response.status_code - // db.response.status_code + // error.type (handled by RecordException) - // error.type (handled by RecordException) + // server.port - // server.port + // db.operation.batch.size - // db.operation.batch.size + // db.query_summary - // db.query_summary + activity.SetTag("db.query.text", command.CommandText); - activity.SetTag("db.query.text", command.CommandText); + // network.peer.address - // network.peer.address + // network.peer.port - // network.peer.port + if (command.Connection.DataSource != null) + { + activity.SetTag("server.address", command.Connection.DataSource); + } - if (command.Connection.DataSource != null) - { - activity.SetTag("server.address", command.Connection.DataSource); - } + foreach (FbParameter p in command.Parameters) + { + var name = p.ParameterName; + var value = NormalizeDbNull(p.InternalValue); + activity.SetTag($"db.query.parameter.{name}", value); + } - foreach (FbParameter p in command.Parameters) - { - var name = p.ParameterName; - var value = NormalizeDbNull(p.InternalValue); - activity.SetTag($"db.query.parameter.{name}", value); + // Only for explicit transactions. + if (command.Transaction != null) + { + FbTransactionInfo fbInfo = new FbTransactionInfo(command.Transaction); - } + var transactionId = fbInfo.GetTransactionId(); + activity.SetTag($"db.transaction_id", transactionId); - // Only for explicit transactions. - if (command.Transaction != null) + // TODO: Firebird 4+ only (or remove?) + /* + var snapshotId = fbInfo.GetTransactionSnapshotNumber(); + if (snapshotId != 0) { - FbTransactionInfo fbInfo = new FbTransactionInfo(command.Transaction); - - var transactionId = fbInfo.GetTransactionId(); - activity.SetTag($"db.transaction_id", transactionId); - - // TODO: Firebird 4+ only (or remove?) - /* - var snapshotId = fbInfo.GetTransactionSnapshotNumber(); - if (snapshotId != 0) - { - activity.SetTag($"db.snapshot_id", snapshotId); - } - */ + activity.SetTag($"db.snapshot_id", snapshotId); } + */ } return activity; From 9b25148f567b05e33301dadc0f3b09bc86d25b36 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:28:17 -0300 Subject: [PATCH 05/36] Fix Stopwatch elapsed time conversion Stopwatch ticks are not TimeSpan ticks when Stopwatch.IsHighResolution is true (most systems). TimeSpan.FromTicks(stopwatchTicks) produces incorrect durations. Use Stopwatch.GetElapsedTime() which correctly handles the frequency conversion. --- .../Metrics/FbMetricsStore.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs index d4470a2af..ecfe5bf2d 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs @@ -74,10 +74,9 @@ internal static void CommandStop(long startedAtTicks, FbConnection connection) { if (OperationDuration.Enabled && startedAtTicks > 0) { - var elapsedTicks = Stopwatch.GetTimestamp() - startedAtTicks; - var elapsedSeconds = TimeSpan.FromTicks(elapsedTicks).TotalSeconds; + var elapsed = Stopwatch.GetElapsedTime(startedAtTicks); - OperationDuration.Record(elapsedSeconds, connection.MetricsConnectionAttributes); + OperationDuration.Record(elapsed.TotalSeconds, connection.MetricsConnectionAttributes); } } @@ -87,10 +86,9 @@ internal static void ConnectionOpened(long startedAtTicks, string poolName) { if (ConnectionCreateTime.Enabled && startedAtTicks > 0) { - var elapsedTicks = Stopwatch.GetTimestamp() - startedAtTicks; - var elapsedSeconds = TimeSpan.FromTicks(elapsedTicks).TotalSeconds; + var elapsed = Stopwatch.GetElapsedTime(startedAtTicks); - ConnectionCreateTime.Record(elapsedSeconds, [new(ConnectionPoolNameAttributeName, poolName)]); + ConnectionCreateTime.Record(elapsed.TotalSeconds, [new(ConnectionPoolNameAttributeName, poolName)]); } } From c198a6a9afc6c7e6bd3f91d116d5b9ce0fa174db Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:28:40 -0300 Subject: [PATCH 06/36] Rename db.system to db.system.name per OTel semantic conventions v1.40+ The semantic conventions renamed db.system to db.system.name. The well-known value for Firebird is 'firebirdsql', not 'firebird'. Updated in both FbActivitySource (spans) and FbConnection (metrics). --- .../FirebirdClient/FbConnection.cs | 2 +- src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs index 102dc2beb..c2d5a6862 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs @@ -194,7 +194,7 @@ public override string ConnectionString _connectionString = value; MetricsConnectionAttributes = [ - new("db.system", "firebird"), + new("db.system.name", "firebirdsql"), new("db.namespace", _options.Database), new("server.address", $"{_options.DataSource}:{_options.Port}") ]; diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs index c02b72320..2f9889217 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -44,7 +44,7 @@ internal static Activity CommandStart(FbCommand command) if (activity is not { IsAllDataRequested: true }) return activity; - activity.SetTag("db.system", "firebird"); + activity.SetTag("db.system.name", "firebirdsql"); if (dbCollectionName != null) { From 47819006d03bc660f92cdb5cfebc8daa0a1d04e4 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:28:59 -0300 Subject: [PATCH 07/36] Set db.namespace attribute on spans from connection database name The semantic conventions mark db.namespace as Conditionally Required when available. The connection's Database property is used as the value. --- src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs index 2f9889217..6d9de8d4d 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -52,6 +52,10 @@ internal static Activity CommandStart(FbCommand command) } // db.namespace + if (dbName != null) + { + activity.SetTag("db.namespace", dbName); + } if (dbOperationName != null) { From 4f83e23478858a8207420c8a47ad3ef1bbd111ca Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:29:23 -0300 Subject: [PATCH 08/36] Set error.type attribute on failure spans The semantic conventions mark error.type as Conditionally Required on failure. For FbException, use the SQLSTATE code; for other exceptions, use the full exception type name. --- src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs index 6d9de8d4d..e54c011ba 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -128,6 +128,10 @@ internal static void CommandException(Activity activity, Exception exception, bo ? fbException.SQLSTATE : exception.Message; + activity.SetTag("error.type", exception is FbException fbEx + ? fbEx.SQLSTATE ?? exception.GetType().FullName + : exception.GetType().FullName); + activity.SetStatus(ActivityStatusCode.Error, errorDescription); activity.Dispose(); } From 78c6d3b9d002fc811a3c46fcb715d2cf2bc76268 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:30:04 -0300 Subject: [PATCH 09/36] Gate db.query.text and db.query.parameter.* behind opt-in flags Per OTel semantic conventions, db.query.text and db.query.parameter.* are Opt-In level attributes that may expose sensitive data. They should not be collected by default. Added IsQueryTextTracingEnabled flag and reuse IsParameterLoggingEnabled for parameter tracing. --- .../Logging/FbLogManager.cs | 4 ++++ .../Trace/FbActivitySource.cs | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Logging/FbLogManager.cs b/src/FirebirdSql.Data.FirebirdClient/Logging/FbLogManager.cs index e82cfd8fd..7349b1f2f 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Logging/FbLogManager.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Logging/FbLogManager.cs @@ -23,6 +23,7 @@ namespace FirebirdSql.Data.Logging; public static class FbLogManager { public static bool IsParameterLoggingEnabled { get; private set; } = false; + public static bool IsQueryTextTracingEnabled { get; private set; } = false; private static ILoggerFactory LoggerFactory = NullLoggerFactory.Instance; @@ -32,6 +33,9 @@ public static void UseLoggerFactory(ILoggerFactory loggerFactory) => public static void EnableParameterLogging(bool enable = true) => IsParameterLoggingEnabled = enable; + public static void EnableQueryTextTracing(bool enable = true) => + IsQueryTextTracingEnabled = enable; + internal static ILogger CreateLogger() => LoggerFactory.CreateLogger(); } diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs index e54c011ba..92f1ef42e 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -3,6 +3,7 @@ using System.Data; using System.Diagnostics; using FirebirdSql.Data.FirebirdClient; +using FirebirdSql.Data.Logging; namespace FirebirdSql.Data.Trace { @@ -72,7 +73,10 @@ internal static Activity CommandStart(FbCommand command) // db.query_summary - activity.SetTag("db.query.text", command.CommandText); + if (FbLogManager.IsQueryTextTracingEnabled) + { + activity.SetTag("db.query.text", command.CommandText); + } // network.peer.address @@ -83,11 +87,14 @@ internal static Activity CommandStart(FbCommand command) activity.SetTag("server.address", command.Connection.DataSource); } - foreach (FbParameter p in command.Parameters) + if (FbLogManager.IsParameterLoggingEnabled) { - var name = p.ParameterName; - var value = NormalizeDbNull(p.InternalValue); - activity.SetTag($"db.query.parameter.{name}", value); + foreach (FbParameter p in command.Parameters) + { + var name = p.ParameterName; + var value = NormalizeDbNull(p.InternalValue); + activity.SetTag($"db.query.parameter.{name}", value); + } } // Only for explicit transactions. From ecdb4cd61c1c3aabd7bdf3363ea28ac56cb91055 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:30:30 -0300 Subject: [PATCH 10/36] Read ActivitySource/Meter version from assembly metadata Replace hardcoded '1.0.0' version with the assembly's informational version, keeping the telemetry version in sync with the NuGet package. --- .../Metrics/FbMetricsStore.cs | 5 ++++- .../Trace/FbActivitySource.cs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs index ecfe5bf2d..a865cff6e 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.Metrics; using System.Linq; +using System.Reflection; using FirebirdSql.Data.FirebirdClient; namespace FirebirdSql.Data.Metrics @@ -14,7 +15,9 @@ internal static class FbMetricsStore private const string ConnectionStateIdleValue = "idle"; private const string ConnectionStateUsedValue = "used"; - internal static readonly Meter Source = new("FirebirdSql.Data", "1.0.0"); + static readonly string Version = typeof(FbMetricsStore).Assembly.GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; + + internal static readonly Meter Source = new("FirebirdSql.Data", Version); static readonly Histogram OperationDuration; static readonly Histogram ConnectionCreateTime; diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs index 92f1ef42e..b415aa450 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Data; using System.Diagnostics; +using System.Reflection; using FirebirdSql.Data.FirebirdClient; using FirebirdSql.Data.Logging; @@ -9,7 +10,9 @@ namespace FirebirdSql.Data.Trace { internal static class FbActivitySource { - internal static readonly ActivitySource Source = new("FirebirdSql.Data", "1.0.0"); + static readonly string Version = typeof(FbActivitySource).Assembly.GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; + + internal static readonly ActivitySource Source = new("FirebirdSql.Data", Version); internal static Activity CommandStart(FbCommand command) { From 49382fb2bc7465a16fca5a8f5b8b2e57ba95d150 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:31:18 -0300 Subject: [PATCH 11/36] Fix activity lifecycle: record metrics on error path, prevent stale recording TraceCommandException now records metrics immediately and resets _startedAtTicks so the later TraceCommandStop (called during Release) does not record a second, inflated duration that includes cleanup time. --- .../FirebirdClient/FbCommand.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs index c352df5bf..c5c803e33 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs @@ -1343,6 +1343,9 @@ private void TraceCommandException(Exception e) FbActivitySource.CommandException(_currentActivity, e); _currentActivity = null; } + + FbMetricsStore.CommandStop(_startedAtTicks, Connection); + _startedAtTicks = 0; } #endregion Tracing From 594a23bed33feba607f919e0573dc2bedba96da3 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:32:11 -0300 Subject: [PATCH 12/36] Remove non-standard and deprecated telemetry attributes - Remove db.transaction_id custom tag (not in OTel spec, causes server round-trip per traced command) - Remove commented-out db.snapshot_id code - Remove deprecated exception.escaped attribute - Remove NormalizeDbNull helper, inline DBNull check --- .../Trace/FbActivitySource.cs | 31 ++----------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs index b415aa450..c8f62726a 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -94,42 +94,20 @@ internal static Activity CommandStart(FbCommand command) { foreach (FbParameter p in command.Parameters) { - var name = p.ParameterName; - var value = NormalizeDbNull(p.InternalValue); - activity.SetTag($"db.query.parameter.{name}", value); + activity.SetTag($"db.query.parameter.{p.ParameterName}", p.InternalValue == DBNull.Value ? null : p.InternalValue); } } - // Only for explicit transactions. - if (command.Transaction != null) - { - FbTransactionInfo fbInfo = new FbTransactionInfo(command.Transaction); - - var transactionId = fbInfo.GetTransactionId(); - activity.SetTag($"db.transaction_id", transactionId); - - // TODO: Firebird 4+ only (or remove?) - /* - var snapshotId = fbInfo.GetTransactionSnapshotNumber(); - if (snapshotId != 0) - { - activity.SetTag($"db.snapshot_id", snapshotId); - } - */ - } - return activity; } - internal static void CommandException(Activity activity, Exception exception, bool escaped = true) + internal static void CommandException(Activity activity, Exception exception) { - // Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md activity.AddEvent( new("exception", tags: new() { { "exception.message", exception.Message }, { "exception.type", exception.GetType().FullName }, - { "exception.escaped", escaped }, { "exception.stacktrace", exception.ToString() }, }) ); @@ -145,10 +123,5 @@ internal static void CommandException(Activity activity, Exception exception, bo activity.SetStatus(ActivityStatusCode.Error, errorDescription); activity.Dispose(); } - - private static object NormalizeDbNull(object value) => - value == DBNull.Value || value == null - ? null - : value; } } From f68747fef668824f63ef6bbcdc4e535cc94d1789 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:33:28 -0300 Subject: [PATCH 13/36] Add server.port, db.stored_procedure.name; fix server.address in metrics - Set server.port on spans when using a non-default port - Set db.stored_procedure.name for StoredProcedure command types - Fix server.address in MetricsConnectionAttributes to not concatenate the port (server.port is now a separate attribute) - Clean up leftover placeholder comments --- .../FirebirdClient/FbConnection.cs | 3 ++- .../Trace/FbActivitySource.cs | 26 ++++++++----------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs index c2d5a6862..ff24fb59f 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs @@ -196,7 +196,8 @@ public override string ConnectionString MetricsConnectionAttributes = [ new("db.system.name", "firebirdsql"), new("db.namespace", _options.Database), - new("server.address", $"{_options.DataSource}:{_options.Port}") + new("server.address", _options.DataSource), + new("server.port", _options.Port), ]; } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs index c8f62726a..3d551b1b4 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -3,6 +3,7 @@ using System.Data; using System.Diagnostics; using System.Reflection; +using FirebirdSql.Data.Common; using FirebirdSql.Data.FirebirdClient; using FirebirdSql.Data.Logging; @@ -16,7 +17,6 @@ internal static class FbActivitySource internal static Activity CommandStart(FbCommand command) { - // Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md var dbName = command.Connection.Database; string dbOperationName = null; @@ -55,7 +55,6 @@ internal static Activity CommandStart(FbCommand command) activity.SetTag("db.collection.name", dbCollectionName); } - // db.namespace if (dbName != null) { activity.SetTag("db.namespace", dbName); @@ -66,30 +65,27 @@ internal static Activity CommandStart(FbCommand command) activity.SetTag("db.operation.name", dbOperationName); } - // db.response.status_code - - // error.type (handled by RecordException) - - // server.port - - // db.operation.batch.size - - // db.query_summary + if (command.CommandType == CommandType.StoredProcedure) + { + activity.SetTag("db.stored_procedure.name", command.CommandText); + } if (FbLogManager.IsQueryTextTracingEnabled) { activity.SetTag("db.query.text", command.CommandText); } - // network.peer.address - - // network.peer.port - if (command.Connection.DataSource != null) { activity.SetTag("server.address", command.Connection.DataSource); } + var port = command.Connection.ConnectionOptions.Port; + if (port != ConnectionString.DefaultValuePortNumber) + { + activity.SetTag("server.port", port); + } + if (FbLogManager.IsParameterLoggingEnabled) { foreach (FbParameter p in command.Parameters) From 98f6f6d80f39b70a1ef1ade367e1a6c0d28091f4 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:34:19 -0300 Subject: [PATCH 14/36] Add db.query.summary and db.operation.name for Text commands Set db.query.summary as the low-cardinality activity name on all spans. For Text commands, extract the first SQL verb (SELECT, INSERT, etc.) and use it as db.operation.name and in the activity name. --- .../Trace/FbActivitySource.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs index 3d551b1b4..7bb6937f4 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -37,7 +37,8 @@ internal static Activity CommandStart(FbCommand command) break; case CommandType.Text: - activityName = dbName; + dbOperationName = ExtractSqlVerb(command.CommandText); + activityName = dbOperationName ?? dbName; break; default: @@ -65,6 +66,8 @@ internal static Activity CommandStart(FbCommand command) activity.SetTag("db.operation.name", dbOperationName); } + activity.SetTag("db.query.summary", activityName); + if (command.CommandType == CommandType.StoredProcedure) { activity.SetTag("db.stored_procedure.name", command.CommandText); @@ -119,5 +122,16 @@ internal static void CommandException(Activity activity, Exception exception) activity.SetStatus(ActivityStatusCode.Error, errorDescription); activity.Dispose(); } + + static string ExtractSqlVerb(string sql) + { + if (string.IsNullOrEmpty(sql)) + return null; + var span = sql.AsSpan().TrimStart(); + var spaceIndex = span.IndexOfAny(' ', '\t', '\n', '\r'); + if (spaceIndex <= 0) + return span.Length > 0 ? span.ToString().ToUpperInvariant() : null; + return span.Slice(0, spaceIndex).ToString().ToUpperInvariant(); + } } } From 4aa1e014743362efd6c8ad6a17ff8f57622ca422 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:34:45 -0300 Subject: [PATCH 15/36] Add debug assertions in TraceCommandStop and TraceCommandException Debug.Assert checks that TraceCommandStart was called before the stop or exception path, helping catch lifecycle bugs during development. --- .../FirebirdClient/FbCommand.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs index c5c803e33..66bff831d 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs @@ -1325,6 +1325,8 @@ private void TraceCommandStart() private void TraceCommandStop() { + Debug.Assert(_startedAtTicks > 0 || _currentActivity == null, "TraceCommandStop called without TraceCommandStart"); + if (_currentActivity != null) { // Do not set status to Ok: https://opentelemetry.io/docs/concepts/signals/traces/#span-status @@ -1338,6 +1340,8 @@ private void TraceCommandStop() private void TraceCommandException(Exception e) { + Debug.Assert(_startedAtTicks > 0 || _currentActivity == null, "TraceCommandException called without TraceCommandStart"); + if (_currentActivity != null) { FbActivitySource.CommandException(_currentActivity, e); From 43d9462f4c392b22295f0c03a7bb7c2749768b2f Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:35:18 -0300 Subject: [PATCH 16/36] Initialize MetricsConnectionAttributes to empty array Prevents potential NullReferenceException if metrics are recorded before the connection string is set. --- .../FirebirdClient/FbConnection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs index ff24fb59f..b1a71223b 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs @@ -279,7 +279,7 @@ internal bool IsClosed get { return _state == ConnectionState.Closed; } } - internal KeyValuePair[] MetricsConnectionAttributes; + internal KeyValuePair[] MetricsConnectionAttributes = []; #endregion From 425ae5a23e99061e5160f8e7e175c8fec96c80d2 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:35:58 -0300 Subject: [PATCH 17/36] Avoid dictionary allocation in observable metric callbacks Change GetMetrics() to return IEnumerable of tuples via Select instead of allocating a Dictionary via ToDictionary on every collection cycle. Also simplify GetConnectionMax to use Select instead of SelectMany. --- .../FirebirdClient/FbConnectionPoolManager.cs | 7 ++----- .../Metrics/FbMetricsStore.cs | 21 ++++++++----------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionPoolManager.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionPoolManager.cs index a72bd7669..bc80ebd8a 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionPoolManager.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionPoolManager.cs @@ -224,11 +224,8 @@ internal void ClearPool(ConnectionString connectionString) } } - internal Dictionary GetMetrics() => - _pools.ToDictionary( - kvp => kvp.Key, - kvp => (kvp.Value.AvailableCount, kvp.Value.BusyCount, kvp.Value.MaxSize) - ); + internal IEnumerable<(string poolName, int idleCount, int busyCount, int maxSize)> GetMetrics() => + _pools.Select(kvp => (kvp.Key, kvp.Value.AvailableCount, kvp.Value.BusyCount, kvp.Value.MaxSize)); public void Dispose() { diff --git a/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs index a865cff6e..0435333f8 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs @@ -97,29 +97,26 @@ internal static void ConnectionOpened(long startedAtTicks, string poolName) static IEnumerable> GetConnectionCount() => FbConnectionPoolManager.Instance.GetMetrics() - .SelectMany(kvp => new List> + .SelectMany(m => new List> { new( - kvp.Value.idleCount, - new(ConnectionPoolNameAttributeName, kvp.Key), + m.idleCount, + new(ConnectionPoolNameAttributeName, m.poolName), new(ConnectionStateAttributeName, ConnectionStateIdleValue) ), new( - kvp.Value.busyCount, - new(ConnectionPoolNameAttributeName, kvp.Key), + m.busyCount, + new(ConnectionPoolNameAttributeName, m.poolName), new(ConnectionStateAttributeName, ConnectionStateUsedValue) ), }); static IEnumerable> GetConnectionMax() => FbConnectionPoolManager.Instance.GetMetrics() - .SelectMany(kvp => new List> - { - new( - kvp.Value.maxSize, - [new(ConnectionPoolNameAttributeName, kvp.Key)] - ), - }); + .Select(m => new Measurement( + m.maxSize, + [new(ConnectionPoolNameAttributeName, m.poolName)] + )); } } From 395a90706115d72f6ecde5b3550fa8623e653c23 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:36:40 -0300 Subject: [PATCH 18/36] Add InstrumentAdvice with histogram bucket boundaries on .NET 9+ Suggest default histogram bucket boundaries for operation duration and connection create time histograms, improving out-of-the-box resolution. Guarded by NET9_0_OR_GREATER since InstrumentAdvice is a .NET 9 API. --- .../Metrics/FbMetricsStore.cs | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs index 0435333f8..fafe7cc21 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs @@ -24,12 +24,20 @@ internal static class FbMetricsStore static FbMetricsStore() { - // Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md +#if NET9_0_OR_GREATER + var durationAdvice = new InstrumentAdvice + { + HistogramBucketBoundaries = [0.001, 0.005, 0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10], + }; +#endif OperationDuration = Source.CreateHistogram( "db.client.operation.duration", unit: "s", description: "Duration of database client operations." +#if NET9_0_OR_GREATER + , advice: durationAdvice +#endif ); Source.CreateObservableUpDownCounter( @@ -39,12 +47,6 @@ static FbMetricsStore() description: "The number of connections that are currently in state described by the 'state' attribute." ); - // db.client.connection.idle.max - // The maximum number of idle open connections allowed - - // db.client.connection.idle.min - // The minimum number of idle open connections allowed - Source.CreateObservableUpDownCounter( "db.client.connection.max", GetConnectionMax, @@ -52,16 +54,13 @@ static FbMetricsStore() description: "The maximum number of open connections allowed." ); - // db.client.connection.pending_requests - // The number of current pending requests for an open connection - - // db.client.connection.timeouts - // The number of connection timeouts that have occurred trying to obtain a connection from the pool - ConnectionCreateTime = Source.CreateHistogram( "db.client.connection.create_time", unit: "s", description: "The time it took to create a new connection." +#if NET9_0_OR_GREATER + , advice: durationAdvice +#endif ); // db.client.connection.wait_time From 74025400ad7e521c6c155b40d0fad03279666023 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:37:25 -0300 Subject: [PATCH 19/36] Add FbTelemetry public class with ActivitySource and Meter names Expose the telemetry source names as public constants so consumers can easily subscribe: builder.AddSource(FbTelemetry.ActivitySourceName) and builder.AddMeter(FbTelemetry.MeterName). Internal code updated to use the constants. --- .../Metrics/FbMetricsStore.cs | 3 +- .../Trace/FbActivitySource.cs | 2 +- .../Trace/FbTelemetry.cs | 41 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/FirebirdSql.Data.FirebirdClient/Trace/FbTelemetry.cs diff --git a/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs index fafe7cc21..31b1f5a20 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Metrics/FbMetricsStore.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using FirebirdSql.Data.FirebirdClient; +using FirebirdSql.Data.Trace; namespace FirebirdSql.Data.Metrics { @@ -17,7 +18,7 @@ internal static class FbMetricsStore static readonly string Version = typeof(FbMetricsStore).Assembly.GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; - internal static readonly Meter Source = new("FirebirdSql.Data", Version); + internal static readonly Meter Source = new(FbTelemetry.MeterName, Version); static readonly Histogram OperationDuration; static readonly Histogram ConnectionCreateTime; diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs index 7bb6937f4..3905dad85 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -13,7 +13,7 @@ internal static class FbActivitySource { static readonly string Version = typeof(FbActivitySource).Assembly.GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; - internal static readonly ActivitySource Source = new("FirebirdSql.Data", Version); + internal static readonly ActivitySource Source = new(FbTelemetry.ActivitySourceName, Version); internal static Activity CommandStart(FbCommand command) { diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbTelemetry.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbTelemetry.cs new file mode 100644 index 000000000..3410a1a7c --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbTelemetry.cs @@ -0,0 +1,41 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +namespace FirebirdSql.Data.FirebirdClient; + +/// +/// Provides the names of the ActivitySource and Meter used by the Firebird ADO.NET provider +/// for OpenTelemetry integration. +/// +/// +/// To subscribe to Firebird traces, use: +/// builder.AddSource(FbTelemetry.ActivitySourceName) +/// To subscribe to Firebird metrics, use: +/// builder.AddMeter(FbTelemetry.MeterName) +/// +public static class FbTelemetry +{ + /// + /// The name of the used for distributed tracing. + /// Use with TracerProviderBuilder.AddSource(). + /// + public const string ActivitySourceName = "FirebirdSql.Data"; + + /// + /// The name of the used for metrics. + /// Use with MeterProviderBuilder.AddMeter(). + /// + public const string MeterName = "FirebirdSql.Data"; +} From 36d884f02f9a6f3cd546e131f4811fca6f78ca71 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:38:00 -0300 Subject: [PATCH 20/36] Add OpenTelemetry integration documentation Document the ActivitySource and Meter names, available span attributes, opt-in sensitive attributes, and available metrics with their types and semantic convention references. --- docs/otel.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/otel.md diff --git a/docs/otel.md b/docs/otel.md new file mode 100644 index 000000000..940a4009a --- /dev/null +++ b/docs/otel.md @@ -0,0 +1,80 @@ +# OpenTelemetry Integration + +The Firebird ADO.NET provider includes built-in support for [OpenTelemetry](https://opentelemetry.io/) distributed tracing and metrics using the native .NET `System.Diagnostics` APIs. No dependency on the OpenTelemetry SDK is required in the provider itself — your application opts in by configuring the appropriate listeners. + +## Distributed Tracing + +The provider emits `Activity` spans for database command execution using `System.Diagnostics.ActivitySource`. + +### Enabling Traces + +```csharp +using OpenTelemetry.Trace; + +var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(FirebirdSql.Data.FirebirdClient.FbTelemetry.ActivitySourceName) + .AddConsoleExporter() // or any other exporter + .Build(); +``` + +The `ActivitySource` name is `"FirebirdSql.Data"`, also available as the constant `FbTelemetry.ActivitySourceName`. + +### Span Attributes + +Spans follow the [OTel Database Client Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/database/database-spans/): + +| Attribute | Description | +|-----------|-------------| +| `db.system.name` | Always `"firebirdsql"` | +| `db.namespace` | The database name from the connection | +| `db.operation.name` | The SQL verb (`SELECT`, `INSERT`, etc.) or `EXECUTE PROCEDURE` | +| `db.collection.name` | The table name (for `TableDirect` commands) | +| `db.stored_procedure.name` | The stored procedure name (for `StoredProcedure` commands) | +| `db.query.summary` | A low-cardinality summary of the operation | +| `db.query.text` | The full SQL text (**opt-in**, see below) | +| `db.query.parameter.*` | Parameter values (**opt-in**, see below) | +| `server.address` | The database server hostname | +| `server.port` | The database server port (only when non-default, i.e. != 3050) | +| `error.type` | The SQLSTATE code (for `FbException`) or exception type name | + +### Opt-In Sensitive Attributes + +By default, `db.query.text` and `db.query.parameter.*` are **not** collected, as they may contain sensitive data. Enable them explicitly: + +```csharp +using FirebirdSql.Data.Logging; + +// Enable SQL text in traces +FbLogManager.EnableQueryTextTracing(); + +// Enable parameter values in traces (and logs) +FbLogManager.EnableParameterLogging(); +``` + +## Metrics + +The provider emits metrics via `System.Diagnostics.Metrics.Meter`. + +### Enabling Metrics + +```csharp +using OpenTelemetry.Metrics; + +var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(FirebirdSql.Data.FirebirdClient.FbTelemetry.MeterName) + .AddConsoleExporter() // or any other exporter + .Build(); +``` + +The `Meter` name is `"FirebirdSql.Data"`, also available as the constant `FbTelemetry.MeterName`. + +### Available Metrics + +Metrics follow the [OTel Database Client Metrics Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/database/database-metrics/): + +| Metric | Type | Unit | Description | +|--------|------|------|-------------| +| `db.client.operation.duration` | Histogram | `s` | Duration of database client operations | +| `db.client.connection.create_time` | Histogram | `s` | Time to create a new connection | +| `db.client.connection.count` | ObservableUpDownCounter | `{connection}` | Current connection count by state (`idle`/`used`) | +| `db.client.connection.max` | ObservableUpDownCounter | `{connection}` | Maximum number of open connections allowed | From ec1c68b881d2052bb5a0faff8ae013e4f681edca Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 22:39:33 -0300 Subject: [PATCH 21/36] Fix IndexOfAny compilation on all target frameworks --- src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs index 3905dad85..95a938499 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Trace/FbActivitySource.cs @@ -128,7 +128,7 @@ static string ExtractSqlVerb(string sql) if (string.IsNullOrEmpty(sql)) return null; var span = sql.AsSpan().TrimStart(); - var spaceIndex = span.IndexOfAny(' ', '\t', '\n', '\r'); + var spaceIndex = span.IndexOfAny([' ', '\t', '\n', '\r']); if (spaceIndex <= 0) return span.Length > 0 ? span.ToString().ToUpperInvariant() : null; return span.Slice(0, spaceIndex).ToString().ToUpperInvariant(); From 7b3e3a07356414d03e8a4072a1bc8efd8577ea8d Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sat, 26 Oct 2024 17:32:52 -0300 Subject: [PATCH 22/36] Refactor: Benchmarks. - Rename project 'Perf' to 'FirebirdSql.Data.FirebirdClient.Benchmarks'. - Update project to use .net8. - Upgrade BenchmarkDotNet to version 0.14.0. - Update baseline nuget package to v10.3.1. - Add /BenchmarkDotNet.Artifacts to .gitignore. - Pass command-line arguments to BenchmarkDotNet engine. - Apply SQL Formatting. Use raw strings. - Add script run-benchmark.ps1. --- .gitignore | 1 + run-benchmark.ps1 | 15 ++++ .../CommandBenchmark.Execute.cs | 48 ++++++------ .../CommandBenchmark.Fetch.cs | 71 ++++++++++++++++++ .../CommandBenchmark.cs | 32 ++++++-- ...Sql.Data.FirebirdClient.Benchmarks.csproj} | 2 +- .../Program.cs | 13 +--- src/Perf/CommandBenchmark.Fetch.cs | 73 ------------------- 8 files changed, 137 insertions(+), 118 deletions(-) create mode 100644 run-benchmark.ps1 rename src/{Perf => FirebirdSql.Data.FirebirdClient.Benchmarks}/CommandBenchmark.Execute.cs (54%) create mode 100644 src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs rename src/{Perf => FirebirdSql.Data.FirebirdClient.Benchmarks}/CommandBenchmark.cs (70%) rename src/{Perf/Perf.csproj => FirebirdSql.Data.FirebirdClient.Benchmarks/FirebirdSql.Data.FirebirdClient.Benchmarks.csproj} (97%) rename src/{Perf => FirebirdSql.Data.FirebirdClient.Benchmarks}/Program.cs (81%) delete mode 100644 src/Perf/CommandBenchmark.Fetch.cs diff --git a/.gitignore b/.gitignore index fda195abb..2b35ae1c4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ bin obj out/ .idea +/BenchmarkDotNet.Artifacts diff --git a/run-benchmark.ps1 b/run-benchmark.ps1 new file mode 100644 index 000000000..10776ad67 --- /dev/null +++ b/run-benchmark.ps1 @@ -0,0 +1,15 @@ +param( + [ValidateSet('CommandBenchmark')] + $Benchmark = 'CommandBenchmark' +) + +$ErrorActionPreference = 'Stop' + +$projectFile = '.\src\FirebirdSql.Data.FirebirdClient.Benchmarks\FirebirdSql.Data.FirebirdClient.Benchmarks.csproj' + +# Run selected benchmark +dotnet run ` + --project $projectFile ` + --configuration 'Release' ` + -- ` + --filter "*$($Benchmark)*" diff --git a/src/Perf/CommandBenchmark.Execute.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Execute.cs similarity index 54% rename from src/Perf/CommandBenchmark.Execute.cs rename to src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Execute.cs index ea05f6d3b..606e292f8 100644 --- a/src/Perf/CommandBenchmark.Execute.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Execute.cs @@ -16,44 +16,40 @@ //$Authors = Jiri Cincura (jiri@cincura.net) using BenchmarkDotNet.Attributes; -using FirebirdSql.Data.FirebirdClient; -namespace Perf; +namespace FirebirdSql.Data.FirebirdClient.Benchmarks; -partial class CommandBenchmark +public partial class CommandBenchmark { [GlobalSetup(Target = nameof(Execute))] public void ExecuteGlobalSetup() { - GlobalSetupBase(); - using (var conn = new FbConnection(ConnectionString)) - { - conn.Open(); - using (var cmd = conn.CreateCommand()) - { - cmd.CommandText = $"create table foobar (x {DataType})"; - cmd.ExecuteNonQuery(); - } - } + CreateDatabase(); + + using var conn = new FbConnection(ConnectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = $"CREATE TABLE foobar (x {DataType})"; + cmd.ExecuteNonQuery(); } [Benchmark] public void Execute() { - using (var conn = new FbConnection(ConnectionString)) + using var conn = new FbConnection(ConnectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = @"INSERT INTO foobar (x) VALUES (@cnt)"; + + var p = new FbParameter() { ParameterName = "@cnt" }; + cmd.Parameters.Add(p); + + for (var i = 0; i < Count; i++) { - conn.Open(); - using (var cmd = conn.CreateCommand()) - { - cmd.CommandText = @"insert into foobar values (@cnt)"; - var p = new FbParameter() { ParameterName = "@cnt" }; - cmd.Parameters.Add(p); - for (var i = 0; i < Count; i++) - { - p.Value = i; - cmd.ExecuteNonQuery(); - } - } + p.Value = i; + cmd.ExecuteNonQuery(); } } } diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs new file mode 100644 index 000000000..bff88363e --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs @@ -0,0 +1,71 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using BenchmarkDotNet.Attributes; + +namespace FirebirdSql.Data.FirebirdClient.Benchmarks; + +public partial class CommandBenchmark +{ + [GlobalSetup(Target = nameof(Fetch))] + public void FetchGlobalSetup() + { + CreateDatabase(); + + using var conn = new FbConnection(ConnectionString); + conn.Open(); + + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = $"CREATE TABLE foobar (x {DataType})"; + cmd.ExecuteNonQuery(); + } + + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = $""" + EXECUTE BLOCK AS + DECLARE cnt INT; + BEGIN + cnt = {Count}; + WHILE (cnt > 0) DO + BEGIN + INSERT INTO foobar VALUES (:cnt); + cnt = cnt - 1; + END + END + """; + cmd.ExecuteNonQuery(); + } + } + + [Benchmark] + public void Fetch() + { + using var conn = new FbConnection(ConnectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT x FROM foobar"; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var _ = reader[0]; + } + } +} diff --git a/src/Perf/CommandBenchmark.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs similarity index 70% rename from src/Perf/CommandBenchmark.cs rename to src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs index b6a02f112..881dbd033 100644 --- a/src/Perf/CommandBenchmark.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs @@ -21,9 +21,9 @@ using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Toolchains.CsProj; -using FirebirdSql.Data.FirebirdClient; +using BenchmarkDotNet.Validators; -namespace Perf; +namespace FirebirdSql.Data.FirebirdClient.Benchmarks; [Config(typeof(Config))] public partial class CommandBenchmark @@ -34,30 +34,46 @@ public Config() { var baseJob = Job.Default .WithWarmupCount(3) - .WithToolchain(CsProjCoreToolchain.NetCoreApp60) .WithPlatform(Platform.X64) .WithJit(Jit.RyuJit); + + AddJob( + baseJob + .WithToolchain(CsProjCoreToolchain.NetCoreApp80) + .WithCustomBuildConfiguration("ReleaseNuGet") + .WithId("NuGet80") + .AsBaseline() + ); + + AddJob( + baseJob + .WithToolchain(CsProjCoreToolchain.NetCoreApp80) + .WithCustomBuildConfiguration("Release") + .WithId("Core80") + ); + AddDiagnoser(MemoryDiagnoser.Default); - AddJob(baseJob.WithCustomBuildConfiguration("Release").WithId("Project")); - AddJob(baseJob.WithCustomBuildConfiguration("ReleaseNuGet").WithId("NuGet").AsBaseline()); + + AddValidator(BaselineValidator.FailOnError); + AddValidator(JitOptimizationsValidator.FailOnError); } } protected const string ConnectionString = "database=localhost:benchmark.fdb;user=sysdba;password=masterkey"; - [Params("bigint", "varchar(10) character set utf8")] + [Params("BIGINT", "VARCHAR(10) CHARACTER SET UTF8")] public string DataType { get; set; } [Params(100)] public int Count { get; set; } - void GlobalSetupBase() + static void CreateDatabase() { FbConnection.CreateDatabase(ConnectionString, 16 * 1024, false, true); } [GlobalCleanup] - public void GlobalCleanup() + public static void GlobalCleanup() { FbConnection.ClearAllPools(); FbConnection.DropDatabase(ConnectionString); diff --git a/src/Perf/Perf.csproj b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/FirebirdSql.Data.FirebirdClient.Benchmarks.csproj similarity index 97% rename from src/Perf/Perf.csproj rename to src/FirebirdSql.Data.FirebirdClient.Benchmarks/FirebirdSql.Data.FirebirdClient.Benchmarks.csproj index eca7e7c51..60ae7ec76 100644 --- a/src/Perf/Perf.csproj +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/FirebirdSql.Data.FirebirdClient.Benchmarks.csproj @@ -19,6 +19,6 @@ - + diff --git a/src/Perf/Program.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/Program.cs similarity index 81% rename from src/Perf/Program.cs rename to src/FirebirdSql.Data.FirebirdClient.Benchmarks/Program.cs index 484f5634f..dbb557d7e 100644 --- a/src/Perf/Program.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/Program.cs @@ -15,15 +15,8 @@ //$Authors = Jiri Cincura (jiri@cincura.net) -using System.Reflection; using BenchmarkDotNet.Running; -namespace Perf; - -class Program -{ - static void Main(string[] args) - { - BenchmarkRunner.Run(Assembly.GetExecutingAssembly()); - } -} +BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run(args); diff --git a/src/Perf/CommandBenchmark.Fetch.cs b/src/Perf/CommandBenchmark.Fetch.cs deleted file mode 100644 index b57ebf562..000000000 --- a/src/Perf/CommandBenchmark.Fetch.cs +++ /dev/null @@ -1,73 +0,0 @@ -/* - * The contents of this file are subject to the Initial - * Developer's Public License Version 1.0 (the "License"); - * you may not use this file except in compliance with the - * License. You may obtain a copy of the License at - * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. - * - * Software distributed under the License is distributed on - * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either - * express or implied. See the License for the specific - * language governing rights and limitations under the License. - * - * All Rights Reserved. - */ - -//$Authors = Jiri Cincura (jiri@cincura.net) - -using BenchmarkDotNet.Attributes; -using FirebirdSql.Data.FirebirdClient; - -namespace Perf; - -partial class CommandBenchmark -{ - [GlobalSetup(Target = nameof(Fetch))] - public void FetchGlobalSetup() - { - GlobalSetupBase(); - using (var conn = new FbConnection(ConnectionString)) - { - conn.Open(); - using (var cmd = conn.CreateCommand()) - { - cmd.CommandText = $"create table foobar (x {DataType})"; - cmd.ExecuteNonQuery(); - } - using (var cmd = conn.CreateCommand()) - { - cmd.CommandText = $@"execute block as -declare cnt int; -begin - cnt = {Count}; - while (cnt > 0) do - begin - insert into foobar values (:cnt); - cnt = cnt - 1; - end -end"; - cmd.ExecuteNonQuery(); - } - } - } - - [Benchmark] - public void Fetch() - { - using (var conn = new FbConnection(ConnectionString)) - { - conn.Open(); - using (var cmd = conn.CreateCommand()) - { - cmd.CommandText = "select x from foobar"; - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - var dummy = reader[0]; - } - } - } - } - } -} From 0c4a76455fda550234604f7d01c48570014fc53e Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sat, 17 May 2025 22:13:33 -0300 Subject: [PATCH 23/36] Updates run-benchmark.ps1. --- run-benchmark.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run-benchmark.ps1 b/run-benchmark.ps1 index 10776ad67..0d4430fc8 100644 --- a/run-benchmark.ps1 +++ b/run-benchmark.ps1 @@ -1,5 +1,5 @@ param( - [ValidateSet('CommandBenchmark')] + [ValidateSet('CommandBenchmark','LargeFetchBenchmark')] $Benchmark = 'CommandBenchmark' ) From ed46d7aff13dc4943fadd0ca368ee073904a9f30 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:32:21 -0300 Subject: [PATCH 24/36] Fix solution file: update benchmark project path and folder name --- src/NETProvider.slnx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NETProvider.slnx b/src/NETProvider.slnx index 0aa0bceff..6ba53bf37 100644 --- a/src/NETProvider.slnx +++ b/src/NETProvider.slnx @@ -13,8 +13,8 @@ - - + + From 04208fe0d72cc31f3bfbad5d64ca3735eb9077d3 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:32:43 -0300 Subject: [PATCH 25/36] Add LargeFetchBenchmark for bulk read throughput across data types --- .../LargeFetchBenchmark.cs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs new file mode 100644 index 000000000..7bf94b2c6 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs @@ -0,0 +1,106 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Toolchains.CsProj; +using BenchmarkDotNet.Validators; + +namespace FirebirdSql.Data.FirebirdClient.Benchmarks; + +[Config(typeof(Config))] +public class LargeFetchBenchmark +{ + class Config : ManualConfig + { + public Config() + { + var baseJob = Job.Default + .WithWarmupCount(3) + .WithPlatform(Platform.X64) + .WithJit(Jit.RyuJit); + + AddJob( + baseJob + .WithToolchain(CsProjCoreToolchain.NetCoreApp80) + .WithCustomBuildConfiguration("ReleaseNuGet") + .WithId("NuGet80") + .AsBaseline() + ); + + AddJob( + baseJob + .WithToolchain(CsProjCoreToolchain.NetCoreApp80) + .WithCustomBuildConfiguration("Release") + .WithId("Core80") + ); + + AddDiagnoser(MemoryDiagnoser.Default); + + AddValidator(BaselineValidator.FailOnError); + AddValidator(JitOptimizationsValidator.FailOnError); + } + } + + protected const string ConnectionString = "database=localhost:benchmark.fdb;user=sysdba;password=masterkey"; + + [Params(100_000)] + public int Count { get; set; } + + [Params( + "BIGINT", + "CHAR(255) CHARACTER SET UTF8", + "CHAR(255) CHARACTER SET OCTETS", + "BLOB SUB_TYPE TEXT CHARACTER SET UTF8", + "BLOB SUB_TYPE BINARY" + )] + public string DataType { get; set; } + + [GlobalSetup(Target = nameof(Fetch))] + public void FetchGlobalSetup() + { + FbConnection.CreateDatabase(ConnectionString, 8192, false, true); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + FbConnection.ClearAllPools(); + FbConnection.DropDatabase(ConnectionString); + } + + [Benchmark] + public void Fetch() + { + using var conn = new FbConnection(ConnectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = $@" + EXECUTE BLOCK RETURNS (result {DataType}) AS + DECLARE cnt INTEGER; + BEGIN + SELECT {GetFillExpression(DataType)} FROM rdb$database INTO result; + cnt = {Count}; + WHILE (cnt > 0) DO + BEGIN + SUSPEND; + cnt = cnt - 1; + END + END + "; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + _ = reader[0]; + } + } + private static string GetFillExpression(string dataType) => + dataType switch + { + { } when dataType.StartsWith("BLOB") => $"LPAD('', 1023, '{dataType};')", + { } when dataType.StartsWith("CHAR") => $"LPAD('', 255, '{dataType};')", + _ => "9223372036854775807" /* BIGINT */ + }; +} \ No newline at end of file From 8fa39ed431e7442c4a2ffa1906b14e9bd4015751 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:33:33 -0300 Subject: [PATCH 26/36] Extract shared BenchmarkConfig to eliminate duplication --- .../BenchmarkConfig.cs | 53 +++++++++++++++++++ .../CommandBenchmark.cs | 39 +------------- .../LargeFetchBenchmark.cs | 39 +------------- 3 files changed, 55 insertions(+), 76 deletions(-) create mode 100644 src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs new file mode 100644 index 000000000..97eb70127 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs @@ -0,0 +1,53 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Toolchains.CsProj; +using BenchmarkDotNet.Validators; + +namespace FirebirdSql.Data.FirebirdClient.Benchmarks; + +class BenchmarkConfig : ManualConfig +{ + public BenchmarkConfig() + { + var baseJob = Job.Default + .WithWarmupCount(3); + + AddJob( + baseJob + .WithToolchain(CsProjCoreToolchain.NetCoreApp80) + .WithCustomBuildConfiguration("ReleaseNuGet") + .WithId("NuGet80") + .AsBaseline() + ); + + AddJob( + baseJob + .WithToolchain(CsProjCoreToolchain.NetCoreApp80) + .WithCustomBuildConfiguration("Release") + .WithId("Core80") + ); + + AddDiagnoser(MemoryDiagnoser.Default); + + AddValidator(BaselineValidator.FailOnError); + AddValidator(JitOptimizationsValidator.FailOnError); + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs index 881dbd033..2e0e861a5 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs @@ -16,49 +16,12 @@ //$Authors = Jiri Cincura (jiri@cincura.net) using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Environments; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Toolchains.CsProj; -using BenchmarkDotNet.Validators; namespace FirebirdSql.Data.FirebirdClient.Benchmarks; -[Config(typeof(Config))] +[Config(typeof(BenchmarkConfig))] public partial class CommandBenchmark { - class Config : ManualConfig - { - public Config() - { - var baseJob = Job.Default - .WithWarmupCount(3) - .WithPlatform(Platform.X64) - .WithJit(Jit.RyuJit); - - AddJob( - baseJob - .WithToolchain(CsProjCoreToolchain.NetCoreApp80) - .WithCustomBuildConfiguration("ReleaseNuGet") - .WithId("NuGet80") - .AsBaseline() - ); - - AddJob( - baseJob - .WithToolchain(CsProjCoreToolchain.NetCoreApp80) - .WithCustomBuildConfiguration("Release") - .WithId("Core80") - ); - - AddDiagnoser(MemoryDiagnoser.Default); - - AddValidator(BaselineValidator.FailOnError); - AddValidator(JitOptimizationsValidator.FailOnError); - } - } - protected const string ConnectionString = "database=localhost:benchmark.fdb;user=sysdba;password=masterkey"; [Params("BIGINT", "VARCHAR(10) CHARACTER SET UTF8")] diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs index 7bf94b2c6..30d0f2e36 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs @@ -1,47 +1,10 @@ using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Environments; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Toolchains.CsProj; -using BenchmarkDotNet.Validators; namespace FirebirdSql.Data.FirebirdClient.Benchmarks; -[Config(typeof(Config))] +[Config(typeof(BenchmarkConfig))] public class LargeFetchBenchmark { - class Config : ManualConfig - { - public Config() - { - var baseJob = Job.Default - .WithWarmupCount(3) - .WithPlatform(Platform.X64) - .WithJit(Jit.RyuJit); - - AddJob( - baseJob - .WithToolchain(CsProjCoreToolchain.NetCoreApp80) - .WithCustomBuildConfiguration("ReleaseNuGet") - .WithId("NuGet80") - .AsBaseline() - ); - - AddJob( - baseJob - .WithToolchain(CsProjCoreToolchain.NetCoreApp80) - .WithCustomBuildConfiguration("Release") - .WithId("Core80") - ); - - AddDiagnoser(MemoryDiagnoser.Default); - - AddValidator(BaselineValidator.FailOnError); - AddValidator(JitOptimizationsValidator.FailOnError); - } - } - protected const string ConnectionString = "database=localhost:benchmark.fdb;user=sysdba;password=masterkey"; [Params(100_000)] From 5852c9a889501686efe192920dc7f493018b2d37 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:35:08 -0300 Subject: [PATCH 27/36] Extract BenchmarkBase with shared ConnectionString and env-var override --- .../BenchmarkBase.cs | 41 +++++++++++++++++++ .../CommandBenchmark.cs | 16 +------- .../LargeFetchBenchmark.cs | 13 +----- 3 files changed, 44 insertions(+), 26 deletions(-) create mode 100644 src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkBase.cs diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkBase.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkBase.cs new file mode 100644 index 000000000..b71fc2a0d --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkBase.cs @@ -0,0 +1,41 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System; +using BenchmarkDotNet.Attributes; + +namespace FirebirdSql.Data.FirebirdClient.Benchmarks; + +public abstract class BenchmarkBase +{ + const string DefaultConnectionString = "database=localhost:benchmark.fdb;user=sysdba;password=masterkey"; + + protected static readonly string ConnectionString = + Environment.GetEnvironmentVariable("FIREBIRD_BENCHMARK_CS") ?? DefaultConnectionString; + + protected static void CreateDatabase(int pageSize = 16 * 1024) + { + FbConnection.CreateDatabase(ConnectionString, pageSize, false, true); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + FbConnection.ClearAllPools(); + FbConnection.DropDatabase(ConnectionString); + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs index 2e0e861a5..b346b16fc 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs @@ -20,25 +20,11 @@ namespace FirebirdSql.Data.FirebirdClient.Benchmarks; [Config(typeof(BenchmarkConfig))] -public partial class CommandBenchmark +public partial class CommandBenchmark : BenchmarkBase { - protected const string ConnectionString = "database=localhost:benchmark.fdb;user=sysdba;password=masterkey"; - [Params("BIGINT", "VARCHAR(10) CHARACTER SET UTF8")] public string DataType { get; set; } [Params(100)] public int Count { get; set; } - - static void CreateDatabase() - { - FbConnection.CreateDatabase(ConnectionString, 16 * 1024, false, true); - } - - [GlobalCleanup] - public static void GlobalCleanup() - { - FbConnection.ClearAllPools(); - FbConnection.DropDatabase(ConnectionString); - } } diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs index 30d0f2e36..272e88523 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs @@ -3,10 +3,8 @@ namespace FirebirdSql.Data.FirebirdClient.Benchmarks; [Config(typeof(BenchmarkConfig))] -public class LargeFetchBenchmark +public class LargeFetchBenchmark : BenchmarkBase { - protected const string ConnectionString = "database=localhost:benchmark.fdb;user=sysdba;password=masterkey"; - [Params(100_000)] public int Count { get; set; } @@ -22,14 +20,7 @@ public class LargeFetchBenchmark [GlobalSetup(Target = nameof(Fetch))] public void FetchGlobalSetup() { - FbConnection.CreateDatabase(ConnectionString, 8192, false, true); - } - - [GlobalCleanup] - public void GlobalCleanup() - { - FbConnection.ClearAllPools(); - FbConnection.DropDatabase(ConnectionString); + CreateDatabase(pageSize: 8192); } [Benchmark] From 3ad15fa5223c96d0d6fcbdb6f554700d9d600ed0 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:36:59 -0300 Subject: [PATCH 28/36] Update benchmark toolchain from .NET 8 to .NET 10 --- .../BenchmarkConfig.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs index 97eb70127..6655dfc2c 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs @@ -19,12 +19,16 @@ using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Toolchains.CsProj; +using BenchmarkDotNet.Toolchains.DotNetCli; using BenchmarkDotNet.Validators; namespace FirebirdSql.Data.FirebirdClient.Benchmarks; class BenchmarkConfig : ManualConfig { + static readonly IToolchain Net100Toolchain = + CsProjCoreToolchain.From(new NetCoreAppSettings("net10.0", null, ".NET 10")); + public BenchmarkConfig() { var baseJob = Job.Default @@ -32,17 +36,17 @@ public BenchmarkConfig() AddJob( baseJob - .WithToolchain(CsProjCoreToolchain.NetCoreApp80) + .WithToolchain(Net100Toolchain) .WithCustomBuildConfiguration("ReleaseNuGet") - .WithId("NuGet80") + .WithId("NuGet100") .AsBaseline() ); AddJob( baseJob - .WithToolchain(CsProjCoreToolchain.NetCoreApp80) + .WithToolchain(Net100Toolchain) .WithCustomBuildConfiguration("Release") - .WithId("Core80") + .WithId("Core100") ); AddDiagnoser(MemoryDiagnoser.Default); From 0ecaa3addd5ffbfe3ffde625a1961708e62a392b Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:37:34 -0300 Subject: [PATCH 29/36] Add GitHub Markdown exporter and fastest-to-slowest ordering --- .../BenchmarkConfig.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs index 6655dfc2c..8724ae310 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs @@ -17,7 +17,9 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Order; using BenchmarkDotNet.Toolchains.CsProj; using BenchmarkDotNet.Toolchains.DotNetCli; using BenchmarkDotNet.Validators; @@ -51,6 +53,10 @@ public BenchmarkConfig() AddDiagnoser(MemoryDiagnoser.Default); + AddExporter(MarkdownExporter.GitHub); + + Orderer = new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest); + AddValidator(BaselineValidator.FailOnError); AddValidator(JitOptimizationsValidator.FailOnError); } From 671604a86313415f96960194b00131d6c5ab010c Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:38:12 -0300 Subject: [PATCH 30/36] Replace single-value [Params] with constants --- .../CommandBenchmark.cs | 5 ++--- .../LargeFetchBenchmark.cs | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs index b346b16fc..71a291afc 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.cs @@ -22,9 +22,8 @@ namespace FirebirdSql.Data.FirebirdClient.Benchmarks; [Config(typeof(BenchmarkConfig))] public partial class CommandBenchmark : BenchmarkBase { + const int Count = 100; + [Params("BIGINT", "VARCHAR(10) CHARACTER SET UTF8")] public string DataType { get; set; } - - [Params(100)] - public int Count { get; set; } } diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs index 272e88523..5a5e4a8d2 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs @@ -5,8 +5,7 @@ namespace FirebirdSql.Data.FirebirdClient.Benchmarks; [Config(typeof(BenchmarkConfig))] public class LargeFetchBenchmark : BenchmarkBase { - [Params(100_000)] - public int Count { get; set; } + const int Count = 100_000; [Params( "BIGINT", From 32c46bbaec6a1445289e67314ac17788a4075c4f Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:38:56 -0300 Subject: [PATCH 31/36] Return values from Fetch benchmarks to prevent dead-code elimination --- .../CommandBenchmark.Fetch.cs | 6 ++++-- .../LargeFetchBenchmark.cs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs index bff88363e..b3625bd51 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs @@ -54,7 +54,7 @@ EXECUTE BLOCK AS } [Benchmark] - public void Fetch() + public object Fetch() { using var conn = new FbConnection(ConnectionString); conn.Open(); @@ -62,10 +62,12 @@ public void Fetch() using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT x FROM foobar"; + object last = null; using var reader = cmd.ExecuteReader(); while (reader.Read()) { - var _ = reader[0]; + last = reader[0]; } + return last; } } diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs index 5a5e4a8d2..17fd86619 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/LargeFetchBenchmark.cs @@ -23,7 +23,7 @@ public void FetchGlobalSetup() } [Benchmark] - public void Fetch() + public object Fetch() { using var conn = new FbConnection(ConnectionString); conn.Open(); @@ -43,11 +43,13 @@ EXECUTE BLOCK RETURNS (result {DataType}) AS END "; + object last = null; using var reader = cmd.ExecuteReader(); while (reader.Read()) { - _ = reader[0]; + last = reader[0]; } + return last; } private static string GetFillExpression(string dataType) => dataType switch From 3c3ce6e48f5b0502d82abb75e48a77b78f77a947 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:40:05 -0300 Subject: [PATCH 32/36] Add async benchmark variants for Execute and Fetch --- .../CommandBenchmark.Execute.cs | 22 +++++++++++++++++- .../CommandBenchmark.Fetch.cs | 23 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Execute.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Execute.cs index 606e292f8..fe28016c5 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Execute.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Execute.cs @@ -15,13 +15,14 @@ //$Authors = Jiri Cincura (jiri@cincura.net) +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; namespace FirebirdSql.Data.FirebirdClient.Benchmarks; public partial class CommandBenchmark { - [GlobalSetup(Target = nameof(Execute))] + [GlobalSetup(Targets = new[] { nameof(Execute), nameof(ExecuteAsync) })] public void ExecuteGlobalSetup() { CreateDatabase(); @@ -52,4 +53,23 @@ public void Execute() cmd.ExecuteNonQuery(); } } + + [Benchmark] + public async Task ExecuteAsync() + { + await using var conn = new FbConnection(ConnectionString); + await conn.OpenAsync(); + + await using var cmd = conn.CreateCommand(); + cmd.CommandText = @"INSERT INTO foobar (x) VALUES (@cnt)"; + + var p = new FbParameter() { ParameterName = "@cnt" }; + cmd.Parameters.Add(p); + + for (var i = 0; i < Count; i++) + { + p.Value = i; + await cmd.ExecuteNonQueryAsync(); + } + } } diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs index b3625bd51..43ea3a7e2 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs @@ -15,13 +15,14 @@ //$Authors = Jiri Cincura (jiri@cincura.net) +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; namespace FirebirdSql.Data.FirebirdClient.Benchmarks; public partial class CommandBenchmark { - [GlobalSetup(Target = nameof(Fetch))] + [GlobalSetup(Targets = new[] { nameof(Fetch), nameof(FetchAsync) })] public void FetchGlobalSetup() { CreateDatabase(); @@ -70,4 +71,24 @@ public object Fetch() } return last; } + + [Benchmark] + public async Task FetchAsync() + { + await using var conn = new FbConnection(ConnectionString); + await conn.OpenAsync(); + + await using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT x FROM foobar"; + + object last = null; + await using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + last = reader[0]; + } + return last; + } +} + } } From c12627e8947665b8971e36ed5bf7160ccdb74838 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:40:33 -0300 Subject: [PATCH 33/36] Add connection open/close benchmark --- .../ConnectionBenchmark.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/FirebirdSql.Data.FirebirdClient.Benchmarks/ConnectionBenchmark.cs diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/ConnectionBenchmark.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/ConnectionBenchmark.cs new file mode 100644 index 000000000..edc201017 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/ConnectionBenchmark.cs @@ -0,0 +1,45 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace FirebirdSql.Data.FirebirdClient.Benchmarks; + +[Config(typeof(BenchmarkConfig))] +public class ConnectionBenchmark : BenchmarkBase +{ + [GlobalSetup] + public void GlobalSetup() + { + CreateDatabase(); + } + + [Benchmark] + public void OpenClose() + { + using var conn = new FbConnection(ConnectionString); + conn.Open(); + } + + [Benchmark] + public async Task OpenCloseAsync() + { + await using var conn = new FbConnection(ConnectionString); + await conn.OpenAsync(); + } +} From 6f9e2d8e7f4fb51cdce1ae4a877e2e7ab65858cd Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:41:00 -0300 Subject: [PATCH 34/36] Add -Disasm and -Profile flags to run-benchmark.ps1 --- run-benchmark.ps1 | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/run-benchmark.ps1 b/run-benchmark.ps1 index 0d4430fc8..580dafa01 100644 --- a/run-benchmark.ps1 +++ b/run-benchmark.ps1 @@ -1,15 +1,23 @@ param( - [ValidateSet('CommandBenchmark','LargeFetchBenchmark')] - $Benchmark = 'CommandBenchmark' + [ValidateSet('CommandBenchmark','ConnectionBenchmark','LargeFetchBenchmark')] + $Benchmark = 'CommandBenchmark', + + [switch]$Disasm, + [switch]$Profile ) $ErrorActionPreference = 'Stop' $projectFile = '.\src\FirebirdSql.Data.FirebirdClient.Benchmarks\FirebirdSql.Data.FirebirdClient.Benchmarks.csproj' +$extraArgs = @() +if ($Disasm) { $extraArgs += '--disasm' } +if ($Profile) { $extraArgs += '--profiler', 'ETW' } + # Run selected benchmark dotnet run ` --project $projectFile ` --configuration 'Release' ` -- ` - --filter "*$($Benchmark)*" + --filter "*$($Benchmark)*" ` + @extraArgs From 1ec795de1ac51120a1ad0553f68831c548ded38c Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sun, 22 Mar 2026 23:42:06 -0300 Subject: [PATCH 35/36] Fix build errors: remove duplicate braces, add missing using --- .../BenchmarkConfig.cs | 1 + .../CommandBenchmark.Fetch.cs | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs index 8724ae310..41b768756 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/BenchmarkConfig.cs @@ -20,6 +20,7 @@ using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Order; +using BenchmarkDotNet.Toolchains; using BenchmarkDotNet.Toolchains.CsProj; using BenchmarkDotNet.Toolchains.DotNetCli; using BenchmarkDotNet.Validators; diff --git a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs index 43ea3a7e2..821b2c5be 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Benchmarks/CommandBenchmark.Fetch.cs @@ -89,6 +89,4 @@ public async Task FetchAsync() } return last; } -} - } } From 533d57cefd3aab50d67a7fce6f6e23d5a5058ce9 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 23 Mar 2026 00:38:08 -0300 Subject: [PATCH 36/36] Add benchmark documentation --- docs/benchmark.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 docs/benchmark.md diff --git a/docs/benchmark.md b/docs/benchmark.md new file mode 100644 index 000000000..6a45c7f84 --- /dev/null +++ b/docs/benchmark.md @@ -0,0 +1,120 @@ +# Benchmarks + +The benchmark project (`FirebirdSql.Data.FirebirdClient.Benchmarks`) measures performance of the Firebird .NET provider using [BenchmarkDotNet](https://benchmarkdotnet.org/). + +## Prerequisites + +- A running Firebird server accessible at `localhost` (default configuration) +- .NET 10 SDK + +## Running Benchmarks + +Use the convenience script from the repository root: + +```powershell +.\run-benchmark.ps1 +``` + +By default this runs `CommandBenchmark`. To select a different benchmark class: + +```powershell +.\run-benchmark.ps1 -Benchmark ConnectionBenchmark +.\run-benchmark.ps1 -Benchmark LargeFetchBenchmark +``` + +### Advanced Options + +Enable JIT disassembly output: + +```powershell +.\run-benchmark.ps1 -Disasm +``` + +Enable ETW profiling (Windows only): + +```powershell +.\run-benchmark.ps1 -Profile +``` + +Options can be combined: + +```powershell +.\run-benchmark.ps1 -Benchmark LargeFetchBenchmark -Disasm +``` + +### Running Directly with BenchmarkDotNet + +For full control over BenchmarkDotNet options, pass arguments directly after `--`: + +```powershell +dotnet run --project src\FirebirdSql.Data.FirebirdClient.Benchmarks\FirebirdSql.Data.FirebirdClient.Benchmarks.csproj --configuration Release -- --list flat +dotnet run --project src\FirebirdSql.Data.FirebirdClient.Benchmarks\FirebirdSql.Data.FirebirdClient.Benchmarks.csproj --configuration Release -- --filter "*Fetch*" +``` + +## Connection String + +By default the benchmark connects to: + +``` +database=localhost:benchmark.fdb;user=sysdba;password=masterkey +``` + +Override this with the `FIREBIRD_BENCHMARK_CS` environment variable: + +```powershell +$env:FIREBIRD_BENCHMARK_CS = "database=myhost:benchmark.fdb;user=sysdba;password=masterkey" +.\run-benchmark.ps1 +``` + +## Configuration + +All benchmarks share a common configuration (`BenchmarkConfig`): + +- **Baseline job**: `ReleaseNuGet` build configuration — references the latest published `FirebirdSql.Data.FirebirdClient` NuGet package. +- **Candidate job**: `Release` build configuration — references the local project source. +- **Runtime**: .NET 10 +- **Diagnostics**: Memory allocations (`MemoryDiagnoser`) +- **Export**: GitHub-flavored Markdown table (written to `BenchmarkDotNet.Artifacts/`) +- **Ordering**: Fastest to slowest + +The NuGet baseline lets you compare the locally built provider against the published release to detect regressions or measure improvements. + +## Available Benchmarks + +### `CommandBenchmark` + +Measures command execution over two data types (`BIGINT`, `VARCHAR(10) CHARACTER SET UTF8`): + +| Benchmark | Description | +|-----------|-------------| +| `Execute` / `ExecuteAsync` | Inserts 100 rows using `ExecuteNonQuery` / `ExecuteNonQueryAsync` | +| `Fetch` / `FetchAsync` | Reads 100 rows using `ExecuteReader` / `ExecuteReaderAsync` | + +### `ConnectionBenchmark` + +Measures connection pool throughput: + +| Benchmark | Description | +|-----------|-------------| +| `OpenClose` / `OpenCloseAsync` | Opens and closes a pooled connection | + +### `LargeFetchBenchmark` + +Measures bulk read throughput for 100,000 rows across five data types: + +| Data Type | Notes | +|-----------|-------| +| `BIGINT` | Fixed-size integer | +| `CHAR(255) CHARACTER SET UTF8` | Fixed-length string | +| `CHAR(255) CHARACTER SET OCTETS` | Fixed-length binary | +| `BLOB SUB_TYPE TEXT CHARACTER SET UTF8` | Text blob | +| `BLOB SUB_TYPE BINARY` | Binary blob | + +## Results + +BenchmarkDotNet writes results to `BenchmarkDotNet.Artifacts/` in the repository root. This directory is listed in `.gitignore`. Each run produces: + +- A summary table in the console +- A GitHub-flavored Markdown file (`.md`) suitable for pasting into issues or pull requests +- An HTML report +- CSV data