Add Memory Analyzer# with '#' will be ignored, and an empty message aborts the commit.
This commit is contained in:
+4
-1
@@ -1,7 +1,7 @@
|
||||
# CONFIG
|
||||
*.toml
|
||||
|
||||
# Chatch-all default Python gitignore
|
||||
# Catch-all default Python gitignore
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -152,3 +152,6 @@ projects.json
|
||||
|
||||
# Authentication directoy
|
||||
auth/
|
||||
|
||||
# Backup Files
|
||||
*~
|
||||
@@ -50,6 +50,9 @@
|
||||
default_profile_picture="src/elm/assets/img/anon.png"
|
||||
dev_mode_active=true # Supresses E-Mail sending, activates PayPal sandbox API
|
||||
|
||||
[debug]
|
||||
tracemalloc_enabled=false
|
||||
|
||||
[paypal]
|
||||
client_id_sandbox=""
|
||||
secret_sandbox=""
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ services:
|
||||
- "8000:8000"
|
||||
- "8001:8001"
|
||||
volumes:
|
||||
- ./:/opt/elm
|
||||
- ./:/opt/elm:z
|
||||
entrypoint: ["/bin/sh", "-c", "cd /opt/elm/src && rio run --release --public --port 8000"]
|
||||
|
||||
db:
|
||||
|
||||
+10
-2
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import tracemalloc
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -8,7 +9,7 @@ from from_root import from_root
|
||||
from rio import App, Theme, Color, Font, Icon, Session
|
||||
|
||||
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
|
||||
|
||||
logger = logging.getLogger("ELM")
|
||||
@@ -51,6 +52,13 @@ database_service = DatabaseService(configuration_service.get_database_configurat
|
||||
mailing_service = MailingService(configuration_service)
|
||||
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:
|
||||
return self.screen_width < self.theme.MAX_MOBILE_SCREEN_WIDTH
|
||||
|
||||
@@ -79,7 +87,7 @@ app = App(
|
||||
theme=theme,
|
||||
assets_dir=Path(__file__).parent / "assets",
|
||||
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_session_start=on_session_start,
|
||||
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 elm.types import UserSession
|
||||
from elm.services import AccountingService
|
||||
from elm.services import AccountingService, MemoryService
|
||||
|
||||
|
||||
class UserNavigationButton(Component):
|
||||
@@ -103,10 +103,13 @@ class UserNavigation(Component):
|
||||
]
|
||||
|
||||
if self.show_admin_navigation():
|
||||
base_nav.extend([
|
||||
admin_buttons = [
|
||||
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(
|
||||
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
|
||||
def BASE_URL(self) -> str:
|
||||
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 .PreloadService import PreloadService
|
||||
from .ReceiptPrintingService import ReceiptPrintingService
|
||||
from .MemoryService import MemoryService
|
||||
|
||||
Reference in New Issue
Block a user