-
-
Notifications
You must be signed in to change notification settings - Fork 4.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
Performance of WebDAV search requests with many shared files #37061
Conversation
6291a67
to
1cadbaf
Compare
@@ -127,9 +161,7 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array | |||
)); | |||
} | |||
|
|||
$storageFilters = array_values(array_map(function (ICache $cache) { | |||
return $cache->getQueryFilterForStorage(); |
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.
From what I can see this was using path and not path hash, did I miss something? Where was the md5 calculation before?
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 is indeed tricky to follow.
SearchComparison
objects are created with thepath
comparison here:server/apps/files_sharing/lib/Cache.php
Line 202 in 1cadbaf
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', $this->getGetUnjailedRoot()), - Once the whole filter part of the query is constructed, it is passed to the
searchOperatorToDBExpr
method of theSearchBuilder
here:$searchExpr = $this->searchBuilder->searchOperatorToDBExpr($builder, $filter); - Inside the
SearchBuilder
the query tree is analysed and theSearchComparison
object is converted to a set of values to create an SQL operator here:private function getOperatorFieldAndValue(ISearchComparison $operator) { - The
getOperatorFieldAndValue
method has a special case for thepath
field that in some cases converts it topath_hash = md5(path)
:$field = 'path_hash';
Sometimes the path
comparison is not converted into path_hash
. If I understand correctly the hint used above can be set by the query optimizer invoked from:
$this->queryOptimizer->processOperator($filter); |
The actual setting of the hint is here:
if ($a instanceof ISearchComparison && $b instanceof ISearchComparison && $a->getField() === 'path' && $b->getField() === 'path') { |
If I understand all of this correctly, for single file shares the path_hash
comparison was always used before my changes.
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.
Then I would feel a lot better if someone who actually understand DB queries perf can review this. I do not have enough knowledge.
We have to be sure your changes does not slow down some other usecases. And it’s complicated to test because some of the optimisations we have are database-specific I think.
Thoughts:
|
Seems doable - but feels more complex and likely more difficult to analyse. On the other hand, I am now wondering if the
Certainly possible. Should I go ahead and split this into a separate PR? |
@starypatyk if you need someone to do some testing, just let me know. I've a lot of shares and search is really slow (takes longer than a minute). |
1cadbaf
to
7c958d1
Compare
ISearchBinaryOperator::OPERATOR_AND, | ||
[ | ||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $storage), | ||
new SearchComparison(ISearchComparison::COMPARE_IN, 'path_hash', array_map(fn ($path) => md5($path), $paths)) |
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.
Be careful with "IN" it'll break i.e. on Oracle for 1000+ items.
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.
Indeed. Would it make sense to provide a workaround at a lower level?
Rewrite the query with a sequence of ((x IN (list1-1000)) or (x IN (list1001-2000)) ...)
e.g. here:
public function in($x, $y, $type = null): string { |
I thought this might be resolved already by Doctrine DBAL, but apparently a similar workaround has been rejected. 😞
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 can't tell since I am not an expert on php/doctrine, but @nickvergessen @juliushaertl or @icewind1991 might know.
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.
We are using (IN(1-1000) OR IN(1001-2000))
in mail: https://github.com/nextcloud/mail/blob/0da32c97f64e0cdf950368257447ec8fa6fe7fea/lib/Db/MessageMapper.php#L986-L992.
Not a big fan, but there is no other way if you want to work with all IDs in one result.
Would be great to know if these changes improve performance in other cases. 👍 Would you be able to pick up my changes directly from GitHub? I have rebased this PR today - it should be easier to apply it on a recent Nextcloud version. |
I've applied this pull to my testing instance (NC 27.0.0). I've added 1,000 shares via talk to a test account (share_NNNN.txt). Search for filename 'test' (I hope that's the right way to do this, I never used webdav search so far ;) ):
Although, I've applied the patch, the query log still includes all those blocks: Full log: @starypatyk, anything special which must be done after applying this pull? |
Hi @XueSheng-GIT, The patch should "just work" - no special configuration required. For the next two weeks I will not be able to diagnose this. I should have time in early July. |
Hi @XueSheng-GIT, Your WebDAV search query looks correct. I have tried it on my dev instance and it behaves as expected - i.e. the sequence of individual conditions like:
gets replaced by a much shorter list of:
I do not understand yet what may be the reason this does not work for you. I have committed additional diagnostic code that might help me diagnose the issue. Would you be able to pick the newest version of code from this PR and re-run your tests? Please make sure to set logging level to DEBUG as described in the documentation and provide relevant fragment of the nextcloud.log file. There should be many lines similar to this:
|
@starypatyk thanks for following up. I've picked up the newest version. Loglevel was already set to DEBUG on my test instance.
Do you need a more complete log? I don't want to paste it here for privacy reasons. |
Hi @XueSheng-GIT, Thank you for the log line. It does help much 👍 A key difference is here: My environment:
Your environment:
I suspect that you use the File access control app and my code is not prepared to handle this case. I need to rethink the approach. |
I've disabled File access control to double check the results. nextcloud log now contains:
Webdav search used:
pgsql query log: That's a major increase in search speed. 31s reduced to 1s!!! |
Hi @XueSheng-GIT,
Great to know that this change can help for your case! Thanks 👍 I am now considering a different approach that should also work with the File access control plugin. BTW - The queries in your logs seem to suggest that you use PostgreSQL as the database backend. Could you please confirm/deny? |
Yes, I'm using postgreSQL. |
Hi @XueSheng-GIT, I have implemented a slightly different approach. The code should now work also when the File access control plugin is active. Would you be able to get the latest changes and verify? |
After applying the latest changes, webdav search fails (with and without File access control enabled). Reverting to the previous patch makes it work again. Here's my search and the error message.
Error message (response within the console):
nextcloud.log
This test was done on NC 27.0.0. |
Hi @XueSheng-GIT, Sorry, this was my mistake. 😞 I hope that I have corrected it in my last commit. If possible, please try again. |
Thanks! A quick test was succesful with the updated pull and as fast as the previous approach. Tested with and without File access control. |
Here are the logs using the latest pull on NC 27.0.0 with File access control enabled (same webdav search as before #37061 (comment)). pgsql query log: nextcloud log: Search speed is still amazing (same as with previous version of the pull). I was not able to see a difference with/without File access control enabled. I've also applied this patch to one of my production systems (NC 26.0.3). At least initial testing went fine so far. @starypatyk, thanks again for your effort! Hope that this will find its way to a stable release. EDIT: |
Hi @XueSheng-GIT, Thanks for your testing and for the logs. 👍 Good to know that my changes help. I now need to do some clean-up before submitting it for final review. |
Signed-off-by: Dariusz Olszewski <starypatyk@users.noreply.github.com> (cherry picked from commit 0485c07)
Signed-off-by: Dariusz Olszewski <starypatyk@users.noreply.github.com> (cherry picked from commit 1cadbaf)
Signed-off-by: Dariusz Olszewski <starypatyk@users.noreply.github.com>
Signed-off-by: Dariusz Olszewski <starypatyk@users.noreply.github.com>
Signed-off-by: Dariusz Olszewski <starypatyk@users.noreply.github.com>
Signed-off-by: Dariusz Olszewski <starypatyk@users.noreply.github.com>
Signed-off-by: Dariusz Olszewski <starypatyk@users.noreply.github.com>
6075f81
to
2987c0d
Compare
Hi @AndyScherzinger, @come-nc, @icewind1991, After conversation with @XueSheng-GIT, I have updated this PR recently. It seems that these changes can provide even greater performance improvements is some cases - see e.g.: #35776 (comment) Would you be able to review my changes again? I have changed the approach slightly - currently I post-process the queries generated by I tried to put the optimization code in an additional
I do not plan any more changes, unless there are additional suggestions from the review. There are a few additional topics that I think could be addressed in follow-up PRs:
|
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.
Could you also test the opposite case, for a search with not a lot of shared files, which is currently fast (<1s), does your PR add visible overhead?
$storage = $a->getValue(); | ||
/** @psalm-suppress UndefinedInterfaceMethod */ | ||
$path = $b->getValue(); | ||
\OC::$server->getLogger()->debug("QuerySearchHelper::checkStorageAndPathFilter: storage=" . $storage . " " . "path=" . $path); |
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.
\OC::$server->getLogger()->debug("QuerySearchHelper::checkStorageAndPathFilter: storage=" . $storage . " " . "path=" . $path); | |
$this->logger->debug("QuerySearchHelper::checkStorageAndPathFilter: storage=" . $storage . " " . "path=" . $path); |
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 forgot to remove the logging. This was useful during development, quite unlikely to be useful in the future. Removed in f86dab9
// Create filters for single file shares | ||
$singleFileFilters = []; | ||
foreach ($storageToPathsMap as $storage => $paths) { | ||
\OC::$server->getLogger()->debug("QuerySearchHelper::optimizeStorageFilters: storage=" . $storage . " " . "paths=" . implode(", ", $paths)); |
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.
\OC::$server->getLogger()->debug("QuerySearchHelper::optimizeStorageFilters: storage=" . $storage . " " . "paths=" . implode(", ", $paths)); | |
$this->logger->debug("QuerySearchHelper::optimizeStorageFilters: storage=" . $storage . " " . "paths=" . implode(", ", $paths)); |
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 forgot to remove the logging. This was useful during development, quite unlikely to be useful in the future. Removed in f86dab9
Signed-off-by: Dariusz Olszewski <starypatyk@users.noreply.github.com>
Hi @come-nc,
I have not been able to measure the overhead. In my "production" environment I have a lot of shared files, so this does not apply. When I tried to measure differences in my dev environment with no shared files, variation of timing between the runs (100-140 ms) was much higher than the difference between original code and my PR (~ 1 ms). When there are no shared files the query should not be modified at all by my optimization code, so query execution time should not be affected. Regarding time required to perform the optimization itself - this is an in-memory operation that should grow roughly linearly with the number of shares. So, even with thousands of shares should be negligible compared to the query execution time. |
I've started implementing something similar but at a level lower and more generalized: #40555 It applies similar transformations on the search operation before building the db query. |
I have noticed that when many files are shared between users (e.g. via Talk) the WebDAV search requests are quite slow. This is visible in the Photos app when entering the initial page or in the Android client when entering the Media tab. I suspect that this issue #35776 might have the same cause.
After some analysis I have found that in such situation the database query that retrieves the list of files is very complex and takes significant time to execute. For each shared file the following section is added to the SQL WHERE clause:
In my environment I see queries with over 250 sections like this. That makes the query use over 500 separate parameters.
Since usually many files are shared between two users, the
storage
part of the filter is repeated many times. This provides an opportunity to refactor the query to use the following form (and provide allpath_hash
values for the samestorage
in an array):This reduces the number of parameters significantly (in my case to about 25) and seems easier to optimize by the database engine.
In this PR I propose a change to the
QuerySearchHelper::searchInCaches
method that generates the query refactored as described above.In my environment (MySQL database, ~250k rows in the
oc_filecache
table) I measured a very nice improvement in the query execution time (retrieval of the first 200 images in the Photos app):Comments? Thoughts?