From fec9506f0641b7335b66476ca9b71e7a8327a2b8 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Wed, 16 Aug 2023 14:12:48 -0700 Subject: [PATCH] Cache and reuse ColumnDefinitionPayload objects. Also reuse the array of payloads created by ResultSet. ResultSet was already caching the byte array from which the column definitions were deserialized; it now caches the deserialized objects, too. This memory is reused when the MySqlDataReader/Connection/session is returned to the pool then retrieved to execute a second query. If a result set retrieves a huge number of columns, a large amount of memory may be kept alive in the pool until the session is expired. This should hopefully be a rare occurrence but is also offset by the benefit of reusing that memory if very wide queries are being executed frequently. --- src/MySqlConnector/Core/ResultSet.cs | 65 ++++++++++++------- src/MySqlConnector/Core/ServerSession.cs | 4 +- src/MySqlConnector/MySqlDataReader.cs | 21 +++--- .../Payloads/ColumnDefinitionPayload.cs | 55 +++++++++------- 4 files changed, 85 insertions(+), 60 deletions(-) diff --git a/src/MySqlConnector/Core/ResultSet.cs b/src/MySqlConnector/Core/ResultSet.cs index 8cdedb2d4..9511722b3 100644 --- a/src/MySqlConnector/Core/ResultSet.cs +++ b/src/MySqlConnector/Core/ResultSet.cs @@ -20,7 +20,7 @@ public void Reset() { // ResultSet can be re-used, so initialize everything BufferState = ResultSetState.None; - ColumnDefinitions = null; + m_columnDefinitions = default; WarningCount = 0; State = ResultSetState.None; ContainsCommandParameters = false; @@ -55,7 +55,7 @@ public async Task ReadResultSetHeaderAsync(IOBehavior ioBehavior) WarningCount = ok.WarningCount; if (ok.NewSchema is not null) Connection.Session.DatabaseOverride = ok.NewSchema; - ColumnDefinitions = null; + m_columnDefinitions = default; State = (ok.ServerStatus & ServerStatus.MoreResultsExist) == 0 ? ResultSetState.NoMoreData : ResultSetState.HasMoreData; @@ -115,38 +115,50 @@ public async Task ReadResultSetHeaderAsync(IOBehavior ioBehavior) else { var columnCountPacket = ColumnCountPayload.Create(payload.Span, Session.SupportsCachedPreparedMetadata); + var columnCount = columnCountPacket.ColumnCount; if (!columnCountPacket.MetadataFollows) { // reuse previous metadata - ColumnDefinitions = DataReader.LastUsedPreparedStatement!.Columns!; - if (ColumnDefinitions.Length != columnCountPacket.ColumnCount) - throw new InvalidOperationException($"Expected result set to have {ColumnDefinitions.Length} columns, but it contains {columnCountPacket.ColumnCount} columns"); + m_columnDefinitions = DataReader.LastUsedPreparedStatement!.Columns!; + if (m_columnDefinitions.Length != columnCount) + throw new InvalidOperationException($"Expected result set to have {m_columnDefinitions.Length} columns, but it contains {columnCount} columns"); } else { // parse columns // reserve adequate space to hold a copy of all column definitions (but note that this can be resized below if we guess too small) - Utility.Resize(ref m_columnDefinitionPayloads, columnCountPacket.ColumnCount * 96); + Utility.Resize(ref m_columnDefinitionPayloadBytes, columnCount * 96); - ColumnDefinitions = new ColumnDefinitionPayload[columnCountPacket.ColumnCount]; - for (var column = 0; column < ColumnDefinitions.Length; column++) + // increase the cache size to be large enough to hold all the column definitions + if (m_columnDefinitionPayloadCache is null) + m_columnDefinitionPayloadCache = new ColumnDefinitionPayload[columnCount]; + else if (m_columnDefinitionPayloadCache.Length < columnCount) + Array.Resize(ref m_columnDefinitionPayloadCache, Math.Max(columnCount, m_columnDefinitionPayloadCache.Length * 2)); + m_columnDefinitions = m_columnDefinitionPayloadCache.AsMemory(0, columnCount); + + // if the server supports metadata caching but has re-sent it, something has changed since last prepare/execution and we need to update the columns + var preparedColumns = Session.SupportsCachedPreparedMetadata ? DataReader.LastUsedPreparedStatement?.Columns : null; + + for (var column = 0; column < columnCount; column++) { payload = await Session.ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false); var payloadLength = payload.Span.Length; // 'Session.ReceiveReplyAsync' reuses a shared buffer; make a copy so that the column definitions can always be safely read at any future point - if (m_columnDefinitionPayloadUsedBytes + payloadLength > m_columnDefinitionPayloads.Count) - Utility.Resize(ref m_columnDefinitionPayloads, m_columnDefinitionPayloadUsedBytes + payloadLength); - payload.Span.CopyTo(m_columnDefinitionPayloads.AsSpan(m_columnDefinitionPayloadUsedBytes)); + if (m_columnDefinitionPayloadUsedBytes + payloadLength > m_columnDefinitionPayloadBytes.Count) + Utility.Resize(ref m_columnDefinitionPayloadBytes, m_columnDefinitionPayloadUsedBytes + payloadLength); + payload.Span.CopyTo(m_columnDefinitionPayloadBytes.AsSpan(m_columnDefinitionPayloadUsedBytes)); + + // create/update the column definition in our cache + var payloadBytesSegment = new ResizableArraySegment(m_columnDefinitionPayloadBytes, m_columnDefinitionPayloadUsedBytes, payloadLength); + ColumnDefinitionPayload.Initialize(ref m_columnDefinitionPayloadCache[column], payloadBytesSegment); + + // if there was a prepared statement, update its cached columns too + if (preparedColumns is not null) + ColumnDefinitionPayload.Initialize(ref preparedColumns[column], payloadBytesSegment); - var columnDefinition = ColumnDefinitionPayload.Create(new ResizableArraySegment(m_columnDefinitionPayloads, m_columnDefinitionPayloadUsedBytes, payloadLength)); - ColumnDefinitions[column] = columnDefinition; m_columnDefinitionPayloadUsedBytes += payloadLength; } - - // server supports metadata caching, but has re-sent it, so something has changed since last prepare/execution - if (Session.SupportsCachedPreparedMetadata && DataReader.LastUsedPreparedStatement is { } preparedStatement) - preparedStatement.Columns = ColumnDefinitions; } if (!Session.SupportsDeprecateEof) @@ -277,7 +289,7 @@ public async Task ReadAsync(IOBehavior ioBehavior, CancellationToken cance public string GetName(int ordinal) { - if (ColumnDefinitions is null) + if (!HasResultSet) throw new InvalidOperationException("There is no current result set."); if (ordinal < 0 || ordinal >= ColumnDefinitions.Length) throw new IndexOutOfRangeException($"value must be between 0 and {ColumnDefinitions.Length - 1}"); @@ -286,7 +298,7 @@ public string GetName(int ordinal) public string GetDataTypeName(int ordinal) { - if (ColumnDefinitions is null) + if (!HasResultSet) throw new InvalidOperationException("There is no current result set."); if (ordinal < 0 || ordinal >= ColumnDefinitions.Length) throw new IndexOutOfRangeException($"value must be between 0 and {ColumnDefinitions.Length - 1}"); @@ -299,7 +311,7 @@ public string GetDataTypeName(int ordinal) public Type GetFieldType(int ordinal) { - if (ColumnDefinitions is null) + if (!HasResultSet) throw new InvalidOperationException("There is no current result set."); if (ordinal < 0 || ordinal >= ColumnDefinitions.Length) throw new IndexOutOfRangeException($"value must be between 0 and {ColumnDefinitions.Length - 1}"); @@ -311,9 +323,9 @@ public Type GetFieldType(int ordinal) } public MySqlDbType GetColumnType(int ordinal) => - TypeMapper.ConvertToMySqlDbType(ColumnDefinitions![ordinal], Connection.TreatTinyAsBoolean, Connection.GuidFormat); + TypeMapper.ConvertToMySqlDbType(ColumnDefinitions[ordinal], Connection.TreatTinyAsBoolean, Connection.GuidFormat); - public int FieldCount => ColumnDefinitions?.Length ?? 0; + public int FieldCount => ColumnDefinitions.Length; public bool HasRows { @@ -329,7 +341,7 @@ public int GetOrdinal(string name) { if (name is null) throw new ArgumentNullException(nameof(name)); - if (ColumnDefinitions is null) + if (!HasResultSet) throw new InvalidOperationException("There is no current result set."); for (var column = 0; column < ColumnDefinitions.Length; column++) @@ -355,14 +367,17 @@ public Row GetCurrentRow() public ServerSession Session => DataReader.Session!; public ResultSetState BufferState { get; private set; } - public ColumnDefinitionPayload[]? ColumnDefinitions { get; private set; } + public ReadOnlySpan ColumnDefinitions => m_columnDefinitions.Span; public int WarningCount { get; private set; } public ResultSetState State { get; private set; } + public bool HasResultSet => !(State == ResultSetState.None || ColumnDefinitions.Length == 0); public bool ContainsCommandParameters { get; private set; } - private ResizableArray? m_columnDefinitionPayloads; + private ResizableArray? m_columnDefinitionPayloadBytes; private int m_columnDefinitionPayloadUsedBytes; private Queue? m_readBuffer; private Row? m_row; private bool m_hasRows; + private ReadOnlyMemory m_columnDefinitions; + private ColumnDefinitionPayload[]? m_columnDefinitionPayloadCache; } diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index 10a92607c..e1f94540f 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -234,7 +234,7 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can var payloadLength = payload.Span.Length; Utility.Resize(ref columnsAndParameters, columnsAndParametersSize + payloadLength); payload.Span.CopyTo(columnsAndParameters.AsSpan(columnsAndParametersSize)); - parameters[i] = ColumnDefinitionPayload.Create(new(columnsAndParameters, columnsAndParametersSize, payloadLength)); + ColumnDefinitionPayload.Initialize(ref parameters[i], new(columnsAndParameters, columnsAndParametersSize, payloadLength)); columnsAndParametersSize += payloadLength; } if (!SupportsDeprecateEof) @@ -254,7 +254,7 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can var payloadLength = payload.Span.Length; Utility.Resize(ref columnsAndParameters, columnsAndParametersSize + payloadLength); payload.Span.CopyTo(columnsAndParameters.AsSpan(columnsAndParametersSize)); - columns[i] = ColumnDefinitionPayload.Create(new(columnsAndParameters, columnsAndParametersSize, payloadLength)); + ColumnDefinitionPayload.Initialize(ref columns[i], new(columnsAndParameters, columnsAndParametersSize, payloadLength)); columnsAndParametersSize += payloadLength; } if (!SupportsDeprecateEof) diff --git a/src/MySqlConnector/MySqlDataReader.cs b/src/MySqlConnector/MySqlDataReader.cs index e05db849e..cde036039 100644 --- a/src/MySqlConnector/MySqlDataReader.cs +++ b/src/MySqlConnector/MySqlDataReader.cs @@ -345,12 +345,16 @@ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int /// A containing metadata about the result set. public ReadOnlyCollection GetColumnSchema() { + var hasNoSchema = !m_resultSet.HasResultSet || m_resultSet.ContainsCommandParameters; + if (hasNoSchema) + return new ReadOnlyCollection(Array.Empty()); + var columnDefinitions = m_resultSet.ColumnDefinitions; - var hasNoSchema = columnDefinitions is null || m_resultSet.ContainsCommandParameters; - return hasNoSchema ? new List().AsReadOnly() : - columnDefinitions! - .Select((c, n) => (DbColumn) new MySqlDbColumn(n, c, Connection!.AllowZeroDateTime, GetResultSet().GetColumnType(n))) - .ToList().AsReadOnly(); + var resultSet = GetResultSet(); + var schema = new List(columnDefinitions.Length); + for (var n = 0; n < columnDefinitions.Length; n++) + schema.Add(new MySqlDbColumn(n, columnDefinitions[n], Connection!.AllowZeroDateTime, resultSet.GetColumnType(n))); + return schema.AsReadOnly(); } /// @@ -459,8 +463,6 @@ internal async Task InitAsync(CommandListPosition commandListPosition, ICommandP throw new InvalidOperationException("Expected m_hasMoreResults to be false"); if (m_resultSet.BufferState != ResultSetState.None || m_resultSet.State != ResultSetState.None) throw new InvalidOperationException("Expected BufferState and State to be ResultSetState.None."); - if (m_resultSet.ColumnDefinitions is not null) - throw new InvalidOperationException("Expected ColumnDefinitions to be null"); m_closed = false; m_hasWarnings = false; RealRecordsAffected = null; @@ -504,12 +506,11 @@ internal async Task InitAsync(CommandListPosition commandListPosition, ICommandP internal DataTable? BuildSchemaTable() { - var columnDefinitions = m_resultSet.ColumnDefinitions; - if (columnDefinitions is null || m_resultSet.ContainsCommandParameters) + if (!m_resultSet.HasResultSet || m_resultSet.ContainsCommandParameters) return null; var schemaTable = new DataTable("SchemaTable") { Locale = CultureInfo.InvariantCulture }; - schemaTable.MinimumCapacity = columnDefinitions.Length; + schemaTable.MinimumCapacity = m_resultSet.ColumnDefinitions.Length; var columnName = new DataColumn(SchemaTableColumn.ColumnName, typeof(string)); var ordinal = new DataColumn(SchemaTableColumn.ColumnOrdinal, typeof(int)); diff --git a/src/MySqlConnector/Protocol/Payloads/ColumnDefinitionPayload.cs b/src/MySqlConnector/Protocol/Payloads/ColumnDefinitionPayload.cs index 7673c854c..3bbc54a73 100644 --- a/src/MySqlConnector/Protocol/Payloads/ColumnDefinitionPayload.cs +++ b/src/MySqlConnector/Protocol/Payloads/ColumnDefinitionPayload.cs @@ -16,13 +16,13 @@ public string Name } } - public CharacterSet CharacterSet { get; } + public CharacterSet CharacterSet { get; private set; } - public uint ColumnLength { get; } + public uint ColumnLength { get; private set; } - public ColumnType ColumnType { get; } + public ColumnType ColumnType { get; private set; } - public ColumnFlags ColumnFlags { get; } + public ColumnFlags ColumnFlags { get; private set; } public string SchemaName { @@ -74,11 +74,18 @@ public string PhysicalName } } - public byte Decimals { get; } + public byte Decimals { get; private set; } - public static ColumnDefinitionPayload Create(ResizableArraySegment arraySegment) + public static void Initialize(ref ColumnDefinitionPayload payload, ResizableArraySegment arraySegment) { - var reader = new ByteArrayReader(arraySegment); + payload ??= new ColumnDefinitionPayload(); + payload.Initialize(arraySegment); + } + + private void Initialize(ResizableArraySegment originalData) + { + m_originalData = originalData; + var reader = new ByteArrayReader(originalData); SkipLengthEncodedByteString(ref reader); // catalog SkipLengthEncodedByteString(ref reader); // schema SkipLengthEncodedByteString(ref reader); // table @@ -86,15 +93,24 @@ public static ColumnDefinitionPayload Create(ResizableArraySegment arraySe SkipLengthEncodedByteString(ref reader); // name SkipLengthEncodedByteString(ref reader); // physical name reader.ReadByte(0x0C); // length of fixed-length fields, always 0x0C - var characterSet = (CharacterSet) reader.ReadUInt16(); - var columnLength = reader.ReadUInt32(); - var columnType = (ColumnType) reader.ReadByte(); - var columnFlags = (ColumnFlags) reader.ReadUInt16(); - var decimals = reader.ReadByte(); // 0x00 for integers and static strings, 0x1f for dynamic strings, double, float, 0x00 to 0x51 for decimals + CharacterSet = (CharacterSet) reader.ReadUInt16(); + ColumnLength = reader.ReadUInt32(); + ColumnType = (ColumnType) reader.ReadByte(); + ColumnFlags = (ColumnFlags) reader.ReadUInt16(); + Decimals = reader.ReadByte(); // 0x00 for integers and static strings, 0x1f for dynamic strings, double, float, 0x00 to 0x51 for decimals reader.ReadByte(0); // reserved byte 1 reader.ReadByte(0); // reserved byte 2 - return new ColumnDefinitionPayload(arraySegment, characterSet, columnLength, columnType, columnFlags, decimals); + if (m_readNames) + { + m_catalogName = null; + m_schemaName = null; + m_table = null; + m_physicalTable = null; + m_name = null; + m_physicalName = null; + m_readNames = false; + } } private static void SkipLengthEncodedByteString(ref ByteArrayReader reader) @@ -103,19 +119,13 @@ private static void SkipLengthEncodedByteString(ref ByteArrayReader reader) reader.Offset += length; } - private ColumnDefinitionPayload(ResizableArraySegment originalData, CharacterSet characterSet, uint columnLength, ColumnType columnType, ColumnFlags columnFlags, byte decimals) + private ColumnDefinitionPayload() { - OriginalData = originalData; - CharacterSet = characterSet; - ColumnLength = columnLength; - ColumnType = columnType; - ColumnFlags = columnFlags; - Decimals = decimals; } private void ReadNames() { - var reader = new ByteArrayReader(OriginalData); + var reader = new ByteArrayReader(m_originalData); m_catalogName = Encoding.UTF8.GetString(reader.ReadLengthEncodedByteString()); m_schemaName = Encoding.UTF8.GetString(reader.ReadLengthEncodedByteString()); m_table = Encoding.UTF8.GetString(reader.ReadLengthEncodedByteString()); @@ -125,8 +135,7 @@ private void ReadNames() m_readNames = true; } - private ResizableArraySegment OriginalData { get; } - + private ResizableArraySegment m_originalData; private bool m_readNames; private string? m_name; private string? m_schemaName;