Skip to content
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

Closed
wants to merge 17 commits into from
Closed

Change timer from method variable to property of SqlConnectioninteral #26495

wants to merge 17 commits into from

Conversation

ghost
Copy link

@ghost ghost commented Nov 1, 2021

  • I've read the guidelines for contributing and seen the walkthrough
  • I've posted a comment on an issue with a detailed description of how I am planning to contribute and got approval from a member of the team
  • The code builds and tests pass locally (also verified by our automated build checks)
  • Commit messages follow this format:
        Summary of the changes
        - Detail 1
        - Detail 2

        Fixes #bugnumber
  • Tests for the changes have been added (for bug fixes / features)
    Issue but not a bug nor a feature. Does this need a test?
  • Code follows the same patterns and style as existing code in this repo

@dnfadmin
Copy link

dnfadmin commented Nov 1, 2021

CLA assistant check
All CLA requirements met.

src/Microsoft.Data.Sqlite.Core/SqliteConnectionInternal.cs Outdated Show resolved Hide resolved
@@ -98,6 +98,9 @@ public override string ConnectionString
internal SqliteConnectionStringBuilder ConnectionOptions
=> PoolGroup.ConnectionOptions;

internal Stopwatch? Timer
Copy link
Contributor

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.

Copy link
Author

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);
Copy link
Contributor

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.

Copy link
Author

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
Copy link
Contributor

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.

Copy link
Author

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.

Dr-Ogden-Wernstrom and others added 4 commits November 1, 2021 16:07
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.
Copy link
Contributor

@vonzshik vonzshik left a 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!

@vonzshik
Copy link
Contributor

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

Method Mean Error StdDev Op/s Gen 0 Gen 1 Gen 2 Allocated
SelectOne 313.8 ns 1.00 ns 0.89 ns 3,187,115.3 0.0186 - - 312 B

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

Method Mean Error StdDev Op/s Gen 0 Gen 1 Gen 2 Allocated
SelectOne 299.0 ns 1.73 ns 1.44 ns 3,344,267.4 0.0153 - - 256 B
Benchmark code
public 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();
}

@ghost ghost closed this Nov 20, 2021
@ghost ghost reopened this Nov 20, 2021
@ghost
Copy link
Author

ghost commented Dec 2, 2021

@vonzshik @bricelam Is there anything I need to do to get the PR completed?

@vonzshik
Copy link
Contributor

vonzshik commented Dec 3, 2021

@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.

@ghost
Copy link
Author

ghost commented Dec 3, 2021

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();
Copy link
Contributor

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...

Copy link
Contributor

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?

Copy link
Member

@roji roji Jan 26, 2022

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).

Copy link
Contributor

@vonzshik vonzshik Jan 26, 2022

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.

Copy link
Contributor

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:

_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.

Copy link
Author

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?

Copy link
Contributor

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);

Copy link
Author

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.

@bricelam
Copy link
Contributor

bricelam commented Jun 7, 2022

I'm going to run our SQLite benchmarks to see the impact of this change, then I'll discuss it with the team.

@bricelam
Copy link
Contributor

bricelam commented Jun 7, 2022

  Before After
CPU Usage (%) 54 57
Working Set (MB) 394 396
Requests/sec 29,055 28,704
Mean latency (ms) 8.83 8.92

@vonzshik
Copy link
Contributor

vonzshik commented Jun 8, 2022

@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

Method Mean Error StdDev Op/s Gen 0 Allocated
SelectOne 277.5 ns 0.70 ns 0.59 ns 3,603,227.7 0.0186 312 B

To

Method Mean Error StdDev Op/s Gen 0 Allocated
SelectOne 274.8 ns 0.86 ns 0.81 ns 3,639,623.7 0.0162 272 B
Code
public 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:

Method Mean Error StdDev Op/s Gen 0 Allocated
SelectOne 256.9 ns 0.89 ns 0.84 ns 3,893,317.3 0.0138 232 B

Before After
CPU Usage (%) 54 57
Working Set (MB) 394 396
Requests/sec 29,055 28,704
Mean latency (ms) 8.83 8.92

I assume you were running Orchard benchmark? I'll try to take a look in the next few days.

@bricelam
Copy link
Contributor

bricelam commented Jun 8, 2022

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.

@ajcvickers
Copy link
Member

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants