mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 16:38:16 +01:00
Implement a memory cache for dashboard entries to avoid frequent disk reads (#5687)
This commit is contained in:
parent
31fec2d692
commit
78e3ce7718
1 changed files with 87 additions and 5 deletions
|
@ -67,6 +67,9 @@ class DashboardSettings:
|
||||||
self.on_ha_addon = False
|
self.on_ha_addon = False
|
||||||
self.cookie_secret = None
|
self.cookie_secret = None
|
||||||
self.absolute_config_dir = None
|
self.absolute_config_dir = None
|
||||||
|
self._entry_cache: dict[
|
||||||
|
str, tuple[tuple[int, int, float, int], DashboardEntry]
|
||||||
|
] = {}
|
||||||
|
|
||||||
def parse_args(self, args):
|
def parse_args(self, args):
|
||||||
self.on_ha_addon = args.ha_addon
|
self.on_ha_addon = args.ha_addon
|
||||||
|
@ -121,9 +124,70 @@ class DashboardSettings:
|
||||||
Path(joined_path).resolve().relative_to(self.absolute_config_dir)
|
Path(joined_path).resolve().relative_to(self.absolute_config_dir)
|
||||||
return joined_path
|
return joined_path
|
||||||
|
|
||||||
def list_yaml_files(self):
|
def list_yaml_files(self) -> list[str]:
|
||||||
return util.list_yaml_files([self.config_dir])
|
return util.list_yaml_files([self.config_dir])
|
||||||
|
|
||||||
|
def entries(self) -> list[DashboardEntry]:
|
||||||
|
"""Fetch all dashboard entries, thread-safe."""
|
||||||
|
path_to_cache_key: dict[str, tuple[int, int, float, int]] = {}
|
||||||
|
#
|
||||||
|
# The cache key is (inode, device, mtime, size)
|
||||||
|
# which allows us to avoid locking since it ensures
|
||||||
|
# every iteration of this call will always return the newest
|
||||||
|
# items from disk at the cost of a stat() call on each
|
||||||
|
# file which is much faster than reading the file
|
||||||
|
# for the cache hit case which is the common case.
|
||||||
|
#
|
||||||
|
# Because there is no lock the cache may
|
||||||
|
# get built more than once but that's fine as its still
|
||||||
|
# thread-safe and results in orders of magnitude less
|
||||||
|
# reads from disk than if we did not cache at all and
|
||||||
|
# does not have a lock contention issue.
|
||||||
|
#
|
||||||
|
for file in self.list_yaml_files():
|
||||||
|
try:
|
||||||
|
# Prefer the json storage path if it exists
|
||||||
|
stat = os.stat(ext_storage_path(os.path.basename(file)))
|
||||||
|
except OSError:
|
||||||
|
try:
|
||||||
|
# Fallback to the yaml file if the storage
|
||||||
|
# file does not exist or could not be generated
|
||||||
|
stat = os.stat(file)
|
||||||
|
except OSError:
|
||||||
|
# File was deleted, ignore
|
||||||
|
continue
|
||||||
|
path_to_cache_key[file] = (
|
||||||
|
stat.st_ino,
|
||||||
|
stat.st_dev,
|
||||||
|
stat.st_mtime,
|
||||||
|
stat.st_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
entry_cache = self._entry_cache
|
||||||
|
|
||||||
|
# Remove entries that no longer exist
|
||||||
|
removed: list[str] = []
|
||||||
|
for file in entry_cache:
|
||||||
|
if file not in path_to_cache_key:
|
||||||
|
removed.append(file)
|
||||||
|
|
||||||
|
for file in removed:
|
||||||
|
entry_cache.pop(file)
|
||||||
|
|
||||||
|
dashboard_entries: list[DashboardEntry] = []
|
||||||
|
for file, cache_key in path_to_cache_key.items():
|
||||||
|
if cached_entry := entry_cache.get(file):
|
||||||
|
entry_key, dashboard_entry = cached_entry
|
||||||
|
if entry_key == cache_key:
|
||||||
|
dashboard_entries.append(dashboard_entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
dashboard_entry = DashboardEntry(file)
|
||||||
|
dashboard_entries.append(dashboard_entry)
|
||||||
|
entry_cache[file] = (cache_key, dashboard_entry)
|
||||||
|
|
||||||
|
return dashboard_entries
|
||||||
|
|
||||||
|
|
||||||
settings = DashboardSettings()
|
settings = DashboardSettings()
|
||||||
|
|
||||||
|
@ -657,18 +721,26 @@ class EsphomeVersionHandler(BaseHandler):
|
||||||
self.finish()
|
self.finish()
|
||||||
|
|
||||||
|
|
||||||
def _list_dashboard_entries():
|
def _list_dashboard_entries() -> list[DashboardEntry]:
|
||||||
files = settings.list_yaml_files()
|
return settings.entries()
|
||||||
return [DashboardEntry(file) for file in files]
|
|
||||||
|
|
||||||
|
|
||||||
class DashboardEntry:
|
class DashboardEntry:
|
||||||
def __init__(self, path):
|
"""Represents a single dashboard entry.
|
||||||
|
|
||||||
|
This class is thread-safe and read-only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("path", "_storage", "_loaded_storage")
|
||||||
|
|
||||||
|
def __init__(self, path: str) -> None:
|
||||||
|
"""Initialize the DashboardEntry."""
|
||||||
self.path = path
|
self.path = path
|
||||||
self._storage = None
|
self._storage = None
|
||||||
self._loaded_storage = False
|
self._loaded_storage = False
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
"""Return the representation of this entry."""
|
||||||
return (
|
return (
|
||||||
f"DashboardEntry({self.path} "
|
f"DashboardEntry({self.path} "
|
||||||
f"address={self.address} "
|
f"address={self.address} "
|
||||||
|
@ -679,10 +751,12 @@ class DashboardEntry:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename(self):
|
def filename(self):
|
||||||
|
"""Return the filename of this entry."""
|
||||||
return os.path.basename(self.path)
|
return os.path.basename(self.path)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def storage(self) -> StorageJSON | None:
|
def storage(self) -> StorageJSON | None:
|
||||||
|
"""Return the StorageJSON object for this entry."""
|
||||||
if not self._loaded_storage:
|
if not self._loaded_storage:
|
||||||
self._storage = StorageJSON.load(ext_storage_path(self.filename))
|
self._storage = StorageJSON.load(ext_storage_path(self.filename))
|
||||||
self._loaded_storage = True
|
self._loaded_storage = True
|
||||||
|
@ -690,48 +764,56 @@ class DashboardEntry:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def address(self):
|
def address(self):
|
||||||
|
"""Return the address of this entry."""
|
||||||
if self.storage is None:
|
if self.storage is None:
|
||||||
return None
|
return None
|
||||||
return self.storage.address
|
return self.storage.address
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def no_mdns(self):
|
def no_mdns(self):
|
||||||
|
"""Return the no_mdns of this entry."""
|
||||||
if self.storage is None:
|
if self.storage is None:
|
||||||
return None
|
return None
|
||||||
return self.storage.no_mdns
|
return self.storage.no_mdns
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def web_port(self):
|
def web_port(self):
|
||||||
|
"""Return the web port of this entry."""
|
||||||
if self.storage is None:
|
if self.storage is None:
|
||||||
return None
|
return None
|
||||||
return self.storage.web_port
|
return self.storage.web_port
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
"""Return the name of this entry."""
|
||||||
if self.storage is None:
|
if self.storage is None:
|
||||||
return self.filename.replace(".yml", "").replace(".yaml", "")
|
return self.filename.replace(".yml", "").replace(".yaml", "")
|
||||||
return self.storage.name
|
return self.storage.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def friendly_name(self):
|
def friendly_name(self):
|
||||||
|
"""Return the friendly name of this entry."""
|
||||||
if self.storage is None:
|
if self.storage is None:
|
||||||
return self.name
|
return self.name
|
||||||
return self.storage.friendly_name
|
return self.storage.friendly_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def comment(self):
|
def comment(self):
|
||||||
|
"""Return the comment of this entry."""
|
||||||
if self.storage is None:
|
if self.storage is None:
|
||||||
return None
|
return None
|
||||||
return self.storage.comment
|
return self.storage.comment
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_platform(self):
|
def target_platform(self):
|
||||||
|
"""Return the target platform of this entry."""
|
||||||
if self.storage is None:
|
if self.storage is None:
|
||||||
return None
|
return None
|
||||||
return self.storage.target_platform
|
return self.storage.target_platform
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def update_available(self):
|
def update_available(self):
|
||||||
|
"""Return if an update is available for this entry."""
|
||||||
if self.storage is None:
|
if self.storage is None:
|
||||||
return True
|
return True
|
||||||
return self.update_old != self.update_new
|
return self.update_old != self.update_new
|
||||||
|
|
Loading…
Reference in a new issue