-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Change timer from method variable to property of SqlConnectioninteral #26495
Conversation
- Change timer from method variable to property of SqlConnectioninteral. Resolves #26295
…n-Wernstrom/efcore into 26295_SqliteTimerAsField
@@ -98,6 +98,9 @@ public override string ConnectionString | |||
internal SqliteConnectionStringBuilder ConnectionOptions | |||
=> PoolGroup.ConnectionOptions; | |||
|
|||
internal Stopwatch? Timer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think making it non-nullable here makes a little bit more sense since we're not supposed to access it without an internal connection (plus, it saves some !
). I would have added Debug.Assert
here just to make sure that _innerConnection
is not null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented as per your suggestion.
var closeConnection = behavior.HasFlag(CommandBehavior.CloseConnection); | ||
|
||
var dataReader = new SqliteDataReader(this, timer, GetStatements(timer), closeConnection); | ||
var dataReader = new SqliteDataReader(this, _connection.Timer!, Statements, closeConnection); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we reuse a connection's timer here, we have to reset it just before we pass it to a SqliteDataReader
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the reset.
dataReader.NextResult(); | ||
|
||
return DataReader = dataReader; | ||
} | ||
|
||
private IEnumerable<sqlite3_stmt> GetStatements(Stopwatch timer) | ||
private IEnumerable<sqlite3_stmt> Statements |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Taking into account the sheer size of this method, I'm not sure we should change it into a property. There is also a potential optimization here (raised in #26494), which doesn't look that great with it being a property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted the conversion to a property.
Co-authored-by: Nikita Kazmin <vonzshik@gmail.com>
- Reset timer before passing into SqliteDataReader constructor. - Revert convertion of 'GetStatements' method to a property. - Make SqliteConnectionInternal.Timer public instead of internal. - Make SqliteConnection.Timer non-nullable.
- Remove unneccarry null forgivings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the wait, was a little bit busy with 6.0 release and other stuff. Looks, great, thank you!
Benchmarked the change. Before (main): BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 5 5600X, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=6.0.100
[Host] : .NET Core 6.0.0 (CoreCLR 6.0.21.52210, CoreFX 6.0.21.52210), X64 RyuJIT
DefaultJob : .NET Core 6.0.0 (CoreCLR 6.0.21.52210, CoreFX 6.0.21.52210), X64 RyuJIT
After (Dr-Ogden-Wernstrom:26295_SqliteTimerAsField): BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 5 5600X, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=6.0.100
[Host] : .NET Core 6.0.0 (CoreCLR 6.0.21.52210, CoreFX 6.0.21.52210), X64 RyuJIT
DefaultJob : .NET Core 6.0.0 (CoreCLR 6.0.21.52210, CoreFX 6.0.21.52210), X64 RyuJIT
Benchmark codepublic class Prepared
{
private readonly SqliteCommand command;
public Prepared()
{
var csb = new SqliteConnectionStringBuilder
{
DataSource = ":memory:",
};
var conn = new SqliteConnection(csb.ToString());
conn.Open();
command = conn.CreateCommand();
command.CommandText = "SELECT 1";
command.Prepare();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public object? SelectOne() => command.ExecuteScalar();
} |
…n-Wernstrom/efcore into 26295_SqliteTimerAsField
…n-Wernstrom/efcore into 26295_SqliteTimerAsField
…n-Wernstrom/efcore into 26295_SqliteTimerAsField
@Dr-Ogden-Wernstrom I'm afraid, we just have to wait for @bricelam to review: he's the one responsible for Sqlite from efcore team. |
Thanks @vonzshik. My first PR on GitHub so just wanted to make sure it wasn't sitting with me unawares. |
var closeConnection = behavior.HasFlag(CommandBehavior.CloseConnection); | ||
|
||
var dataReader = new SqliteDataReader(this, timer, GetStatements(timer), closeConnection); | ||
_connection.Timer.Reset(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem right to me. A connection can have multiple readers open at the same time. I'm pretty sure we need a new instance for every reader...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would using something like ValueStopwatch instead also help perf?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See dotnet/runtime#48570 for some discussion on ValueStopWatch (which was closed for some reason).
If we don't do ValueStopWatch, we can still "pool" a single StopWatch instance on the connection, so that uses which don't involve multiple open readers can benefit (but those with multiple readers would allocate).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, didn't know Sqlite supported MARS...
To me, using ValueStopwatch
seems to be the simplest solution (in a sense that there is no pooling involved, which does complicate things a bit), but pooling is also fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, ValueStopwatch
in a way it's implemented in ASP.NET Core wouldn't work here. The main offender is SqliteDataReader
:
efcore/src/Microsoft.Data.Sqlite.Core/SqliteDataReader.cs
Lines 157 to 173 in afe299f
_timer.Start(); | |
while (IsBusy(rc = sqlite3_step(stmt))) | |
{ | |
if (_command.CommandTimeout != 0 | |
&& _timer.ElapsedMilliseconds >= _command.CommandTimeout * 1000L) | |
{ | |
break; | |
} | |
sqlite3_reset(stmt); | |
// TODO: Consider having an async path that uses Task.Delay() | |
Thread.Sleep(150); | |
} | |
_timer.Stop(); |
Every time a NextResult
is called on a reader the timer isn't reset, but continues to measure the elapsed time from where it left on a previous call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't do ValueStopWatch, we can still "pool" a single StopWatch instance on the connection, so that uses which don't involve multiple open readers can benefit (but those with multiple readers would allocate).
@roji Could this be something as simple as allocating a new Stopwatch if the connection has multiple commands like my latest change?
Every time a
NextResult
is called on a reader the timer isn't reset, but continues to measure the elapsed time from where it left on a previous call.
@vonzshik My initial feeling is that adding stop/start functionality to ValueStopwatch would result in something almost the same as the standard Stopwatch implementation, so not much benefit. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vonzshik My initial feeling is that adding stop/start functionality to ValueStopwatch would result in something almost the same as the standard Stopwatch implementation, so not much benefit. What do you think?
Well, there is the fun fact that both SqliteCommand.PrepareAndEnumerateStatements
and SqliteDataReader.NextResult
are supposed to use the exact same instance of Stopwatch
and the result from SqliteCommand.PrepareAndEnumerateStatements
is passed as IEnumerable
to the reader...
var dataReader = new SqliteDataReader(this, timer, GetStatements(timer), closeConnection); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @vonzshik, that is a fun fact.
Sorry, I've been so slow getting to this, and for being a total noob on this project, but would you mind taking another look, please.
I'm going to run our SQLite benchmarks to see the impact of this change, then I'll discuss it with the team. |
|
@bricelam that's interesting, since I do see a small improvements in a simple benchmark BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1706 (21H2)
AMD Ryzen 5 5600X, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.100-preview.4.22252.9
[Host] : .NET 7.0.0 (7.0.22.22904), X64 RyuJIT
DefaultJob : .NET 7.0.0 (7.0.22.22904), X64 RyuJIT
From
To
Codepublic class Prepared
{
private readonly SqliteCommand command;
public Prepared()
{
var csb = new SqliteConnectionStringBuilder
{
DataSource = ":memory:",
};
var conn = new SqliteConnection(csb.ToString());
conn.Open();
command = conn.CreateCommand();
command.CommandText = "SELECT 1";
command.Prepare();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public object? SelectOne() => command.ExecuteScalar();
} Though, the main point here was to reduce allocations, which it does. To improve the rps, I was thinking on rewriting SqliteCommand.GetStatements to something like this: private IEnumerable<sqlite3_stmt> GetStatements(Stopwatch timer)
{
if (_prepared)
{
for (var statementIndex = 0; statementIndex < _preparedStatements.Count; statementIndex++)
{
(var stmt, var expectedParams) = _preparedStatements[statementIndex];
yield return GetStatementsCore(stmt, expectedParams, _parameters, timer);
}
}
else
{
foreach ((var stmt, var expectedParams) in PrepareAndEnumerateStatements(timer))
{
yield return GetStatementsCore(stmt, expectedParams, _parameters, timer);
}
}
static sqlite3_stmt GetStatementsCore(sqlite3_stmt stmt, int expectedParams, SqliteParameterCollection? parameters, Stopwatch timer)
{
var boundParams = parameters?.Bind(stmt) ?? 0;
if (expectedParams != boundParams)
{
var unboundParams = new List<string>();
for (var i = 1; i <= expectedParams; i++)
{
var name = sqlite3_bind_parameter_name(stmt, i).utf8_to_string();
if (parameters != null
&& !parameters.Cast<SqliteParameter>().Any(p => p.ParameterName == name))
{
unboundParams.Add(name);
}
}
if (sqlite3_libversion_number() < 3028000 || sqlite3_stmt_isexplain(stmt) == 0)
{
throw new InvalidOperationException(Resources.MissingParameters(string.Join(", ", unboundParams)));
}
}
return stmt;
}
} Which gives a nice rps bump, while also reducing allocations:
I assume you were running Orchard benchmark? I'll try to take a look in the next few days. |
I'm using the TechEmpower Fortunes benchmark (technically not valid for TechEmpower since the database doesn't run on a separate server). crank --scenario fortunes --profile sqlite --config https://raw.githubusercontent.com/aspnet/Benchmarks/main/scenarios/database.benchmarks.yml It's also worth noting that we haven't done any performance work on this driver. Instead, I wanted to tackle the bigger architectural wins like #14044 first. I also want to re-implement the connection pool. I used the generic ADO.NET code as a starting point, but in hindsight, that was a mistake. |
@vonzshik We really appreciate your work on this, but we discussed this as a team, and given the code is a little more complex and the performance results are not compelling, we have decided not to merge this. |
Issue but not a bug nor a feature. Does this need a test?