Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Reduce memory footprint of caches #9886

Merged
merged 4 commits into from
Apr 28, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/9886.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reduce memory usage of the LRU caches.
75 changes: 57 additions & 18 deletions synapse/util/caches/lrucache.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
from typing import (
Any,
Callable,
Collection,
Generic,
Iterable,
List,
Optional,
Type,
TypeVar,
Expand Down Expand Up @@ -57,13 +59,54 @@ class _Node:
__slots__ = ["prev_node", "next_node", "key", "value", "callbacks"]

def __init__(
self, prev_node, next_node, key, value, callbacks: Optional[set] = None
self,
prev_node,
next_node,
key,
value,
callbacks: Collection[Callable[[], None]] = (),
):
self.prev_node = prev_node
self.next_node = next_node
self.key = key
self.value = value
self.callbacks = callbacks or set()

# Set of callbacks to run when the node gets deleted. We store as a list
# rather than a set to keep memory usage down (and since we expect few
# entries per node the performance of checking for duplication in a list
# vs using a set is negligible).
#
# Note that we store this as an optional list to keep the memory
# footprint down. Empty lists are 56 bytes (and empty sets are 216 bytes).
self.callbacks = None # type: Optional[List[Callable[[], None]]]

self.add_callbacks(callbacks)

def add_callbacks(self, callbacks: Collection[Callable[[], None]]) -> None:
"""Add to stored list of callbacks, removing duplicates."""

if not callbacks:
return

if not self.callbacks:
self.callbacks = []

for callback in callbacks:
if callback not in self.callbacks:
self.callbacks.append(callback)

def run_and_clear_callbacks(self) -> None:
"""Run all callbacks and clear the stored set of callbacks. Used when
the node is being deleted.
"""

if not self.callbacks:
return

for callback in self.callbacks:
callback()

self.callbacks = None


class LruCache(Generic[KT, VT]):
Expand Down Expand Up @@ -177,10 +220,10 @@ def cache_len():

self.len = synchronized(cache_len)

def add_node(key, value, callbacks: Optional[set] = None):
def add_node(key, value, callbacks: Collection[Callable[[], None]] = ()):
prev_node = list_root
next_node = prev_node.next_node
node = _Node(prev_node, next_node, key, value, callbacks or set())
node = _Node(prev_node, next_node, key, value, callbacks)
prev_node.next_node = node
next_node.prev_node = node
cache[key] = node
Expand Down Expand Up @@ -211,16 +254,15 @@ def delete_node(node):
deleted_len = size_callback(node.value)
cached_cache_len[0] -= deleted_len

for cb in node.callbacks:
cb()
node.callbacks.clear()
node.run_and_clear_callbacks()

return deleted_len

@overload
def cache_get(
key: KT,
default: Literal[None] = None,
callbacks: Iterable[Callable[[], None]] = ...,
callbacks: Collection[Callable[[], None]] = ...,
update_metrics: bool = ...,
) -> Optional[VT]:
...
Expand All @@ -229,7 +271,7 @@ def cache_get(
def cache_get(
key: KT,
default: T,
callbacks: Iterable[Callable[[], None]] = ...,
callbacks: Collection[Callable[[], None]] = ...,
update_metrics: bool = ...,
) -> Union[T, VT]:
...
Expand All @@ -238,13 +280,13 @@ def cache_get(
def cache_get(
key: KT,
default: Optional[T] = None,
callbacks: Iterable[Callable[[], None]] = (),
callbacks: Collection[Callable[[], None]] = (),
update_metrics: bool = True,
):
node = cache.get(key, None)
if node is not None:
move_node_to_front(node)
node.callbacks.update(callbacks)
node.add_callbacks(callbacks)
if update_metrics and metrics:
metrics.inc_hits()
return node.value
Expand All @@ -260,10 +302,8 @@ def cache_set(key: KT, value: VT, callbacks: Iterable[Callable[[], None]] = ()):
# We sometimes store large objects, e.g. dicts, which cause
# the inequality check to take a long time. So let's only do
# the check if we have some callbacks to call.
if node.callbacks and value != node.value:
for cb in node.callbacks:
cb()
node.callbacks.clear()
if value != node.value:
node.run_and_clear_callbacks()

# We don't bother to protect this by value != node.value as
# generally size_callback will be cheap compared with equality
Expand All @@ -273,7 +313,7 @@ def cache_set(key: KT, value: VT, callbacks: Iterable[Callable[[], None]] = ()):
cached_cache_len[0] -= size_callback(node.value)
cached_cache_len[0] += size_callback(value)

node.callbacks.update(callbacks)
node.add_callbacks(callbacks)

move_node_to_front(node)
node.value = value
Expand Down Expand Up @@ -326,8 +366,7 @@ def cache_clear() -> None:
list_root.next_node = list_root
list_root.prev_node = list_root
for node in cache.values():
for cb in node.callbacks:
cb()
node.run_and_clear_callbacks()
cache.clear()
if size_callback:
cached_cache_len[0] = 0
Expand Down