diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDatabaseInfoTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDatabaseInfoTests.cs index d1a34b86..12f5bb2f 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDatabaseInfoTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDatabaseInfoTests.cs @@ -16,6 +16,7 @@ //$Authors = Carlos Guzman Alvarez, Jiri Cincura (jiri@cincura.net) using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; @@ -61,4 +62,124 @@ public void CompleteDatabaseInfoTest() Assert.DoesNotThrowAsync(() => (Task)m.Invoke(dbInfo, new object[] { CancellationToken.None }), m.Name); } } + + [Test] + public async Task PerformanceAnalysis_SELECT_Test() + { + var tableNameList = await GetTableNameList(); + var tableIdTest = tableNameList["TEST"]; + + var dbInfo = new FbDatabaseInfo(Connection); + var insertCount = await dbInfo.GetInsertCountAsync(); + var updateCount = await dbInfo.GetUpdateCountAsync(); + var readSeqCount = await dbInfo.GetReadSeqCountAsync(); + var readIdxCount = await dbInfo.GetReadIdxCountAsync(); + + var fbCommand = new FbCommand("SELECT MAX(INT_FIELD) FROM TEST", Connection); + var maxIntField = await fbCommand.ExecuteScalarAsync() as int?; + + insertCount = await GetAffectedTables(insertCount, await dbInfo.GetInsertCountAsync()); + updateCount = await GetAffectedTables(updateCount, await dbInfo.GetUpdateCountAsync()); + readSeqCount = await GetAffectedTables(readSeqCount, await dbInfo.GetReadSeqCountAsync()); + readIdxCount = await GetAffectedTables(readIdxCount, await dbInfo.GetReadIdxCountAsync()); + + Assert.That(insertCount.ContainsKey(tableIdTest), Is.False); + Assert.That(updateCount.ContainsKey(tableIdTest), Is.False); + Assert.That(readSeqCount.ContainsKey(tableIdTest), Is.True); + Assert.That(readSeqCount[tableIdTest], Is.EqualTo(maxIntField + 1)); + Assert.That(readIdxCount.ContainsKey(tableIdTest), Is.False); + } + + [Test] + public async Task PerformanceAnalysis_INSERT_Test() + { + var tableNameList = await GetTableNameList(); + var tableIdTest = tableNameList["TEST"]; + + var dbInfo = new FbDatabaseInfo(Connection); + var insertCount = await dbInfo.GetInsertCountAsync(); + var updateCount = await dbInfo.GetUpdateCountAsync(); + var readSeqCount = await dbInfo.GetReadSeqCountAsync(); + var readIdxCount = await dbInfo.GetReadIdxCountAsync(); + + var fbCommand = new FbCommand("INSERT INTO TEST (INT_FIELD) VALUES (900)", Connection); + await fbCommand.ExecuteNonQueryAsync(); + + insertCount = await GetAffectedTables(insertCount, await dbInfo.GetInsertCountAsync()); + updateCount = await GetAffectedTables(updateCount, await dbInfo.GetUpdateCountAsync()); + readSeqCount = await GetAffectedTables(readSeqCount, await dbInfo.GetReadSeqCountAsync()); + readIdxCount = await GetAffectedTables(readIdxCount, await dbInfo.GetReadIdxCountAsync()); + + Assert.That(insertCount.ContainsKey(tableIdTest), Is.True); + Assert.That(insertCount[tableIdTest], Is.EqualTo(1)); + Assert.That(updateCount.ContainsKey(tableIdTest), Is.False); + Assert.That(readSeqCount.ContainsKey(tableIdTest), Is.False); + Assert.That(readIdxCount.ContainsKey(tableIdTest), Is.False); + } + + [Test] + public async Task PerformanceAnalysis_UPDATE_Test() + { + var tableNameList = await GetTableNameList(); + var tableIdTest = tableNameList["TEST"]; + + var fbCommand = new FbCommand("INSERT INTO TEST (INT_FIELD) VALUES (900)", Connection); + await fbCommand.ExecuteNonQueryAsync(); + + var dbInfo = new FbDatabaseInfo(Connection); + var insertCount = await dbInfo.GetInsertCountAsync(); + var updateCount = await dbInfo.GetUpdateCountAsync(); + var readSeqCount = await dbInfo.GetReadSeqCountAsync(); + var readIdxCount = await dbInfo.GetReadIdxCountAsync(); + + fbCommand.CommandText = "UPDATE TEST SET SMALLINT_FIELD = 900 WHERE (INT_FIELD = 900)"; + await fbCommand.ExecuteNonQueryAsync(); + + insertCount = await GetAffectedTables(insertCount, await dbInfo.GetInsertCountAsync()); + updateCount = await GetAffectedTables(updateCount, await dbInfo.GetUpdateCountAsync()); + readSeqCount = await GetAffectedTables(readSeqCount, await dbInfo.GetReadSeqCountAsync()); + readIdxCount = await GetAffectedTables(readIdxCount, await dbInfo.GetReadIdxCountAsync()); + + Assert.That(insertCount.ContainsKey(tableIdTest), Is.False); + Assert.That(updateCount.ContainsKey(tableIdTest), Is.True); + Assert.That(updateCount[tableIdTest], Is.EqualTo(1)); + Assert.That(readSeqCount.ContainsKey(tableIdTest), Is.False); + Assert.That(readIdxCount.ContainsKey(tableIdTest), Is.True); + Assert.That(readIdxCount[tableIdTest], Is.EqualTo(1)); + } + + async Task> GetAffectedTables(IDictionary statisticInfoBefore, IDictionary statisticInfoAfter) + { + var result = new Dictionary(); + foreach (var keyValuePair in statisticInfoAfter) + { + if (statisticInfoBefore.TryGetValue(keyValuePair.Key, out var value)) + { + var counter = keyValuePair.Value - value; + if (counter > 0) + { + result.Add(keyValuePair.Key, counter); + } + } + else + result.Add(keyValuePair.Key, keyValuePair.Value); + } + return await Task.FromResult(result); + } + + async Task> GetTableNameList() + { + IDictionary result = new Dictionary(); + await using (var command = new FbCommand("select R.RDB$RELATION_ID, TRIM(R.RDB$RELATION_NAME) from RDB$RELATIONS R WHERE RDB$SYSTEM_FLAG = 0", Connection)) + { + await using (var reader = await command.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + result.Add(reader.GetString(1), reader.GetInt16(0)); + } + } + } + return result; + } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs b/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs index b3d5d432..3f7c7907 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs @@ -51,6 +51,20 @@ public static string ToHexString(this byte[] b) return BitConverter.ToString(b).Replace("-", string.Empty); } + public static IDictionary GetTableStatistics(this byte[] b, int length) + { + var capacity = length > 3 ? (length - 3) / 6 + 1 : 0; + + var tableStatistics = new Dictionary(capacity); + for (var i = 3; i < length; i += 6) + { + var tableId = (short)IscHelper.VaxInteger(b, i, 2); + var count = (ulong)IscHelper.VaxInteger(b, i + 2, 4); + tableStatistics.Add(tableId, count); + } + return tableStatistics; + } + public static IEnumerable> Split(this T[] array, int size) { for (var i = 0; i < (float)array.Length / size; i++) diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/IscHelper.cs b/src/FirebirdSql.Data.FirebirdClient/Common/IscHelper.cs index 578895f7..bb8ab763 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/IscHelper.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/IscHelper.cs @@ -54,14 +54,6 @@ public static List ParseDatabaseInfo(byte[] buffer, Charset charset) case IscCodes.isc_info_marks: case IscCodes.isc_info_reads: case IscCodes.isc_info_writes: - case IscCodes.isc_info_backout_count: - case IscCodes.isc_info_delete_count: - case IscCodes.isc_info_expunge_count: - case IscCodes.isc_info_insert_count: - case IscCodes.isc_info_purge_count: - case IscCodes.isc_info_read_idx_count: - case IscCodes.isc_info_read_seq_count: - case IscCodes.isc_info_update_count: case IscCodes.isc_info_db_size_in_pages: case IscCodes.isc_info_oldest_transaction: case IscCodes.isc_info_oldest_active: @@ -77,6 +69,17 @@ public static List ParseDatabaseInfo(byte[] buffer, Charset charset) info.Add(VaxInteger(buffer, pos, length)); break; + case IscCodes.isc_info_backout_count: + case IscCodes.isc_info_delete_count: + case IscCodes.isc_info_expunge_count: + case IscCodes.isc_info_insert_count: + case IscCodes.isc_info_purge_count: + case IscCodes.isc_info_update_count: + case IscCodes.isc_info_read_seq_count: + case IscCodes.isc_info_read_idx_count: + info.Add(buffer.GetTableStatistics(length)); + break; + case IscCodes.isc_info_no_reserve: case IscCodes.isc_info_forced_writes: case IscCodes.isc_info_db_read_only: diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDatabaseInfo.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDatabaseInfo.cs index 7f6dc6c9..18ee51d2 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDatabaseInfo.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDatabaseInfo.cs @@ -17,7 +17,6 @@ using System; using System.Collections.Generic; -using System.Data; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -225,76 +224,141 @@ public Task GetWritesAsync(CancellationToken cancellationToken = default) return GetValueAsync(IscCodes.isc_info_writes, cancellationToken); } - public int GetBackoutCount() + /// + /// Returns the number of removals of a version of a record of the affected tables. + /// + /// Dictionary with the number of removals, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public IDictionary GetBackoutCount() { - return GetValue(IscCodes.isc_info_backout_count); + return GetValue>(IscCodes.isc_info_backout_count); } - public Task GetBackoutCountAsync(CancellationToken cancellationToken = default) + /// + /// Returns the number of removals of a version of a record of the affected tables. + /// + /// Dictionary with the number of removals, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public Task> GetBackoutCountAsync(CancellationToken cancellationToken = default) { - return GetValueAsync(IscCodes.isc_info_backout_count, cancellationToken); + return GetValueAsync>(IscCodes.isc_info_backout_count, cancellationToken); } - public int GetDeleteCount() + /// + /// Returns the number of database deletes of the affected tables since the database was last attached. + /// + /// Dictionary with the number of deletes, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public IDictionary GetDeleteCount() { - return GetValue(IscCodes.isc_info_delete_count); + return GetValue>(IscCodes.isc_info_delete_count); } - public Task GetDeleteCountAsync(CancellationToken cancellationToken = default) + /// + /// Returns the number of database deletes of the affected tables since the database was last attached. + /// + /// Dictionary with the number of deletes, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public Task> GetDeleteCountAsync(CancellationToken cancellationToken = default) { - return GetValueAsync(IscCodes.isc_info_delete_count, cancellationToken); + return GetValueAsync>(IscCodes.isc_info_delete_count, cancellationToken); } - public int GetExpungeCount() + /// + /// Returns the number of removals of a record and all of its ancestors, for records whose deletions have been committed of the affected tables since the database was last attached. + /// + /// Dictionary with the number of removals, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public IDictionary GetExpungeCount() { - return GetValue(IscCodes.isc_info_expunge_count); + return GetValue>(IscCodes.isc_info_expunge_count); } - public Task GetExpungeCountAsync(CancellationToken cancellationToken = default) + + /// + /// Returns the number of removals of a record and all of its ancestors, for records whose deletions have been committed of the affected tables since the database was last attached. + /// + /// Dictionary with the number of removals, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public Task> GetExpungeCountAsync(CancellationToken cancellationToken = default) { - return GetValueAsync(IscCodes.isc_info_expunge_count, cancellationToken); + return GetValueAsync>(IscCodes.isc_info_expunge_count, cancellationToken); } - public int GetInsertCount() + /// + /// Returns the number of inserts into the database of the affected tables since the database was last attached. + /// + /// Dictionary with the number of inserts, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public IDictionary GetInsertCount() { - return GetValue(IscCodes.isc_info_insert_count); + return GetValue>(IscCodes.isc_info_insert_count); } - public Task GetInsertCountAsync(CancellationToken cancellationToken = default) + /// + /// Returns the number of inserts into the database of the affected tables since the database was last attached. + /// + /// Dictionary with the number of inserts, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public Task> GetInsertCountAsync(CancellationToken cancellationToken = default) { - return GetValueAsync(IscCodes.isc_info_insert_count, cancellationToken); + return GetValueAsync>(IscCodes.isc_info_insert_count, cancellationToken); } - public int GetPurgeCount() + /// + /// Returns the number of removals of old versions of fully mature records (records that are committed, so that older ancestor versions are no longer needed) of the affected tables. + /// + /// Dictionary with the number of removals, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public IDictionary GetPurgeCount() { - return GetValue(IscCodes.isc_info_purge_count); + return GetValue>(IscCodes.isc_info_purge_count); } - public Task GetPurgeCountAsync(CancellationToken cancellationToken = default) + /// + /// Returns the number of removals of old versions of fully mature records (records that are committed, so that older ancestor versions are no longer needed) of the affected tables. + /// + /// Dictionary with the number of removals, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public Task> GetPurgeCountAsync(CancellationToken cancellationToken = default) { - return GetValueAsync(IscCodes.isc_info_purge_count, cancellationToken); + return GetValueAsync>(IscCodes.isc_info_purge_count, cancellationToken); } - public long GetReadIdxCount() + /// + /// Returns the number of reads done via an index of the affected tables since the database was last attached. + /// + /// Dictionary with the number of reads, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public IDictionary GetReadIdxCount() { - return GetValue(IscCodes.isc_info_read_idx_count); + return GetValue>(IscCodes.isc_info_read_idx_count); } - public Task GetReadIdxCountAsync(CancellationToken cancellationToken = default) + /// + /// Returns the number of reads done via an index of the affected tables since the database was last attached. + /// + /// Dictionary with the number of reads, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public Task> GetReadIdxCountAsync(CancellationToken cancellationToken = default) { - return GetValueAsync(IscCodes.isc_info_read_idx_count, cancellationToken); + return GetValueAsync>(IscCodes.isc_info_read_idx_count, cancellationToken); } - public long GetReadSeqCount() + /// + /// Returns the number of sequential table scans (row reads) of the affected tables since the database was last attached. + /// + /// Dictionary with the number of reads, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public IDictionary GetReadSeqCount() { - return GetValue(IscCodes.isc_info_read_seq_count); + return GetValue>(IscCodes.isc_info_read_seq_count); } - public Task GetReadSeqCountAsync(CancellationToken cancellationToken = default) + /// + /// Returns the number of sequential table scans (row reads) of the affected tables since the database was last attached. + /// + /// Dictionary with the number of reads, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public Task> GetReadSeqCountAsync(CancellationToken cancellationToken = default) { - return GetValueAsync(IscCodes.isc_info_read_seq_count, cancellationToken); + return GetValueAsync>(IscCodes.isc_info_read_seq_count, cancellationToken); } - public long GetUpdateCount() + /// + /// Returns the number of database updates of the affected tables since the database was last attached. + /// + /// Dictionary with the number of updates, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public IDictionary GetUpdateCount() { - return GetValue(IscCodes.isc_info_update_count); + return GetValue>(IscCodes.isc_info_update_count); } - public Task GetUpdateCountAsync(CancellationToken cancellationToken = default) + /// + /// Returns the number of database updates of the affected tables since the database was last attached. + /// + /// Dictionary with the number of updates, with table id (field RDB$RELATION_ID of the system table RDB$RELATIONS) as key. + public Task> GetUpdateCountAsync(CancellationToken cancellationToken = default) { - return GetValueAsync(IscCodes.isc_info_update_count, cancellationToken); + return GetValueAsync>(IscCodes.isc_info_update_count, cancellationToken); } public int GetDatabaseSizeInPages()