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

Optionally track memory usage of each LruCache #9881

Merged
merged 13 commits into from
May 5, 2021
1 change: 1 addition & 0 deletions changelog.d/9881.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add experimental option to track memory usage of the caches.
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,6 @@ ignore_missing_imports = True

[mypy-txacme.*]
ignore_missing_imports = True

[mypy-pympler.*]
ignore_missing_imports = True
1 change: 1 addition & 0 deletions synapse/app/generic_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ def start(config_options):
config.server.update_user_directory = False

synapse.events.USE_FROZEN_DICTS = config.use_frozen_dicts
synapse.util.caches.lrucache.TRACK_MEMORY_USAGE = config.caches.track_memory_usage
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved

hs = GenericWorkerServer(
config.server_name,
Expand Down
1 change: 1 addition & 0 deletions synapse/app/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ def setup(config_options):
sys.exit(0)

events.USE_FROZEN_DICTS = config.use_frozen_dicts
synapse.util.caches.lrucache.TRACK_MEMORY_USAGE = config.caches.track_memory_usage
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved

hs = SynapseHomeServer(
config.server_name,
Expand Down
11 changes: 11 additions & 0 deletions synapse/config/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import threading
from typing import Callable, Dict

from synapse.python_dependencies import DependencyException, check_requirements

from ._base import Config, ConfigError

# The prefix for all cache factor-related environment variables
Expand Down Expand Up @@ -189,6 +191,15 @@ def read_config(self, config, **kwargs):
)
self.cache_factors[cache] = factor

self.track_memory_usage = cache_config.get("track_memory_usage", False)
if self.track_memory_usage:
try:
check_requirements("cache_memory")
except DependencyException as e:
raise ConfigError(
e.message # noqa: B306, DependencyException.message is a property
)

# Resize all caches (if necessary) with the new factors we've loaded
self.resize_all_caches()

Expand Down
1 change: 1 addition & 0 deletions synapse/python_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
# hiredis is not a *strict* dependency, but it makes things much faster.
# (if it is not installed, we fall back to slow code.)
"redis": ["txredisapi>=1.4.7", "hiredis"],
"cache_memory": ["pympler"],
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
}

ALL_OPTIONAL_REQUIREMENTS = set() # type: Set[str]
Expand Down
26 changes: 26 additions & 0 deletions synapse/util/caches/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@

logger = logging.getLogger(__name__)


# Whether to track estimated memory usage of the LruCaches.
TRACK_MEMORY_USAGE = False


caches_by_name = {} # type: Dict[str, Sized]
collectors_by_name = {} # type: Dict[str, CacheMetric]

Expand All @@ -32,6 +37,11 @@
cache_evicted = Gauge("synapse_util_caches_cache:evicted_size", "", ["name"])
cache_total = Gauge("synapse_util_caches_cache:total", "", ["name"])
cache_max_size = Gauge("synapse_util_caches_cache_max_size", "", ["name"])
cache_memory_usage = Gauge(
"synapse_util_caches_cache_memory_usage",
"Estimated size in bytes of the caches",
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
["name"],
)

response_cache_size = Gauge("synapse_util_caches_response_cache:size", "", ["name"])
response_cache_hits = Gauge("synapse_util_caches_response_cache:hits", "", ["name"])
Expand All @@ -52,6 +62,7 @@ class CacheMetric:
hits = attr.ib(default=0)
misses = attr.ib(default=0)
evicted_size = attr.ib(default=0)
memory_usage = attr.ib(default=None)

def inc_hits(self):
self.hits += 1
Expand All @@ -62,6 +73,19 @@ def inc_misses(self):
def inc_evictions(self, size=1):
self.evicted_size += size

def inc_memory_usage(self, memory: int):
if self.memory_usage is None:
self.memory_usage = 0

self.memory_usage += memory

def dec_memory_usage(self, memory: int):
self.memory_usage -= memory

def clear_memory_usage(self):
if self.memory_usage is not None:
self.memory_usage = 0

def describe(self):
return []

Expand All @@ -81,6 +105,8 @@ def collect(self):
cache_total.labels(self._cache_name).set(self.hits + self.misses)
if getattr(self._cache, "max_size", None):
cache_max_size.labels(self._cache_name).set(self._cache.max_size)
if self.memory_usage is not None:
cache_memory_usage.labels(self._cache_name).set(self.memory_usage)
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
if self._collect_callback:
self._collect_callback()
except Exception as e:
Expand Down
50 changes: 48 additions & 2 deletions synapse/util/caches/lrucache.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,35 @@
from typing_extensions import Literal

from synapse.config import cache as cache_config
from synapse.util.caches import CacheMetric, register_cache
from synapse.util.caches import TRACK_MEMORY_USAGE, CacheMetric, register_cache
from synapse.util.caches.treecache import TreeCache

try:
from pympler.asizeof import Asizer

def _get_size_of(val: Any, *, recurse=True) -> int:
"""Get an estimate of the size in bytes of the object.

Args:
val: The object to size.
recurse: If true will include referenced values in the size,
otherwise only sizes the given object.
"""
# Ignore singleton values when calculating memory usage.
if val in ((), None, ""):
return 0

sizer = Asizer()
sizer.exclude_refs((), None, "")
return sizer.asizeof(val, limit=100 if recurse else 0)


except ImportError:

def _get_size_of(val: Any, *, recurse=True) -> int:
return 0


# Function type: the type used for invalidation callbacks
FT = TypeVar("FT", bound=Callable[..., Any])

Expand All @@ -54,7 +80,7 @@ def enumerate_leaves(node, depth):


class _Node:
__slots__ = ["prev_node", "next_node", "key", "value", "callbacks"]
__slots__ = ["prev_node", "next_node", "key", "value", "callbacks", "memory"]

def __init__(
self, prev_node, next_node, key, value, callbacks: Optional[set] = None
Expand All @@ -65,6 +91,16 @@ def __init__(
self.value = value
self.callbacks = callbacks or set()

self.memory = 0
if TRACK_MEMORY_USAGE:
self.memory = (
_get_size_of(key)
+ _get_size_of(value)
+ _get_size_of(self.callbacks, recurse=False)
+ _get_size_of(self, recurse=False)
)
self.memory += _get_size_of(self.memory, recurse=False)


class LruCache(Generic[KT, VT]):
"""
Expand Down Expand Up @@ -188,6 +224,9 @@ def add_node(key, value, callbacks: Optional[set] = None):
if size_callback:
cached_cache_len[0] += size_callback(node.value)

if TRACK_MEMORY_USAGE and metrics:
metrics.inc_memory_usage(node.memory)

def move_node_to_front(node):
prev_node = node.prev_node
next_node = node.next_node
Expand All @@ -214,6 +253,10 @@ def delete_node(node):
for cb in node.callbacks:
cb()
node.callbacks.clear()

if TRACK_MEMORY_USAGE and metrics:
metrics.dec_memory_usage(node.memory)

return deleted_len

@overload
Expand Down Expand Up @@ -332,6 +375,9 @@ def cache_clear() -> None:
if size_callback:
cached_cache_len[0] = 0

if TRACK_MEMORY_USAGE and metrics:
metrics.clear_memory_usage()

@synchronized
def cache_contains(key: KT) -> bool:
return key in cache
Expand Down