Merge pull request #2194 from mcrha/wip/performance
multifilesystem: Improve performance of PROPFIND
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 3.8.0.dev
|
## 3.8.0.dev
|
||||||
|
* Fix: storage/multifilesystem: depth:1 PROPFIND no longer re-runs the filesystem collision check (path_to_filesystem) for every item in a collection; this made listing large collections O(n^2) on file systems not detected as collision-free
|
||||||
|
* Improve: storage/multifilesystem: avoid redundant stat() calls per item in get/upload when use_mtime_and_size_for_item_cache is enabled
|
||||||
|
|
||||||
## 3.7.8
|
## 3.7.8
|
||||||
* Fix: time-range filter on a VTODO having DTSTART/DUE and also CREATED/COMPLETED used the CREATED->COMPLETED duration instead of the DTSTART->DUE one, so completed tasks were missing from (or wrongly returned by) calendar-query REPORT results
|
* Fix: time-range filter on a VTODO having DTSTART/DUE and also CREATED/COMPLETED used the CREATED->COMPLETED duration instead of the DTSTART->DUE one, so completed tasks were missing from (or wrongly returned by) calendar-query REPORT results
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ class StoragePartDiscover(StorageBase):
|
|||||||
|
|
||||||
for href in collection._list():
|
for href in collection._list():
|
||||||
with child_context_manager(sane_path, href):
|
with child_context_manager(sane_path, href):
|
||||||
item = collection._get(href)
|
# We don't need to check for collisions, because the file
|
||||||
|
# names are from _list() (os.scandir).
|
||||||
|
item = collection._get(href, verify_href=False)
|
||||||
if item is not None:
|
if item is not None:
|
||||||
yield item
|
yield item
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
path = os.path.join(self._filesystem_path, href)
|
path = os.path.join(self._filesystem_path, href)
|
||||||
|
item_stat: Optional[os.stat_result] = None
|
||||||
try:
|
try:
|
||||||
if self._storage._use_mtime_and_size_for_item_cache is True:
|
if self._storage._use_mtime_and_size_for_item_cache is True:
|
||||||
# try to avoid "open"
|
# try to avoid "open"
|
||||||
@@ -78,6 +79,9 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
|
|||||||
raise IsADirectoryError(path)
|
raise IsADirectoryError(path)
|
||||||
if not os.access(path, os.R_OK):
|
if not os.access(path, os.R_OK):
|
||||||
raise PermissionError(path)
|
raise PermissionError(path)
|
||||||
|
# Single stat() reused below for both the cache hash and
|
||||||
|
# last_modified, instead of stat-ing the same path 3 times.
|
||||||
|
item_stat = os.stat(path)
|
||||||
else:
|
else:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
# early read of the content
|
# early read of the content
|
||||||
@@ -95,7 +99,8 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
|
|||||||
# The hash of the component in the file system. This is used to check,
|
# The hash of the component in the file system. This is used to check,
|
||||||
# if the entry in the cache is still valid.
|
# if the entry in the cache is still valid.
|
||||||
if self._storage._use_mtime_and_size_for_item_cache is True:
|
if self._storage._use_mtime_and_size_for_item_cache is True:
|
||||||
cache_hash = self._item_cache_mtime_and_size(os.stat(path).st_size, os.stat(path).st_mtime_ns)
|
assert item_stat is not None
|
||||||
|
cache_hash = self._item_cache_mtime_and_size(item_stat.st_size, item_stat.st_mtime_ns)
|
||||||
if self._storage._debug_cache_actions is True:
|
if self._storage._debug_cache_actions is True:
|
||||||
logger.debug("Item cache check for: %r with mtime and size %r", path, cache_hash)
|
logger.debug("Item cache check for: %r with mtime and size %r", path, cache_hash)
|
||||||
else:
|
else:
|
||||||
@@ -149,7 +154,8 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
|
|||||||
logger.debug("Item cache hit for: %r", path)
|
logger.debug("Item cache hit for: %r", path)
|
||||||
last_modified = time.strftime(
|
last_modified = time.strftime(
|
||||||
"%a, %d %b %Y %H:%M:%S GMT",
|
"%a, %d %b %Y %H:%M:%S GMT",
|
||||||
time.gmtime(os.path.getmtime(path)))
|
time.gmtime(item_stat.st_mtime if item_stat is not None
|
||||||
|
else os.path.getmtime(path)))
|
||||||
# Don't keep reference to ``vobject_item``, because it requires a lot
|
# Don't keep reference to ``vobject_item``, because it requires a lot
|
||||||
# of memory.
|
# of memory.
|
||||||
return radicale_item.Item(
|
return radicale_item.Item(
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ class CollectionPartUpload(CollectionPartGet, CollectionPartCache,
|
|||||||
(href, self.path, e)) from e
|
(href, self.path, e)) from e
|
||||||
# store cache file
|
# store cache file
|
||||||
if self._storage._use_mtime_and_size_for_item_cache is True:
|
if self._storage._use_mtime_and_size_for_item_cache is True:
|
||||||
cache_hash = self._item_cache_mtime_and_size(os.stat(path).st_size, os.stat(path).st_mtime_ns)
|
path_stat = os.stat(path)
|
||||||
|
cache_hash = self._item_cache_mtime_and_size(path_stat.st_size, path_stat.st_mtime_ns)
|
||||||
if self._storage._debug_cache_actions is True:
|
if self._storage._debug_cache_actions is True:
|
||||||
logger.debug("Item cache store for: %r with mtime and size %r", path, cache_hash)
|
logger.debug("Item cache store for: %r with mtime and size %r", path, cache_hash)
|
||||||
else:
|
else:
|
||||||
@@ -121,7 +122,8 @@ class CollectionPartUpload(CollectionPartGet, CollectionPartCache,
|
|||||||
|
|
||||||
# store cache file
|
# store cache file
|
||||||
if self._storage._use_mtime_and_size_for_item_cache is True:
|
if self._storage._use_mtime_and_size_for_item_cache is True:
|
||||||
cache_hash = self._item_cache_mtime_and_size(os.stat(path).st_size, os.stat(path).st_mtime_ns)
|
path_stat = os.stat(path)
|
||||||
|
cache_hash = self._item_cache_mtime_and_size(path_stat.st_size, path_stat.st_mtime_ns)
|
||||||
if self._storage._debug_cache_actions is True:
|
if self._storage._debug_cache_actions is True:
|
||||||
logger.debug("Item cache store for: %r with mtime and size %r", path, cache_hash)
|
logger.debug("Item cache store for: %r with mtime and size %r", path, cache_hash)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -344,6 +344,41 @@ class TestMultiFileSystem(BaseTest):
|
|||||||
logs = caplog.messages
|
logs = caplog.messages
|
||||||
assert len([log for log in logs if "File name collision" in log]) == 2
|
assert len([log for log in logs if "File name collision" in log]) == 2
|
||||||
|
|
||||||
|
def test_propfind_depth1_does_not_check_collisions_per_item(self) -> None:
|
||||||
|
"""Listing a collection (PROPFIND depth:1) must not re-run the
|
||||||
|
filesystem collision check (path_to_filesystem) once per item.
|
||||||
|
"""
|
||||||
|
self.mkcalendar("/calendar.ics/")
|
||||||
|
item_count = 20
|
||||||
|
for index in range(item_count):
|
||||||
|
uid = "event%d" % index
|
||||||
|
event = ("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n"
|
||||||
|
"BEGIN:VEVENT\r\nUID:%s\r\nSUMMARY:Event %d\r\n"
|
||||||
|
"DTSTART:20130901T180000Z\r\nEND:VEVENT\r\n"
|
||||||
|
"END:VCALENDAR\r\n" % (uid, index))
|
||||||
|
self.put("/calendar.ics/%s.ics" % uid, event)
|
||||||
|
|
||||||
|
call_count = 0
|
||||||
|
original_path_to_filesystem = pathutils.path_to_filesystem
|
||||||
|
|
||||||
|
def counting_path_to_filesystem(*args, **kwargs):
|
||||||
|
nonlocal call_count
|
||||||
|
call_count += 1
|
||||||
|
return original_path_to_filesystem(*args, **kwargs)
|
||||||
|
|
||||||
|
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||||
|
monkeypatch.setattr(pathutils, "path_to_filesystem",
|
||||||
|
counting_path_to_filesystem)
|
||||||
|
_, responses = self.propfind("/calendar.ics/", """\
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<D:propfind xmlns:D="DAV:">
|
||||||
|
<D:prop><D:getetag/></D:prop>
|
||||||
|
</D:propfind>
|
||||||
|
""", HTTP_DEPTH="1")
|
||||||
|
|
||||||
|
assert len(responses) == item_count + 1
|
||||||
|
assert call_count < item_count
|
||||||
|
|
||||||
@pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found")
|
@pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found")
|
||||||
@pytest.mark.skipif(radicale.log.logger.getEffectiveLevel() == logging.INFO, reason="requires loglevel DEBUG")
|
@pytest.mark.skipif(radicale.log.logger.getEffectiveLevel() == logging.INFO, reason="requires loglevel DEBUG")
|
||||||
def test_hook_placeholders_PUT(self, caplog) -> None:
|
def test_hook_placeholders_PUT(self, caplog) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user