From a131a11d7dd15caf6249fa958ea2cc28262e8231 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Thu, 24 Oct 2024 22:43:08 -0300 Subject: [PATCH 1/5] 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 d3a7d990..144375b7 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 578895f7..3cf67ac7 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 df3cb619..41c92847 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 9a42b18b3b6d6decdb5638ab4dc66ce4f5bc61f2 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Thu, 24 Oct 2024 22:45:59 -0300 Subject: [PATCH 2/5] 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 c5a2cdea..74d61ff3 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; namespace FirebirdSql.Data.FirebirdClient; @@ -49,6 +51,7 @@ public sealed class FbCommand : DbCommand, IFbPreparedCommand, IDescriptorFiller private int? _commandTimeout; private int _fetchSize; private Type[] _expectedColumnTypes; + private Activity _currentActivity; #endregion @@ -1094,6 +1097,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) @@ -1112,6 +1122,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(); @@ -1332,6 +1349,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) @@ -1476,57 +1513,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 bd28a79c..f1b8b804 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj @@ -61,6 +61,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 00000000..1a21fe53 --- /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 8ac5cbe8fcd8cc2fa22436ced6f97be89dcbaa4d Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sat, 26 Oct 2024 03:26:56 -0300 Subject: [PATCH 3/5] 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 74d61ff3..bfc7b216 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; namespace FirebirdSql.Data.FirebirdClient; @@ -52,6 +53,7 @@ public sealed class FbCommand : DbCommand, IFbPreparedCommand, IDescriptorFiller private int _fetchSize; private Type[] _expectedColumnTypes; private Activity _currentActivity; + private long _startedAtTicks; #endregion @@ -1098,13 +1100,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) { @@ -1123,12 +1121,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(); @@ -1356,6 +1349,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 14d0c568..c1c1d75b 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; namespace FirebirdSql.Data.FirebirdClient; @@ -189,6 +191,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}") + ]; } } } @@ -269,6 +277,8 @@ internal bool IsClosed get { return _state == ConnectionState.Closed; } } + internal KeyValuePair[] MetricsConnectionAttributes; + #endregion #region Protected Properties @@ -553,6 +563,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)) { @@ -645,10 +656,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)) { @@ -741,6 +755,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 59924b46..a72bd766 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 00000000..d4470a2a --- /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 f984b0bfbef36f3c73ad6bfaea0b0711af690daa Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sat, 26 Oct 2024 17:32:52 -0300 Subject: [PATCH 4/5] 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/NETProvider.sln | 14 ++-- src/Perf/CommandBenchmark.Fetch.cs | 73 ------------------- 9 files changed, 144 insertions(+), 125 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 fda195ab..2b35ae1c 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 00000000..10776ad6 --- /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 ea05f6d3..606e292f 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 00000000..bff88363 --- /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 b6a02f11..881dbd03 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 f47a017c..ab9f8e79 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 484f5634..dbb557d7 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/NETProvider.sln b/src/NETProvider.sln index 6c41c6e3..365bb491 100644 --- a/src/NETProvider.sln +++ b/src/NETProvider.sln @@ -34,9 +34,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FirebirdSql.EntityFramework EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "FirebirdSql.Data.External", "FirebirdSql.Data.External\FirebirdSql.Data.External.shproj", "{884EE120-B22E-4940-8C1C-626F13028376}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Perf", "Perf\Perf.csproj", "{BB846245-545A-4506-A0DA-0041C535D3A9}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{CE2BB2BB-4639-49EA-8369-0215A1D7245D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Perf", "Perf", "{CE2BB2BB-4639-49EA-8369-0215A1D7245D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FirebirdSql.Data.FirebirdClient.Benchmarks", "FirebirdSql.Data.FirebirdClient.Benchmarks\FirebirdSql.Data.FirebirdClient.Benchmarks.csproj", "{77A3DB18-75E9-451D-B434-6A95F9015FF7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scratchpad", "Scratchpad", "{5F1D1BB9-4657-424C-B3DE-75818824940E}" EndProject @@ -76,14 +76,14 @@ Global {2925DB97-5B39-4D9E-90CC-F7470F9AA8F3}.Debug|Any CPU.Build.0 = Debug|Any CPU {2925DB97-5B39-4D9E-90CC-F7470F9AA8F3}.Release|Any CPU.ActiveCfg = Release|Any CPU {2925DB97-5B39-4D9E-90CC-F7470F9AA8F3}.Release|Any CPU.Build.0 = Release|Any CPU - {BB846245-545A-4506-A0DA-0041C535D3A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BB846245-545A-4506-A0DA-0041C535D3A9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BB846245-545A-4506-A0DA-0041C535D3A9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BB846245-545A-4506-A0DA-0041C535D3A9}.Release|Any CPU.Build.0 = Release|Any CPU {C3F47B3D-FA1F-4665-A58D-63B6FFDCD65F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C3F47B3D-FA1F-4665-A58D-63B6FFDCD65F}.Debug|Any CPU.Build.0 = Debug|Any CPU {C3F47B3D-FA1F-4665-A58D-63B6FFDCD65F}.Release|Any CPU.ActiveCfg = Release|Any CPU {C3F47B3D-FA1F-4665-A58D-63B6FFDCD65F}.Release|Any CPU.Build.0 = Release|Any CPU + {77A3DB18-75E9-451D-B434-6A95F9015FF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77A3DB18-75E9-451D-B434-6A95F9015FF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77A3DB18-75E9-451D-B434-6A95F9015FF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77A3DB18-75E9-451D-B434-6A95F9015FF7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -98,10 +98,10 @@ Global {42A00B25-673E-449A-9B89-BE89344F96F0} = {AD392B88-6637-4744-BDF9-8FB9453C9042} {2925DB97-5B39-4D9E-90CC-F7470F9AA8F3} = {AD392B88-6637-4744-BDF9-8FB9453C9042} {884EE120-B22E-4940-8C1C-626F13028376} = {C94B5B06-9023-43C1-9B0D-6BDD504F9A06} - {BB846245-545A-4506-A0DA-0041C535D3A9} = {CE2BB2BB-4639-49EA-8369-0215A1D7245D} {CE2BB2BB-4639-49EA-8369-0215A1D7245D} = {C94B5B06-9023-43C1-9B0D-6BDD504F9A06} {5F1D1BB9-4657-424C-B3DE-75818824940E} = {C94B5B06-9023-43C1-9B0D-6BDD504F9A06} {C3F47B3D-FA1F-4665-A58D-63B6FFDCD65F} = {5F1D1BB9-4657-424C-B3DE-75818824940E} + {77A3DB18-75E9-451D-B434-6A95F9015FF7} = {CE2BB2BB-4639-49EA-8369-0215A1D7245D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D574B071-15C1-4024-BB37-78D690F61070} diff --git a/src/Perf/CommandBenchmark.Fetch.cs b/src/Perf/CommandBenchmark.Fetch.cs deleted file mode 100644 index b57ebf56..00000000 --- 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 e5e4075a90d085eeeb09a7454cf5aaab068dcf69 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Sat, 17 May 2025 22:13:33 -0300 Subject: [PATCH 5/5] 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 10776ad6..0d4430fc 100644 --- a/run-benchmark.ps1 +++ b/run-benchmark.ps1 @@ -1,5 +1,5 @@ param( - [ValidateSet('CommandBenchmark')] + [ValidateSet('CommandBenchmark','LargeFetchBenchmark')] $Benchmark = 'CommandBenchmark' )