Add Memory Analyzer
This commit was merged in pull request #8.
This commit is contained in:
+4
-1
@@ -1,7 +1,7 @@
|
|||||||
# CONFIG
|
# CONFIG
|
||||||
*.toml
|
*.toml
|
||||||
|
|
||||||
# Chatch-all default Python gitignore
|
# Catch-all default Python gitignore
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@@ -152,3 +152,6 @@ projects.json
|
|||||||
|
|
||||||
# Authentication directoy
|
# Authentication directoy
|
||||||
auth/
|
auth/
|
||||||
|
|
||||||
|
# Backup Files
|
||||||
|
*~
|
||||||
@@ -50,6 +50,9 @@
|
|||||||
default_profile_picture="src/elm/assets/img/anon.png"
|
default_profile_picture="src/elm/assets/img/anon.png"
|
||||||
dev_mode_active=true # Supresses E-Mail sending, activates PayPal sandbox API
|
dev_mode_active=true # Supresses E-Mail sending, activates PayPal sandbox API
|
||||||
|
|
||||||
|
[debug]
|
||||||
|
tracemalloc_enabled=false
|
||||||
|
|
||||||
[paypal]
|
[paypal]
|
||||||
client_id_sandbox=""
|
client_id_sandbox=""
|
||||||
secret_sandbox=""
|
secret_sandbox=""
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ services:
|
|||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
- "8001:8001"
|
- "8001:8001"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/opt/elm
|
- ./:/opt/elm:z
|
||||||
entrypoint: ["/bin/sh", "-c", "cd /opt/elm/src && rio run --release --public --port 8000"]
|
entrypoint: ["/bin/sh", "-c", "cd /opt/elm/src && rio run --release --public --port 8000"]
|
||||||
|
|
||||||
db:
|
db:
|
||||||
|
|||||||
+10
-2
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import tracemalloc
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ from from_root import from_root
|
|||||||
from rio import App, Theme, Color, Font, Icon, Session
|
from rio import App, Theme, Color, Font, Icon, Session
|
||||||
|
|
||||||
from elm.components.RootComponent import RootComponent
|
from elm.components.RootComponent import RootComponent
|
||||||
from elm.services import ConfigurationService, DatabaseService, UserService, LocalData, LocalDataService, MailingService, AccountingService, PreloadService, ReceiptPrintingService
|
from elm.services import ConfigurationService, DatabaseService, UserService, LocalData, LocalDataService, MailingService, AccountingService, PreloadService, ReceiptPrintingService, MemoryService
|
||||||
from elm.types import UserSession
|
from elm.types import UserSession
|
||||||
|
|
||||||
logger = logging.getLogger("ELM")
|
logger = logging.getLogger("ELM")
|
||||||
@@ -51,6 +52,13 @@ database_service = DatabaseService(configuration_service.get_database_configurat
|
|||||||
mailing_service = MailingService(configuration_service)
|
mailing_service = MailingService(configuration_service)
|
||||||
lan_info = configuration_service.get_lan_info()
|
lan_info = configuration_service.get_lan_info()
|
||||||
|
|
||||||
|
tracemalloc_enabled = configuration_service.TRACEMALLOC_ENABLED
|
||||||
|
|
||||||
|
if tracemalloc_enabled:
|
||||||
|
tracemalloc.start(10)
|
||||||
|
|
||||||
|
memory_service = MemoryService(tracemalloc_enabled)
|
||||||
|
|
||||||
def is_mobile(self: Session) -> bool:
|
def is_mobile(self: Session) -> bool:
|
||||||
return self.screen_width < self.theme.MAX_MOBILE_SCREEN_WIDTH
|
return self.screen_width < self.theme.MAX_MOBILE_SCREEN_WIDTH
|
||||||
|
|
||||||
@@ -79,7 +87,7 @@ app = App(
|
|||||||
theme=theme,
|
theme=theme,
|
||||||
assets_dir=Path(__file__).parent / "assets",
|
assets_dir=Path(__file__).parent / "assets",
|
||||||
build=RootComponent,
|
build=RootComponent,
|
||||||
default_attachments=[LocalData(), configuration_service, database_service, UserService(), LocalDataService(), mailing_service, AccountingService(configuration_service, mailing_service), PreloadService(), ReceiptPrintingService(configuration_service.get_receipt_printing_configuration(), configuration_service.DEV_MODE_ACTIVE)],
|
default_attachments=[LocalData(), configuration_service, database_service, UserService(), LocalDataService(), mailing_service, AccountingService(configuration_service, mailing_service), PreloadService(), ReceiptPrintingService(configuration_service.get_receipt_printing_configuration(), configuration_service.DEV_MODE_ACTIVE), memory_service],
|
||||||
on_app_start=on_app_start,
|
on_app_start=on_app_start,
|
||||||
on_session_start=on_session_start,
|
on_session_start=on_session_start,
|
||||||
icon=from_root("src/elm/assets/img/favicon.png"),
|
icon=from_root("src/elm/assets/img/favicon.png"),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Callable
|
|||||||
from rio import Component, Row, Column, Color, PointerEventListener, PointerEvent, Rectangle, Text, TextStyle, event
|
from rio import Component, Row, Column, Color, PointerEventListener, PointerEvent, Rectangle, Text, TextStyle, event
|
||||||
|
|
||||||
from elm.types import UserSession
|
from elm.types import UserSession
|
||||||
from elm.services import AccountingService
|
from elm.services import AccountingService, MemoryService
|
||||||
|
|
||||||
|
|
||||||
class UserNavigationButton(Component):
|
class UserNavigationButton(Component):
|
||||||
@@ -103,10 +103,13 @@ class UserNavigation(Component):
|
|||||||
]
|
]
|
||||||
|
|
||||||
if self.show_admin_navigation():
|
if self.show_admin_navigation():
|
||||||
base_nav.extend([
|
admin_buttons = [
|
||||||
UserNavigationButton("Admin: Benutzer", "/user-admin", self.close_navigation),
|
UserNavigationButton("Admin: Benutzer", "/user-admin", self.close_navigation),
|
||||||
UserNavigationButton("Admin: Catering", "/catering-admin", self.close_navigation)
|
UserNavigationButton("Admin: Catering", "/catering-admin", self.close_navigation),
|
||||||
])
|
]
|
||||||
|
if self.session[MemoryService].is_enabled():
|
||||||
|
admin_buttons.append(UserNavigationButton("Admin: Speicher", "/memory-admin", self.close_navigation))
|
||||||
|
base_nav.extend(admin_buttons)
|
||||||
|
|
||||||
base_nav.append(
|
base_nav.append(
|
||||||
UserNavigationButton("Ausloggen", "/logout", self.close_navigation)
|
UserNavigationButton("Ausloggen", "/logout", self.close_navigation)
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, GuardEvent, PointerEventListener, PointerEvent, Set
|
||||||
|
from rio.event import on_populate
|
||||||
|
|
||||||
|
from elm.types import UserSession
|
||||||
|
from elm.services import MemoryService
|
||||||
|
from elm.components import ElmButton
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
|
|
||||||
|
def memory_analyzer_page_guard(event: GuardEvent) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
if event.session[UserSession].is_team_member:
|
||||||
|
return None
|
||||||
|
return "/"
|
||||||
|
except KeyError:
|
||||||
|
return "/"
|
||||||
|
|
||||||
|
|
||||||
|
@page(name="Speicheranalyse", url_segment="memory-admin", guard=memory_analyzer_page_guard)
|
||||||
|
class MemoryAnalyzerPage(Component):
|
||||||
|
diff_entries: list = []
|
||||||
|
has_baseline: bool = False
|
||||||
|
is_loading: bool = True
|
||||||
|
expanded_ids: Set[int] = Set()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_size(size_bytes: int) -> str:
|
||||||
|
if size_bytes >= 1024 * 1024:
|
||||||
|
return f"{size_bytes / (1024 * 1024):.2f} MiB"
|
||||||
|
if size_bytes >= 1024:
|
||||||
|
return f"{size_bytes / 1024:.2f} KiB"
|
||||||
|
return f"{size_bytes} B"
|
||||||
|
|
||||||
|
@on_populate
|
||||||
|
async def on_populate(self) -> None:
|
||||||
|
mem_service = self.session[MemoryService]
|
||||||
|
self.has_baseline = mem_service.get_baseline() is not None
|
||||||
|
if self.has_baseline:
|
||||||
|
self.diff_entries = sorted(mem_service.get_diff(), key=lambda s: abs(s.size_diff), reverse=True)[:50]
|
||||||
|
self.is_loading = False
|
||||||
|
|
||||||
|
async def refresh(self) -> None:
|
||||||
|
mem_service = self.session[MemoryService]
|
||||||
|
self.has_baseline = mem_service.get_baseline() is not None
|
||||||
|
if self.has_baseline:
|
||||||
|
self.diff_entries = sorted(mem_service.get_diff(), key=lambda s: abs(s.size_diff), reverse=True)[:50]
|
||||||
|
self.expanded_ids.clear()
|
||||||
|
|
||||||
|
async def take_baseline(self) -> None:
|
||||||
|
self.session[MemoryService].store_baseline()
|
||||||
|
self.has_baseline = True
|
||||||
|
self.diff_entries = []
|
||||||
|
self.expanded_ids.clear()
|
||||||
|
|
||||||
|
async def clear_baseline(self) -> None:
|
||||||
|
self.session[MemoryService].clear_baseline()
|
||||||
|
self.has_baseline = False
|
||||||
|
self.diff_entries = []
|
||||||
|
self.expanded_ids.clear()
|
||||||
|
|
||||||
|
def _filter_frames(self, stat) -> list[str]:
|
||||||
|
frames = stat.traceback.format()
|
||||||
|
return [f for f in frames if "tracemalloc.py" not in f] if frames else ["unknown"]
|
||||||
|
|
||||||
|
def _get_leaf_frame(self, stat) -> str:
|
||||||
|
frames = self._filter_frames(stat)
|
||||||
|
return frames[-1] if frames else "unknown"
|
||||||
|
|
||||||
|
def _get_all_frames(self, stat) -> list[str]:
|
||||||
|
return self._filter_frames(stat)
|
||||||
|
|
||||||
|
async def toggle_expand(self, entry_id: int) -> None:
|
||||||
|
if entry_id in self.expanded_ids:
|
||||||
|
self.expanded_ids.discard(entry_id)
|
||||||
|
else:
|
||||||
|
self.expanded_ids.add(entry_id)
|
||||||
|
|
||||||
|
def _build_diff_row(self, stat, index: int) -> Component:
|
||||||
|
size_str = self._format_size(abs(stat.size_diff))
|
||||||
|
sign = "+" if stat.size_diff >= 0 else "-"
|
||||||
|
leaf = self._get_leaf_frame(stat)
|
||||||
|
all_frames = self._get_all_frames(stat)
|
||||||
|
is_expanded = index in self.expanded_ids
|
||||||
|
has_multiple = len(all_frames) > 1
|
||||||
|
|
||||||
|
indicator = "▼" if (is_expanded and has_multiple) else ("▶" if has_multiple else " ")
|
||||||
|
|
||||||
|
content_parts = [
|
||||||
|
Row(
|
||||||
|
PointerEventListener(
|
||||||
|
content=Rectangle(
|
||||||
|
content=Text(indicator, selectable=False),
|
||||||
|
cursor="pointer" if has_multiple else "default"
|
||||||
|
),
|
||||||
|
on_press=(lambda event, i=index: self.toggle_expand(i)) if has_multiple else None,
|
||||||
|
),
|
||||||
|
Text(leaf, grow_x=True, selectable=True, overflow="wrap"),
|
||||||
|
Text(f"{sign}{size_str}", margin_left=1, selectable=False),
|
||||||
|
margin_bottom=0.1,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
if is_expanded and has_multiple:
|
||||||
|
for frame in all_frames:
|
||||||
|
content_parts.append(
|
||||||
|
Row(
|
||||||
|
Text(" └ " + frame, grow_x=True, selectable=True, overflow="wrap", margin_bottom=0.05),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Column(*content_parts, margin_bottom=0.3)
|
||||||
|
|
||||||
|
def build(self) -> Component:
|
||||||
|
mem_service = self.session[MemoryService]
|
||||||
|
|
||||||
|
if not mem_service.is_enabled():
|
||||||
|
return Rectangle(
|
||||||
|
content=Column(
|
||||||
|
Rectangle(
|
||||||
|
content=Rectangle(
|
||||||
|
content=Text("Speicheranalyse (tracemalloc)", margin=0.5, selectable=False, overflow="wrap"),
|
||||||
|
fill=self.session.theme.header_box_background_color,
|
||||||
|
margin=0.4
|
||||||
|
),
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
Text(
|
||||||
|
"tracemalloc ist nicht aktiviert. Aktiviere es in config.toml unter [debug] tracemalloc_enabled=true und starte die Anwendung neu.",
|
||||||
|
margin=1,
|
||||||
|
selectable=False
|
||||||
|
),
|
||||||
|
Spacer()
|
||||||
|
),
|
||||||
|
Spacer()
|
||||||
|
),
|
||||||
|
fill=self.session.theme.box_color,
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
margin=1,
|
||||||
|
min_width=40
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.is_loading:
|
||||||
|
return Rectangle(
|
||||||
|
content=Column(
|
||||||
|
Text("Lade...", margin=1, selectable=False),
|
||||||
|
Spacer()
|
||||||
|
),
|
||||||
|
fill=self.session.theme.box_color,
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
margin=1,
|
||||||
|
min_width=40
|
||||||
|
)
|
||||||
|
|
||||||
|
diff_rows = []
|
||||||
|
if self.has_baseline:
|
||||||
|
for index, stat in enumerate(self.diff_entries):
|
||||||
|
diff_rows.append(self._build_diff_row(stat, index))
|
||||||
|
else:
|
||||||
|
diff_rows = [Text("Noch keine Baseline vorhanden. Nutze 'Baseline-Snapshot aufnehmen' um eine Baseline zu erstellen.", margin=0.5, selectable=False)]
|
||||||
|
|
||||||
|
return Rectangle(
|
||||||
|
content=Column(
|
||||||
|
Rectangle(
|
||||||
|
content=Rectangle(
|
||||||
|
content=Text("Speicheranalyse (tracemalloc)", margin=0.5, selectable=False, overflow="wrap"),
|
||||||
|
fill=self.session.theme.header_box_background_color,
|
||||||
|
margin=0.4
|
||||||
|
),
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
Row(
|
||||||
|
ElmButton(text="Baseline-Snapshot aufnehmen", on_press=self.take_baseline),
|
||||||
|
ElmButton(text="Baseline-Snapshot löschen", on_press=self.clear_baseline, is_disabled=not self.has_baseline),
|
||||||
|
ElmButton(text="Aktualisieren", on_press=self.refresh, is_disabled=not self.has_baseline),
|
||||||
|
spacing=0.5,
|
||||||
|
margin_bottom=1
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
*diff_rows,
|
||||||
|
margin=1,
|
||||||
|
spacing=0.2
|
||||||
|
),
|
||||||
|
Spacer()
|
||||||
|
),
|
||||||
|
Spacer()
|
||||||
|
),
|
||||||
|
fill=self.session.theme.box_color,
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
margin=1,
|
||||||
|
min_width=60
|
||||||
|
)
|
||||||
@@ -141,3 +141,10 @@ class ConfigurationService:
|
|||||||
@property
|
@property
|
||||||
def BASE_URL(self) -> str:
|
def BASE_URL(self) -> str:
|
||||||
return self._config["misc"]["base_url"]
|
return self._config["misc"]["base_url"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def TRACEMALLOC_ENABLED(self) -> bool:
|
||||||
|
try:
|
||||||
|
return self._config["debug"]["tracemalloc_enabled"]
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import tracemalloc
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryService:
|
||||||
|
_baseline: Optional[tracemalloc.Snapshot] = None
|
||||||
|
|
||||||
|
def __init__(self, is_enabled: bool) -> None:
|
||||||
|
self._is_enabled = is_enabled
|
||||||
|
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
return self._is_enabled
|
||||||
|
|
||||||
|
def take_snapshot(self) -> tracemalloc.Snapshot:
|
||||||
|
return tracemalloc.take_snapshot()
|
||||||
|
|
||||||
|
def store_baseline(self) -> None:
|
||||||
|
self._baseline = self.take_snapshot()
|
||||||
|
|
||||||
|
def get_baseline(self) -> Optional[tracemalloc.Snapshot]:
|
||||||
|
return self._baseline
|
||||||
|
|
||||||
|
def clear_baseline(self) -> None:
|
||||||
|
self._baseline = None
|
||||||
|
|
||||||
|
def get_diff(self) -> list[tracemalloc.StatisticDiff]:
|
||||||
|
if self._baseline is None:
|
||||||
|
return []
|
||||||
|
current = self.take_snapshot()
|
||||||
|
return current.compare_to(self._baseline, "traceback")
|
||||||
@@ -6,3 +6,4 @@ from .MailingService import MailingService
|
|||||||
from .AccountingService import AccountingService
|
from .AccountingService import AccountingService
|
||||||
from .PreloadService import PreloadService
|
from .PreloadService import PreloadService
|
||||||
from .ReceiptPrintingService import ReceiptPrintingService
|
from .ReceiptPrintingService import ReceiptPrintingService
|
||||||
|
from .MemoryService import MemoryService
|
||||||
|
|||||||
Reference in New Issue
Block a user