Skip to content

Commit 8ee7128

Browse files
authored
Merge pull request #245 from bgrainger/performance
Improve performance by reducing allocations. One benchmark scenario now allocates 1/10 as much, and runs almost 2x faster.
2 parents 138f436 + 409b060 commit 8ee7128

File tree

13 files changed

+243
-180
lines changed

13 files changed

+243
-180
lines changed

src/MySqlConnector/ByteArrayReader.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace MySql.Data
44
{
5-
internal sealed class ByteArrayReader
5+
internal struct ByteArrayReader
66
{
77
public ByteArrayReader(byte[] buffer, int offset, int length)
88
{
@@ -118,6 +118,8 @@ public ulong ReadLengthEncodedInteger()
118118
byte encodedLength = m_buffer[m_offset++];
119119
switch (encodedLength)
120120
{
121+
case 0xFB:
122+
throw new FormatException("Length-encoded integer cannot have 0xFB prefix byte.");
121123
case 0xFC:
122124
return ReadFixedLengthUInt32(2);
123125
case 0xFD:
@@ -131,6 +133,17 @@ public ulong ReadLengthEncodedInteger()
131133
}
132134
}
133135

136+
public int ReadLengthEncodedIntegerOrNull()
137+
{
138+
if (m_buffer[m_offset] == 0xFB)
139+
{
140+
// "NULL is sent as 0xfb" (https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-ProtocolText::ResultsetRow)
141+
m_offset++;
142+
return -1;
143+
}
144+
return checked((int) ReadLengthEncodedInteger());
145+
}
146+
134147
public ArraySegment<byte> ReadLengthEncodedByteString()
135148
{
136149
var length = checked((int) ReadLengthEncodedInteger());

src/MySqlConnector/MySqlClient/CommandExecutors/TextCommandExecutor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public virtual async Task<DbDataReader> ExecuteReaderAsync(string commandText, M
5151
CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)
5252
{
5353
cancellationToken.ThrowIfCancellationRequested();
54-
using (cancellationToken.Register(m_command.Cancel))
54+
using (m_command.RegisterCancel(cancellationToken))
5555
{
5656
m_command.Connection.Session.StartQuerying(m_command);
5757
m_command.LastInsertedId = -1;

src/MySqlConnector/MySqlClient/MySqlCommand.cs

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -114,30 +114,23 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) =>
114114
public override Task<int> ExecuteNonQueryAsync(CancellationToken cancellationToken) =>
115115
ExecuteNonQueryAsync(Connection.AsyncIOBehavior, cancellationToken);
116116

117-
internal async Task<int> ExecuteNonQueryAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
118-
{
119-
VerifyValid();
120-
return await m_commandExecutor.ExecuteNonQueryAsync(CommandText, m_parameterCollection, ioBehavior, cancellationToken).ConfigureAwait(false);
121-
}
117+
internal Task<int> ExecuteNonQueryAsync(IOBehavior ioBehavior, CancellationToken cancellationToken) =>
118+
!IsValid(out var exception) ? Utility.TaskFromException<int>(exception) :
119+
m_commandExecutor.ExecuteNonQueryAsync(CommandText, m_parameterCollection, ioBehavior, cancellationToken);
122120

123121
public override Task<object> ExecuteScalarAsync(CancellationToken cancellationToken) =>
124122
ExecuteScalarAsync(Connection.AsyncIOBehavior, cancellationToken);
125123

126-
internal async Task<object> ExecuteScalarAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
127-
{
128-
VerifyValid();
129-
return await m_commandExecutor.ExecuteScalarAsync(CommandText, m_parameterCollection, ioBehavior, cancellationToken).ConfigureAwait(false);
130-
}
124+
internal Task<object> ExecuteScalarAsync(IOBehavior ioBehavior, CancellationToken cancellationToken) =>
125+
!IsValid(out var exception) ? Utility.TaskFromException<object>(exception) :
126+
m_commandExecutor.ExecuteScalarAsync(CommandText, m_parameterCollection, ioBehavior, cancellationToken);
131127

132128
protected override Task<DbDataReader> ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) =>
133129
ExecuteReaderAsync(behavior, Connection.AsyncIOBehavior, cancellationToken);
134130

135-
internal async Task<DbDataReader> ExecuteReaderAsync(CommandBehavior behavior, IOBehavior ioBehavior,
136-
CancellationToken cancellationToken)
137-
{
138-
VerifyValid();
139-
return await m_commandExecutor.ExecuteReaderAsync(CommandText, m_parameterCollection, behavior, ioBehavior, cancellationToken).ConfigureAwait(false);
140-
}
131+
internal Task<DbDataReader> ExecuteReaderAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken) =>
132+
!IsValid(out var exception) ? Utility.TaskFromException<DbDataReader>(exception) :
133+
m_commandExecutor.ExecuteReaderAsync(CommandText, m_parameterCollection, behavior, ioBehavior, cancellationToken);
141134

142135
protected override void Dispose(bool disposing)
143136
{
@@ -156,6 +149,23 @@ protected override void Dispose(bool disposing)
156149

157150
internal new MySqlConnection Connection => (MySqlConnection) DbConnection;
158151

152+
/// <summary>
153+
/// Registers <see cref="Cancel"/> as a callback with <paramref name="token"/> if cancellation is supported.
154+
/// </summary>
155+
/// <param name="token">The <see cref="CancellationToken"/>.</param>
156+
/// <returns>An object that must be disposed to revoke the cancellation registration.</returns>
157+
/// <remarks>This method is more efficient than calling <code>token.Register(Command.Cancel)</code> because it avoids
158+
/// unnecessary allocations.</remarks>
159+
internal IDisposable RegisterCancel(CancellationToken token)
160+
{
161+
if (!token.CanBeCanceled)
162+
return null;
163+
164+
if (m_cancelAction == null)
165+
m_cancelAction = Cancel;
166+
return token.Register(m_cancelAction);
167+
}
168+
159169
private void VerifyNotDisposed()
160170
{
161171
if (m_parameterCollection == null)
@@ -164,21 +174,32 @@ private void VerifyNotDisposed()
164174

165175
private void VerifyValid()
166176
{
167-
VerifyNotDisposed();
168-
if (DbConnection == null)
169-
throw new InvalidOperationException("Connection property must be non-null.");
170-
if (DbConnection.State != ConnectionState.Open && DbConnection.State != ConnectionState.Connecting)
171-
throw new InvalidOperationException("Connection must be Open; current state is {0}".FormatInvariant(DbConnection.State));
172-
if (DbTransaction != Connection.CurrentTransaction)
173-
throw new InvalidOperationException("The transaction associated with this command is not the connection's active transaction.");
174-
if (string.IsNullOrWhiteSpace(CommandText))
175-
throw new InvalidOperationException("CommandText must be specified");
177+
Exception exception;
178+
if (!IsValid(out exception))
179+
throw exception;
180+
}
181+
182+
private bool IsValid(out Exception exception)
183+
{
184+
exception = null;
185+
if (m_parameterCollection == null)
186+
exception = new ObjectDisposedException(GetType().Name);
187+
else if (DbConnection == null)
188+
exception = new InvalidOperationException("Connection property must be non-null.");
189+
else if (DbConnection.State != ConnectionState.Open && DbConnection.State != ConnectionState.Connecting)
190+
exception = new InvalidOperationException("Connection must be Open; current state is {0}".FormatInvariant(DbConnection.State));
191+
else if (DbTransaction != Connection.CurrentTransaction)
192+
exception = new InvalidOperationException("The transaction associated with this command is not the connection's active transaction.");
193+
else if (string.IsNullOrWhiteSpace(CommandText))
194+
exception = new InvalidOperationException("CommandText must be specified");
195+
return exception == null;
176196
}
177197

178198
internal void ReaderClosed() => (m_commandExecutor as StoredProcedureCommandExecutor)?.SetParams();
179199

180200
MySqlParameterCollection m_parameterCollection;
181201
CommandType m_commandType;
182202
ICommandExecutor m_commandExecutor;
203+
Action m_cancelAction;
183204
}
184205
}

src/MySqlConnector/MySqlClient/MySqlDataReader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ private ValueTask<ResultSet> ScanResultSetAsync(IOBehavior ioBehavior, ResultSet
108108

109109
private async Task<ResultSet> ScanResultSetAsyncAwaited(IOBehavior ioBehavior, ResultSet resultSet, CancellationToken cancellationToken)
110110
{
111-
using (cancellationToken.Register(Command.Cancel))
111+
using (Command.RegisterCancel(cancellationToken))
112112
{
113113
try
114114
{

src/MySqlConnector/MySqlClient/MySqlStatementPreparer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public MySqlStatementPreparer(string commandText, MySqlParameterCollection param
1717

1818
public ArraySegment<byte> ParseAndBindParameters()
1919
{
20-
using (var stream = new MemoryStream(m_commandText.Length))
20+
using (var stream = new MemoryStream(m_commandText.Length + 1))
2121
using (var writer = new BinaryWriter(stream, Encoding.UTF8))
2222
{
2323
writer.Write((byte) CommandKind.Query);

src/MySqlConnector/MySqlClient/Results/ResultSet.cs

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ private ValueTask<Row> ScanRowAsync(IOBehavior ioBehavior, Row row, Cancellation
162162
if (BufferState == ResultSetState.HasMoreData || BufferState == ResultSetState.NoMoreData || BufferState == ResultSetState.None)
163163
return new ValueTask<Row>((Row)null);
164164

165-
using (cancellationToken.Register(Command.Cancel))
165+
using (Command.RegisterCancel(cancellationToken))
166166
{
167167
var payloadValueTask = Session.ReceiveReplyAsync(ioBehavior, CancellationToken.None);
168168
return payloadValueTask.IsCompletedSuccessfully
@@ -197,7 +197,7 @@ Row ScanRowAsyncRemainder(PayloadData payload)
197197
var reader = new ByteArrayReader(payload.ArraySegment);
198198
for (var column = 0; column < m_dataOffsets.Length; column++)
199199
{
200-
var length = checked((int) ReadFieldLength(reader));
200+
var length = reader.ReadLengthEncodedIntegerOrNull();
201201
m_dataLengths[column] = length == -1 ? 0 : length;
202202
m_dataOffsets[column] = length == -1 ? -1 : reader.Offset;
203203
reader.Offset += m_dataLengths[column];
@@ -211,24 +211,6 @@ Row ScanRowAsyncRemainder(PayloadData payload)
211211
}
212212
}
213213

214-
private static long ReadFieldLength(ByteArrayReader reader)
215-
{
216-
var leadByte = reader.ReadByte();
217-
switch (leadByte)
218-
{
219-
case 0xFB:
220-
return -1;
221-
case 0xFC:
222-
return reader.ReadFixedLengthUInt32(2);
223-
case 0xFD:
224-
return reader.ReadFixedLengthUInt32(3);
225-
case 0xFE:
226-
return checked((long) reader.ReadFixedLengthUInt64(8));
227-
default:
228-
return leadByte;
229-
}
230-
}
231-
232214
public string GetName(int ordinal)
233215
{
234216
if (ColumnDefinitions == null)

src/MySqlConnector/Protocol/Serialization/CompressedPayloadHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public IByteHandler ByteHandler
3131
}
3232

3333
public ValueTask<ArraySegment<byte>> ReadPayloadAsync(ProtocolErrorBehavior protocolErrorBehavior, IOBehavior ioBehavior) =>
34-
ProtocolUtility.ReadPayloadAsync(m_bufferedByteReader, new CompressedByteHandler(this, protocolErrorBehavior), () => default(int?), default(ArraySegment<byte>), protocolErrorBehavior, ioBehavior);
34+
ProtocolUtility.ReadPayloadAsync(m_bufferedByteReader, new CompressedByteHandler(this, protocolErrorBehavior), () => -1, default(ArraySegment<byte>), protocolErrorBehavior, ioBehavior);
3535

3636
public ValueTask<int> WritePayloadAsync(ArraySegment<byte> payload, IOBehavior ioBehavior)
3737
{

0 commit comments

Comments
 (0)