Open dragokas opened 3 years ago
I can guarantee this is due to a silly mechanism in SourceMod. You're allowed to close the handle while the query is in-flight. To make this work, we duplicate the handle while it's in the thread queue. So effectively here you have 50,000 handles.
We should check to see if there are any compatible handles in the thread queue and re-use them before duplicating.
The actual issue here is some artificial delay that was added because there were people with bad disks. Specifically, on Linux, SQLite was so effective at increasing the disk utilisation that people were unable to hit the 15ms budget before the next frame. This became more apparent after the (much improved) threading work was added for queries. This work removed the previous even longer delay, which would cause clientprefs and other plugins to not get data on level change for minutes. Additionally, with this usecase, it wasn't even possible until the handle limit was moved from 16k to 64k which was done for TF2Items.ext.
While it isn't used often, I believe in this case you should actually be able to specify multiple values for your REPLACE (I believe this query can be much more effecient outside of this) which will greatly improve the through of the driver in general, our database system, and finally your plugin.
https://stackoverflow.com/a/5009740
You might be limited to 500 values in a single statement, or 1000000 bytes, but that could give you atleast a 500x performance improvement from a SM point of view.
https://stackoverflow.com/a/5009740
You might be limited to 500 values in a single statement, or 1000000 bytes, but that could give you atleast a 500x performance improvement from a SM point of view.
I can also suggest using Transactions so that SM doesn't clone the Database handles many times.
I can also suggest using Transactions so that SM doesn't clone the Database handles many times.
@Wend4r, thanks. At first glance it looks like also the leak, when get called with 50000 q/per single transaction.
L 06/21/2021 - 20:06:50: Type IQuery | Count 31777
L 06/21/2021 - 20:06:50: Type IQuery | Count 31777
L 06/21/2021 - 20:06:50: Type IDatabase | Count 2
L 06/21/2021 - 20:06:50: Type IDatabase | Count 2
However, if we decrease N of queries per transaction to be ~ <= 31770 (even if total N of queries per same 1 frame become > 50000), no errors happen, and no leaks displayed in handles dump. So, with > 32000 q/per transaction, that's much like an overflow.
Here is a good working sample (for history):
public Action Timer_Transaction(Handle timer)
{
const int tr_count = 5;
const int chunks = 10000; // should be < 31000
char key[] = "key";
char value[] = "value";
char query[2*(sizeof(key)+sizeof(value)+100)+1];
g_hDB.Format(query, sizeof(query), "REPLACE INTO `" ... MY_TABLE ... "` (lang, phrase, translation) \
VALUES (%i, '%s', '%s');", 0, key, value);
Transaction tx;
for( int j = 0; j < tr_count; j ++ )
{
tx = new Transaction();
for( int i = 0; i < chunks; i++ )
{
tx.AddQuery(query);
}
g_hDB.Execute(tx, SQL_Tx_Success, SQL_Tx_Failure);
}
PrintToServer("total queries: %i", tr_count * chunks);
return Plugin_Stop;
}
public void SQL_Tx_Success(Database db, any data, int numQueries, DBResultSet[] results, any[] queryData)
{
PrintToServer("TX Success");
}
public void SQL_Tx_Failure (Database db, any data, int numQueries, const char[] error, int failIndex, any[] queryData)
{
PrintToServer("TX Failed");
LogError(error);
}
@KyleSanderson, thanks for your suggestions and your time.
Surely you didn't know my real use case, which is:
You might be limited to 500 values in a single statement, or 1000000 bytes, but that could give you atleast a 500x performance improvement from a SM point of view.
So, that is also a possible solution, but in a cost of:
And I still need to know - what is a MAX number of simultaneous queries per frame are allowed without handles leaking. (because even those long query is not enough to fit my needs with one sigle call).
All at all, the above method was very useful for my other project. So, thanks again!
hmmm, looks like
for( int i = 0; i < chunks; i++ )
{
tx.AddQuery(query);
}
also required additional dynamic memory, but I've no idea how to calculate it properly. Sorry for off-topic.
UPD. Well, it seems if I split transactions by much little chunks, like:
const int tr_count = 25;
const int chunks = 2000;
@KyleSanderson the frame budget has no relation to this bug at all. As mentioned, the bug is handle duplication.
https://cs.alliedmods.net/sourcemod/source/core/logic/smn_database.cpp#203
This is the culprit, but I don't have a quick fix. The handle needs to be shared or re-used in some way rather than re-created for every op.
https://cs.alliedmods.net/sourcemod/source/core/logic/smn_database.cpp#203
This is the culprit, but I don't have a quick fix. The handle needs to be shared or re-used in some way rather than re-created for every op. @KyleSanderson the frame budget has no relation to this bug at all. As mentioned, the bug is handle duplication.
Not true (unless if we're leaking this - I don't think we are). Each query takes 2x handles as indicated (so yes, there's this issue). The actual issue is the artificial frame delay at 20ms which coupled with an unknown DBI exec time doesn't give a definite amount of queries per frame. A 66 tick server can only run a frame within 15ms, so at 20ms it's guaranteed to skip a frame at the very least. Even if we change this it's moving the goal post again (which before was 8k, now it's 32k because they're blowing the (already too high) 64k budget).
Surely you didn't know my real use case, which is: reading and parsing huge "resource" file(s) line by line saving key-value of each line into DB.
OK so don't query per line...
So, that is also a possible solution, but in a cost of: additional memory more CPU time consuming (due to my use case) to construct such a long string for query, e.g. with StrCat() the requirement to increase pragma dynamic for declaring large char[], which is not desirable since I'm coding inc-file, and those limit can be accidentally re-defined in the main sp by other devs.
Also not true. Remember you can pass an index into any String just like C. This skips all reformatting, recopying, and is a direct write to memory. if (sizeof (sKeyBuffer) - iLength < strlen(elements))
submit your query, if it isn't then keep adding Values to the query.
iLength = FormatEx(sKeyBuffer, sizeof(sKeyBuffer), "Target: %N\n", iTarget);
if(iButtons & IN_FORWARD)
iLength += strcopy(sKeyBuffer[iLength], (sizeof(sKeyBuffer) - iLength), " W ");
else
iLength += strcopy(sKeyBuffer[iLength], (sizeof(sKeyBuffer) - iLength), " - ");
Anyway, there's other core things that can be done to improve this, but ultimately this is a script error and there are massive performance improvements that can be done by writing much more efficient code.
Also not true. Remember you can pass an index into any String just like C.
Well, you better know. Of course, if that cost an effort, if you mean by that having noticeably more performance in the second thread by the cost of wasting minor operations with memory & CPU time in the main thread which much more significant. Thanks for code. I thought StrCat doing the same. I'll take a note that is not.
Kyle the frame timing is completely unrelated. Please understand this. It has nothing to do with this bug.
Help us help you
Environment
Description
I do like to make 50000 single queries to a local DB, but I'm receiving handles leak. What am I doing wrong?
According to docs1 & docs2, cloned handles are automatically closed after callback of threaded operation is executed.
Problematic Code (or Steps to Reproduce)
Logs