perf(db): disable SQLite memory accounting#1988
Conversation
There was a problem hiding this comment.
Pull request overview
This PR aims to improve SQLite-heavy read performance (notably GetAccount) by disabling SQLite’s memory allocation statistics, which otherwise introduce a global mutex and significant contention under concurrency.
Changes:
- Add a one-time SQLite global configuration initializer.
- Disable SQLite memory accounting via
sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0). - Wire the initializer into
Db::newand add thelibsqlite3-sysdependency.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
crates/db/src/lib.rs |
Calls the new SQLite global initializer during DB pool creation. |
crates/db/src/init.rs |
Implements Once-guarded SQLite global configuration and disables MEMSTATUS. |
crates/db/Cargo.toml |
Adds libsqlite3-sys for calling sqlite3_config. |
Cargo.lock |
Locks the new direct dependency for the db crate. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
iiuc, the core bench results can be summarized by the for a total runtime change from That's pretty nuts.. |
Mirko-von-Leipzig
left a comment
There was a problem hiding this comment.
I think this looks correct, though the stress-test failure makes me wonder why the Once is being set twice? Or why is the assert tripping..
Some places are creating SQLiteConnections directly so we have to explicitly call the initialization function there.
Co-authored-by: Mirko <48352201+Mirko-von-Leipzig@users.noreply.github.com>
We're apparently using "naked" I've now added an explicit call to the config init function to bootstrap. (BTW: is there a good reason we can't use |
No, in general we need a better database interface. |
Mirko-von-Leipzig
left a comment
There was a problem hiding this comment.
Should we also add in the additional compile-time flag where appropriate? Or separate PR?
I'd rather have that as a separate PR. |
|
Hmm, wondering if we should just disable memory accounting compile time instead of this runtime setup. The advantage would be that we don't need code changes at all. The disadvantage is that it's easy to miss setting the environment variable and then you'll get bad performance... Maybe we should just add a runtime check and print a warning on startup? |
Something we could inject in a |
That's too late. This env var has to be present when the I'm now quite convinced that this is what we should be doing... This matters only for our release builds after all, so why complicate the code with ugly initializers? |
In theory we could also wrap all these crates into one, with a And then re-exports the relevant things.. sounds invasive though |
|
I've removed all code changes from this PR and have just added setting the (Please note that the Dockerfile was actually broken: the node wouldn't even start up in that image because of the mismatching runner base image.) The results are the same, and we're not littering our code with these settings. |
We may have additional places where this is required. Could you check the debian workflows? We also need to inform our infra runners who currently have their own docker images. |
|
Applying these build options on my local 0.13 node and re-running the K6 That is, gRPC request median duration is down to 42.58ms, total runtime is 1:26.5. |
|
As @Mirko-von-Leipzig pointed out, we can just add the environment variable to |
|
With this change applied: This is an extreme edge case though due to the SQLite queries involved. |
While doing some
GetAccountbenchmarking on the 0.13 node implementation with a snapshot of the testnet database I've noticed that a a significant amount of CPU cycles are spent on mutex contention:Lock contention profiling results are showing that SQLite memory allocation (
sqlite3Malloc) is serialized using a global mutex: futex_syscall_profile_short.txtAs described in #1966 some of our SQLite queries are not cacheable, leading to the queries being re-parsed by SQLite on every invocation. Making sure we're using cached prepared statements as much as we can would be the best, but unfortunately that's not trivial.
A short-term improvement is making sure SQLite memory allocation is not using a global mutex. It turns out that the mutex is required because SQLite by default maintains some statistics about allocations. Disabling that in the runtime configuration leads to a measurable improvement in
GetAccountperformance (which on 0.13 is using a query that's not cacheable).Baseline performance measured with a local 0.13 node using a Grafana K6 script doing 1M
GetAccountqueries (using random account IDs that are in the DB snapshot), with 500 concurrent virtual users:With SQLite memory accounting disabled: